Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Writing an Asynchronous API for a Windows Forms Client Application

4.59/5 (11 votes)
2 Sep 2009CPOL10 min read 57.6K   850  
An article with a goal of simplifying multithreading on a .NET client application.

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.

C#
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:

C#
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.

C#
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:

C#
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 ClassDescription
public object AsyncStateGets a user-defined object that qualifies or contains information about an asynchronous operation.
public WaitHandle AsyncWaitHandleGets a <link tabindex="0" keywords= "frlrfSystemThreading WaitHandleClassTopic" /> WaitHandle </link /> that is used to wait for an asynchronous operation to complete.
public bool CompletedSynchronouslyGets an indication of whether the asynchronous operation completed synchronously.
public bool IsCompletedGets 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 MethodReturnedValueReturns the value from the call to the synchronous method.
Table 2

Asynchronizer Class

Description

public bool InvokeRequiredGets 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.

C#
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.

C#
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().

C#
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.

C#
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:

C#
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:

C#
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.

C#
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:

  1. Including Control as a parameter to the asynchronous API. Any callbacks to Control go through Control.Invoke.
  2. 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.

C#
//Business Object
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 ) ;
}

//Asynchronizer
public Asynchronizer( Control control, AsyncCallback callBack, object 
   asyncState)
{
   asyncCallBack = callBack;
   state = asyncState;
   cntrl = control;
}

//AsynchronizerResult
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:

C#
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:

C#
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.

C#
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:

C#
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).

C#
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

  • the MSDN library

Capture.JPG

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)