UI blocked when using await to run a time consuming task - c#

I want to build a folder cleaner program. It is expected to report deleted files to a TextBox control at real-time. So I use await Task.Run(() => CleanFolder(folderPath, progress)) function in my button click event. But the UI blocked when running. After a while when the CheanFolder() method run complete, all the deleted files are showed at one time.
namespace FolderCleaner
{
public partial class MainWindow : Window
{
string folderPath;
string matchPattern;
private void ButtonOpen_Click(object sender, RoutedEventArgs e)
{
FolderBrowserDialog fbd = new FolderBrowserDialog() { Description = "Select a folder" };
if (fbd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
folderPath = fbd.SelectedPath;
textBoxPath.Text = folderPath;
buttonClean.IsEnabled = true;
textBoxList.Text = "Folder path: " + folderPath + "\n";
}
}
private async void ButtonClean_Click(object sender, RoutedEventArgs e)
{
matchPattern = textBoxPattern.Text;
buttonOpen.IsEnabled = false;
buttonClean.IsEnabled = false;
Progress<string> progress = new Progress<string>(msg =>
{
textBoxList.AppendText("File deleted: " + msg + "\n");
textBoxList.CaretIndex = textBoxList.Text.Length;
textBoxList.ScrollToEnd();
});
try
{
await Task.Run(() => CleanFolder(folderPath, progress));
textBoxList.AppendText("Mission complete!");
textBoxList.CaretIndex = textBoxList.Text.Length;
textBoxList.ScrollToEnd();
}
catch
{
System.Windows.MessageBox.Show("Error!");
}
finally
{
buttonOpen.IsEnabled = true;
}
}
private void CleanFolder(string path, IProgress<string> progress)
{
var filePaths = Directory.EnumerateFiles(path, "*.*", System.IO.SearchOption.AllDirectories);
foreach (var filePath in filePaths)
{
var matchResult = Regex.Match(filePath, matchPattern);
if (matchResult.Success)
{
File.Delete(filePath);
progress.Report(filePath);
}
}
}
}
}

GUI can`t be controlled from another thread.
But i think, that real problem is that concatenating of string and output to a TextBox is a very inefficient operation.
In your case it is better to show progress of removal in a single line or by using the progress bar.
Here is my solution of your problem (i`ve changed 2 methods):
private async void ButtonClean_Click(object sender, RoutedEventArgs e)
{
matchPattern = textBoxPattern.Text;
buttonOpen.IsEnabled = false;
buttonClean.IsEnabled = false;
await Task.Run(() => CleanFolder(folderPath));
textBoxList.Text += "Mission complete!";
buttonOpen.IsEnabled = true;
}
private void CleanFolder(string path)
{
var filePaths = Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories);
foreach (var filePath in filePaths)
{
var matchResult = Regex.Match(filePath, matchPattern);
if (matchResult.Success)
{
File.Delete(filePath);
System.Windows.Application.Current.Dispatcher.Invoke(delegate
{
// this working fast
textBoxList.Text = "File deleted: " + filePath + "\n";
// this working slow and slower over time
//textBoxList.Text += "File deleted: " + filePath + "\n";
textBoxList.ScrollToEnd();
});
}
}
}
I hope this will help.

Thanks for everyone. Thanks to the book C# 6.0 in a nutshell
I have figured out the solution and have a better understanding of async/await.
First of all, Dispatcher.Invoke is not recommended to use since .Net Framework 4.5, task-based asynchrony has become the dominant pattern (using async/awit).
Second, there are a few principles of using async/await:
The expression after await must be a Task or Task<TResult>
object
If you use async modifier to a method, then the method dont
need to return aTaskmethod manually. The compile will wrap the
method as aTask` object.
If you use a method like async Task Foo(), you must use an await keyword in it.
If there is nothing to await, then remove the async modifier, return a Task object by using return Task.Run(() => { Do Something });. Now you can use await Foo() in the method that calling Foo().
Task Foo() can not operate UI, but async Task Foo() can.

Related

Await / async locking up UI

I am currently having issies with the await / asyn methods, i'm trying to import a .txt file of URLs but the UI is still locking up.
Code
public async void DoImport(string[] files)
{
ListViewItem lv = null;
foreach (string file in files)
{
lv = new ListViewItem(file);
listViewSites.Items.Add(lv);
lv.SubItems.Add("...");
lv.SubItems.Add("...");
}
}
public async Task ImportTheSitesToUse(string inputFileIncludingFullPath)
{
try
{
string[] files = File.ReadAllLines(inputFileIncludingFullPath);
await Task.Run(() => DoImport(files));
}
catch (Exception ex)
{
Helpers.DebugLogging("[" + DateTime.Now + "]-[" + ex.ToString() + "]");
}
}
private async void BtnImportSitesList_Click(object sender, EventArgs e)
{
try
{
OpenFileDialog ofd = new OpenFileDialog();
ofd.Title = "Select your .txt list of URLs ...";
ofd.Filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*";
BtnImportSitesList.Enabled = false;
if (ofd.ShowDialog() == DialogResult.OK)
{
await ImportTheSitesToUse(ofd.FileName.ToString());
}
BtnImportSitesList.Enabled = true;
Helpers.ReturnMessage("Successfully imported [ x ] links.");
}
catch (Exception ex)
{
Helpers.DebugLogging("[" + DateTime.Now + "]-[" + ex.ToString() + "]");
}
}
I don't see any obvious issues with the code, any help would be appreciated.
There are a couple of common issues in the code:
public async void DoImport(string[] files)
This method is marked as async, but it doesn't use await, which makes the method run synchronously. If you look at the Errors window, you will see a warning that complains about that method. Make it public void, that's enough.
Let's break down ImportTheSitesToUse now. It basically does two things:
Read all lines in a file, and
Requests a thread pool thread to call DoImport
If we keep in mind that with async/await, the control is returned to the caller on an await call, the issue becomes obvious when the biggest part of the work is executed synchronously.
The second issue is that there is no reason to call DoImport from a thread pool thread, since it is an inexpensive method, and it also requires to be run on the UI thread (due to the UI object that is being altered).
That said, ImportTheSitesToUse should then have the opposite structure:
Read all lines in a file asynchronously, and
Call DoImport
Putting it all together:
public void DoImport(string[] files)
{
foreach (string file in files)
{
ListViewItem lv = new ListViewItem(file);
listViewSites.Items.Add(lv);
lv.SubItems.Add("...");
lv.SubItems.Add("...");
}
}
public async Task ImportTheSitesToUse(string inputFileIncludingFullPath)
{
try
{
string[] files = await Task.Run(() => File.ReadAllLines(inputFileIncludingFullPath));
DoImport(files);
}
catch (Exception ex)
{
Helpers.DebugLogging("[" + DateTime.Now + "]-[" + ex.ToString() + "]");
}
}

Convert BackgroundWorker to Async

I used to be weary of using BackgroundWorker because it required so many functions to work correctly. However when I swapped to C# from VB.NET (about a month ago) I stumbled across a really easy way to instance them;
Example;
private void cmdMaxCompressPNG_Click(object sender, EventArgs e) {
pbStatus.Maximum = lstFiles.Items.Count;
List<string> FileList = Load_Listbox_Data();
var bw = new BackgroundWorker();
bw.WorkerReportsProgress = true;
bw.DoWork += delegate {
foreach (string FileName in FileList) {
ShellandWait("optipng.exe", String.Format("\"{0}\"", FileName));
bw.ReportProgress(1);
}
};
bw.ProgressChanged += (object s, ProgressChangedEventArgs ex) => {
pbStatus.Value += 1;
};
bw.RunWorkerCompleted += delegate {
lstFiles.Items.Clear();
pbStatus.Value = 0;
MessageBox.Show(text: "Task Complete", caption: "Status Update");
};
bw.RunWorkerAsync();
}
There it is, all in one function! Simple to write, easy to understand, and no real leg work. I even made a Snippet out of it. I've since converted all my multiple part BackgroundWorker functions, into this little piece of elegant code. I've also started using them more liberally than in the past. Yesterday I was reading an article regarding Async and Await and how that's apparently how I should be doing things. I'm having trouble wrapping my head around it.
I've tried to use local functions, but I can't get the wording correct. It keeps trying to put it as synchronous.
How would I convert the above into an equally tight implementation of Await/Async logic?
[Edit]
ShellandWait;
private void ShellandWait(string ProcessPath, string Arguments, bool boolWait = true) {
System.Diagnostics.Process ShellProcess = new System.Diagnostics.Process();
ShellProcess.StartInfo.FileName = ProcessPath;
ShellProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
ShellProcess.StartInfo.Arguments = Arguments;
ShellProcess.StartInfo.CreateNoWindow = true;
ShellProcess.StartInfo.UseShellExecute = false;
ShellProcess.StartInfo.RedirectStandardOutput = true;
ShellProcess.Start();
if (boolWait) { ShellProcess.WaitForExit(); }
if (boolWait) { ShellProcess.Close(); }
}
The original code processes only one file at a time so you could use a simple loop and only execute ShellandAwait asynchronously:
private void cmdMaxCompressPNG_Click(object sender, EventArgs e)
{
pbStatus.Maximum = lstFiles.Items.Count;
var FileList = Load_Listbox_Data();
foreach (var FileName in FileList)
{
//Only thing that needs to run in the background
await Task.Run(()=>ShellandWait("optipng.exe", String.Format("\"{0}\"", FileName));
//Back in the UI
pbStatus.Value += 1;
}
};
lstFiles.Items.Clear();
pbStatus.Value = 0;
MessageBox.Show(text: "Task Complete", caption: "Status Update");
It would be even better if ShellandWait was modified so it *doesn't block. I assume it uses Process.WaitForExit() to block. The method should await asynchronously instead by listening to the Exited event. Such events can be converted to tasks as shown in Tasks and the Event-based Asynchronous Pattern.
The method would look something like this :
Task<string> ShellAsync(string commandPath,string argument)
{
var tcs = new TaskCompletionSource<string>();
var process = new Process();
//Configure the process
//...
process.EnableRaisingEvents = true;
process.Exited += (s,e) => tcs.TrySetResult(argument);
process.Start();
return tcs.Task;
}
This would allow the loop to be simplified to :
foreach (var FileName in FileList)
{
await ShellAsync("optipng.exe", String.Format("\"{0}\"", FileName));
//Back in the UI
pbStatus.Value += 1;
}
The way I went about this (after reading some more) is to use Task.Run()
private async void cmdMaxCompressPNG_Click(object sender, EventArgs e) {
pbStatus.Maximum = lstFiles.Items.Count;
List<string> FileList = Load_Listbox_Data();
await Task.Run(() => {
foreach (string FileName in FileList) {
ShellandWait("optipng.exe", String.Format("\"{0}\"", FileName));
pbStatus.GetCurrentParent().Invoke(new MethodInvoker(delegate { pbStatus.Value += 1; }));
}
});
lstFiles.Items.Clear();
pbStatus.Value = 0;
MessageBox.Show(text: "Task Complete", caption: "Status Update");
}
Note the async next to private.
I had to get fancy with the progress bar since it was a status strip progress bar. If it would have been a standard control I could have used;
pbStatus.Invoke((Action)(() => pbStatus.Value += 1))
Answer to progress bar found at -> Update progress bar from Task.Run async
And here -> How to Invoke the progress bar in Status strip?
I'd consider doing this with Microsoft's Reactive Framework. I think it's much more powerful than using Tasks.
private void cmdMaxCompressPNG_Click(object sender, EventArgs e)
{
pbStatus.Maximum = lstFiles.Items.Count;
var query =
from FileName in Load_Listbox_Data().ToObservable()
from u in Observable.Start(() =>
System.Diagnostics.Process
.Start("optipng.exe", String.Format("\"{0}\"", FileName))
.WaitForExit())
select u;
query
.ObserveOn(this) //marshall back to UI thread
.Subscribe(
x => pbStatus.Value += 1,
() =>
{
lstFiles.Items.Clear();
pbStatus.Value = 0;
MessageBox.Show(text: "Task Complete", caption: "Status Update");
});
}
Just NuGet "System.Reactive.Windows.Forms" and add using System.Reactive.Linq; to get it working.

Parallel forEach search file

I have a program that contains 2 listbox, this program is bassed to search file, and then compare with a StopWatch the difference to use AsyncAwait and TPL... The first listbox does the function using AsyncAwait (I don't know if it's the better way to do but it works, see my code below)
private async void button1_Click(object sender, EventArgs e)
{
Stopwatch stopWatch = new Stopwatch();
foreach (string d in Directory.GetDirectories(#"C:\Visual Studio Projectes\Hash\AsyncAwait\Carpetes"))
{
foreach (string s in Directory.GetFiles(d))
{
stopWatch.Start();
listBox1.Items.Add(s);
await Task.Delay(1);
btIniciar1.Enabled = false;
}
}
btIniciar1.Enabled = true;
stopWatch.Stop();
TimeSpan ts = stopWatch.Elapsed;
textBox1.Text = ts.ToString("mm\\:ss\\.ff") + (" minuts");
}
And then in my second listbox is where I'm stuck, I don't know how to implement the Parallel.ForEach to act like async, what's the better way to do this? I can't find the way to use TPL in this case to do the same as my first listbox, could you help me please?
There's no point in using async in your example code, since it's not actually doing anything asynchronously. If you want to wrap the synchronous code in a background thread, use Task.Run.
Regarding Parallel.ForEach, you can treat it asynchronously by wrapping it in Task.Run: await Task.Run(() => Parallel.ForEach(...));
Note that parallel/background threads cannot directly access UI elements. You can use IProgress<T>/Progress<T> if you want to update the UI from a background/threadpool thread.
Update:
The serial code would look like:
private async void button1_Click(object sender, EventArgs e)
{
IProgress<string> progress = new Progress<string>(update =>
{
listBox1.Items.Add(s);
btIniciar1.Enabled = false;
});
var ts = await Task.Run(() =>
{
Stopwatch stopWatch = new Stopwatch();
foreach (string d in Directory.GetDirectories(#"C:\Visual Studio Projectes\Hash\AsyncAwait\Carpetes"))
{
foreach (string s in Directory.GetFiles(d))
{
stopWatch.Start();
progress.Report(s);
}
}
stopWatch.Stop();
return stopWatch.Elapsed;
});
btIniciar1.Enabled = true;
textBox1.Text = ts.ToString("mm\\:ss\\.ff") + (" minuts");
}
The parallel code would look like:
private async void button1_Click(object sender, EventArgs e)
{
IProgress<string> progress = new Progress<string>(update =>
{
listBox1.Items.Add(s);
btIniciar1.Enabled = false;
});
var ts = await Task.Run(() => Parallel.ForEach( ...
));
btIniciar1.Enabled = true;
textBox1.Text = ts.ToString("mm\\:ss\\.ff") + (" minuts");
}
Finally I've solved this issue doing this :
DirectoryInfo nodeDir = new DirectoryInfo(#"c:\files");
Parallel.ForEach(nodeDir.GetDirectories(), async dir =>
{
foreach (string s in Directory.GetFiles(dir.FullName))
{
Invoke(new MethodInvoker(delegate { lbxParallel.Items.Add(s); }));
contador++;
await Task.Delay(1);
}
}

How to chain a task to a previous instance of it?

Looking to chain a task to a previous instance if it exists. Currently, both are executed at the same time.
Initial code that works for one task :
private async void MenuMediaAddFiles_OnClick(object sender, RoutedEventArgs e)
{
var dialog = GetDefaultOpenFileDialog();
using (dialog)
{
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
using (var progress = new SimpleProgress(this))
{
int addFiles = await _context.AddFiles(dialog.FileNames, progress);
Console.WriteLine("Files added: {0}", addFiles);
}
}
}
}
A failed attempt to make it work :
Task<int> _files;
private async void MenuMediaAddFiles_OnClick(object sender, RoutedEventArgs e)
{
var dialog = GetDefaultOpenFileDialog();
using (dialog)
{
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
using (var progress = new SimpleProgress(this))
{
int addFiles;
Task<int> files = _context.AddFiles(dialog.FileNames, progress);
if (_files == null)
{
_files = files;
}
else
{
var task1 = await _files.ContinueWith(task => _context.AddFiles(dialog.FileNames, new SimpleProgress(this)));
}
addFiles = await _files;
Console.WriteLine("Files added: {0}", addFiles);
}
}
}
}
You were pretty close, but there were a few things that needed to be modified:
private Task<int> previousTask = Task.FromResult(0);
private async void MenuMediaAddFiles_OnClick(object sender, RoutedEventArgs e)
{
var dialog = GetDefaultOpenFileDialog();
using (dialog)
{
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
using (var progress = new SimpleProgress(this))
{
previousTask = previousTask.ContinueWith(t =>
_context.AddFiles(dialog.FileNames, progress))
.UnWrap(); ;
int addFiles = await previousTask;
Console.WriteLine("Files added: {0}", addFiles);
}
}
}
}
Things to note:
Rather than having the previous task be null sometimes, it was easier to initialize it to an already completed task (Task.FromResult(0)). This avoids the null check code.
You were calling AddFiles twice. You shouldn't have been calling it before the if, and you weren't ever assigning the task to the instance field inside the if.
I used UnWrap instead of await to turn the Task<Task<int>> into a Task<int>. Both work, but in this case I felt UnWrap made its intentions clearer.
Note that since the entire event handler will be running in the UI thread there's no need to synchronize access to previousTask, if it doesn't, you'd need to do some locking.

how to run asynchronously

I have to load two large files in parallels
so far I have this code
The code below is click button method
private async void MILoadLogFile_Click(object sender, RoutedEventArgs e)
{
...
if (oFD.ShowDialog() == true)
{
await myLogSession.LoadCompassLogAsync(oFD.FileName);
await myLogSession.LoadCoreServiceLogAsync(oFD.FileName);
}
}
loading method:
public async Task LoadCompassLogAsync(String fileName)
{
StreamReader streamReader = new StreamReader(fileName);
if (fileName.Contains("Compass"))
{
...
try
{
using (streamReader)
{
//Console.Out.WriteLine("lineCount: " + lineCount);
while (((line = await streamReader.ReadLineAsync()) != null)
&& !CompassLogLoadCompleted)
{
...
loggingLvl = new LoggingLvl(eLoggingLvl);
CompassLogData cLD = new CompassLogData(id, dateTime, loggingLvl, threadId, loggingMessage);
await addRoCompassLogCollectionAsync(cLD);
}
}
}
catch (Exception e)
{
Console.WriteLine("The file could not be read:");
Console.WriteLine(e.Message);
}
}
}
the LoadCoreServiceLogAsync is almost identical to LoadCompassLogAsync.
The two loading methods runs sequentially. I want them to run in parallel.
Your code will run one task after the other. To run the two tasks in parallel you can use the Task.WaitAll method:
var loadCompassLogTask = myLogSession.LoadCompassLogAsync(oFD.FileName);
var loadCoreServiceLogTask = myLogSession.LoadCoreServiceLogAsync(oFD.FileName);
Task.WaitAll(loadCompassLogTask, loadCoreServiceLogTask);
Or if you want to use await you can use Task.WhenAll:
var loadCompassLogTask = myLogSession.LoadCompassLogAsync(oFD.FileName);
var loadCoreServiceLogTask = myLogSession.LoadCoreServiceLogAsync(oFD.FileName);
await Task.WhenAll(loadCompassLogTask, loadCoreServiceLogTask);

Categories

Resources