So this is how I currently use background worker to save a lot of stuff to file while presenting the user with a progress bar and preventing any changes to the UI while saving is in progress. I think I've captured the essential features. The modal ProgressWindow
displays a progress bar and not much else. How would I go about changing this to 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();
}
BackgroundWorker is explicitly labeled as obsolete in .
The main benefits of asynchronous programming using async / await include the following: Increase the performance and responsiveness of your application, particularly when you have long-running operations that do not require to block the execution.
An await expression in an async method doesn't block the current thread while the awaited task is running. Instead, the expression signs up the rest of the method as a continuation and returns control to the caller of the async method. The async and await keywords don't cause additional threads to be created.
await waits for async to resolve or reject. if you dont need the results, you wont need to await. If you don't await it (or otherwise handle the error, e.g. with . catch ), and it errors, you have an "unhandled promise rejection".
I have a blog series that covers this in detail.
In short, BackgroundWorker
is replaced by Task.Run
, and ReportProgress
(and friends) is replaced by IProgress<T>
.
So, a straightforward translation would 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:
// get some stuff from UI
). It would probably work better if you could collect all the information from the UI before calling Task.Run
and just pass it into the Save
method.I guess the reason why you let another thread do the time consuming stuff is because you want to keep the user interface responsive. Your method will meet this requirement.
The advantage of using async-await is that the code will look more synchronous, while the user interface seems to be responsive. You don't have to work with events and functions like Control.IsInvokeRequired, because it is the main thread that will do the work.
The disadvantage of async-await is that as long as the main thread is really doing something (= not awaiting for a task to finish), your UI is not responsive.
Having said that, making a function async is easy:
So to make your function async:
The async SaveFile function is easy:
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 async is also easy. Because of all my comment it seems a lot of code, but in fact it is a small function.
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 is scheduled to run as soon as a thread in the thread pool is free. Meanwhile the main thread has time to do other things, like showing and updating the progress window.
this.ProgressWindow.Visible = true;
this.ProgressWindow.Value = ...
Now repeatedly wait a sec and adjust progress. Stop as soon as the saveFileTask task is finished.
We can't just let the main thread wait for the task to finish because that would stop UI from being responsive, besides the main thread should repeatedly update the progressbar.
Solution: Don't use the Task.Wait functions, but the Task.When functions. The difference is that Task.When functions return awaitable Tasks, and thus you can await for the task to finish, thus keeping the UI responsive.
The Task.When funcitons don't have a timeout version. For this we start a Task.Delay
while (!fileSaveTask.IsCompleted)
{
await Task.WhenAny( new Task[]
{
fileSaveTask,
Task.Delay(TimeSpan.FromSeconds(1)),
};
if (!fileSaveTask.IsCompleted
this.UpdateProgressWindow(...);
}
The Task.WhenAny stops as soon as the fileSaveTask is completed, or if the delay task is completed.
Things to do: react on errors if fileSave encounters problems. Consider returning a Task < TResult > instead of Task.
TResult fileSaveResult = fileSaveTask.Result;
or throw an exception. The main window thread catches this as an AggregateException. The InnerExceptions (plural) contain the exceptions thrown by any of the tasks.
If you need to be able to stop the saving process, you need to pass a CacellationToken to every function and let the SaveFile
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With