Introduction
Communicating across a network can be time consuming manifesting itself as an unresponsive user interface. Avoiding long and non-deterministic operations on the UI thread will not increase overall performance but it will give the user some degree of confidence that the application hasn’t silently died. The solution seems rather simple; spawn another thread for the lengthy operation. The one gotcha in this scenario is that if you attempt to update the UI from the spawned thread, you will experience all sorts of nastiness in your application. It is very important that you do not break the thread affinity rule when operating upon form resident controls. If you want to manipulate a control from a worker thread, the update must be marshaled to the thread that created the control.
Design Solution
After some experimentation, I noticed the ease with which I was cluttering up the form code with delegate invocations in order to ensure the proper thread affinity when talking to controls. At this point, I decided to encapsulate the long running operations within a separate task object which utilized the Event Handling mechanism to report back upon its completion.
My initial design was mirrored after a Task pattern I found somewhere within MSDN. The only bit I wasn’t very keen on was that the form code still needed to be aware that it might be called upon by a worker thread. That fact necessitated the use of an additional delegate to ensure the thread that created the control was the thread that updated the control.
protected void LoopDoneHandler(double Calculations, int Count)
{
this.BeginInvoke(new LDHandler(LDoneHandler),new Object[]{Calculations,Count});
}
public delegate void LDHandler(double Calculations, int Count);
public void LDoneHandler(double Calculations, int Count)
{
btnRunLoops.Enabled = true;
}
My goal was to hide all the implementation details. That meant that I needed the Task to be aware of the object that subscribed to the event. After a bit of digging, I discovered that I could query the event subscriber (class instance) in order to determine if it is a control.
If the class instance is found to be a control, the code calls the BeginInvoke
method which executes a delegate on the thread that owns the control’s underlying window handle. All other class instances would have their method executed within the current thread.
if (LoopComplete.Target is System.Windows.Forms.Control)
{
System.Windows.Forms.Control t =
LoopComplete.Target as System.Windows.Forms.Control;
t.BeginInvoke(LoopComplete, new Object[]{varTotalAsOfNow, varLoopValue});
}
else
LoopComplete(varTotalAsOfNow, varLoopValue);
Summary
IMHO, making these few alterations improves the functionality of the Task pattern. The Form based code can be built without needing to be concerned about thread affinity issues. It is my conjecture that the code is easier to follow and as such easier to maintain.
Thinking out loud; I am wondering if this pattern can be made more generic through templates and or generics? Hmmm; something to consider.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.