Frequently with multithreaded operations, you not only want to be notified when the thread completes, but you also want the method to provide an update on the status of the operation. Often, users want to be able to cancel long-running tasks. The .NET Framework 2.0 includes a BackgroundWorker class for programming this type of pattern. Listing 16.8 is an example of this pattern. It calculates pi to the number of digits specified. Listing 16.8. Using the Background Worker Pattern using System; using System.Threading; using System.ComponentModel; using System.Text; public class PiCalculator { public static BackgroundWorker calculationWorker = new BackgroundWorker(); public static AutoResetEvent resetEvent = new AutoResetEvent(false); public static void Main() { int digitCount; Console.Write( "Enter the number of digits to calculate:"); if (int.TryParse(Console.ReadLine(), out digitCount)) { Console.WriteLine("ENTER to cancel"); // C# 2.0 Syntax for registering delegates calculationWorker.DoWork += CalculatePi; // Register the ProgressChanged callback calculationWorker.ProgressChanged+= UpdateDisplayWithMoreDigits; calculationWorker.WorkerReportsProgress = true; // Register a callback for when the // calculation completes calculationWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(Complete); calculationWorker.WorkerSupportsCancellation = true; // Begin calculating pi for up to digitCount digits calculationWorker.RunWorkerAsync(digitCount); Console.ReadLine(); // If cancel is called after the calculation // has completed it doesn't matter. calculationWorker.CancelAsync(); // Wait for Complete() to run. resetEvent.WaitOne(); } else { Console.WriteLine( "The value entered is an invalid integer."); } } private static void CalculatePi( object sender, DoWorkEventArgs eventArgs) { int digits = (int)eventArgs.Argument; StringBuilder pi = new StringBuilder("3.", digits + 2); calculationWorker.ReportProgress(0, pi.ToString()); // Calculate rest of pi, if required if (digits > 0) { for (int i = 0; i < digits; i += 9) { // Calculate next i decimal places int nextDigit = PiDigitCalculator.StartingAt( i + 1); int digitCount = Math.Min(digits - i, 9); string ds = string.Format("{0:D9}", nextDigit); pi.Append(ds.Substring(0, digitCount)); // Show current progress calculationWorker.ReportProgress( 0, ds.Substring(0, digitCount)); // Check for cancellation if (calculationWorker.CancellationPending) { // Need to set Cancel if you need to // distinguish how a worker thread completed // i.e., by checking // RunWorkerCompletedEventArgs.Cancelled eventArgs.Cancel = true; break; } } } eventArgs.Result = pi.ToString(); } private static void UpdateDisplayWithMoreDigits( object sender, ProgressChangedEventArgs eventArgs) { string digits = (string)eventArgs.UserState; Console.Write(digits); } static void Complete( object sender, RunWorkerCompletedEventArgs eventArgs) { // ... } } ----------------------------------------------------------------------------- ----------------------------------------------------------------------------- public class PiDigitCalculator { // ... } | Establishing the Pattern The process of hooking up the background worker pattern is as follows. Register the long-running method with the BackgroundWorker.DoWork event. In this example, the long-running task is the call to CalculatePi(). To receive progress or status notifications, hook up a listener to BackgroundWorker.ProgressChanged and set BackgroundWorker.WorkerReportsProgress to true. In Listing 16.8, the UpdateDisplayWithMoreDigits() method takes care of updating the display as more digits become available. Register a method (Complete()) with the BackgroundWorker.RunWorkerCompleted event. Assign the WorkerSupportsCancellation property to support cancellation. Once this property is assigned the value true, a call to BackgroundWorker.CancelAsync will set the DoWorkEventArgs.CancellationPending flag. Within the DoWork-provided method (CalculatePi()), check the DoWorkEventArgs.CancellationPending property and exit the method when it is true. Once everything is set up, you can start the work by calling BackgroundWorker.RunWorkerAsync() and providing a state parameter that is passed to the specified DoWork() method. When it is broken down into steps, the background worker pattern is relatively easy to follow and provides the advantage over the asynchronous results pattern of a mechanism for cancellation and progress notification. The drawback is that you cannot use it arbitrarily on any method. Instead, the DoWork() method has to conform to a System.ComponentModel. DoWorkEventHandler delegate, which takes arguments of type object and DoWorkEventArgs. If this isn't the case, then a wrapper function is required. The cancellation- and progress-related methods also require specific signatures, but these are in control of the programmer setting up the background worker pattern. Exception Handling If an unhandled exception occurs while the background worker thread is executing, then the RunWorkerCompletedEventArgs parameter of the RunWorkerCompleted' delegate (Completed's eventArgs) will have an Error property set with the exception. As a result, checking the Error property within the RunWorkerCompleted callback in Listing 16.9 provides a means of handling the exception. Listing 16.9. Handling Unhandled Exceptions from the Worker Thread // ... static void Complete( object sender, RunWorkerCompletedEventArgs eventArgs) { Console.WriteLine(); if (eventArgs.Cancelled) { Console.WriteLine("Cancelled"); } else if (eventArgs.Error!= null) { // IMPORTANT: check error to retrieve any exceptions. Console.WriteLine( "ERROR: {0}", eventArgs.Error.Message); } else { Console.WriteLine("Finished"); } resetEvent.Set(); } // ... | It is important that the code check eventArgs.Error inside the RunWorkerCompleted callback. Otherwise, the exception will go undetected; it won't even be reported to AppDomain. |