Understanding C#: BackgroundWorker tutorial for multithreaded GUIs

By Andrew Stellman
June 28, 2010 | Comments: 1

Someone once told me that he could tell a form was built by a novice C# developer if it stopped responding when he pressed a button. I'm not 100% sure I agree, but I definitely think that an intermediate or advanced C# developer should be able to build a form that stays responsive even when the program is doing something CPU intensive. Luckily, C# and .NET give us a simple way to do that using the BackgroundWorker class. This post walks you through a simple BackgroundWorker example that shows you how to use it to let your form report progress while doing work in the background on another thread.

Head First C# Cover

Throughout Head First C#, Jennifer Greene and I show a few ways that you can make your programs do more than one thing at once. In Chapter 2, we used the Application.DoEvents() method to let your form respond to button clicks while still in a loop. But that's not a good solution (for a bunch of reasons we didn't get into), so we showed you a much better solution in Chapter 4: using a timer to trigger an event at a regular interval. But even when you know how to use timers, there will be times when your program will still be busy and will become nonresponsive. Luckily, .NET gives us the BackgroundWorker class. It's a really useful component that makes it very easy to let your program do work in the background. And it's really easy to put together a very simple example to see how it works.

(If you're curious about how we introduce Timer in chapter 4, you can see it here this Typing Game project [PDF]. It's a neat little project -- you should definitely check it out!)

Not only is BackgroundWorker an effective way to get your program to do more than one thing at a time, it's also a very good entry for intermediate developers to start experimenting with threading. It's very easy to introduce serious bugs in your program if you use System.Threading and don't really know what you're doing. But if you use BackgroundWorker properly, you can make your C# program multithreaded without running into those nasty little traps.

Build a simple project to try out BackgroundWorker

Here's a simple Windows Forms Application project that you can use to experiment with BackgroundWorker. Start by building the form in the picture below. You'll need to drag a CheckBox onto it (name it useBackgroundWorkerCheckbox), two buttons (named goButton and cancelButton) and a ProgressBar (named progressBar1). Then drag a BackgroundWorker onto the form. It'll show up in the gray box on the bottom of the designer.

Screenshot - BackgroundWorker example.png

Click on BackgroundWorker icon in the grey area at the bottom of the form designer to select it. Keep its name backgroundWorker1, and set its WorkerReportsProgress and WorkerSupportsCancellation properties to true>. Then go to the Events page in the Properties window (by clicking on the lightning-bolt icon). It's got three events: DoWork, ProgressChanged, and RunWorkerCompleted. Double-click on each of them to add an event handler for each event.

Screenshot - BackgroundWorker events.png

Next you'll need to add a class called Guy to your project. It's the same class from my post about XML Comments. You can find the source to the Guy class here.

Once you've added the Guy class to your project, it's time to add the code for the form. A few things to note about it -- if you want to get a handle on how BackgroundWorker works, see if you can find where each of these things shows up in the code:

  • We need a way to "freeze up" the form. The WasteCPUCycles() method does a whole bunch of mathematical calculations to tie up the CPU for 100 milliseconds, and then it returns.
  • When the user clicks on the Go! button, the event handler checks to see if the "Use BackgroundWorker" checkbox is checked. If it isn't, the form wastes CPU cycles for 10 seconds. If it is, the form calls the BackgroundWorker's RunWorkerAsync() method to tell it to start doing its work in the background.
  • When the BackgroundWorker's RunWorkerAsync() method is called, it starts running its DoWork event handler method in the background. Notice how it's still calling the same WasteCPUCycles() method to waste CPU cycles. It's also calling the ReportProgress() method to report a percent complete (a number from 0 to 100).
  • Take a look at how we call the BackgroundWorker object's RunWorkerAsync() method. When you tell a BackgroundWorker to start work, you can give it an argument. In this case, we're passing it a Guy object (which is why you added it to your project). The argument is passed in as e.Argument to the BackgroundWorker's DoWork event handler. Also, note how we're using the null coalescing ?? operator -- that's a really useful tool if you've got a reference that might be null.
  • BackgroundWorker has a method called CancelAsync() that you can call when you want to tell the BackgroundWorker to cancel. The BackgroundWorker's CancellationPending property checks if the BackgroundWorker's CancelAsync() method was called.

Here's the code for the form.

/// <summary>
/// Waste CPU cycles causing the program to slow down by doing calculations for 100ms
/// </summary>
private void WasteCPUCycles() {
     DateTime startTime = DateTime.Now;
     double value = Math.E;
     while (DateTime.Now < startTime.AddMilliseconds(100)) {
          value /= Math.PI;
          value *= Math.Sqrt(2);
      }
}
/// <summary>
/// Clicking the Go button starts wasting CPU cycles for 10 seconds
/// </summary>
private void goButton_Click(object sender, EventArgs e) {
     goButton.Enabled = false;
     if (!useBackgroundWorkerCheckbox.Checked) {
          // If we're not using the background worker, just start wasting CPU cycles
          for (int i = 1; i <= 100; i++) {
               WasteCPUCycles();
               progressBar1.Value = i;
           }
          goButton.Enabled = true;
      } else {
          // If the form's using the background worker, it enables the Cancel button.
          cancelButton.Enabled = true;
  
          // If we are using the background worker, use its RunWorkerAsync()
          // to tell it to start its work
          backgroundWorker1.RunWorkerAsync(new Guy("Bob", 37, 146));
      }
}
/// <summary>
/// The BackgroundWorker object runs its DoWork event handler in the background
/// </summary>
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
     // The e.Argument property returns the argument that was passed to RunWorkerAsync()
     Console.WriteLine("Background worker argument: " + (e.Argument ?? "null"));
 
     // Start wasting CPU cycles
     for (int i = 1; i <= 100; i++) {
          WasteCPUCycles();
          // Use the BackgroundWorker.ReportProgress method to report the % complete
          backgroundWorker1.ReportProgress(i);
  
          // If the BackgroundWorker.CancellationPending property is true, cancel
          if (backgroundWorker1.CancellationPending) {
               Console.WriteLine("Cancelled");
               break;
           }
      }
}

/*
 * The BackgroundWorker only fires its ProgressChanged
 * and RunWorkerCompleted events if its WorkerReportsProgress
 * and WorkerSupportsCancellation properties are true. 
 */

/// <summary>
/// BackgroundWorker fires its ProgressChanged event when the worker thread reports progress
/// </summary>
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) {
     progressBar1.Value = e.ProgressPercentage;
}

/*
 * When the DoWork event handler calls the ProgressChanged() method, it 
 * causes the BackgroundWorker to raise its ProgressChanged event. and 
 * set e.ProgressPercentage to the percent passed to it.
 */

/// <summary>
/// BackgroundWorker fires its RunWorkerCompleted event when its work is done (or cancelled)
/// </summary>
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
     // When the work is complete, the RunWorkerCompleted event handler 
     // re-enables the Go! button and disables the Cancel button.
     goButton.Enabled = true;
     cancelButton.Enabled = false;
}

/// <summary>
/// When the user clicks Cancel, call BackgroundWorker.CancelAsync() to send it a cancel message
/// </summary>
private void cancelButton_Click(object sender, EventArgs e) {
     // If the user clicks Cancel, it calls the BackgroundWorker's CancelAsync()
     // method to give it the message to cancel.
     backgroundWorker1.CancelAsync();
}

Once you've got your form working, run the program. It's easy to see how BackgroundWorker makes your program much more responsive. Here's what to do next:

  1. Make sure the "Use BackgroundWorker" checkbox isn't checked, then click the Go! button. You'll see the progress bar start to fill up. Try to drag the form around--you can't. The form's all locked up. If you're lucky, it might jump a bit as it eventually responds to your mouse drag.
  2. In this program, checking the BackgroundWorker box causes it to write output to the console. Select View >> Output from the menu to show the Output window so you can see the output.
  3. When it's done, check the "Use BackgroundWorker" checkbox and click the Go! button again. This time, the form is perfectly responsive. You can move it around and even close it, and there's no delay. When it finishes, it uses the RunWorkerCompleted method to re-enable the buttons.
  4. While the program is running (using BackgroundWorker), click the Cancel button. It will update its CancellationPending property, which will tell the program to cancel and exit the loop.

A threading pitfall

Are you wondering why you need to use the ReportProgress() method rather than setting the ProgressBar's Value property directly? Try it out. Add the following line to the DoWork event handler:

progressBar1.Value = 10;

Then run your program again. As soon as it hits that line, it throws an InvalidOperationException with this message: "Cross-thread operation not valid: Control 'progressBar1' accessed from a thread other than the thread it was created on." The reason it throws that exception is that BackgroundWorker starts a separate thread and executes the DoWork method on it. So there are two threads: the GUI thread that's running the form and the background thread. One of the .NET threading rules is that only the GUI thread can update form controls; otherwise, that exception is thrown.

This is just one of the many threading pitfalls that can trap a new developer--that's why we didn't talk about threading anywhere in this book. If you're looking to get started with threads, we highly recommend Joe Albahari's excellent e-book about threading in C# and .NET

Andrew Stellman is the author of Head First C# and other books from O'Reilly. You can read more from Andrew at Building Better Software.


You might also be interested in:

1 Comment

thnx

News Topics

Recommended for You

Got a Question?