Safe, Even Simpler Multithreading in Windows Forms 2.0

 

Windows Forms applications sometimes need to execute a long-running or background activity, often on behalf of the user. Such tasks are often secondary, and should not prevent users from continuing on with their primary work, such as searching for data. In these cases, a Windows Forms application, which operates on a main or “UI” thread, will create a separate worker thread for the secondary code to execute on. The concept is simple, but until Windows Forms 2.0, required a fair degree of work to implement. Much of this work involves dealing with ensuring safe multithreading, which means avoiding writing code that directly manipulates controls on the UI thread from the worker thread. Of course, Windows Forms applications are visual applications and, when a long-running process is initiated by a user, they need to see progress and completion information, and should probably be able to cancel the activity at any stage.

 

Calc Pi

This implies that information must be passed between UI and worker threads, also safely. Chris Sells shows how to implement a safe, simple multithreading solution for Windows Forms, although it takes a three-part series (see References section), which is a testament to the effort involved. At this point, I suggest you read his series if you are unfamiliar with the material as this discussion builds on the Pi Calculator sample built up in that series and shown in Figure 1.

 

Figure 1: Chris Sells’ Pi Calculator

 

Chris’s solution is great (and recommended for pre-Windows Forms 2.0), but it does require you to write both the code you need asynchronously processed, as well as the infrastructural code to make it work. Ultimately, this means a lot of effort for you, the developer. You’ll hear it from me now and you’ll hear it from me over and over again: productivity is a major theme of Windows Forms 2.0. My favorite example of this goal to date is the BackgroundWorker, which repackages the multithreading infrastructure (not unlike the solution Chris offers) into a single, reusable component that you can drag onto your forms from the Toolbox. Importantly, BackgroundWorker relieves you from the burden of writing (and re-writing) the safe, multithreading code, allowing you to simply focus on your functional problem. The rest of this discussion highlights the benefits by reimplementing the Pi Calculator (CalcPi) for the BackgroundWorker, resulting in less time, simpler code and more productivity.

 

Kicking Off An Asynchronous Activity

Visually, an asynchronous activity is initiated by the user through some UI mechanism. CalcPi’s mechanism is to simply click the Calc button, which then becomes the Cancel button during processing. Once you have placed a BackgroundWorker component on your form, you can programmatically initiate async processing by calling the BackgroundWorker’s RunWorkerAsync method, to create a worker thread and begin asynchronous code execution, shown here:

 

public partial class AsynchCalcPiForm : System.Windows.Forms.Form {

  ...

  private void calcButton_Click(object sender, EventArgs e) {

   

    // Doubling up duty as Cancel button     

    this.calcButton.Text = "Cancel";

    this.piCalcWorker.RunWorkerAsync(this.digitsUpDown.Value);

    this.progressBar.Maximum =

      Convert.ToInt32(this.digitsUpDown.Value);

  }

  ...

}

 

If your async code begins in a state determined by data captured on the UI thread, you can package it up into a simple or complex type as needed and send it to the worker thread inside the Arguments parameter to the RunWorkerAsync overload as I do with the digitsUpDown value. If this isn’t required, simply call RunWorkerAsync without providing any arguments. The async code itself needs to be placed in a BackgroundWorker-managed location that executes on the worker thread. You do this by handling the DoWork event:

 

public partial class AsynchCalcPiForm : System.Windows.Forms.Form {

  ...

  private void calcButton_Click(object sender, EventArgs e) {...}

  ...

  private void piCalcWorker_DoWork(object sender, DoWorkEventArgs e) {

   

    // This method will run on a thread other than the UI thread.

    // Be sure not to manipulate any Windows Forms controls created

    // on the UI thread from this method.

     

    // Show initial progress

    int digits = int.Parse(e.Argument.ToString());

    StringBuilder pi = new StringBuilder("3", digits + 2);

    CalcPiUserState userState =

      new CalcPiUserState(pi.ToString(), digits, 0);

    this.piCalcWorker.ReportProgress(0, userState);

 

    // Calculate rest of pi, if required

    ...

  }

  ...

}

 

Of course, one rule is always consistent: you must not directly manipulate controls on the UI thread from DoWork, aka the worker thread. Just so you know, the Windows Forms designer generates appropriate comments into the generated DoWork event handler, located at the top of the handler in the previous code.

 

Reporting Progress

Of course, visually-oriented long-running async operations often need to manipulate controls on the UI thread, such as providing regular progress information during the course of async processing. As demonstrated in Chris’ series, this can be achieved through message passing built around delegates by first calling the appropriate delegate from the worker thread and passing data to an appropriate handler on the UI thread. The BackgroundWorker uses the same process, albeit without you writing the goop. First, you call ReportProgress from DoWork on the worker thread and pass either a percentage completion argument and/or a custom object with any further, more complex information you need. If you’ve handled it, this data is sent to the ProgressChanged event handler that executes on the UI thread. From there, you can unpackage and process your progress data, updating any and all controls you like with complete safety. CalcPi creates a custom object, CalcPiUserState, to store both progress information and a calculated pi figure, which is passed back to the UI thread and subsequently used to update the progress bar and the pi text box, shown here:

 

public partial class AsynchCalcPiForm : System.Windows.Forms.Form {

  ...

  private void calcButton_Click(object sender, EventArgs e) {...}

  ...

  private void piCalcWorker_DoWork(object sender, DoWorkEventArgs e) {

   

    // This method will run on a thread other than the UI thread.

    // Be sure not to manipulate any Windows Forms controls created

    // on the UI thread from this method.

     

    // Show initial progress

    ...

 

    // Calculate rest of pi, if required

    if(digits > 0) {

      pi.Append(".");

 

      for(int i = 0; i < digits; i += 9) {

       

      // Calculate next i decimal places

      ...

 

      // Show current progress

      userState.Pi = pi.ToString();

      userState.DigitsSoFar = i + digitCount;

      this.piCalcWorker.ReportProgress(0, userState);

    }

  }

  ...

  private void piCalcWorker_ProgressChanged(

    object sender,

    ProgressChangedEventArgs e) {

 

    CalcPiUserState userToken = (CalcPiUserState)e.UserState;

    this.progressBar.Value = userToken.DigitsSoFar;

    this.piText.Text = userToken.Pi;     

  }

  ...

}

 

Handling Completion

When async processing completes, you may need to perform cleanup code and/or update the UI. For example, CalcPi’s Cancel button needs to change back to a “Calc” button and the progress bar needs to be reset. Unlike reporting progress, normal completion of execution results in the worker thread packing up and going home. It doesn’t leave without saying goodbye, though, by firing a RunWorkerCompleted event that you handle on the UI thread. If you need to pass custom completion information to this event, you can set DoWorkEventArgs’ Result property from DoWork prior to completion of code execution, or be happy with BackgroundWorker’s default value, System.InvalidOperationException. As an example, I send a simple string value back, although you could use a complex and/or custom type if needed. Here’s how Calc Pi cleans up:

 

public partial class AsynchCalcPiForm : System.Windows.Forms.Form {

  ...

  private void calcButton_Click(object sender, EventArgs e) {...}

  ...

  private void piCalcWorker_DoWork(object sender, DoWorkEventArgs e) {

   

    // This method will run on a thread other than the UI thread.

    // Be sure not to manipulate any Windows Forms controls created

    // on the UI thread from this method.

     

    // Show initial progress

    ...

 

    // Calculate rest of pi, if required

    if(digits > 0) {

      pi.Append(".");

 

      for(int i = 0; i < digits; i += 9) {

       

      // Calculate next i decimal places

      ...

 

      // Show current progress

      ...

    }

    e.Result = "Finished Normally.";

  }

  ...

  private void piCalcWorker_RunWorkerCompleted(

    object sender,

    RunWorkerCompletedEventArgs e) {

    this.calcButton.Text = "Calc";

    this.piText.Text = "";

    this.progressBar.Value = 0;

  }

  ...

}

 

Cancelling A Long-Running Asynchronous Activity

Long-running operations may run longer than an end-user’s patience allows, such as calculating Pi to 3000000 decimal places. In these cases, a UI typically offers some type of cancellation mechanism. In the case of CalcPi, this is simply pressing the Cancel button. BackgroundWorker helps you out by letting you signal from the UI thread to the worker thread that a cancellation has been requested, which is achieved by calling CancelAsync()on the UI thread. This information is transmitted across the UI-worker thread divide to DoWork where you can check it via DoWorkEventArgs’ CancellationPending property and handle appropriately:

 

public partial class AsynchCalcPiForm : System.Windows.Forms.Form {

  ...

  private void calcButton_Click(object sender, EventArgs e) {...}

  ...

  private void piCalcWorker_DoWork(object sender, DoWorkEventArgs e) {

   

    // This method will run on a thread other than the UI thread.

    // Be sure not to manipulate any Windows Forms controls created

    // on the UI thread from this method.

     

    // Show initial progress

    ...

 

    // Calculate rest of pi, if required

    if(digits > 0) {

      pi.Append(".");

 

      for(int i = 0; i < digits; i += 9) {

       

      // Calculate next i decimal places

      ...

 

      // Show current progress

      ...

 

      // Check for cancellation

      if(this.piCalcWorker.CancellationPending) {

        e.Cancel = true;

        break;

       }

    }

    ...

  }

  ...

  private void piCalcWorker_RunWorkerCompleted(

    object sender,

    RunWorkerCompletedEventArgs e) {

    ...

    if( !e.Cancelled ) this.Text = e.Result.ToString();

 

  }

  ...

}

 

Notice that I set DoWorkEventArgs’ Cancel property to true. This is required if you need to determine what state the worker thread completed in from RunWorkerCompleted, which I use defensively to check for results data, that is to avoid processing a System.InvalidOperationException object return in the RunWorkerCompletedEventArgs Result property. The sum of our efforts is shown in Figure 2.

 

Figure 2: Completed Pi Calculator for Windows Forms 2.0

 

Visual vs Non-Visual Asynchronous Tasks

As often as not, async processing does not need to be a visual affair and, subsequently, does not require visual progress updates or cancellation. In these cases, you can instruct the BackgroundWorker to not fire ProgressChanged or process cancellation by ensuring WorkerSupportsProgress and WorkerSupportsCancellation respectively are set to False, which is the default. Note that BackgroundWorker resides in System.ComponentModel, not System.Windows.Forms, which is due to the fact that async tasks can be both visual and non-visual.

 

Where Are We?

That pretty much sums up the basic usage of the BackgroundWorker component for Windows Forms 2.0 applications. BackgroundWorker turns out to be a major time-saver by taking on the burden of providing a UI-safe multithreading infrastructure that you can leverage through a simplified and familiar method/event style interface. BackgroundWorker also nicely caters for message-passing of custom progress and completion data and cancellation logic to let you focus on the problem at hand. Simply put, this saves you effort, code and time and, as such, is a nice tool to have in your utility belt.

 

Code Sample

CalcPi For Windows Forms 2.0

 

Acknowledgements

Thanks to Chris Sells for a great series, one outrageously ripped off image. Thanks to Mr System.ComponentModel.BackgroundWorker at Microsoft, whoever you are.

 

References

Safe, Simple Multithreading in Windows Forms, Part 1 (http://msdn.microsoft.com/library/en-us/dnforms/html/winforms06112002.asp)

 

Safe, Simple Multithreading in Windows Forms, Part 2 (http://msdn.microsoft.com/library/en-us/dnforms/html/winforms08162002.asp)

 

Safe, Simple Multithreading in Windows Forms, Part 3 (http://msdn.microsoft.com/library/en-us/dnforms/html/winforms01232003.asp)