indows Forms applications often require some sort of asynchronous invocation option. You rarely want to block the user interface while a lengthy operation executes in the background. Windows Forms pose a set of complicated design and implementation issues when it comes to asynchronous method invocation and multithreading due to the underlying Windows messages processing.
Although .NET does provide a uniform asynchronous invocation mechanism (described in my article, “Asynchronous .NET Programming,” CoDe Magazine, May 2003) you cannot apply it as-is in a Windows Forms application. To address this problem, the next version of .NET (version 2.0, code-name Whidbey) provides a new component designed to ease the task of developing asynchronous Windows Forms applications. This article starts by describing the current asynchronous programming model available to Windows Forms developers. Then, not only does this article describe the Whidbey solution, it also provides a .NET 1.1 implementation of the solution so that you can take advantage of this superior programming model today and ease the transition into Windows Forms 2.0 in the future.
Windows Forms 1.1 and Asynchronous Execution
Developers commonly use a delegate to invoke an asynchronous method call in .NET. Each delegate provides a BeginInvoke() and EndInvoke() method. To invoke a method asynchronously, wrap it in a delegate and call BeginInvoke() on the delegate. The delegate will delegate the act of invoking the method to a thread from the thread pool, thus making it asynchronous towards the caller. You have several options available that let you deal with the method completion, from fire-and-forget to polling, but the most common option (and the one best fitting an event-driven application such as a Windows Forms application) is to use a callback method. When the asynchronous operation is completed, you can have .NET call back into a method you provide, where you can call EndInvoke() to harvest returned values or deal with errors. The thread from the thread pool will do the callback after it executes the method.
|
You identify the completion callback method using a delegate of the type AsyncCallback that you pass to the BeginInvoke() call used to dispatch the method asynchronously. You need to be familiar with a few additional details including how to gain access to the original delegate used to invoke the call from within the completion callback, but in general, the programming model just described is sufficient for most applications, except when it comes to Windows Forms. Why are Windows Forms the exception? First, any decent client application should want to display some progress or indication of the current status of the asynchronous task to the user. In fact, you may want to pass the progress report to multiple parties?you may need to update both the status bar and a progress bar. Second, you may want to allow the user to cancel the asynchronous method invocation in progress.
The crucial limiting factor in implementing these features is the thread affinity Windows Forms controls and forms have to the underlying thread that created them. All Windows messages are actually messages between threads, and each thread has its own message queue. Each and every thread message can only be processed on the thread it belongs to. When a thread creates a window, that window’s messages are actually messages destined for the message queue of the creating thread. Consequently, all windows (such as forms and controls) can only process messages on the thread that created them. Method calls on forms and controls often result internally with posting of at least one such message.
In a simple Windows Forms application (such as the one created by the Visual Studio .NET application wizard), all forms and controls execute on the same thread?the primordial thread used to launch the application by calling Main()?so those forms and controls can freely call each other’s methods.
When a Windows Forms window invokes a call asynchronously (using a delegate), that call executes on a worker thread from the thread pool, not the thread that created the window. The completion callback method is also executed on the worker thread. As a result, the completion callback method should never update the user interface directly because that would be calling a Windows Forms form or control on a different thread from the thread that created it. Similarly, progress reports triggered by the worker thread are not allowed to directly update controls such as a progress bar. In all these cases, you must marshal the call from the worker thread to the user interface thread. This is exactly what the ISynchronizeInvoke interface, defined in the System.ComponentModel namespace, is designed to do:
public interface ISynchronizeInvoke { object Invoke(Delegate method,object[] args); IAsyncResult BeginInvoke(Delegate method, object[] args); object EndInvoke(IAsyncResult result); bool InvokeRequired {get;} }
ISynchronizeInvoke provides a generic and standard mechanism for marshaling calls between threads.
For example, imagine a client on thread T1 and an object on thread T2. If the object implements ISynchronizeInvoke, the client on thread T1 can call ISynchronizeInvoke’s Invoke() on the object. The implementation of Invoke() will block the calling thread (T1), marshal the call to T2, execute the call on T2, marshal the returned values to T1, and return control to the calling client on T1. Invoke() accepts a delegate targeting the method to invoke on T2, and a generic array of objects as parameters.
Because the call is marshaled to a different thread from that of the caller, you might want to be able to invoke it asynchronously. Microsoft created the BeginInvoke() and EndInvoke() methods to provide this functionality, but I won’t address these methods in this article.
In addition, because ISynchronizeInvoke can be called on the same thread as the thread the caller tries to redirect the call to; the caller can check the InvokeRequired property. If it returns false, then the caller can call the object methods directly.
Windows Forms 1.1 and Asynchronous Execution (cont.)
Windows Forms base classes make extensive use of ISynchronizeInvoke. The Control class, and every class that derives from Control (such as Form) relies on the underlying Windows messages and a message-processing loop. As explained previously, the message loop must have thread affinity because messages to a window are delivered only to the thread that created it.
|
In general, you must use ISynchronizeInvoke to access a Windows Forms object from another thread, and this is what you should do to support an asynchronous call progress report and completion notification.
Listing 1 demonstrates using ISynchronizeInvoke in a Windows Forms application. The application has a single form called AsyncClient. The form contains a Start button, a Cancel button, a progress bar, and a status label (Figure 1). When the application starts, the Start button is enabled, the Cancel button is disabled, the progress bar shows no progress, and the status label states “Status: Not Started.”
![]() | ? |
Figure 1: The Async Client application. |
When you click the Start button it calls the OnStart() method, which asynchronously invokes the DoBackgroundProcessing() method. The asynchronous invocation is dispatched via the private DoWork delegate:
delegate void DoWork(int iterations);
The application calls the BeginInvoke() method of DoWork, passing it the number of iterations to perform (basically the parameter to DoBackgroundProcessing) and a delegate of type AsyncCallback targeting a completion callback method called OnCompleted(). OnStart() also sets the status label to “Status: In Progress,” and it disables and enables the Start and Cancel buttons, respectively. The DoBackgroundProcessing() method simulates some lengthy execution by putting the calling thread (the worker thread from the thread pool in this example) to sleep for half a second. DoBackgroundProcessing() does that in a loop for the specified number of iterations it receives as a parameter. To support canceling an asynchronous method call in progress, AsyncClient has a Boolean member variable called m_CancelPending. AsyncClient provides thread-safe access to m_CancelPending via the CancelPending property, which locks the object upon access. You require thread-safe access to m_CancelPending because both the worker thread and the main thread may access it concurrently. Before starting each new iteration, DoBackgroundProcessing() checks the value of the CancelPending property. If it is true, DoBackgroundProcessing() breaks from the loop and returns. After each iteration, DoBackgroundProcessing() reports its current progress by calling the helper method ReportProgress(). The ReportProgress() method should update the progress bar with the current progress. However, DoBackgroundProcessing() calls ReportProgress(), which runs on the worker thread, so it cannot access the progress bar directly. Instead, it obtains from the progress bar its implementation of ISynchronizeInvoke:
ISynchronizeInvoke synchronizer = m_ProgressBar;
ReportProgress() then checks whether the Invoke() method is required. If it is required, ReportProgress() declares a variable of the private delegate type SetInt. SetInt calls the SetProgress() method, which sets the value of the progress bar. ReportProgress() passes the SetInt delegate to the Invoke() method:
SetInt del = new SetInt(SetProgress); try { synchronizer.Invoke(del,new object[]{value}); } catch {}
Invoke() will marshal the call to the main thread where it will be executed by invoking the delegate on the main thread.
As a result, during the execution of DoBackgroundProcessing(), the progress bar reflects the current status properly. When DoBackgroundProcessing() is completed, .NET will automatically call the OnCompleted() method. In OnCompleted() you must call EndInvoke() on the original delegate used to invoke the asynchronous call, even if there are no returned value you care about. You call EndInvoke() to prevent resource leaks because the delegate will keep track of the call it dispatched indefinitely until EndInvoke() is called. You can obtain the original delegate used to invoke the call by down-casting IAsyncResult into AsyncResult and accessing the AsyncDelegate property. In addition, OnCompleted() needs to update the text of the status label. It does so by checking the value of the CancelPending property. If it is true, it sets the status to cancelled, and otherwise to completed. However, OnCompleted() is executed on the worker thread so it cannot access the status label directly. Instead, it uses a similar pattern to the reporting progress and it calls the UpdateStatus() helper method which uses the ISynchronizeInvoke of the status label to update it. For UpdateStatus(), you also need to define a delegate that can invoke a method with a single string parameter, the SetString delegate in Listing 1. All SetString does is set the Text property of the status label.
Advanced developers can somewhat optimize the code in Listing 1 and abstract away the differences between ReportProgress() and UpdateStatus(), but still the net result is a very cumbersome programming model when it comes to incorporating common requirements of asynchronous programming and Windows Forms. Furthermore, the major deficiency of this programming model is the high degree of internal coupling between the various asynchronous execution handling methods, the user interface layout, the controls of the forms, and the required behavior. A small change in any of these will result in major changes to the code in Listing 1.
.NET 2.0 BackgroundWorker
The Windows Forms team and the .NET architects are well-aware of the problems just described. To address them, the next version of .NET (Whidbey) contains a new component called BackgroundWorker defined in the System.ComponentModel namespace. If you have access to the Whidbey beta distributed at PDC, you can find BackgroundWorker in the Components tab of a Windows Forms project. If you drop it on a form, you can use BackgroundWorker to dispatch asynchronous work, report progress and completion, and do all that while encapsulating the interaction with ISynchronizeInvoke. Using this approach gives developers a much smoother and superior programming model. Listing 2 shows the definition of BackgroundWorker and its supporting classes.
|
BackgroundWorker has a public delegate called DoWork of the type DoWorkEventHandler. To invoke a method asynchronously, wrap a method with a matching signature to DoWorkEventHandler and add it as a target to DoWork. Then, call the RunWorkerAsync() method to invoke the method on a thread from the thread pool:
BackgroundWorker backgroundWorker; backgroundWorker = new BackgroundWorker(); backgroundWorker.DoWork += OnDoWork; backgroundWorker.RunWorkerAsync(100); void OnDoWork(object sender,DoWorkEventArgs doWorkArgs) {...}
BackgroundWorker offers two overloaded versions of RunWorkerAsync():
public void RunWorkerAsync(); public void RunWorkerAsync(object argument);
You can use the argument parameter to pass any argument to the asynchronous method. Internally, RunWorkerAsync() will construct a DoWorkEventArgs object containing the argument, invoke the DoWork delegate asynchronously, and pass it the DoWorkEventArgs object and itself as the sender.
The asynchronous method can access the Argument property of DoWorkEventArgs to retrieve the argument passed to RunWorkerAsync(). The asynchronous method should also set the value of the Result property of DoWorkEventArgs with the retuned value.
Any party interested in being notified when the asynchronous method is completed should subscribe to the RunWorkerCompletedmember delegate of BackgroundWorker. RunWorkerCompletedis a delegate of the type RunWorkerCompletedEventHandler. The completion notification method accepts a parameter of type RunWorkerCompletedEventArgs, which contains the result of the method execution (the value you set in the Result property of DoWorkEventArgs inside the asynchronous method), as well as error and cancellation information.
When the asynchronous method execution is completed, BackgroundWorker cannot simply invoke the RunWorkerCompleted delegate because that invocation will be on the thread from the thread pool, and any control or form that subscribed to RunWorkerCompleted cannot be called directly. Instead, BackgroundWorker checks whether each of the target objects in RunWorkerCompleted supports ISynchronizeInvoke, and if invoke is required. If so, it will marshal the call to the owning thread of the target object.
To support progress reports, BackgroundWorker provides a member delegate called ProgressChanged of the type ProgressChangedEventHandler. Any party interested in progress notification should subscribe to ProgressChanged. When the asynchronous method wishes to notify about progress, it calls BackgroundWorker’s method, ReportProgress, to correctly marshal the progress notification to any Windows Forms object.
To cancel a method, anybody from any thread can call BackgroundWorker’s CancelAsync() method. Calling CancelAsync() results in having the CancellationPending property of BackgroundWorker return true. Inside the asynchronous method, you should periodically check the value of CancellationPending, and if it is true, you should set the Cancel property of DoWorkEventArgs to true and return from the method. In the completion method you could check the value of the Cancelled property of RunWorkerCompletedEventArgs to detect whether the method has run to its completion or it was cancelled.
In case you derive from BackgroundWorker in order to specialize its behavior, the subclass you provide may also want to be notified of progress reports and completion events. One way to do that would be to provide event handling methods at the scope of the subclass, and add these methods as targets for the ProgressChanged or RunWorkerCompleted member delegates. However, it is a bit inconvenient to have to subscribe to events of your own base class. To that end, BackgroundWorker provides two protected virtual methods: OnProgressChanged() and OnRunWorkerCompleted(). These methods invoke their respective delegates, ProgressChanged and RunWorkerCompleted. You can override them, and perform some pre or post event processing:
public class MyBackgroundWorker : BackgroundWorker { protected override void OnRunWorkerCompleted( RunWorkerCompletedEventArgs completedArgs) { // do some pre-event processing, then: base.OnRunWorkerCompleted(completedArgs); } }
Note that it is very important to call BackgroundWorker’s implementation of these methods; otherwise the events will never be raised.
BackgroundWorker has two Boolean properties that control progress reports and cancellation?WorkerReportsProgress and WorkerSupportsCancellation. Both properties are false by default, but of course you need to set them to true in most cases.
Listing 3 shows the same application as in Listing 1, except this time it uses a single BackgroundWorker component. Note how smooth and easy the code is for handling progress reports and completion. Because you are guaranteed to always execute on the correct thread, you can access and assigned the controls’ properties directly.
Implementing BackgroundWorker with .NET 1.1
What disturbs me about Listing 3 is the disparity between it and Listing 1. Windows Forms Developers today are stuck with an awkward, fragile programming model, and have to wait (probably) until early 2005 to benefit from BackgroundWorker. Most Windows Forms applications that use asynchronous calls today will not be able to use BackgroundWorker easily because of the high degree of internal coupling. To address this predicament, I implemented BackgroundWorker using .NET 1.1, and it is available with the source code for this article. Listing 4 shows my implementation of BackgroundWorker, defined in the WinFormsEx namespace. The implementation uses some advanced .NET programming techniques and tricks, and makes for an interesting walkthrough.
|
The first step in implementing BackgroundWorker was to define and implement the supporting delegates and event arguments classes shown in Listing 2?a simple matter of implementing properties. Implementing RunWorkerAsync() was simple too: first, it checks that there is a target method (when DoWork is not null) to invoke. RunWorkerAsync() then constructs a new DoWorkEventArgs object with the method argument and invokes DoWork using BeginInvoke() to dispatch the call asynchronously:
DoWorkEventArgs args = new DoWorkEventArgs(argument); AsyncCallback callback; callback = new AsyncCallback(ReportCompletion); DoWork.BeginInvoke(this,args,callback,args);
Using a delegate of type AsyncCallback, RunWorkerAsync() designates the private helper method named ReportCompletion() as the completion callback method. .NET will call ReportCompletion() once the asynchronous method execution completes.
Note that RunWorkerAsync() passes the DoWorkEventArgs argument twice to BeginInvoke(). RunWorkerAsync() uses the first DoWorkEventArgs as the parameter for the target method of DoWork(). It uses the second DoWorkEventArgs as an additional parameter to ReportCompletion(). Because both args reference the same object, changes made to args inside the asynchronous method (such as cancellation or result setting) will be available to ReportCompletion() as well. When you call ReportCompletion(), .NET provides it with a parameter of type IAsyncResult. You down cast it to AsyncResult and use it to obtain the original delegate used to invoke the call:
AsyncResult ar = (AsyncResult)asyncResult; DoWorkEventHandler del; del = (DoWorkEventHandler)ar.AsyncDelegate;
You need the original delegate so that you could call EndInvoke() and end the call. Note that you cannot simply access the DoWork delegate due to a potential race condition. If the asynchronous method was executing, something could remove the target method from DoWork. In that case, not only would you not be able to call EndInvoke(), you will also trigger a resource leak. AsyncResult contains a copy of the original delegate and its state at the time of invocation.
Any exception thrown by the asynchronous method will be caught by the worker thread, and it will be re-thrown when you call EndInvoke(). ReportCompletion() catches such an exception and saves it in a temporary variable. In addition, ReportCompletion() saves the result of the asynchronous method made available through the DoWorkEventArgs passed in as a state object to BeginInvoke(), and set inside the asynchronous method:
object result = null; Exception error = null; try { del.EndInvoke(asyncResult); result = doWorkArgs.Result; } catch(Exception exception) { error = exception; }
ReportCompletion() then needs to notify the subscribers of the RunWorkerCompleted delegate about the method completion. It constructs an object of the type RunWorkerCompletedEventArgs and populates it with the result, error, and cancellation information.
However, ReportCompletion() is called on the worker thread, therefore it cannot invoke the RunWorkerCompleted delegate directly. Instead, it calls the protected virtual method OnRunWorkerCompleted(), which calls the private ProcessDelegate() helper method defined as:
void ProcessDelegate(Delegate del,params object[] args);
Implementing BackgroundWorker with .NET 1.1 (cont.)
OnRunWorkerCompleted() passes to ProcessDelegate() the delegate to invoke and the invocation parameters. Since Delegate is the base class of all delegates, ProcessDelegate() can accept any delegate. The use of the params object array means the caller to ProcessDelegate() can pass it any combination of parameters and the compiler will package it into an object array when calling ProcessDelegate(). ProcessDelegate() first needs to check that the delegate passed in has targets in it, because if it does not, it will be set to null and will be inaccessible. There is a little-known race condition when dealing with delegates in a multithreaded environment. It is not good enough to just check the delegate because between the time you check and the time you access the delegate, there could have been a thread context switch and somebody else could have removed the targets, causing your access to throw an exception. Fortunately, there is a simple workaround?delegates are immutable, so you can safely copy the delegate to a temporary variable, check on it, and then access the temporary variable, as done by ProcessDelegate():
void ProcessDelegate(Delegate del,params object[] args) { Delegate temp = del; if(temp == null) { return; } Delegate[] delegates = temp.GetInvocationList(); //Rest of the method }
You could potentially have multiple parties interested in the event represented by the delegate, and any or all of these parties could be Windows Forms objects that require the special use of ISynchronizeInvoke. ProcessDelegate() must therefore process each of them individually. It retrieves the array of targets in the delegate by calling GetInvocationList(), which returns an array of individual delegates.
For each delegate in that collection, ProcessDelegate() then calls the private helper method InvokeDelegate(), passing the delegate as a parameter along with the arguments:
foreach(Delegate handler in delegates) { InvokeDelegate(handler,args); }
InvokeDelegate() is defined as :
void InvokeDelegate(Delegate del,object[] args);
InvokeDelegate() abstracts the act of invoking a delegate, whether you use it for targeting a method on a Windows Forms object or not.
The first thing InvokeDelegate() needs to do is to obtain from the delegate the object it targets. It does this using the Target property of the delegate. Once InvokeDelegate() has the target object, it queries it for ISynchronizeInvoke:
ISynchronizeInvoke synchronizer = del.Target as ISynchronizeInvoke;
If the target object does not support ISynchronizeInvoke then synchronizer will be set to null, in which case InvokeDelegate() simply invokes the delegate using the DynamicInvoke() method that every delegate supports, passing in the arguments for the target method:
del.DynamicInvoke(args);
You must use DynamicInvoke() because InvokeDelegate() does not know the exact type of delegate it is invoking. If synchronizer is not null, InvokeDelegate() checks whether invoke is required, and if so, it uses the Invoke method to marshal the call to the correct thread:
if(synchronizer != null)//A Windows Forms object { if(synchronizer.InvokeRequired == false) { del.DynamicInvoke(args); return; } try { synchronizer.Invoke(del,args); } catch {} }
The implementation of ReportProgress() is straightforward: It constructs a new object of type ProgressChangedEventArgs wrapping the percent argument, then it calls the protected virtual OnProgressChanged() method, which calls ProcessDelegate() on the ProgressChanged delegate.
The rest of the methods and properties of BackgroundWorker are merely thread-safe accessors to the corresponding member variables.
![]() | ? |
Figure 2: The BackgroundWorker component in VS.NET 1.1. |
Finishing Touches
To add my BackgroundWorker component to the Components toolbox, right-click on the toolbox and select Add/Remove Items… from the pop-up context menu. Browse to the location of the WinFormsEx assembly and add it to the toolbox. I wanted my BackgroundWorker to have the same great icon as in .NET 2.0, so after I copied the icon from .NET 2.0 (all rights to the icon go to Microsoft), I added it to the WinFormsEx assembly as an embedded resource. To assign the icon to the BackgroundWorker class I used the ToolboxBitmap attribute. The attribute accepts a type identifying the assembly where the resource is embedded and the name of the resource. Once you drop BackgroundWorker on a form, you will see it under the form, just like in .NET 2.0. You can use the Visual Designer to set the BackgroundWorker properties and events, as shown in Figure 2. Your code that uses BackgroundWorker today will transparently be able to use BackgroundWorker in .NET 2.0, simply by removing the reference to the WinFormsEx namespace, because the two components are polymorphic and function in an identical manner.