Background worker update for async wait

So this is how I am currently using a background worker to save a lot of material to a file, presenting a progress bar to the user and preventing any changes to the user interface when saving. I think I captured the main features. The modal ProgressWindow displays a progress bar and not much more. How can I change this to an async-await pattern if I had to?

 private ProgressForm ProgressWindow { get; set; } /// <summary>On clicking save button, save stuff to file</summary> void SaveButtonClick(object sender, EventArgs e) { if (SaveFileDialog.ShowDialog() == DialogResult.OK) { if (!BackgroundWorker.IsBusy) { BackgroundWorker.RunWorkerAsync(SaveFileDialog.FileName); ProgressWindow= new ProgressForm(); ProgressWindow.SetPercentageDone(0); ProgressWindow.ShowDialog(this); } } } /// <summary>Background worker task to save stuff to file</summary> void BackgroundWorkerDoWork(object sender, DoWorkEventArgs e) { string path= e.Argument as string; // open file for (int i=0; i < 100; i++) { // get some stuff from UI // save stuff to file BackgroundWorker.ReportProgress(i); } // close file } /// <summary>On background worker progress, report progress</summary> void BackgroundWorkerProgressChanged(object sender, ProgressChangedEventArgs e) { ProgressWindow.SetPercentageDone(e.ProgressPercentage); } /// <summary>On background worker finished, close progress form</summary> void BackgroundWorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { ProgressWindow.Close(); } 
+5
source share
3 answers

I have a blog series that details this.

In short, BackgroundWorker is replaced by Task.Run , and ReportProgress (and friends) is replaced by IProgress<T> .

So, a simple translation will look like this:

 async void SaveButtonClick(object sender, EventArgs e) { if (SaveFileDialog.ShowDialog() == DialogResult.OK) { ProgressWindow = new ProgressForm(); ProgressWindow.SetPercentageDone(0); var progress = new Progress<int>(ProgressWindow.SetPercentageDone); var task = SaveAndClose(SaveFileDialog.FileName, progress)); ProgressWindow.ShowDialog(this); await task; } } async Task SaveAndClose(string path, IProgress<int> progress) { await Task.Run(() => Save(path, progress)); ProgressWindow.Close(); } void Save(string path, IProgress<int> progress) { // open file for (int i=0; i < 100; i++) { // get some stuff from UI // save stuff to file if (progress != null) progress.Report(i); } // close file } 

Notes for improvements:

  • As a rule, it's nice to have background threads entering the user interface ( // get some stuff from UI ). It would be better to work if you could collect all the information from the user interface before calling Task.Run and just pass it to the Save method.
+8
source

I assume that the reason you let another thread take up a lot of time is because you want the user interface to respond. Your method will meet this requirement.

The advantage of using async waiting is that the code will look more synchronized while the user interface seems to be responsive. You do not need to work with events and functions, such as Control.IsInvokeRequired, because this is the main thread that will do the job.

The disadvantage of async waiting is that as long as the main thread does something (= does not wait for the task to complete), your user interface does not respond.

Having said that, making the async function is easy:

  • declare async function
  • Instead of void return Task and instead of return TResult Task <TResult>.
  • The only exception to this rule are event handlers. The async event handler returns void.
  • Build your materials in a consistent and whenever possible, call asynchronous versions of other functions.
  • Calling this async function is not performed immediately. Instead, it should be executed as soon as the thread in the available thread pool is ready for it.
  • This means that after your thread has scheduled a task, it can freely do other things.
    • When your thread needs the results of another task, expect tak.
    • Return expectations. The task is invalid. Return expectations. The task <TResult> is TResult.

So, to make your function asynchronous:

The async SaveFile function is simple:

 private async Task SaveFileAsync(string fileName) { // this async function does not know // and does not have to know that a progress bar is used // to show its process. All it has to do is save ... // prepare the data to save, this may be time consuming // but this is not the main thread, so UI still responding // after a while do the saving and use other async functions using (TextWriter writer = ...) { var writeTask = writer.WriteAsync (...) // this thread is free to do other things, // for instance prepare the next item to write // after a while wait until the writer finished writing: await writeTask; // of course if you had nothing to do while writing // you could write: await writer.WriteAsync(...) } 

The SaveButtonClick animation is also simple. Due to all my comments, this seems like a lot of code, but it's actually a small feature.

Note that the function is an event handler: return void instead of Task

 private async void SaveButtonClick(object sender, EventArgs e) { if (SaveFileDialog.ShowDialog() == DialogResult.OK) { // start a task to save the file, but don't wait for it to finish // because we need to update the progress bar var saveFileTask = Task.Run () => SaveFileAsync ( SaveFileDialog.FileName ); 

The task starts as soon as the thread in the thread pool is free. Meanwhile, the main thread manages to do other things, such as showing and updating the progress window.

  this.ProgressWindow.Visible = true; this.ProgressWindow.Value = ... 

Now repeat a few seconds and adjust the stroke. Stop as soon as the saveFileTask task is complete.

We cannot just let the main thread wait for the task to complete, because it will stop the response user interface, in addition to the main thread having to update the progress bar repeatedly.

Decision. Do not use Task.Wait functions, but Task.When functions. The difference is that Task.When functions return the expected tasks, and thus, you can wait for the task to complete, thereby supporting the user interface.

Task. If the functional does not have a timeout version. To do this, run Task.Delay

  while (!fileSaveTask.IsCompleted) { await Task.WhenAny( new Task[] { fileSaveTask, Task.Delay(TimeSpan.FromSeconds(1)), }; if (!fileSaveTask.IsCompleted this.UpdateProgressWindow(...); } 

Task.WhenAny stops as soon as the SaveTask file completes, or if the delay task has completed.

What to do: respond to errors if file problems persist. Consider returning the <TResult> task instead of the task.

 TResult fileSaveResult = fileSaveTask.Result; 

or throw an exception. The main window thread treats this as an AggregateException. InnerExceptions (plural) contains exceptions thrown by any of the tasks.

If you need to stop the save process, you need to pass a CacellationToken for each function and enable SaveFile

+1
source

Stephen Cleary's answer basically covers the case. But there is one complication caused by the blocking ShowDialog , which prevents the normal async/await flow.

Therefore, in addition to his answer, I offer you the following general auxiliary function

 public static class AsyncUtils { public static Task ShowDialogAsync(this Form form, IWin32Window owner = null) { var tcs = new TaskCompletionSource<object>(); EventHandler onShown = null; onShown = (sender, e) => { form.Shown -= onShown; tcs.TrySetResult(null); }; form.Shown += onShown; SynchronizationContext.Current.Post(_ => form.ShowDialog(owner), null); return tcs.Task; } } 

Then remove the ProgressWindow form element and use the following

 async void SaveButtonClick(object sender, EventArgs e) { if (SaveFileDialog.ShowDialog() == DialogResult.OK) { using (var progressWindow = new ProgressForm()) { progressWindow.SetPercentageDone(0); await progressWindow.ShowDialogAsync(this); var path = SaveFileDialog.FileName; var progress = new Progress<int>(progressWindow.SetPercentageDone); await Task.Run(() => Save(path, progress)); } } } static void Save(string path, IProgress<int> progress) { // as in Stephen answer } 

Please note that I marked the actual working static method to prevent access to the form (and any user interface element) inside and only work with passed arguments.

+1
source

Source: https://habr.com/ru/post/1242702/


All Articles