Background Processing in Windows Forms Applications (Part 1)Improvements in the .NET Framework 2.0
Peter Himschoot
Applies to:U2U - Brussels
The first of two companion articles. This article explains how to create and execute background processes in a WinForms application with the .NET Framework 1.x and then Part 2 shows improvements made in the .NET Framework 2.0 with the BackgroundWorker class. Downloads: Contents: IntroductionIn Windows Forms applications, executing jobs that take a long time to complete should be done on a background thread. Otherwise the user-interface will block (appear to hang) for the duration of the job. For example when you print a document in Microsoft Office Word, printing is done in the background, allowing you to continue working. We should also process jobs that take a long time on a background thread, allowing the user to continue doing something else while the job is running. This can change the user’s perception, making them feel that your application is very responsive and fast. We should also allow users to cancel these jobs, giving them control over them. ![]() Starting a background process
Creating a background thread in the .NET Framework is actually quite easy with a delegate. Every delegate can be invoked synchronously or asynchronously. As an example we’ve created a new WinForms project in Visual Studio .NET, added a start button, cancel button, drop-down combo-box, progress bar and label like in figure 1:
![]() We want to call a method that takes a fair amount of time to complete: private void DoWork(int delay)
{
for (int progress = 0; progress < 100; progress += 1)
{
Thread.Sleep(delay);
ReportOnProgress(progress, string.Format("{0}% done", progress));
}
}
The DoWork method emulates a long running job and reports back from time to time with the ReportOnProgress method. The ReportOnProgress method updates the progressBar and label control with a percentage:
private void ReportOnProgress( int progress, string msg )
{
progressBar.Value = progress;
labelProgress.Text = msg;
}
The start button’s click event is implemented like this:
private void buttonStart_Click(object sender, EventArgs e)
{
// Disable the button
buttonStart.Enabled = false;
// Do the work
DoWork(int.Parse(listDelay.Text));
// Enable button back again
buttonStart.Enabled = true;
}
Running this sample and clicking the Start button will actually block the user-interface for the time it takes to complete the method. Some users will think that the application hangs or at least get annoyed with this behavior, so we decide to call the method on a background thread. Doing this is easy using a delegate:
// Delegate used to call a method async
private delegate void DoWorkDelegate(int delay);
private void buttonStart_Click(object sender, EventArgs e)
{
// Disable the button
buttonStart.Enabled = false;
// Create delegate and make async call
DoWorkDelegate worker =
new DoWorkDelegate(DoWork);
worker.BeginInvoke(
int.Parse(listDelay.Text),
new AsyncCallback(DoWorkComplete),
worker);
}
Notice that in order to call the method asynchronously, we have to wrap it in a delegate object and call the BeginInvoke method of the delegate object. BeginInvoke always takes the same number of arguments as the wrapped method plus two extra arguments
(see Asynchronous Method Signatures). The first extra argument is a callback method wrapped in an AsyncCallback delegate. The second extra argument can be anything you like, and we use it to pass the worker delegate object itself to the DoWorkComplete method, which is the callback method that gets called when the background process is complete.
The DoWorkComplete method looks like this: private void DoWorkComplete(IAsyncResult workID)
{
EnableStartButton();
DoWorkDelegate worker = workID.AsyncState as DoWorkDelegate;
worker.EndInvoke(workID);
ReportOnProgress(100, "Done");
}
The DoWorkComplete method gets called when the worker code has finished. The worker code is represented by the IAsyncResult object being passed as an argument. This is necessary because it is possible to start multiple threads by calling BeginInvoke repeatedly to create multiple simultaneous jobs. Every job is thus represented by an AsyncResult object (that is why we called it workID).
The first thing to do is to enable the Start button again with the EnableStartButton method. EnableStartButton();Note that we removed this call from the buttonStart_Click method. Next we need to call the delegate’s EndInvoke method to complete the job. But how can we get it? Remember the second extra argument we passed in the call the BeginInvoke? We passed the worker delegate object, and we can retrieve it using the AsyncState property from the IAsyncResult argument: DoWorkDelegate worker = workID.AsyncState as DoWorkDelegate;So to complete the background job we need to call the worker’s EndInvoke method passing the IAsyncResult object as an argument. worker.EndInvoke(workID);This EndInvoke method has the same return value as the method being wrapped, and also will have any reference or out arguments being returned. Finally we call the ReportOnProgress method to show the user that the task is done: ReportOnProgress(100, "Done");Please note that we don’t actually need to pass the delegate as an argument, because the delegate is also passed in the IAsyncResult argument to the DoWorkComplete method. The IAsyncResult argument actually references an AsyncResult instance (MSDN documentation clearly states that the object passed is an AsyncResult object, so this should not break any code in the future). This object’s AsyncDelegate property is the delegate whose BeginInvoke was called, so to retrieve the delegate we could also use the following code: AsyncResult job = workID as AsyncResult; DoWorkDelegate worker = job.AsyncDelegate as DoWorkDelegate;You decide what you like best. ![]() Updating controls in background threads
The application that we have built now contains an error which can be hard to notice. We access some controls from the background thread which is not allowed on current versions of the Windows operating system. Controls can only be modified on the same thread that created the control (see Control.InvokeRequired Property). This usually is the main thread of the application.
![]() So what is the solution? Well, the solution is to make modifications to controls only on the same thread that created the control. And how do we do this? Every control has an InvokeRequired property and an Invoke method to make the control execute a method on the correct thread.So what is the solution? Well, the solution is to make modifications to controls only on the same thread that created the control. And how do we do this? Every control has an InvokeRequired property and an Invoke method to make the control execute a method on the correct thread. Let us look at an example of this using the EnableStartButton method: private void EnableStartButton()
{
if (buttonStart.InvokeRequired)
{
buttonStart.Invoke(new MethodInvoker(EnableStartButton));
}
else
{
buttonStart.Enabled = true;
}
}
The EnableStartButton method can be called correctly on any thread. This is because the method checks if it is running on the correct thread using the control’s InvokeRequired property. This InvokeRequired property returns false if the current thread is the thread that created the control, so you can modify the control safely – for example setting the Enabled property. Otherwise it returns true and the EnableStartButton method uses the control’s Invoke method to call itself on the thread that created the control. This looks like an endless recursion loop, but because the method gets called the second time on the correct thread, InvokeRequired returns false, stopping the recursion.
The control’s Invoke method takes a delegate as an argument representing the method to call. The delegate object you pass should have the same number of parameters and the same return type as the method that needs to be called. Doing some research reveals that the MethodInvoker delegate is appropriate for calling our method, so we re-use this delegate. The ReportOnProgress method is very similar but with some differences: private delegate void ReportOnProgressDelegate(int progress,
string msg);
private void ReportOnProgress(int progress, string msg)
{
if (progressBar.InvokeRequired)
{
progressBar.Invoke(new ReportOnProgressDelegate(ReportOnProgress),
new object[] { progress, msg });
}
else
{
progressBar.Value = progress;
labelProgress.Text = msg;
}
}
The difference between the ReportOnProgress method and the EnableStartButton method is that the ReportOnProgress method takes some arguments. So this time we need to declare a delegate with the correct parameters:
private delegate void ReportOnProgressDelegate(int progress,
string msg);
And the ProgressBar’s Invoke method needs to pass some arguments to the ReportOnProgress method. This is done by passing and array of objects as Invoke’s second argument:
new object[] { progress, msg }
In the ReportOnProgress method this means that when Invoke is called, the ReportOnProgress method is called on the control’s correct thread with progress as the first argument, and msg as the second.
![]() Adding cancel support
Let us now examine the situation where the user has the option of canceling the long process. This requires the worker thread to test once in a while if the job should be stopped. So we add two booleans to the form:
// Some flags to enable cancelling the worker process private bool IsCancelling = false; private bool IsCancelled = false;The IsCancelled variable is set to true when the worker has cancelled, and the IsCancelling variable is set to true when the process should stop processing. We do this in the click event of the cancel button: private void buttonCancel_Click( object sender, EventArgs e )
{
IsCancelling = true;
}
This IsCancelling variable is tested in the worker job:
private void DoWork(int delay)
{
for (int progress = 0; progress < 100; progress += 1)
{
Thread.Sleep(delay);
ReportOnProgress(progress, string.Format("{0}% done", progress));
// Check for cancellation
if (IsCancelling)
{
IsCancelled = true;
return;
}
}
}
We also want to know if the process completed processing and for this the IsCancelled variable is used, which we test in the DoWorkComplete method.
private void DoWorkComplete(IAsyncResult workID)
{
EnableStartButton();
DoWorkDelegate worker = workID.AsyncState as DoWorkDelegate;
worker.EndInvoke(workID);
// Check if process was cancelled and report progress
if (IsCancelled)
{
ReportOnProgress(0, "Cancelled");
}
else
{
ReportOnProgress(100, "Done");
}
}![]() Exceptions
Now let us now examine the case where the worker thread throws an exception. To test this we add a check of the delay parameter to the beginning of the DoWork method:
private void DoWork(int delay)
{
// Check for invalid delays
if (delay < 0 || delay > 1000)
throw new ArgumentOutOfRangeException("delay", delay,
"Delay should be between 0 and 1000 ms");
...
}
Calling the DoWork method with a delay larger then 1000 will throw an ArgumentOutOfRangeException. What happens when the exception is not caught in the DoWork method? Normally the exception then travels up the thread’s call-stack until it is caught. But when you call a method using the BeginInvoke method of a delegate, the method is running on a worker thread. In this case the exception is actually saved and re-thrown when we call the EndInvoke method. This means that we should use a try – catch block around the call to EndInvoke to catch any exceptions:
try
{
// make sure to call EndInvoke to complete call
worker.EndInvoke(ticket);
}
catch (ArgumentException ex)
{
MessageBox.Show(ex.Message, "Test",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
This way each exception can be handled correctly in our application.
![]() ConclusionDo you think this is complicated? Well, I do. Read part 2 and learn about improvements available in the next release of the .NET Framework. ![]() Using the sampleThe sample presented with this article (download it) has been built with Beta1 of Visual Studio .NET 2005, and includes two forms, one with .NET 1.x code, and the other with .NET 2.0 code. ![]() References![]() About the author
| ||||||