This article is original content by contributor Mike Heffernan
It is well known that most Windows Forms are not thread safe, and all changes to these controls should run (be marshaled on to) the thread processing windows events (the UI thread). Worker threads are often used to do lengthy calculations or process asynchronously messaged events, allowing the UI to react to the user while these tasks are operating.
The typical pattern for receiving an event in a form or control and marshaling it onto the UI thread is as follows:
public void UpdateEventHandler() { if ( ! thisControl.InvokeRequired ) MakeChange(); else thisControl.Invoke(new UpdateEventHandler(this.UpdateEventHandler)); }
This is fine, as long as you have a reference to a UI control. However some update events can occur inside the model layer that require UI thread marshaling yet should not follow this typical pattern, as a direct reference to the UI should be avoided.
An architectural goal of the model layer is to be independent of the UI. We should be unaware of the context within which the layer is being used (i.e. it could be deployed in a web service or web site, be running as a windows service, or bound to a WinForms client).
In the context of a Windows Forms application, we can imagine two assemblies, one of which implements UI forms and controls (here called “Views”) and a separate assembly that implements model layer object abstractions (here called “Model”). At OpusEdge, we have evolved the Model / View / Controller (MVC) concept, and one of our hard and fast rules is that the model does not contain any reference to the UI.
Figure 1: Model Layer with Custom DataSet bound to an UltraGrid in the View Layer.
One of our data abstraction techniques is to subclass DataSet and DataTable in order to present complex underlying data configurations in a familiar and industry standard way. These can be bound to, for example, an UltraGrid via the DataSource attribute.
// Create the data set and initialize the table this.gridDSet = new ModelLayer.CustomDataSet(); gridDSet.InitializeTable(); // Bind the data set to the grid BindingSource bsrc = new BindingSource(this.gridDSet, this.gridDSet.MainTable.TableName); this.ultraGrid1.DataSource = bsrc;
Inside the custom DataSet, we can do all manner of convoluted stuff, and the grid sees it all as rows and columns.
On the client side, one of the responsibilities of the model assembly is to react dynamically to data update notifications that are pushed out from the server asynchronously using Windows Communication Foundation. The processing of these events runs on a separate worker thread, not the main UI thread, and the UI is notified of changes to the data so it can update itself dynamically.
Ordinarily, the model events are messaged to Forms and UserControls, and the standard pattern of InvokeRequired / Invoke() can be used as per standard practice.
We hit a problem should the model layer be running on a worker thread and triggers updates in a custom DataSet used as a DataSource for a control, like the UltraGrid. The values within the DataSet are changed, and the DataSet fires change events which are sent directly into the UltraGrid. These events are not running on the main UI Thread, and boom – the grid will start failing because it is not thread safe. This would be true for any control that reacts dynamically to the data change events in the DataSet.
The answer is to marshal the update within the DataSet on to the UI thread. In order to accomplish this, we need a reference to the UI. As stated above, the model layer shouldn’t be aware that it is being used by a Windows Forms application, so we find ourselves in a bit of bind (pardon the pun).
There really isn’t any perfectly clean way around this, but the goals of enabling the model layer to be runtime environment neutral can be achieved by isolating the thread marshaling into a single class, UIThreadMarshal, which is part of the model assembly. Its role is to know if a Windows Forms UI is present or not, and marshal requests onto the UI thread if there is. If no Windows Forms UI is present, the class is a no-op.
UIThreadMarshal is implemented as a static singleton, both for convenience’s sake and to preserve memory by having the model layer store one, and only one, reference to the UI’s main form. The code is below:
// Singleton class used to marshall events onto the UI thread inside the Model Layer public static class UIThreadMarshal { // This is a reference to the application main form, used to get the UI thread // It is write only, and should be set in the onLoad event of the UI's main form private static Form mainForm = null; public static Form MainForm { set { UIThreadMarshal.mainForm = value; } } // Behaves as per usual, but if no UI is registered, assumes Model Layer is running // in a service or on a server without a UI public static bool InvokeRequired { get { // No UI registered if (UIThreadMarshal.mainForm == null) return false; // Check with the main form return UIThreadMarshal.mainForm.InvokeRequired; } } // Behaves as per usual, but if no UI is registed, // assumes the user didn't check InvokeRequired first // and throws an exception. public static object Invoke( Delegate delegateMethod ) { // No UI registered if( UIThreadMarshal.mainForm == null ) { Exception ex = new Exception( "No UI registered with UIThreadMarshal. Invoke is meaningless."); throw ex; } // Marshal the delegateMethod onto the UI thread return UIThreadMarshal.mainForm.Invoke(delegateMethod); } }
The application’s main form initializes the class in its onLoad() event, as shown here:
private void MainForm_Load(object sender, EventArgs e) { // Initialize the marshal class to be aware of this form ModelLayer.UIThreadMarshal.MainForm = this; Thread.CurrentThread.Name = "Main UI Thread"; }
Using the class then follows the same familiar pattern. The following code is the event handler inside the CustomDataSet:
// Simulates event handler which reacts to data change events coming from the server public void RandomUpdateRequestEventHandler() { // Check to see if we are on the UI thread if ( ! UIThreadMarshal.InvokeRequired ) { MakeRandomChange(); } else { // If we need to marshal, do it now UIThreadMarshal.Invoke(new RandomUpdateRequestHandler(this.MakeRandomChange)); } }
As a companion to this article, I have included a project that serves as a working demonstration of the principles discussed here. The UI for the application is shown below.
Figure 2: UIThreadMarshal project user interface.
It’s pretty straightforward. You boot the application and hit Initialize. In version 2007.2.1063 of the Infragistics toolkit, the UltraGrid is actually reasonably thread safe until you start turning on features. As a result, we turn on Group Cells to cause the grid to actually fail when updates are made off the UI thread.
If you hit the “Run on UI Thread” a set of 1000 requests for random updates will be issued by an event driver (serving as a simulation of the asynchronously pushed events from the server). If you hit “Run Without Marshal” the events will be sent from a worker thread without marshaling to the UI thread. Watch your output window to see the exceptions thrown. If you hit “Run With Marshal” the UIThreadMarshal class will be used to marshal the updates inside the CustomDataSet onto the UI thread.
The basic structure of this demonstration is illustrated below:
Figure 3: UIThreadMarshal project assemblies and classes.
The technique does have the side effect of having your model assembly reference System.Windows.Forms, but it isolates the actual object reference to a single class and a single runtime instance, so the workaround is pretty well fenced off.
The technique enables the use of the Model assembly wherever we like, preserving the Model Layer’s independence from any specific contextual use.
Should the Windows Forms UI not be present there is a negligible drag on the performance of the custom DataSets.
Mike Heffernan is Chief Technology Officer of OpusEdge Inc., an independent software vendor located in Ottawa, Canada. He’s worked in software as a programmer, architect, development manager and product manager since 1982.