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.