Introduction
Whidbey comes along with many exciting features that every developer would love to use. One of the hottest new items it brings along is the Background Worker component. If you have ever written code to execute any asynchronous operation in Windows Forms, you would agree that there are a number of little things that we need to take care of � the most prominent one being that we should not access any controls in the UI from our worker thread. Bugs are common and hard to track when working with multiple threads.
The Background Worker component cleans our plate clean of all these responsibilities while implementing asynchronous operations. It gives us instead, a cool event-based mechanism to call long-running methods with no additional headaches. This article explores how we could use this component in a simple Windows Forms project.
A bit of history
During pre-Whidbey days, the most common and simplest approach for handling long processes was to use asynchronous delegate invocation. This basically involved a call to the BeginInvoke
method of a delegate. Calling BeginInvoke
will queue the method execution to be run from the system thread pool, returning immediately, without waiting for the method execution to complete. This ensured that the caller will not have to wait until the method finishes its task.
After invoking a method asynchronously, you would typically use the Control.InvokeRequired
property and Control.BeginInvoke
method to facilitate UI-worker thread communication. This was how we usually send progress status back to the UI thread to update the progress bar/status bar control in the UI.
The downside of this procedure is that we have to keep track of which thread we are on, and should ensure that we don�t call any UI control members within a worker thread. Typically, the structure for such a code involves the following snippet:
if (this.InvokeRequired)
{
this.BeginInvoke(�the target�);
}
else
{
}
Adding this snippet wherever UI-worker communication exists, would make the code illegible and tough to maintain. A few design patterns in .NET simplifies this code; but all this was a tall-order solution for a simple requirement � you just want to run a long process, and inform the user on the progress.
The Whidbey way
Enter Whidbey. Introducing the BackgroundWorker
component which would save us our time and effort.
The BackgroundWorker
component can be dropped onto your form from the Toolbox Components tab. It finds place in the component tray whose properties will be available in the Properties window.
The BackgroundWorker
component exposes three events and three methods that you will be interested in. An overview of the sequence follows:
- You invoke the
BackgroundWorker.RunWorkerAsync
method, passing any argument if necessary. This raises the DoWork
event.
- The
DoWork
handler will have the long-running code or a call to the long-running method. You retrieve any arguments passed through DoWork
event arguments.
- When the long-running method finishes execution, you set the result to the
DoWork
event argument's Result
property.
- The
RunWorkerCompleted
event is raised.
- In the
RunWorkerCompleted
event handler, you do post operation activities.
A detailed step-by-step description follows.
Running a process asynchronously
The RunWorkerAsync
method starts an operation asynchronously. It takes an optional object
argument which may be used to pass initialization values to the long-running method.
private void startAsyncButton_Click(System.Object sender, System.EventArgs e)
{
backgroundWorker1.RunWorkerAsync(someArgument);
}
The RunWorkerAsync
method raises the DoWork
event, in whose handler you would put your long-running code. This event has a DoWorkEventArgs
parameter, which has two properties � Argument
and Result
. The Argument
property gets its value from the optional parameter we had set while calling RunWorkerAsync
. The Result
property is used to set the final result of our operation, which would be retrieved when the RunWorkerCompleted
event is handled.
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
e.Result = LongRunningMethod((int)e.Argument, worker, e);
}
Instead of referencing the backgroundWorker1
instance directly, we obtain a reference to it through the sender
object. This ensures that when we have multiple instances of the BackgroundWorker
component in our form, we obtain the instance which had actually raised the event.
We need to pass a reference of the BackgroundWorker
instance as well as the event argument to the long running method to facilitate cancellation and progress reporting.
Retrieving the state after completion
The RunWorkerCompleted
event is raised in three different circumstances; either the background operation completed, was cancelled, or it threw an exception. The RunWorkerCompletedEventArgs
class contains the Error
, Cancelled
, and Result
properties which could be used to retrieve the state of the operation, and its final result.
private void backgroundWorker1_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
}
else if (e.Cancelled)
{
}
else
{
}
}
Notifying progress to the UI
To support progress reporting, we first set the BackgroundWorker.WorkerReportsProgress
property to true
, and then attach an event handler to the BackgruondWorker.ProgressChanged
event. The ProgressChangedEventArgs
defines the ProgressPercentage
property which we could use to set the value for say, a progress bar in the UI.
long LongRunningMethod(int someArgument, BackgroundWorker worker, DoWorkEventArgs e)
{
int percentComplete = (currentValue * 100) / maxValue;
worker.ReportProgress(percentComplete);
return result;
}
private void backgroundWorker1_ProgressChanged(object sender,
ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
}
Note that if your long-running method is by itself a recursive operation, then you will have to ensure that the progress percentage you are calculating is for the total task and not for the current iteration. The code for this will take the form as follows:
long LongRunningMethod(int n, BackgroundWorker worker, DoWorkEventArgs e)
{
int percentComplete = (currentValue * 100) / maxValue;
if (percentComplete > highestPercentageReached)
{
highestPercentageReached = percentComplete;
worker.ReportProgress(percentComplete);
}
return result;
}
Supporting cancellation
To cancel a background worker process, we should first set the BackgroundWorker.WorkerSupportsCancellation
property to true
. When we need to cancel an operation, we invoke the BackgroundWorker.CancelAsync
method. This sets the BackgroundWorker.CancellationPending
property to true
. In the worker thread, you have to periodically check whether this value is true
to facilitate cancellation. If it is true
, you set the DoWorkEventArgs.Cancel
property to true
, and skip executing the method. A simple if
block will do great here.
private void cancelAsyncButton_Click(System.Object sender, System.EventArgs e)
{
backgroundWorker1.CancelAsync();
}
long LongRunningMethod(int n, BackgroundWorker worker, DoWorkEventArgs e)
{
if (worker.CancellationPending)
{
e.Cancel = true;
}
else
{
int percentComplete = (currentValue * 100) /
maxValue;worker.ReportProgress(percentComplete);
return result;
}
}
See, no hassles on whether we are on the right thread or not. We simply attach a few handlers to the events exposed by the component, and focus on our task. With this, we have setup a clean implementation to invoke a method asynchronously, get frequent updates as to its progress, support for canceling the method, and finally handling the completion of the method.
Summing it up
Let�s revisit the steps involved in implementing an asynchronous operation using the BackgroundWorker
component.
- Drag and drop a
BackgroundWorker
component from the Toolbox into the form.
- Add handlers to the
DoWork
, ProgressChanged
and RunWorkerCompleted
events as necessary.
- Have your long-running process check periodically the status of the
BackgroundWorker.CancellationPending
property.
- Let your process calculate the percentage of the task completed, and invoke the
BackgroundWorker.ReportProgress
method passing the percentage.
- Handle the
BackgroundWorker.RunWorkerCompleted
event to check whether this process was completed successfully, cancelled, or whether an exception had been thrown.
Cheers!
About the Code
The accompanying code is a Visual Studio .NET 2005 Beta solution for a Windows Forms project which demonstrates the Background Worker component with progress notification and cancellation support.
History
- 11.Nov.2004: Just got published.