Sam Jones                                                                                                                                                                           Magenic Technologies

 

Sample Source Code: WinFormsExample.zip (28KB)

Threading In Windows Forms

 

                One of the most difficult aspects for many developers to grasp in developing custom software products is this fact: Regardless of the quality of the back-end software, or the ingeniousness of the functionality, if the user experience is poor, the software will be regarded as sub-standard in quality, or even worse, will earn the user-assigned epithet: “It doesn’t work.”  The most common cause of a bad user experience with software is where a long-running process causes the dialog the user is viewing to appear to freeze or lock-up during the operation.  This is commonly rendered under Windows XP as a “white screen”, sometimes accompanied with the “(Not Responding)” text appearing in the window title. 

                The appearance of this behavior is caused when a long-running process of one sort or another is executed on the same thread as the main UI thread of the application.  This prevents the UI thread from processing or responding to the operating system messages.  To prevent this from occurring, execution of any processes which may take any significant or noticeable amount of time should be executed on a separate thread from the main UI thread.

                There are other uses for multithreading within applications as well, from simple independent control content updates to socket server listener implementations.  With the .NET Framework, utilizing threading within applications and code libraries has become more accessible and easier to use for developers.  There is one significant caveat, however, when using multithreading within Windows applications that has not changed.  While it remains possible to call properties and methods on UI controls from separate threads, this is a fundamental Bad Idea in Windows.  The first rule of multi-threaded UI development is that all UI form and control properties and methods may only be invoked from the main UI thread which created the dialog or control.  This is the thread to which the operating system continuously sends its messages.  Ignoring this rule in development can cause, as Microsoft puts it, “unpredictable behavior.”

                This rule is as important in the .NET Framework as in any other Windows development system.  Yet, in the .NET 1.0 and 1.1 Frameworks, many developers remained unaware of cross-threaded calls to UI controls.  The .NET 2.0 version allows for an option to be set in Visual Studio allowing an exception to be thrown when this occurs, although even this option may be explicitly turned off.  This does not mean that the cross-threading rule is optional, however. 

                This brings up the question, then, how does an independently-threaded long-running process send status update information to a dialog in the main UI thread?  The following examples demonstrate the methodology used to generate proper UI behavior during long-running processes, as well as the standard form for correctly invoking cross-threaded calls.

 

Creating A Base Mechanism

 

                While it is possible to create special invocation mechanisms to support proper threading and updates on each and every dialog created within an application, this is not a practical approach.  Rather, as with all object-oriented languages, it is best to utilize a set of base classes to provide the functionality required.  To this end, we can begin with a new DialogBase class to extend the basic Form class.

                The goal of this class is to provide a basic mechanism for various types of forms – normal dialogs, MDI dialogs, modal dialogs, and status update dialogs.  We can, at this point, devise and create overridable sections of code for us in different circumstances.  While technically, all these items are executed in the OnLoad and OnClosing implementations, it helps to modularize each activity for specific implementation by a child class.

 


 

 

Figure 1: Modularized Functions

Method Name

Purpose

Called From

AssignEventHandlers

Create and assign event handler delegates for events to be handled by the dialog.

OnLoad

RemoveEventHandlers

Removes any event handler assignments for events handled by the dialog.

OnClosing

SetFormContent

Loads any necessary data and performs business tasks and any other form initialization required.

OnLoad

StartFormThreads

Creates and launches any necessary background form threads.

OnLoad

TerminateFormThreadsw

Terminates any executing form threads before the form is closed and disposed of.

OnClosing

PrepareForDisplay

Performs any necessary final update or drawing tasks on the main UI thread.

OnLoad

 

                While all these tasks can be performed in each implementation of OnLoad and OnClosing, it helps to provide protected virtual methods for modularizing the specific activity.  This provides a safe, simple mechanism for descendant classes to perform initialization and clean-up in a manageable and easy to read manner.

 

Code Example 1: OnLoad And OnClosing For the Base Class

 

protected override void OnLoad(EventArgs e)

{

       //Call the base method.

       base.OnLoad(e);

 

       //Assign any event handlers,

       //set the dialog's initial display content,

       //and launch any independent threads.

       AssignEventHandlers();

       SetFormContent();

       StartFormThreads();

 

       //Perform any remaining tasks before the dialog displays.

       PrepareForDisplay();

}

protected override void OnClosing(System.ComponentModel.CancelEventArgs e)

{

       //Call the base method.

       base.OnClosing(e);

 

       if (!e.Cancel)

       {

              //Terminate any form threads and remove event handlers.

              TerminateFormThreads();

              RemoveEventHandlers();

       }

}

 

 

                Providing this base mechanism allows the descendant classes we create to separate and implement the specific areas of functionality particular to the type of dialog class being created.

 

Executing Long-Running Tasks

                It is advisable in Windows development to execute any long-running tasks in a separate thread for two main reasons: to prevent the “lock-up” of the central UI thread (allowing it to respond to system messages) and to prevent an application crash in case of an exception or other failure.  By isolating the task in a separate thread, any failure will cause the thread, rather than the application, to terminate, allowing the application to perform any necessary recovery or exception handling tasks. 

                Another important aspect to executing long-running tasks is to provide a status update to the user.  With no status update, the user is likely to assume the application is hung, even if it is not.  This requires that the status display mechanism be able to accept input from other executing threads safely.

 

Cross-Threaded Method Invocation

                The .NET Framework allows the application to make cross-threaded calls in most cases without incident.  The singular exception, of course, is when calling methods and properties on UI controls created in a separate thread.  Not all methods are subject to this rule – only those that cause the UI drawing mechanisms to be invoked.  This not only includes explicit calls to drawing or refresh methods, but implicit calls as well.  Often times, when a property value on a control is set, a call to Invalidate() or Refresh() is made by the control or control’s base class.  This causes the main UI thread to invoke a drawing method, and if performed in a cross-threaded context, will raise an exception instance. 

                In order to properly invoke these methods, a mechanism is provided on the base control class that allows a separate thread to force a method to be invoked and executed on the main UI thread rather than the calling thread.  The System.Windows.Forms.Control class provides several version of an Invoke method that may be called by non-UI threads for performing UI drawing or update tasks. 

 

Figure 2: Control Invocation Methods

Method

Purpose

Invoke(Delegate method)

Invokes the control method pointed to by the method parameter.

Invoke(Delegate method, object[] arguments)

Invokes the control method pointed to by the method parameter with the supplied arguments.

BeginInvoke(Delegate method)

Asynchronously invokes the control method pointed to by the method parameter.

BeginInvoke(Delegate method, object[] arguments)

Asynchronously invokes the control method pointed to by the method parameter with the supplied arguments

 

                Using these methods, we can update any display dialogs from separate threads correctly.  Doing so requires an implementation of a delegate definition, and a method for invoking that delegate.  From the perspective of the base class, this may be generalized to allow a seemingly-transparent method call to wrap these necessary functions.  The following example uses a MainDialog form class derived from the DialogBase class:

 

Code Example 2: Simple Cross-Threaded Update Call on Same Dialog Class

 

Define and invoke a background thread:

private Thread _dateTimeUpdateThread;

private bool _threadExec;

 

protected override void StartFormThreads()

{

       _threadExec = true;

       _dateTimeUpdateThread = new Thread(new ThreadStart(RunDateThread));

       _dateTimeUpdateThread.IsBackground = true;

       _dateTimeUpdateThread.Priority = ThreadPriority.BelowNormal;

       _dateTimeUpdateThread.Start();

}

 


 

 

Thread method:

 

private void RunDateThread()

{

       while (_threadExec)

       {

              //Allow other threads to execute.

              Thread.Sleep(100);

 

              //Invoke the update method on the main UI thread.

              BeginInvoke(new MethodInvoker(UpdateLabels));

       }

      

}

 

Method invoked on the main UI Thread:

 

private void UpdateLabels()

{

       //Set the property values.

       DateLabel.Text = DateTime.Now.ToString(DATE_FORMAT);

       TimeLabel.Text = DateTime.Now.ToString(TIME_FORMAT);

 

       //Allow messages to be processed.

       Application.DoEvents();

}

 

Terminating the thread when the dialog closes:

 

protected override void TerminateFormThreads()

{

       _threadExec = false;

       while ((_dateTimeUpdateThread != null) && (_dateTimeUpdateThread.IsAlive))

              Thread.Sleep(100);

 

_dateTimeUpdateThread = null;

}

 

 

 

                The UpdateLabels method is used to determine the current date and time, and assign the string values to controls defined on the dialog.  It is essential that this method is invoked and executed on the central UI thread.  Therefore, the method call must be done with one of the Invoke method variations.  Since the method is rather simple and does not require parameters, the simple MethodInvoker delegate definition can be used.  Note: Since the thread making this call is being executed on the same class as the dialog being displayed, it is optimal to use the asynchronous BeginInvoke method in this case so that the other threads may continue executing while the update is performed. 

 

Cross-Threaded Calls On Separate Dialogs

                It is often necessary in UI development to display a separate status dialog (or dialogs), or event to use independently-threaded UI controls for status display.  In this case, thread-driven and/or event-based status updates are often required.  The simplest solution in these cases is to provide a public method on a status dialog class which allows the calling thread to pass in the update and status values.  The status dialog should then be capable of updating itself in a properly thread-safe and manner.  To this end, we can define more base classes for particular dialog types.  In the example code, two basic versions are defined: the SimpleStatusDialogBase class, and the ComplexStatusDialogBase class.  The simple version is used to display a simple text and percent value status update; whereas the complex version is designed to show a Main operation and the status for any sub-operations.

                For a simple status update, a method is provided for any executing thread to call for updating the dialog: UpdateStatus().  This method accepts a string and integer parameter, used to update the dialog display.  In order for this method to execute correctly, the following must also be defined on the dialog class.  The following example assumes an implementation of SimpleStatusDialogBase derived from the DialogBase class.

 

Code Example 3: Simple Status Dialog Methods For Cross-Threaded Update Calls

 

Field definitions:

 

private string _statusText = string.Empty;

private int _percentageValue;

private bool _showCancel;

 

The function delegate definition:

 

public delegate void FormStatusUpdateMethod(object[] contentsToUpdate);

 

The internal update-invocation call method:

 

protected void SelfUpdate(object[] contentValues)

{

       FormStatusUpdateMethod methodToCall = null;

 

       //Create function pointer.

       methodToCall = new FormStatusUpdateMethod(UpdateFormContent);

 

       //Invoke the method on the main UI thread.

       Invoke(methodToCall, new object[1] { contentValues });

}

 

The overridable update method which is always executed on the main UI thread:

 

protected virtual void UpdateFormContent(object[] contentValues)

{

       //Validate parameters.

       if ((contentValues != null) && (contentValues.Length == 3))

       {

              //Parse the values.

              _statusText = (string)contentValues[0];

              _percentageValue = (int)contentValues[1];

              _showCancel = (bool)contentValues[2];

 

              //Set control content.

              StatusLabel.Text = _statusText;

              StatusProgress.Value = _percentageValue;

              CloseButton.Visible = _showCancel;

 

              if (_showCancel)

                     Cursor = Cursors.AppStarting;

              else

                     Cursor = Cursors.WaitCursor;

 

              Invalidate();

              Application.DoEvents();

       }

}

 

 

The process flow works as follows:

 

 

                In this example, the UpdateStatus() method is responsible for translating the parameters it is supplied into an object array that the child implementation of UpdateFormContent() can parse.  (It may also be possible to implement this system using generics or some other mechanism, based on the requirements of the status dialog class(es) being implementated. 

 

Code Example 4: Update Status Method Implementations

 

public virtual void UpdateStatus(string statusText, int percentDone)

{

       SelfUpdate(new object[3] { statusText, percentDone, false });

}

 

public virtual void UpdateStatus(string statusText, int percentDone, bool showCancelButton)

{

       SelfUpdate(new object[3] { statusText, percentDone, showCancelButton });

}

 

 

                Performing these tasks for more complex update scenarios merely involves adding more logic to the UpdateFormContent() method implementation in the child class and adding other methods for wrapping around the base class SelfUpdate() method.  Since SelfUpdate and UpdateFormContent use an object array of invariant size, any child class can use an unlimited number of parameters.  The following example assumes an implementation of ComplexStatusDialogBase derived from the SimpleStatusDialogBase class.

 

Code Example 5: Complex Update Dialog Method Implementations

 

Private field definitions:

 

/// <summary>

/// Current status text.

/// </summary>

private string _subStatusText = string.Empty;

/// <summary>

/// Percent complete text.

/// </summary>

private int _subPercentageValue;

 

Overridden Update Form Content Method:

 

protected override void UpdateFormContent(object[] contentValues)

{

       bool visible = false;

 

       //Validate parameters.

       if ((contentValues != null) && (contentValues.Length == 5))

       {

              //Parse values.

              StatusText = (string)contentValues[0];

              CompletionPercent = (int)contentValues[1];

              CloseButton.Visible = (bool)contentValues[2];

              _subStatusText = (string)contentValues[3];

              _subPercentageValue = (int)contentValues[4];

 

              //Set cursor.

              if (CloseButton.Visible)

                     Cursor = Cursors.AppStarting;

              else

                     Cursor = Cursors.WaitCursor;

 

              StatusLabel.Text = StatusText;

              StatusProgress.Value = CompletionPercent;

              SubStatusLabel.Text = _subStatusText;

              SubStatusProgress.Value = _subPercentageValue;

 

              Invalidate();

              Application.DoEvents();

       }

}

 

Update Status Methods:

 

public override void UpdateStatus(string statusText, int percentDone)

{

       SelfUpdate(new object[5] { statusText, percentDone, false, SubStatusText, SubCompletionPercent });

}

 

public override void UpdateStatus(string statusText, int percentDone, bool showCancelButton)

{

       SelfUpdate(new object[5] { statusText, percentDone, showCancelButton, SubStatusText,
                                  SubCompletionPercent });

}

 

More status update methods:

 

public virtual void UpdateSubStatus(string subStatusText, int percentDone)

{

       SelfUpdate(new object[5] { StatusText, CompletionPercent, CloseButton.Visible,

              subStatusText, percentDone });

}

 

public virtual void UpdateSubStatus(string subStatusText, int percentDone, bool showCancelButton)

{

       SelfUpdate(new object[5] { StatusText, CompletionPercent, showCancelButton, subStatusText,

                                          percentDone });

}

 

 

                In either case, the SelfUpdate() call ensures that the implementation of UpdateFormContent() is executed in the main UI thread, thus avoiding the cross-threaded call exception.  External callers are then not required to perform thread-specific invocation as these base classes provide a transparent mechanism for thread execution control.