Thursday, February 8, 2007

Background operations in Windows Forms in .Net

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
{
// proceed performing tasks
}
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)
{
// Start the asynchronous operation.
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)
{
// First, handle the case where an exception was thrown.
if (e.Error != null)
{
// Show the error message
}
else if (e.Cancelled)
{
// Handle the case where the user cancelled the operation.
}
else
{
// Operation completed successfully.
// So display the result.
}
// Do post completion operations, like enabling the controls etc.
}
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)
{
// Do something very slow

// Calculate and report the progress as a
// percentage of the total task
int percentComplete = (currentValue * 100) / maxValue;
worker.ReportProgress(percentComplete);

// Return the result
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)
{
// Do something very slow

// Calculate and report the progress as a
// percentage of the total task. Since this is
// a recursive operation, check whether the
// percent just calculated is the highest - if yes,
// it directly represents the percent of the total
// task completed.
int percentComplete = (currentValue * 100) / maxValue;
if (percentComplete > highestPercentageReached)
{
highestPercentageReached = percentComplete;
worker.ReportProgress(percentComplete);
}

// Return the result
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.

Collapse
private void cancelAsyncButton_Click(System.Object sender, System.EventArgs e)
{
// Cancel the asynchronous operation.
backgroundWorker1.CancelAsync();

// Do UI updates like disabling the Cancel button etc.
}

long LongRunningMethod(int n, BackgroundWorker worker, DoWorkEventArgs e)
{
if (worker.CancellationPending)
{
e.Cancel = true;
}
else
{
// The operation has not been cancelled yet.
// So proceed doing something very slow.

// Calculate and report the progress as a

// percentage of the total task
int percentComplete = (currentValue * 100) /
maxValue;worker.ReportProgress(percentComplete);

// Return the result
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.

No comments: