Background Processing in Windows Forms Applications (Part 2)

Improvements in the .NET Framework 2.0

Peter Himschoot
U2U - Brussels

Applies to:
  • Microsoft Visual Studio .NET 2003
  • Microsoft Visual Studio .NET 2005
  • .NET Framework version 1.x and 2.0
  • System.Windows.Forms
  • System.Threading
Summary:

The second of two companion articles. In Part 1 of this article we discussed how to create and execute background processes in a WinForms application with .NET 1.x. Part 2 shows improvements made in the .NET Framework 2.0 with the BackgroundWorker class.

Downloads:
Contents:

The BackgroundWorker class

Version 2.0 of the .NET Framework simplifies the steps you need to make to implement background processes greatly through the BackgroundWorker class. The BackgroundWorker class has a couple of events which call our code, just like controls call our code when events happen to them. No need to worry if the code is running in the correct thread, because the BackgroundWorker class will always trigger the events on the correct thread. With the background worker class we get the same programming model we use for controls! To illustrate this (and compare with the previous version) we again create a form like the one in figure 1:



Then we added a BackgroundWorker from the ToolBox (beneath the Components tab), which looks in the designer like figure 2:



The backgroundworker object has three events:



The DoWork event we implement as follows:

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
  BackgroundWorker worker = sender as BackgroundWorker;
  int delay = (int)e.Argument;
  for (int progress = 0; progress < 100; progress += 1)
  {
    Thread.Sleep(delay);
    worker.ReportProgress(progress, 
      string.Format("{0}% done", progress));
  }
  worker.ReportProgress(100, "Done");
}
This method retrieves some values from the event’s DoWorkEventArgs argument and then emulates a long running job. We use the worker’s ReportProgress method to report on the job’s progress.

The DoWork event is raised by calling the backgroundworker’s RunWorkerAsync method; we do this when the Start button is clicked:

private void buttonStart_Click(object sender, EventArgs e)
{
  // Disable the button
  buttonStart.Enabled = false;
  // Make async call using backgroundWorker
  backgroundWorker.RunWorkerAsync(100);
}
First the start button is disabled so the user can’t start two processes at the same time, and then the backgroundWorker’s RunWorkerAsync method is called with one argument. This argument gets passed to the DoWork event as the DoEventArgs.Argument property. The RunWorkerAsync method has two overloads, one without arguments and another one with one argument of type object. If you want to pass more than one value, pass it as some custom class object or as an array object.


Showing Progress

If we want the worker process to report on progress we need to set the WorkerReportsProgress property to true (otherwise the BackgroundWorker’s ReportProgress method will throw an InvalidOperationException) and implement the ProgressChanged event:

private void backgroundWorker_ProgressChanged(object sender, 
  ProgressChangedEventArgs e)
{
  progressBar.Value = e.ProgressPercentage;
  labelProgress.Text = e.UserState as string;
}
This event is triggered when the worker calls the ReportProgress method. ReportProgress has a couple of overloads and we use the one with two arguments. The first argument is a percentage value, and the second argument can be any object. We use this second argument to pass a message to the ProgressChanged event, which gets passed as the ProgressChangedEventArgs.UserState property.

The ProgressChanged event always triggers on the main thread where it is safe to change your control’s properties, so we don’t need to go and check if we need to use the control’s Invoke method. Nice and easy.

When the process has completed processing the RunWorkerCompleted event is triggered:

private void backgroundWorker_RunWorkerCompleted(object sender,
  RunWorkerCompletedEventArgs e)
{
  buttonStart.Enabled = true;
  labelProgress.Text = "Done";
}
Again this event triggers on a thread where it is safe to modify controls.


Adding cancel support

Now let us see how the worker can be cancelled. First we need to set the worker’s WorkerSupportsCancellation property to true (see figure 4).



To cancel the job we call the worker’s CancelAsync method, which will set the CancellationPending flag. We do this when the user clicks the Cancel button:

private void buttonCancel_Click(object sender, EventArgs e)
{
  backgroundWorker.CancelAsync();
}
For your information, if you call the CancelAsync method on a BackgroundWorker object with its WorkerSupportsCalcellation property set to false, this method will throw an InvalidOperationException.

We check the CancellationPending flag in the DoWork event. If it has been set to true, we stop processing and set the DoWorkEventArgs.Cancel property to true:

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
  BackgroundWorker worker = sender as BackgroundWorker;
  int delay = (int)e.Argument;

  for (int progress = 0; progress < 100; progress += 1)
  {
    Thread.Sleep(delay);
    worker.ReportProgress(progress, 
      string.Format("{0}% done", progress));
    // Check for cancellation
    if (worker.CancellationPending)
    {
      e.Cancel = true;
      worker.ReportProgress(0, "Cancelled");
      return;
    }
  }
  worker.ReportProgress(100, "Done");
}
Because the job could have been cancelled we add a check for this in the RunWorkerCompleted event:

private void backgroundWorker_RunWorkerCompleted(object sender,
  RunWorkerCompletedEventArgs e)
{
  buttonStart.Enabled = true;
  if (e.Cancelled)
  {
    labelProgress.Text = "Cancelled";
  }
  else
  {
    labelProgress.Text = "Done";
  }
}
Adding cancellation support using the BackgroundWorker class is also nice and easy.


Exceptions

How does the BackgroundWorker handle exceptions thrown in the worker’s thread? We will test this in the same way as we did for .NET 1.1 by adding a check in the beginning of the DoJob method which may throw an exception:

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
  BackgroundWorker worker = sender as BackgroundWorker;
  int delay = (int)e.Argument;

  // Check for invalid delays
  if (delay < 0 || delay > 1000)
    throw new ArgumentOutOfRangeException("delay", delay,
      "Delay should be between 0 and 1000 ms");
  ...
}
When an exception is thrown on the worker’s thread, the backgroundworker will catch it and pass it as the RunWorkerCompletedEventArgs.Error property:

private void backgroundWorker_RunWorkerCompleted(object sender,
  RunWorkerCompletedEventArgs e)
{
  buttonStart.Enabled = true;
  // Check for error
  if (e.Error != null)
  {
    MessageBox.Show(e.Error.Message, "Test",
      MessageBoxButtons.OK, MessageBoxIcon.Error);
  }
  ...
}




Conclusion

The BackgroundWorker class makes developing background processes in your WinForms application a lot easier, allowing you to update controls without the use of the Invoke method. However, you still have to use Invoke to update controls in the DoWork event; passing reference-objects between threads with the UserState property still requires the object to be thread-safe should you update it on both threads. So do not under-estimate the impact of multi-threaded programming!


Using the sample

The 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




Peter Himschoot is an architect and trainer for U2U, specializing in .NET, Visual Studio Team System, BizTalk Server and .NET 3.0 (aka WinFx).
He is also a Microsoft Regional Director.

You can reach him at peter@u2u.net.

Or read his blog http://blog.u2u.info/DottextWeb/peter/ .

Favorite T-shirt: >> Code is poetry! <<


U2U Training and Consultancy Services is a Microsoft .NET competence center located in Belgium, to learn more please visit www.u2u.be .


			Peter Himschoot
			U2U - Brussels