Another way to Invoke UI from a Worker Thread
Introduction
This article demonstrates an alternative way of invoking UI event handlers from a worker thread.
Events from worker threads - the "traditional" way
One of the benefits of the .NET platform is a much simpler way of performing lengthy tasks while keeping the user interface responsive. You can create an object, fill its properties and fields with proper data and make one of the object's methods a background thread. And just don't forget to retrieve the result of its work some time later.
When a background also known as worker thread needs to display some information in a UI you just define an event, subscribe a UI object for this event and fire it in the background thread. The only problem is that the UI should be managed from its own thread that contains the message loop. Microsoft offers us a simple workaround.
Suppose we have an event handler like the following one:
void OnEvent(object sender, EventArgs e)
{
// Update the UI
}
It would work for any object derived from System.Windows.Forms.Control
. To make it thread-safe, you should add a little bit of code:
void OnEvent(object sender, EventArgs e)
{
if(InvokeRequired)
Invoke(new EventHandler(OnEvent),
new object[] {sender, e});
else
{
// Update the UI
}
}
The InvokeRequired
property of the Control
class returns true
if the method of the UI element is called from a different thread. In this case we should use interthread marshalling using the Invoke
method.
The "traditional" way of making multithreaded Windows Forms programs is good, but imagine you have a rather chatty worker object that has a lot of events. So for each event you should modify your event handlers. I usually forget to do this and then hunt for bugs. I also think this workaround code is ugly and should be hidden from the worker's client. We should move it to the workers' methods and I'll show you how to do that.
Events from worker threads, an easier way
Microsoft uses a distinct pattern for raising events. Each event with the name AAA is accompanied with a protected method OnAAA that raises the event and can be overridden by the descendant classes. There are many reasons to use this pattern in your programs and I'm going to give you another one. Checking whether we are calling the UI from a non-UI thread should be performed in such a method.
You would ask how? Easy. The Control
class actually implements System.ComponwentModel.ISynchronizeInvoke
interface. This interface declares the property InvokeRequired
and methods Invoke
, BeginInvoke
and EndInvoke
so, in theory there are more message loop aware classes. While firing events we need to check if each target object implements this interface. If it does, we need to check the InvokeRequired
property and instead of calling the event delegate directly we need to use the Invoke
method. It means that for the event subscriber (a Form-derived class in most cases) this event will always be synchronous and the author of the subscriber won't need to bother about interthread marshalling.
However, we should keep in mind that all events are multicast delegates. Because of this we must check all event subscriber objects separately. This is an easy part, because System.MulticastDelegate
class has the GetInvocationList
method that returns an array of single cast delegates that represent the combined multicast delegate.
Suppose we have an event declared as:
public event EventHandler Event;
We should declare the accompanying method like this:
protected virtual void OnEvent(EventArgs e)
{
EventHandler handler = Event;
if(null != handler)
{
foreach(EventHandler singleCast in handler.GetInvocationList())
{
ISynchronizeInvoke syncInvoke =
singleCast.Target as ISynchronizeInvoke;
try
{
if((null != syncInvoke) && (syncInvoke.InvokeRequired))
syncInvoke.Invoke(singleCast,
new object[] {this, e});
else
singleCast(this, e);
}
catch
{}
}
}
}
The first line of the method, the assignment statement, makes the method thread safe. I get a local copy of the event handler and make sure that even if somebody modifies the Event
there won't be any trouble. Then we check if there is any subscriber for the event. If there are subscribers, I check if the event's delegate target object implements the ISynchronizeInvoke
interface. If it does and the object is in the UI thread I perform interthread marshalling. In all other cases I just call the delegate directly. I also catch all exceptions that a subscriber can throw. It is not a very good idea to ignore them but so far I haven't found a good way to pass them to the OnAAA
method caller.
Sample
In the sample you'll find a simple component named Copier
that copies one stream to another in the background thread. You might need a class like that if you copy big files or download data from Internet. The Copier
class has some properties related to work progress, three thread-safe events Started
, Progress
and Finished
and three public
methods Start
, Stop
and Join
that check the state of the Copier
and if it is valid delegates the work to the System.Threading.Thread
method.
The sample also contains the ProgressForm
class that provides a simple UI for the Copier
component and the MainForm
class that allows the user to specify the names of the source and target files. Note that the Copier
does not close the data streams; this is the responsibility of the Copier
's client. The same is true for the ProgressForm
class.
There are two points of interest in this code. First is the implementation of the OnStarted
, OnProgress
and OnFinished
methods, they follow the described pattern. Second is the ability to cancel the worker thread. Initially, I derived ProgressEventArgs
class from CancelEventArgs
, but the Control.Invoke
call does not marshal its arguments back to the calling thread. I've added the Stop
call to the Copier
class that gracefully stops the worker thread.
I wrote the sample using SharpDevelop IDE with NAnt 0.85 as the build system. With NAnt you should call Debug, Release or Doc targets; the latter creates the InvokeUI.chm HTML Help file in the doc folder. You can also use the build.bat file to compile the sample. Sorry, I have no Visual Studio and those of you who do will have to re-create the project. Just create an empty Windows Forms project and add all *.cs files to it.
License
Files Copier.cs, ProfressEvent.cs and ProgressForm.cs are covered by BSD-like license, see comments at the beginning of each file. The rest of the code is in public domain. Use the sample at your own risk.