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.
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.
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);
}
}
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.
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.
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:
I think if you can show us the code for the SetupFile method, that might help clear up some things.
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 == "")
...
}
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(...)
{
...
}
I think if you can show us the code for the SetupFile method, that might help clear up some things.
ASKER
Thank you very much Kevin for your response.
Ok, I'm not clear on all this. I changed "SetupFile" to look like
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.
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
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)
But I'm getting "Severity Code Description Project File Line Suppression StateError 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;
}
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:
1. Update line 43 to the following:
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)
{
...
}
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);
2. Update line 53 to the following:await SetupFile(tbFirstString.Text, tbSecondString.Text, rbAND.Checked, rbOR.Checked);
That *should* get you past the exceptions you're currently seeing on those lines.
ASKER CERTIFIED SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
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' "
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:
Update line 104 (or thereabouts) from:
var isSearch = await Search(sFile, aSearchString, iSearchCount, bAND, bOR);
... to:SF = Search(sFile, aSearchString, iSearchCount, bAND, bOR);
Then update line 119 (or thereabouts) from:var isSearch = await Search(sFile, aSearchString, iSearchCount, bAND, bOR);
... to:SF = Search(sFile, aSearchString, iSearchCount, bAND, bOR);
Looks like this is what you originally had at lines 103 and 118 (or thereabouts).
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
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>.
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.
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():
So you first have to change Bar() so that it returns a Task<string>:
...but if you add the async keyword to Bar(), the error will go away:
...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:
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.
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();
}
private string Bar()
{
return "Hello world!";
}
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();
}
private string Bar()
{
return "Hello world!";
}
...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();
}
private Task<string> Bar()
{
return "Hello world!";
}
...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();
}
private async Task<string> Bar()
{
return "Hello world!";
}
...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();
}
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();
}
private int CalculateAnswerToLifeTheUniverseAndEverything()
{
return 41 + 1;
}
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.
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.
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.
ASKER
gr8gonzo, In code "private async Task<boo> searchFiles()". You have a non await "searchFile());
Is that suppose to be "await Task.Run( ()=> searchFile());" ?
Is that suppose to be "await Task.Run( ()=> searchFile());" ?
Hi auldh,
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.
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.
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.
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
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
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...
Open in new window
... try something like this: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!