Section 3.7. Create Safe Asynchronous Tasks


3.7. Create Safe Asynchronous Tasks

.NET provides extensive support for multithreaded applications without requiring you to manage the threads. In .NET 2.0 the support for safe asynchronous tasks is greatly enhanced with the addition of the BackgroundWorker object, which allows you to work safely in a second thread while maintaining user interface control (for updates and cancellation) in your main thread. There is no need to spawn threads explicitly or to manage resource locking.

3.7.1. How do I do that?

To demonstrate how this works, you'll create a small Windows application that provides a reminder after a specified number of seconds have passed, as shown in Figure 3-24.

Figure 3-24. The Reminder application


You'll write the program so that when you click Start, the Start button and text boxes are disabled, and the Cancel button is enabled. While the timer is ticking down, a progress bar will show what percentage of time has expired, as shown in Figure 3-25.

Figure 3-25. The Reminder application in progress


If you click Cancel, a cancel message is displayed. When the time has elapsed (or the timer is cancelled), the text is displayed, cancel is disabled, and the Start button and text boxes are enabled.

To make this work, you want to have two different threads: one for the user interface and one for the timer. Create a new project and name it BackgroundWorkerDemo. Drag a BackgroundWorker object (from the Components tab in the Toolbox) onto the form (it appears in the component tray at the bottom).

Set the WorkerReportsProgress and WorkerSupportsCancellation properties to true so that you will receive both Progress and Cancellation events. Next, click the Events button in the properties window (the one with the lightening bolt on it) to see the three events supported by the BackgroundWorker object. Double-click each event to have Visual Studio 2005 set up the event handlers, which will be named backgroundWorker1_DoWork, backgroundWorker1_ProgressChanged, and backgroundWorker1_RunWorkerCompleted.

You are ready to create your timer within the BackgroundWorker object's thread, as shown in Example 3-1.

Example 3-1. Using the BackgroundWorker object
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Threading; using System.Windows.Forms;         namespace BackgroundWorkerDemo {            partial class Form1 : Form    {           public class TimerInfo       {          public string reminderMessage;          public double numSeconds;          public TimerInfo(string msg, double secs)          {             this.reminderMessage = msg;             this.numSeconds = secs;          }       }               public Form1( )       {          InitializeComponent( );       }           private void btnStart_Click(object sender, EventArgs e)       {          TimerInfo info =             new TimerInfo(txtMessage.Text, Convert.ToDouble(this.txtSeconds.Text));              txtMessage.Text = string.Empty;          SetEnabled(true);          backgroundWorker1.RunWorkerAsync(info);       }       private void SetEnabled(bool isRunning)       {          this.btnStart.Enabled = !isRunning;          this.btnCancel.Enabled = isRunning;          this.txtSeconds.Enabled = !isRunning;          this.txtMessage.Enabled = !isRunning;          if (isRunning)          {             this.progressBar1.Value = 0;          }       }           private void backgroundWorker1_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.          BackgroundWorker worker = sender as BackgroundWorker;          TimerInfo ti = (TimerInfo)e.Argument;              DateTime startTime = DateTime.Now;          do          {             if (worker.CancellationPending)             {                e.Cancel = true;                return;             }                 System.TimeSpan duration = DateTime.Now.Subtract(startTime);             // chop down total seconds to an int             double totalSeconds = duration.TotalSeconds;             double percent = totalSeconds / ti.numSeconds * 100.0;             int progressPercent = (int)percent;             progressPercent = progressPercent > 100 ? 100 : progressPercent;             worker.ReportProgress(progressPercent);             Thread.Sleep(20);          } while (DateTime.Now < startTime.AddSeconds(ti.numSeconds));              // e.Result is available to the           // RunWorkerCompleted event handler.          e.Result = ti.reminderMessage;       }           //Update the progress bar       private void backgroundWorker1_ProgressChanged(            object sender, ProgressChangedEventArgs e)       {          this.progressBar1.Value = e.ProgressPercentage;       }           // handle work completed (update the ui)       private void backgroundWorker1_RunWorkerCompleted(          object sender, RunWorkerCompletedEventArgs e)       {          // Handle the case where an exception was thrown.          if (e.Error != null)          {             MessageBox.Show(e.Error.Message);          }          else if (e.Cancelled)          {             txtMessage.Text = "CANCELLED. ";             SetEnabled(false);          }          else          {             // the operation succeeded.             txtMessage.Text = e.Result.ToString( );             SetEnabled(false);          }       }           // cancel the thread and post cancellation message       private void btnCancel_Click(object sender, EventArgs e)       {          this.backgroundWorker1.CancelAsync( );          SetEnabled(false);           }        } }

When the user clicks Start, the btnStart_Click event handler creates an instance of TimerInfo, a simple class I created to encapsulate the user's message and the number of seconds to count down. Then the Click event handler blanks out the message window, calls SetEnabled to disable the Start button and the text boxes, and calls the RunWorkerAsync method on the BackgroundWorker object, passing in the TimerInfo object as an argument.

Your call to RunWorkerAsync raises the DoWork event, which will be handled by your backgroundWorker1_DoWork event handler. That handler receives two arguments. The first is the instance of BackgroundWorker that you created, backgroundWorker1, and the second is an instance of DoWorkEventArgs.

The DoWorkEventArgs object has two useful properties: Argument, which is the TimerInfo object you passed in when you called RunWorkerAsync, and Result, which is how you pass results to the RunWorkerCompleted event handler.

Inside your event handler, backgroundWorker1_DoWork, you retrieve the BackgroundWorker object and the TimerInfo object:

BackgroundWorker worker = sender as BackgroundWorker; TimerInfo ti = (TimerInfo)e.Argument;

Then you begin a loop that will continue until the time has expired:

do {    //... }while (DateTime.Now < startTime.AddSeconds(ti.numSeconds));

Each time through the loop you check to see if the user has clicked Cancel. If not, you compute the percentage of time elapsed, and you call ReportProgress on BackgroundWorker, which fires the ProgressChanged event (which is handled by your backgroundWorker1_ProgressChanged event handler). Then you sleep 20 milliseconds, and start over.

The ProgressChanged event handler updates the progress bar based on the progress percentage, which is passed in as a property of ProgressChangedEventArgs.

When the time expires, you set the Result property to the reminder message and exit DoWork:

e.Result = ti.reminderMessage;

Exiting DoWork automatically fires the RunWorkerCompleted event, which is handled by your backgroundWorker1_RunWorkerCompleted event handler.

In the backgroundWorker1_RunWorkerCompleted event handler, first check to see if an exception was thrown (in which case e.Error will be non-null):

if (e.Error != null) {    MessageBox.Show(e.Error.Message); }

Then you check to see if you got to this handler because the work was cancelled (in which case e.Cancelled will be TRue):

else if (e.Cancelled) {    txtMessage.Text = "CANCELLED. ";    SetEnabled(false); }

Finally, if neither of those tests passes, you got here because the work is done and you can set the text of the message box to the value you reclaim from e.Result:

else {    txtMessage.Text = e.Result.ToString( ); }

3.7.2. What about...

...using BackgroundWorker for other lengthy processes, such as downloading a file or data over the Internet?

This is an excellent solution for types that don't already provide asynchronous functionality (for example, overlapped I/O).

3.7.3. Where can I learn more?

BackgroundWorker is a very hot item in .NET 2.0, and a number of good articles are available on this subject on the Internet, including an excellent blog entry by Roy Osherove at http://weblogs.asp.net/rosherove/archive/2004/06/16/156948.aspx. Also, the MSDN includes an extensive write-up on BackgroundWorker.



Visual C# 2005(c) A Developer's Notebook
Visual C# 2005: A Developers Notebook
ISBN: 059600799X
EAN: 2147483647
Year: 2006
Pages: 95
Authors: Jesse Liberty

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net