Click here to Skip to main content
15,879,239 members
Articles / Programming Languages / C#

Writing an Asynchronous API for a Windows Forms Client Application

Rate me:
Please Sign up or sign in to vote.
4.59/5 (11 votes)
2 Sep 2009CPOL10 min read 56.4K   845   61   12
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)


Written By
Software Developer Monroe Community
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
AnswerOn the issue of ' Stolen from MSDN' Pin
sutomi20053-Mar-12 22:56
sutomi20053-Mar-12 22:56 
RantStolen from MSDN. Pin
akidan2-Sep-09 11:48
akidan2-Sep-09 11:48 
GeneralRe: Stolen from MSDN. Pin
logicchild2-Sep-09 17:55
professionallogicchild2-Sep-09 17:55 
RantRe: Stolen from MSDN. Pin
akidan3-Sep-09 5:46
akidan3-Sep-09 5:46 
GeneralRe: Stolen from MSDN. Pin
Greizzerland9-Sep-09 1:11
Greizzerland9-Sep-09 1:11 
GeneralRe: Stolen from MSDN. Pin
a codeproject fan7-Sep-09 20:03
a codeproject fan7-Sep-09 20:03 
GeneralThreadPool traps Pin
Paulo Zemek1-Sep-09 3:16
mvaPaulo Zemek1-Sep-09 3:16 
GeneralRe: ThreadPool traps Pin
logicchild1-Sep-09 18:10
professionallogicchild1-Sep-09 18:10 
Hi, and thanks for the feedback. For what it's worth, asynchronicity and its pursuit began when Win32 developers became used to accessing APIs synchronously. As you stated, a thread-synchronous operation (and in particular, and I/O operation) means that the entire thread waits until the I/O operation is complete; a thread initiated some task, and then waits patiently for the task to complete. If the code reaches a higher level of sophistication, it could create a worker thread to make the synchronous call, freeing the main thread to continue its work. Using worker threads to perform lengthy blocking calls is crucial for GUI applications because blocking the thread that pumps the message queue disables the user interface of the application. But when a thread is blocked, other threads are created that begin to consume resources. They may not be consuming CPU cycles, but they will degrade performance because of that resource consumption. And documentation insists that thread creation is costly. But more importantly, in terms of I/O, delays caused by track and sector seek time on random access devices (such as discs and DVDs), delays caused by the relatively slow data transfer rate between a physical device and memory, and delays in network data transfer using file servers, storage-area networks, et. al, all contribute to either a delay or a routine that requires too long a time without an asynchronous call to a method. In .NET, when a thread makes an asynchronous call to a method, the caller returns immediately. The caller thread is not blocked; it is free to perform some other task. The .NET infrastructure obtains a thread for the method invocation and delivers the parameters passed by the calling code.

A useful underlying concept involves the Stream class of the System.IO namespace. The Stream class enables asynchronous reads and writes by using the the APM pattern, which enables scalable I/O -- for example, I/O completion ports on Windows -- and permits threads to make forward progress in parallel with the stream operation. This can help if you are responding to an event on the UI thread; in such cases, you want to avoid blocking the UI(leading to "hangs", "Not Responding", title bars, etc). Now notice the pattern for BeginRead and EndRead:

public virtual IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state);
public virtual int EndRead (IAsyncResult asyncResult);

public virtual IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state);
public virtual void EndWrite(IAsyncResult asyncResult);

Now here is where the magic of the asynchronous delegate comes in. Consider this delegate declaration:
public delegate int (string str);

This typical delegate code will cause the C# compiler to generate a class upon encountering the delegate keyword. The two important methods are:

public virtual IAsyncResult BeginInvoke(string str, AsyncCallback asc, object stateObject)
public virtual int EndInvoke(IAsyncResult result);

You see, the BeginInvoke starts the asynchronous call to the method, and then the infrastructure queues (puts on sort of waiting list) the method to run on a thread pool thread and create synchronization objects as needed to determine if the method is completed. Now one thing to note here. You are very correct. The default number of threads that the thread pool can create is 25, but that is a minimum number. I believe that maximum number is 1000 threads. But the thread pool has a way of keeping track of threads in an application, so as to start eliminating threads during an application's time where there is less activity; the thread pool will start to create more threads should the application get busier in its execution (perhaps by more user interaction, etc). Having said that, the technical guru’s also content that they try to avoid using the Asynchronous Programming Model for the very reason that you have divide your application code into callback sections.
Strangely though however, my purpose in this article was to show an example of control. BeginInvoke, something that does not use a matching EndInvoke, but rather is meant for Windows Forms applications where a control will require a callback delegate that will takes some time to finish its handling routine. There is an ongoing question mark about its usage if you Google it. But the name of the game is to avoid having that UI freeze
Thanx.
GeneralAsyncOperation.PostOperationCompleted Pin
DaveyM691-Sep-09 1:12
professionalDaveyM691-Sep-09 1:12 
GeneralRe: AsyncOperation.PostOperationCompleted Pin
logicchild1-Sep-09 18:14
professionallogicchild1-Sep-09 18:14 
GeneralGood Job Pin
czecke31-Aug-09 23:01
czecke31-Aug-09 23:01 
GeneralVery Good Pin
Anthony Daly31-Aug-09 22:57
Anthony Daly31-Aug-09 22:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.