Link to home
Start Free TrialLog in
Avatar of auldh
auldh

asked on

Async will not loop through foreach

Hello, I need help please.

I'm struggling to understand how to use ASYNC in my WinForm program.

My WF (WinForm) program has two windows. The first tasks a user for some params to select then will open the second (child) form.

The second form has several buttons. Once all the selection/configuring is done the user presses the "begin" button and it is off to the races. This is where I have my problem.

The first method/function (button_click) does some validation of the given config. Then it calls another function "SetupFileAsync" which call other functions to get this or that.

The second function "SetupFileAsync" awaits for a task to run inside a "foreach" loop. I found that the "foreach" loop really is not called but once.

I have not found the right sample/explanation on the internet that helps me know what I really need to do.
		private async void btnBegin_Click(object sender, EventArgs e)
		{
			btnBegin.Enabled = false;
			btnBegin.Text = "Running...";
			Refresh();

			//debug
			//testCut();
			//debug

			Task<bool> tskSetupFile = new Task<bool>( ()=> SetupFile(tbFirstString.Text, tbSecondString.Text, rbAND.Checked, rbOR.Checked));
			tskSetupFile.Start();
			
			iShortCutCount = 0;

			DialogResult result;
			string mbMsg = "You must populate at least the first string text box. " +
				"To continue click OK. To abort click Cancel";
			string sCaption = "Continue";

			if (lFilenames != null || lFolderList.Count != 0)//don't continue unless there is a selection of files
			{
				foreach (string sDestinationFile in lstDestinationPath)
				{
					sDestinationPath = sDestinationFile;
					if (bDEBUG == true)
					{
						ERR.WriteError("btnBegin::Setting sDetinationPath = ", sDestinationFile, 0);
						//if (sDestinationPath == @"\\192.168.1.120\site\site\Models\Alexis Fawx")
							//MessageBox.Show("Hit");
					}
					//Beginning of foreach destination list path
					GetTextSearchString(sDestinationFile); //<==maybe here because it is inside the "if (sDestinationPath != "")"????

					if (sDestinationPath != "")
					{
						//Beginning of foreach destination list path
						//GetTextSearchString(sDestinationFile); //<==maybe here because it is inside the "if (sDestinationPath != "")"????

						if (tbFirstString.Text == "" && tbSecondString.Text == "")
							GetTextSearchString(sDestinationFile);

						if (tbFirstString.Text == "")
						{
							if (tbSecondString.Text == "")
							{
								result = MessageBox.Show(mbMsg, sCaption, MessageBoxButtons.OKCancel, MessageBoxIcon.Question);

								if (result == DialogResult.OK)
								{
									if (bDEBUG == true)
										ERR.WriteError("btnBegin::tbFirstString.Text", " = \"\"", 0);
									await tskSetupFile;
								}
							}
						}
						else if (tbFirstString.Text != "")
						{
							if (bDEBUG == true)
								ERR.WriteError("btnBegin::tbFirstString.Text", " != \"\"", 0);
							await tskSetupFile;
						}
						tbShortCutCount.Text = iShortCutCount.ToString();
						Refresh();
					}
					else
						MessageBox.Show("Must select the destination.");
				
					btnBegin.Enabled = true;
					btnBegin.Text = "Begin";
					Refresh();

				}//foreach (string sDestinationFile in lstDestinationPath)
			}
			else
			{
				mbMsg = "You must select a file, a list of files to continue.";
				MessageBox.Show(mbMsg);
			}
		}

Open in new window



It is at the "await tskSetupFile;" that I'm not seeing the loop work for each instance in the " lstDestinationPath" which can be hundreds or files.
Avatar of Kelvin McDaniel
Kelvin McDaniel
Flag of United States of America image

Generally speaking, what you're trying to do is to:
1. Start the process (by clicking the button)
2. Do some preliminary stuff
3. Based on 2, do some other stuff
4. Once 3 is ready, finish doing everything.

The way this lays out in psuedo code is:
Handle event >> Do preliminary validation >> Go off and do something >> AFTER THAT COMPLETES, do something else >> AFTER THAT COMPLETES, finish doing all the remaining things.

See the implied order there? You can't mix and match those things that "go off and do something when you get good and ready" unless your architecture KNOWS to wait until everything is ready to begin any dependent work. To put it bluntly, yours is not and, unfortunately, that's a much longer discussion than the scope of this question.

For your situation, the key is going to be to properly use the async/await pattern. Instead of creating your own task as you currently do here...
Task<bool> tskSetupFile = new Task<bool>( ()=> SetupFile(tbFirstString.Text, tbSecondString.Text, rbAND.Checked, rbOR.Checked));
tskSetupFile.Start();

Open in new window

... try something like this:
var isSetup = await SetupFile(tbFirstString.Text, tbSecondString.Text, rbAND.Checked, rbOR.Checked);

Open in new window

Of course, if it isn't already the SetupFile method will need to be updated to be a Task<bool>. You'll also want to rinse and repeat this pattern within your SetupFile method because all that stuff needs to finish before it returns to the event handler and yells "everything's ready!".

Good luck!
I also agree that it's a little strange to have the SetupFile be called like that. You're starting ONE task before your loop, and then awaiting that task multiple times inside the loop. However, the task will only be "completed" once, so after the first iteration of the loop, the "await tskSetupFile" will essentially be skipped over instantly.

So it's a little unclear how your loop is related to the task.

On a side note, you also have some validation criteria INSIDE your loop, which doesn't make a lot of sense to me.

foreach(...)
{
  if (tbFirstString.Text == "" && tbSecondString.Text == "")
  ...
}

Open in new window


Are you expecting that the textboxes could change in the middle of the loop? If not, get their content at the beginning (before the loop), and do any pre-loop validation:

bool firstStringEmpty = string.IsNullOrWhiteSpace(tbFirstString.Text);
bool secondStringEmpty = string.IsNullOrWhiteSpace(tbSecondString.Text); 


if(firstStringEmpty)
{
  ...show your error box about requiring at least the first string and abort...
}


foreach(...)
{
   ...
}

Open in new window


I think if you can show us the code for the SetupFile method, that might help clear up some things.
Avatar of auldh
auldh

ASKER

Thank you very much Kevin for your response.
Ok, I'm not clear on all this. I changed "SetupFile" to look like
private Task<bool> SetupFile(string sFirststring, string sSecondstring, bool bAND, bool bOR)

Open in new window

But I'm getting "Severity   Code   Description   Project   File   Line   Suppression State
Error   CS0029   Cannot implicitly convert type 'bool' to 'System.Threading.Tasks.Task<bool>' "

Maybe I should send more code with alterations so I it can be reviewed and not embarrass myself.
Can't get this right.

      :
      :
      private async void btnBegin_Click(object sender, EventArgs e)
      {
         btnBegin.Enabled = false;
         btnBegin.Text = "Running...";
         Refresh();

         //debug
         //testCut();
         //debug

         //Task<bool> tskSetupFile = new Task<bool>( ()=> SetupFile(tbFirstString.Text, tbSecondString.Text, rbAND.Checked, rbOR.Checked));
         //tskSetupFile.Start();
         
         iShortCutCount = 0;

         DialogResult result;
         string mbMsg = "You must populate at least the first string text box. " +
            "To continue click OK. To abort click Cancel";
         string sCaption = "Continue";

         if (lFilenames != null || lFolderList.Count != 0)//don't continue unless there is a selection of files
         {
            foreach (string sDestinationFile in lstDestinationPath)
            {

               if (sDestinationPath != "")
               {

                  if (tbFirstString.Text == "")
                  {
                     if (tbSecondString.Text == "")
                     {
                        result = MessageBox.Show(mbMsg, sCaption, MessageBoxButtons.OKCancel, MessageBoxIcon.Question);

                        if (result == DialogResult.OK)
                        {
                           if (bDEBUG == true)
                              ERR.WriteError("btnBegin::tbFirstString.Text", " = \"\"", 0);
                           //await tskSetupFile;
                           //SetupFile(tbFirstString.Text, tbSecondString.Text, rbAND.Checked, rbOR.Checked);
                           var isSetup = await SetupFile(tbFirstString.Text, tbSecondString.Text, rbAND.Checked, rbOR.Checked);
                        }
                     }
                  }
                  else if (tbFirstString.Text != "")
                  {
                     if (bDEBUG == true)
                        ERR.WriteError("btnBegin::tbFirstString.Text", " != \"\"", 0);
                     //await tskSetupFile;
                     //SetupFile(tbFirstString.Text, tbSecondString.Text, rbAND.Checked, rbOR.Checked);
                     var isSetup = await SetupFile(tbFirstString.Text, tbSecondString.Text, rbAND.Checked, rbOR.Checked);
                  }
                  tbShortCutCount.Text = iShortCutCount.ToString();
                  Refresh();
               }
               else
                  MessageBox.Show("Must select the destination.");
            

            }//foreach (string sDestinationFile in lstDestinationPath)
         }
         else
         {
            mbMsg = "You must select a file, a list of files to continue.";
            MessageBox.Show(mbMsg);
         }
      }
      :
      :
      private async Task<bool> SetupFile(string sFirststring, string sSecondstring, bool bAND, bool bOR)
      {
         string[] aSearchString = new string[2];
         int iSearchCount = 0;
         bool SF = false;

         if (bDEBUG == true)
         {
            ERR.WriteError("SetupFile::Beginning", "", 0);
         }

         if (sFirststring != "" && sSecondstring == "")
         {
            aSearchString[0] = sFirststring;
            iSearchCount = 1;
         }
         if (sFirststring != "" && sSecondstring != "")
         {
            aSearchString[0] = sFirststring;
            aSearchString[1] = sSecondstring;
            iSearchCount = 2;
         }

         if (lFilenames != null && bUseFolderList == false)
         {
            int iList = lFilenames.Count;
            ///This function/method gets a list of files the user selected and steps through them one at a time.
            ///Then will do the needed search to create short cut(s)
            foreach (string sFile in lFilenames)
            {
               //moved the search to its own function!!!
               //SF = Search(sFile, aSearchString, iSearchCount, bAND, bOR);
               var isSearch = await Search(sFile, aSearchString, iSearchCount, bAND, bOR);
            }
         }

         if (lFolderList.Count > 0 && bUseFolderList == true)
         {
            foreach (string sFolder in lFolderList)
            {
               string[] aFiles = Directory.GetFiles(sFolder);
               lFilenames = aFiles.ToList();

               foreach (string sFile in lFilenames)
               {
                  //moved to Search()!!!
                  //SF = Search(sFile, aSearchString, iSearchCount, bAND, bOR);
                  var isSearch = await Search(sFile, aSearchString, iSearchCount, bAND, bOR);
               }
            }
         }

         return SF;
      }
      :
      :
      :
      :
      private async Task<bool> Search(string sFile, string[] aSearchString, int iSearchCount, bool bAND, bool bOR)
      {
         bool SF = false;
         if (bDEBUG == true)
         {
            ERR.WriteError("Search::sFile: ", sFile, 0);
            ERR.WriteError("Search::first string: ", aSearchString[0], 0);
            ERR.WriteError("Search::first string: ", aSearchString[1], 0);
         }

         //what to search for
         if (iSearchCount == 0)
            ShortcutCreation(sFile);

         if (iSearchCount == 1)
         {
            if (sFile.IndexOf(aSearchString[0], StringComparison.CurrentCultureIgnoreCase) > 0)
               ShortcutCreation(sFile);
            if (bDEBUG == true)
               ERR.WriteError("Search::Search count = 0: ", aSearchString[0], 0);
         }
         else
         {
            if (iSearchCount == 2)
            {
               if (bAND == true)
               {
                  if (sFile.IndexOf(aSearchString[0], StringComparison.CurrentCultureIgnoreCase) > 0 &&
                     sFile.IndexOf(aSearchString[1], StringComparison.CurrentCultureIgnoreCase) > 0)
                  {
                     if (bDEBUG == true)
                     {
                        ERR.WriteError("Search::Search AND count = 2: ", aSearchString[0] + " " + aSearchString[1], 0);
                     }
                     ShortcutCreation(sFile);
                  }
               }
               else if (bOR == true)
               {
                  if (sFile.IndexOf(aSearchString[0], StringComparison.CurrentCultureIgnoreCase) > 0 ||
                     sFile.IndexOf(aSearchString[1], StringComparison.CurrentCultureIgnoreCase) > 0)
                     ShortcutCreation(sFile);
                  if (bDEBUG == true)
                     ERR.WriteError("Search::Search OR count = 2: ", aSearchString[0] + " " + aSearchString[1], 0);
               }
            }
         }

         return SF;
      }


Open in new window

The VS warning message:
Severity   Code   Description   Project   File   Line   Suppression State
Warning   CS1998   This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.   RenameFiles   D:\project\CSharp\RenameFiles\CreateShortCut.cs   485   Active


No worries.  :)

The latter error is being thrown because the Search method doesn't have any calls to an awaitable Task -- in other words, apparently nothing in there calls await {method name}. Simply make Search return a regular bool again, as follows:
private bool Search(string sFile, string[] aSearchString, int iSearchCount, bool bAND, bool bOR)
{
    ...
}

Open in new window

If you're not going to use the isSetup variable then there's no need to define it...
1. Update line 43 to the following:
await SetupFile(tbFirstString.Text, tbSecondString.Text, rbAND.Checked, rbOR.Checked);

Open in new window

2. Update line 53 to the following:
await SetupFile(tbFirstString.Text, tbSecondString.Text, rbAND.Checked, rbOR.Checked);

Open in new window

That *should* get you past the exceptions you're currently seeing on those lines.
ASKER CERTIFIED SOLUTION
Avatar of gr8gonzo
gr8gonzo
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of auldh

ASKER

First thank you gr8gonzo you are right. I'm doing cleanup as continue to work on the program. Kind of chart in front of the horse approach is not pretty. I will make that change.
Secondly Kevin if I make the change to search as:
   private bool Search(string sFile, string[] aSearchString, int iSearchCount, bool bAND, bool bOR)

Now I get these errors:
==
Severity   Code   Description   Project   File   Line   Suppression State
Error   CS1061   'bool' does not contain a definition for 'GetAwaiter' and no accessible extension method 'GetAwaiter' accepting a first argument of type 'bool' could be found (are you missing a using directive or an assembly reference?)  
==

For both "await" in part this is why I'm lost. If I remove "bool" in Search method get an error on the "await". If set "void" then get a "Cannot await 'void' " 
Sorry - had to tweak the code in my last comment a bit - had a bad copy-n-paste.
Yep, now just remove the await before your calls to Search as there weren't originally intended.

Update line 104 (or thereabouts) from:
var isSearch = await Search(sFile, aSearchString, iSearchCount, bAND, bOR);

Open in new window

... to:
SF = Search(sFile, aSearchString, iSearchCount, bAND, bOR);

Open in new window

Then update line 119 (or thereabouts) from:
var isSearch = await Search(sFile, aSearchString, iSearchCount, bAND, bOR);

Open in new window

... to:
SF = Search(sFile, aSearchString, iSearchCount, bAND, bOR);

Open in new window

Looks like this is what you originally had at lines 103 and 118 (or thereabouts).
Avatar of auldh

ASKER

gr8gonzo regarding you comment. I wanted to modularize methods.
The list<> has already gotten the GetDirectory collection.
btnBegin is validate the user selection and input if there is one.
SetupFile does maybe redundant work that I need to clean up which will call the "Search" method/function.
Search does all the heavy work of foreach list<> to compare the search strings. This where I think async is needed to reduce the program locking. I want while Search is busy to free the program to minimize or move on the desktop.

As I stated I'm still improving the code and embarrass by some of the stuff in there at this point. The async has my focus while the cleanup improvement is going on. Sorry
Regarding the "await" errors you got after updating the return type of Search:
In order to use await when calling another function, the call MUST exist within an async function, and the method being called (by the await) MUST return either Task or Task<T>
When you use async/await, the .NET engine is doing a little bit of background work for you.

1. When you add "async" to your method, you're just enabling the ability to use the "await" functionality.
private async void Foo()
{
  string x = Bar();
}

Open in new window

private string Bar()
{
  return "Hello world!";
}

Open in new window


With the above, you'll get a warning from the environment. "Foo" will be underlined in green and the tooltip will tell you that there are no "await" calls. That's because the code will still run but there's no async behavior. If there's nothing being awaited, then .NET will just run it synchronously as if you didn't even have the "async" keyword in there.

2. If you just add "await" to the call to Bar():
private async void Foo()
{
  string x = await Bar();
}

Open in new window

private string Bar()
{
  return "Hello world!";
}

Open in new window

...you'll get an error because "await" will tell .NET that Bar() should return a Task of some kind. Instead, Bar() is returning a string, and you can't await a string any more than you can fly an apple.

So you first have to change Bar() so that it returns a Task<string>:
private async void Foo()
{
  string x = await Bar();
}

Open in new window

private Task<string> Bar()
{
  return "Hello world!";
}

Open in new window

...which will fix the error on the "await" line, but it will trigger another error on the return "Hello world!" line because you haven't told Bar() that it is an "async" method, so .NET sees it as a normal method and is saying, "Hey - the string "Hello World" is not a Task!"

...but if you add the async keyword to Bar(), the error will go away:
private async void Foo()
{
  string x = await Bar();
}

Open in new window

private async Task<string> Bar()
{
  return "Hello world!";
}

Open in new window


...because now .NET is doing some magic background code for you. It's actually putting in some code behind the scenes that sets up the Task, and when the Task completes, it stuffs the "Hello World" string into the Task's result, and the awaiter can automatically grab the result and assign it back to a normal variable.

However, you'll notice that you get the green line/warning on Bar() that tells you that there's no await calls, so it'll run synchronously.

This is what I was talking about earlier where everything runs synchronously UNTIL .NET is told explicitly to do something that requires a separate thread. So let's say you were doing some complex calculations and trying to figure out the answer to life, the universe, and everything, and that calculation took a long time to run. You could use Task.Run() to force a new thread to kick off and execute that method, and then await it:
private async void Foo()
{
  string x = await Bar();
}

Open in new window

private async Task<string> Bar()
{
  // This below line kicks off a new thread
  int result = await Task.Run(() => CalculateAnswerToLifeTheUniverseAndEverything());

  // Once the above thread finishes and gets the result, we can continue
  return "Hello world! The answer to life, the universe, and everything is " + result.ToString();
}

Open in new window

private int CalculateAnswerToLifeTheUniverseAndEverything()
{
  return 41 + 1;
}

Open in new window


Now the green underline/warning disappears and you have a true async setup.

Hopefully that helps explain the errors and warnings you're seeing regarding the "async" and "Task" stuff.
Avatar of auldh

ASKER

gr8gonzo I'm grateful for your sample code.
My mind can't quite wrap it head around how you simplified my code. I have to take time to review this and convert my code to test. This will take time. I wanted to get back to you before then, I find your help valuable and exceptional.
I tweaked my code to fit your "class" and it works as expected.
Right now I want to do the work to fix my side before marking this closed.

I have learned a lot, thanks!

Thank you too Kevin.
Avatar of auldh

ASKER

gr8gonzo, In code "private async Task<boo> searchFiles()". You have a non await "searchFile());
Is that suppose to be "await Task.Run( ()=> searchFile());" ?


Hi auldh,
Is that suppose to be "await Task.Run( ()=> searchFile());" ?
Nope.

As I mentioned, there is a little bit of overhead whenever you spin up a new thread/task. I put the Task.Run() at the searchFiles() level, so that if you had a list of 1,000 files, it wouldn't spin up 1,000 threads back-to-back. Instead, it just spins up one thread and runs through the 1,000 files within that one thread/task.

I did use Task.Run() on searchFolder() because people usually have much less folders than files. However, neither way is right or wrong - it was just a gut feeling about how to optimize the performance while giving you a little flexibility at the same time. You could easily set up a "searchFolders" wrapper method and just Task.Run() that instead, and have one thread handle all the folders just like searchFiles() does.

Again, I just threw together a general idea of what I figured might work well.
Avatar of auldh

ASKER

gr8gonzo, on your line 129 it look wrong or it was not clear.
I see that the "await Task.Run(() =>  " was missing. That was what I was driving at.

I see I have much more to learn to get to the point to build your "searchRequest class". Even more to understand the async construction.
Thanks again.
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial