Simplifying the Use of the APM in Windows Forms
This article is largely referenced from the Microsoft MSDN library and assumes a working knowledge of the Microsoft .NET Framework, threading, and the .NET Asynchronous Programming Model. MSDN, amongst others, insist that the future of application programming involves dividing your application into multithreaded parts, because of the growth of the microprocessor industry. Moreover, MSDN .NET developers insist that the future of multithreading involves dividing your application into callback sections. If this article appears too referenced without original content, then please let me know what constructive advice I should take. If you are creating a Microsoft Windows Forms application, and have an object with methods that may take some time to execute, you may want to consider writing an asynchronous API. Say, for instance, you have an object that downloads large files from a remote location. Without an asynchronous API, a client's UI would freeze for the duration of the call. With an asynchronous UI, the client's UI would not freeze. You could even construct the asynchronous API in such as way as to give progress updates to the caller, and give the client the opportunity to cancel the call. Few situations are as frustrating as a frozen UI that can only be cancelled by resorting to Task Manager to kill the process.
That said, several issues need to be made viable if you want to call objects asynchronously and build asynchronous APIs for your own business objects. This article will focus on these issues by walking through building an asynchronous API for a simple business object. We will also walk through the use and implementation of helper classes which simplify the task of implementing an asynchronous API. These helper classes are included in the sample code; they should be helpful in writing your own asynchronous APIs. Try to keep an open mind as we analyze these issues; they will appear complex at first, but are actually simple. If, in my limited knowledge, I can gain a grasp on them, then they are not too complex for the normal developer.
A Simple Business Object
Our business object has two methods: Method
and GetNextChunk
. Method
takes a string
and returns a string
, and includes a 4-second call to sleep
to simulate a long call. GetNextChunk
simulates getting data from a query in pieces, and also has a built-in delay.
public struct Customer
{
private string _FirstName;
public string FirstName
{
get { return _FirstName; }
set { _FirstName = value; }
}
}
public class BusinessObject
{
public string Method(string append)
{
Thread.Sleep (4000);
return "asyncstring: " + append;
}
public Customer[] GetNextChunk( int chunksize )
{
Random r = new Random ();
Customer[] cus = new Customer [chunksize];
Customer c = new Customer();
for ( int i = 0 ; i < chunksize; i ++ )
{
cus[i].FirstName = r.Next(3000).ToString ();
Thread.Sleep (200);
}
return cus;
}
}
Here is the catch: the problem with having a Microsoft Windows Forms client call these APIs is that they will freeze the UI for significant periods of time. We will have to implement an asynchronous API for the Windows Forms client code writer to use. In this article, we will focus on just that.
Asynchronous API
If we want to implement an asynchronous API for our class, it makes sense to follow the pattern used in the .NET Framework for asynchronous APIs. Moreover, it makes even better sense to follow some of Wintellect’s Power Threading library examples. The .NET Framework makes a method such as GetResponse
asynchronous by implementing a BeginGetResponse
method and an EndGetResponse
method. Take, for example, this excerpt from System.Net.WebRequest
:
public virtual WebResponse GetResponse();
public virtual IAsyncResult BeginGetResponse(
AsyncCallback callback,
object state
);
public virtual WebResponse EndGetResponse(
IAsyncResult asyncResult
);
Following this pattern, we will implement the following four methods: BeginMethod
, EndMethod
, BeginGetNextChunk
, and EndGetNextChunk
. In order for these methods to return immediately to the caller, we cannot call Method
or GetNextChunk
on the thread executing inside these asynchronous APIs. Instead, we will need to queue the calls to the synchronous method, and the client callback to another thread. We will leverage the System.Threading.ThreadPool
class in the .NET Framework to do this. Furthermore, in order to simplify the implementation of BeginGetNextChunk
and EndGetNextChunk
, we will leverage several helper classes. Let's first take a look at the business objects asynchronous API, which is implemented with these helper classes. Then, we can jump into the details of these classes to see how they work.
public class BusinessObjectAsync
{
protected delegate string MethodEventHandler(string append);
protected delegate Customer[] GetNextChunkEventHandler(int chunksize);
public IAsyncResult BeginGetNextChunk( int chunksize, AsyncCallback
callback, object state )
{
Aynchronizer ssi = new Asynchronizer ( callback, state );
return ssi.BeginInvoke ( new GetNextChunkEventHandler (
this.GetNextChunk ), new object [] { chunksize } );
}
public Customer[] EndGetNextChunk(IAsyncResult ar)
{
AsynchronizerResult asr = ( AsynchronizerResult ) ar;
return ( Customer[] ) asr.SynchronizeInvoke.EndInvoke ( ar);
}
}
With a delegate to our synchronous method, we can use Asynchronizer.BeginInvoke
and Asynchronizer.EndInvoke
to implement our methods. For instance, our sample business object has the method GetNextChunk
, which takes an integer as a parameter and returns a customer array. So we declare the delegate:
protected delegate Customer[] GetNextChunkEventHandler(int chunksize);
and pass an instance of it to Asynchronizer.BeginInvoke
. So now, it’s time to take a more detailed look at the helper classes that enabled the simple solution above:
AsyncUIHelper Classes
Table 1
AsynchronizerResult Class | Description |
---|
public object AsyncState | Gets a user-defined object that qualifies or contains information about an asynchronous operation. |
public WaitHandle AsyncWaitHandle | Gets a <link tabindex="0" keywords= "frlrfSystemThreading WaitHandleClassTopic" /> WaitHandle </link /> that is used to wait for an asynchronous operation to complete. |
public bool CompletedSynchronously | Gets an indication of whether the asynchronous operation completed synchronously. |
public bool IsCompleted | Gets an indication of whether the asynchronous operation completed synchronously. |
public AsynchronizerResult (Delegate method, object[] args, AsyncCallback callBack, object asyncState, ISynchronizeInvoke async, Control ctr) | A constructor that initializes AsynchronizerResult with a delegate to the synchronous method (method ), the delegate to call back the client (callBack ), the client state to pass back to the client (asyncState ), a placeholder for the Asynchronizer object that created AsynchronizerResult , and a Control to call Control.Invoke on (ctr ). |
public void DoInvoke(Delegate method, object[] args) | Calls the delegate to the synchronous method through Delegate.DynamicInvoke . |
private void CallBackCaller() | Calls the client callback delegate that was passed to the constructor. |
public object MethodReturnedValue | Returns the value from the call to the synchronous method. |
Table 2
Asynchronizer Class
| Description
|
---|
public bool InvokeRequired | Gets a value indicating whether the caller must call <link tabindex="0" keywords= "frlrfSystemComponentModel ISynchronizeInvokeClassInvokeTopic" /> Invoke </link / > when calling an object that implements this interface. |
public IAsyncResult BeginInvoke(Delegate method, object[] args) | Takes the delegate to the synchronous method, and queues it up for execution on a thread pool. The thread pool calls AsynchronizerResult.DoInvoke to execute. |
public object EndInvoke(IAsyncResult result) | Gets the value from the synchronous method call by inspecting AsynchronizerResult.ReturnedValue . |
public object Invoke(Delegate method, object[] args) | Invokes the delegate to the synchronous method synchronously by calling Delegate.DynamicInvoke . |
public Asynchronizer(AsyncCallback callBack, object asyncState) | A constructor that initializes the object with the delegate to call back the client (callBack ), and the client state to pass back to the client (asyncState ). |
public Asynchronizer(Control control, AsyncCallback callBack, object asyncState) | A constructor that initializes the Asynchronizer with the Control to call Control.Invoke on (control ), the delegate to call back the client (callBack ), and the client state to pass back to the client (asyncState ). |
Table 3
Util Class
| Description
|
---|
public static void InvokeDelegateOnCorrectThread (Delegate d, object[] args) | Inspects the delegate's Target property, and if it is a subclass of Control , calls the delegate through Control.Invoke . |
How These Two Work Together
The constructor for Asynchronizer
saves the client callback function that will be called when the synchronous method completes. It also saves the state the client wants maintained, which is passed back to the client during callback.
public Asynchronizer( AsyncCallback callBack, object asyncState)
{
asyncCallBack = callBack;
state = asyncState;
}
Asynchronizer.BeginInvoke
uses the help of AsychronizerResult
to queue up calls to the synchronous method, and sets the client callback delegate to execute when this method completes. The call to Asynchronizer.DoInvoke
is queued with the help of the .NET Framework class ThreadPool.QueueUserWorkItem
.
public IAsyncResult BeginInvoke(Delegate method, object[] args)
{
AsynchronizerResult result = new AsynchronizerResult ( method, args,
asyncCallBack, state, this, cntrl );
WaitCallback callBack = new WaitCallback ( result.DoInvoke );
ThreadPool.QueueUserWorkItem ( callBack ) ;
return result;
}
AsychronizerResult.DoInvoke
calls the synchronous method of the business object, and then executes the client callback by calling CallBackCaller()
.
public void DoInvoke(Delegate method, object[] args)
{
returnValue = method.DynamicInvoke(args);
canCancel = false;
evnt.Set();
completed = true;
CallBackCaller();
}
Asychronizer.EndInvoke
gets a value from the synchronous method call by inspecting AsynchronizerResult.MethodReturnedValue
. The return value from this method is the return value from the call to the synchronous method.
public object EndInvoke(IAsyncResult result)
{
AsynchronizerResult asynchResult = (AsynchronizerResult) result;
asynchResult.AsyncWaitHandle.WaitOne();
return asynchResult.MethodReturnedValue;
}
Using These Helper Classes
As you will recall, the business object programmer who wants to implement an asynchronous API needs to implement a BeginMethod
and EndMethod
for any method he wants to expose asynchronously. This can be achieved by delegating the work to the helper methods Asynchronizer.BeginInvoke
and Asychronizer.EndInvoke
. Here again is the sample we saw above implementing BeginGetNextChunk
and EndGetNextChunk
. You should now understand how Asynchronizer
and AsynchronizerResult
work together to enable this straightforward implementation:
public IAsyncResult BeginGetNextChunk( int chunksize,
AsyncCallback callback, object state )
{
Asynchronizer ssi = new Asynchronizer ( callback, state );
return ssi.BeginInvoke ( new GetNextChunkEventHandler (
this.GetNextChunk ), new object [] { chunksize } );
}
public Customer[] EndGetNextChunk(IAsyncResult ar)
{
AsynchronizerResult asr = ( AsynchronizerResult ) ar;
return ( Customer[] ) asr.SynchronizeInvoke.EndInvoke ( ar);
}
How a Windows Forms Client Uses an Asynchronous API of the Business Object
Let's see this in action on the client side. This is how a Windows Forms client can use BeginGetNextChunk
and EndGetNextChunk
:
private void btnAsync_Click(object sender, System.EventArgs e)
{
AsyncUIBusinessLayer.BusinessObjectAsync bo = new
AsyncUIBusinessLayer.BusinessObjectAsync ();
CurrentAsyncResult = bo.BeginGetNextChunk (20, new AsyncCallback (
this.ChunkReceived ), bo );
this.statusBar1.Text = "Request Sent";
}
public void ChunkReceived (IAsyncResult ar)
{
this.label2.Text = "Callback thread = " +
Thread.CurrentThread.GetHashCode ();
BusinessObjectAsync bo = (BusinessObjectAsync ) ar.AsyncState ;
Customer [] cus = bo.EndGetNextChunk( ar );
this.dataGrid1.DataSource = cus;
this.dataGrid1.Refresh ();
this.statusBar1.Text = "Request Finished";
}
OK. So far so good. But there is an inevitable problem lurking here. The callback from the thread pool to the Windows Forms client occurs on a thread-pool thread, and not on the Windows Forms thread. This is not the supported way of executing callbacks to a Windows Form. In fact, the only methods of a control that can be called on a different thread are Invoke
, BeginInvoke
, EndInvoke
, and CreateGraphics
. Here, the developer will need to know to only call other methods of Control
through Control.Invoke
inside of the callback handlers. Below is an example implementation of a safe callback from a client's call to BeginGetNextChunk
. The callback ChunkReceivedCallbackSafe
immediately uses Control.Invoke
to execute any code that updates the UI.
public void UpdateGrid (AsyncUIBusinessLayer.Customer[] cus)
{
this.lblCallback.Text = "Callback thread = " +
Thread.CurrentThread.GetHashCode ();
this.dataGrid1.DataSource = cus;
this.dataGrid1.Refresh ();
this.statusBar1.Text = "Request Finished";
}
public void ChunkReceivedCallbackSafe(IAsyncResult ar)
{
this.lblCallback.Text = "Callback thread = " +
Thread.CurrentThread.GetHashCode ();
AsyncUIBusinessLayer.BusinessObjectAsync bo =
(AsyncUIBusinessLayer.BusinessObjectAsync ) ar.AsyncState ;
AsyncUIBusinessLayer.Customer [] cus = bo.EndGetNextChunk( ar );
if (this.InvokeRequired )
{
this.Invoke( new delUpdateGrid (this.UpdateGrid ), new object[]
{cus});
}
else
{
UpdateGrid (cus);
}
}
Making Business Object Callbacks Safe to Call from Controls
It is possible for a class that implements an asynchronous API to do more of the work, simplifying the work of the client programmer. In this article, we'll explore two approaches to accomplish this:
- Including
Control
as a parameter to the asynchronous API. Any callbacks to Control
go through Control.Invoke
. - Investigating
Delegate.Target
, which holds the object that has the callback. If this is a Control
, callback occurs through Control.Invoke
.
Pass Control to Business Object
These additions to Asynchronizer
allow Control
to be passed in so that callback can be through this control's Invoke
method. We add a parameter to the constructor of Asynchronizer
that takes a control as input. This will be the control to call Control.Invoke
on. We need to modify BeginGetNextChunk
as well, so that this control is passed in. We do so by implementing BeginGetNextChunkOnUIThread
, so that the first parameter is a control to execute callback on.
private IAsyncResult BeginGetNextChunkOnUIThread( Control control,
int chunksize, AsyncCallback callback, object state )
{
Asynchronizer ssi = new Asynchronizer ( control, callback, state);
return ssi.BeginInvoke ( new GetNextChunkEventHandler
(this.GetNextChunk ), new Object [] { chunksize } );
}
private Customer[] EndGetNextChunkOnUIThread(IAsyncResult ar)
{
return base.EndGetNextChunk ( ar ) ;
}
public Asynchronizer( Control control, AsyncCallback callBack, object
asyncState)
{
asyncCallBack = callBack;
state = asyncState;
cntrl = control;
}
private void CallBackCaller()
{
if ( resultCancel == false )
{
if (onControlThread)
{
cntrl.Invoke ( asyncCallBack, new object [] { this } );
}
else
{
asyncCallBack ( this );
}
}
}
Below is an example of a Windows Forms client using this API by passing the Windows Forms this
pointer to BeginGetNextChunkOnUIThread
:
private void btnAsyncOnUIThread_Click (object sender, System.EventArgs e)
{
BusinessObjectAsync bo = new BusinessObjectAsync ();
CurrentAsyncResult = bo.BeginGetNextChunkOnUIThread (this, 20,
new AsyncCallback (this.ChunkReceived ), bo );
this.statusBar1.Text = "Request Sent";
}
public void ChunkReceived (IAsyncResult ar)
{
this.lblCallback.Text = "Callback thread = " +
Thread.CurrentThread.GetHashCode ();
AsyncUIBusinessLayer.BusinessObjectAsync bo =
(AsyncUIBusinessLayer.BusinessObjectAsync ) ar.AsyncState ;
AsyncUIBusinessLayer.Customer [] cus = bo.EndGetNextChunk( ar );
this.dataGrid1.DataSource = cus;
this.dataGrid1.Refresh ();
this.statusBar1.Text = "Request Finished";
}
Use Delegate.Target to Test if Callback is on a Windows Forms Control
Unfortunately, using BeginGetNextChunkOnUIThread
places a burden on the client programmer to remember to use this API, versus BeginGetNextChunk
, which can be used by non-Windows Forms clients. But, there is a better way. We can take advantage of the fact that any delegate includes the Target
property. This property holds the target object for the delegate. We can therefore inspect this property to determine whether or not a delegate callback is taking place in a Windows Form, by determining whether or not Target
is a subclass of Control
. Like so:
public Asynchronizer( AsyncCallback callBack, object asyncState)
{
asyncCallBack = callBack;
state = asyncState;
if (callBack.Target.GetType().IsSubclassOf
(typeof(System.Windows.Forms.Control)))
{
cntrl = (Control) callBack.Target ;
}
}
Using this method, we place no burden on the client to pass in the control the client is running on to the business object. So, we don't have to pass the Windows Forms this
pointer into the call to BeginNextChunk
.
private void btnAsyncOnUI_Click(object sender, System.EventArgs e)
{
AsyncUIBusinessLayer.BusinessObjectAsync bo = new
AsyncUIBusinessLayer.BusinessObjectAsync ();
CurrentAsyncResult = bo.BeginGetNextChunk (20, new AsyncCallback (
this.ChunkReceived ), bo );
this.statusBar1.Text = "Request Sent";
}
Using Components to Simplify Client Callback Code
Implementing an asynchronous API requires client-side programmers to be familiar with the .NET Async Programming pattern. Programmers need to be familiar with the BeginMethod
and EndMethod
Model, and with
the use of IAsyncResult
. You can expose an alternative asynchronous API to your class with the help of events. For our sample business class, we can add the GetNextChunkCompleteEvent
event to the class. This way, we can get rid of the requirement to pass a callback to the asynchronous method call. Instead, the client adds and removes handlers for this event. Here is this new API for the business object:
public event GetNextChunkComponentEventHandler GetNextChunkCompleteEvent;
public void GetNextChunkAsync(int chunksize, object state )
{
if (GetNextChunkCompleteEvent==null)
{
throw new Exception ("Need to register event for callback.
bo.GetNextChunkEventHandler += new
GetNextChunkComponentEventHandler (this.ChunkReceived );");
}
GetNextChunkState gState = new GetNextChunkState ();
gState.State = state;
gState.BO = bo;
bo.BeginGetNextChunk (chunksize, new AsyncCallback (
this.ChunkReceived ), gState);
}
private void ChunkReceived( IAsyncResult ar)
{
GetNextChunkState gState = (GetNextChunkState) ar.AsyncState ;
AsyncUIBusinessLayer.BusinessObjectAsync b = gState.BO ;
Customer[] cus = b.EndGetNextChunk (ar);
AsyncUIHelper.Util.InvokeDelegateOnCorrectThread (
GetNextChunkCompleteEvent, new object[] { this, new
GetNextChunkEventArgs ( cus, gState.State) });
}
If the business object is a component, the client event handlers can be set through the UI. If the business object component has been dragged onto the design surface of the client, as in Figure 1, then you can select properties of this component to set the event handlers. On the client side, because we no longer call EndGetNextChunk
to get the results of the method call, we use GetNextChunkEventArgs
, which has the Customers
property to pull off the array of customers (after dragging and dropping the business component onto the form).
private void btnAsyncThroughComponent2_Click(object sender,
System.EventArgs e)
{
businessObjectComponent1.GetNextChunkAsync (15, this.dataGrid2 );
this.statusBar1.Text = "Request Sent";
}
private void businessObjectComponent1_GetNextChunkComplete(object sender,
BusinessObjectComponent.GetNextChunkEventArgs args)
{
this.lblCallback.Text = "Callback thread = " +
Thread.CurrentThread.GetHashCode ();
AsyncUIBusinessLayer.Customer[] cus = args.Customers ;
DataGrid grid = (DataGrid) args.State ;
grid.DataSource = cus;
grid.Refresh ();
}
References