Export (0) Print
Expand All
Build Providers for Windows Forms
Draft a Rich UI: Ground Rules for Building Enhanced Windows Forms Support into Your .NET App
A New Grid Control in Windows Forms
Owner-Drawing in .NET
P2P Comm Using Web Services
Smart Clients: Build A Windows Forms Control To Consume And Render WSRP Portlets
Spice It Up: Sprinkle Some Pizzazz on Your Plain Vanilla Windows Forms Apps
Synchronizing Multiple Windows Forms
Text Rendering: Build World-Ready Apps Using Complex Scripts In Windows Forms Controls
Windows Forms Controls: Z-order and Copying Collections
Winning Forms: Practical Tips For Boosting The Performance Of Windows Forms Apps
Code Samples
Expand Minimize

Creating Asynchronous Business Objects for Use in .NET Windows Forms Clients

 

Kurt Schenk
Microsoft Corporation

October 25, 2002

Applies to:
   Microsoft® Windows® Forms
   Microsoft® .NET Framework

Summary: Writing asynchronous APIs for your business objects will prevent freezing a Microsoft Windows Forms Client UI while your object's methods execute. Walks through building an asynchronous API for a simple business object, and shows how to use helper classes to greatly simplify the process. (15 printed pages)

Download AsyncUI_Code.exe.


Contents

Overview
Simple Business Object
Asynchronous API
AsyncUIHelper Classes
How Asynchronizer and AsynchronizerResult Work Together
Using These Helper Classes
How Windows Forms Client Uses an Asynchronous API of the Business Object
Making Business Object Callbacks Safe to Call from Controls
Using Components to Simplify Client Callback Code
Conclusion

Overview

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 cancel the call. Few situations are as frustrating as a frozen UI that can only be cancelled by resorting to Task Manager.

That said, a variety of issues need to be tackled if you want to call objects asynchronously and build asynchronous APIs for your own business objects. This article will examine 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 drastically 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. Keep in mind that although we will delve into the implementation details of these helper classes, which is complex, using them is quite simple.

This article assumes familiarity with the Microsoft® .NET Framework, threading, and the .NET Asynchronous Programming Model.

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;
   }
}

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 can either force the client to call our synchronous API asynchronously through a client-side delegate, or we will have to implement an asynchronous API for the Windows Forms client code writer to use. In this article we will focus on the latter option.

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. 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 4 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.

Lets take a more detailed look at the helper classes that enabled the simple solution above.

AsyncUIHelper Classes

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 WaitHandle 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); client state to pass back to client (asyncState); a placeholder for Asynchronizer object that created AsynchronizerResult; and 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 call to the synchronous method.

   

Asynchronizer Class Description
public bool InvokeRequired Gets a value indicating whether the caller must call Invoke 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).

   

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 Asynchronizer and AsynchronizerResult 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 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 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 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";
}

All is well and good, but there is an unexpected 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.

One approach is to leave the problem totally in the hands of the client programmer. The programmer will need to know to only call other methods of Control through Control.Invoke inside of 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:

  1. Including Control as a parameter to that 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 the 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.

//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 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.

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 Forms, 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

We have made significant progress in simplifying the job of implementing an asynchronous API, but the disadvantage of the above model is that it 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) });
}

The great advantage of this is that 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 (Figure 2 below).

Figure 1. Drag the component onto the form

Click here for larger image.

Figure 2. Set component 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.

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 ();
}

Conclusion

Freezing a client's UI while your business object's methods execute can readily be avoided by writing asynchronous APIs for your objects. Though writing a solution on your own would involve some work, the job can be greatly simplified by using the helper classes AsynchronizerResult, Asynchronizer, and Util available in the sample code and discussed in this article. The result for your clients (in saved frustration) can be well worth your effort.

Show:
© 2014 Microsoft