Components that support the event-based asynchronous pattern significantly facilitate the use of asynchronous operations by making note of the synchronization context that is active when an asynchronous operation is started, and then using that context to make sure that the component's call to your operation-has-completed event handler later on happens in that same context. Component's pull this off by internally using the AsyncOperationManager class that made it's debut in version 2.0 of the .NET Framework. This alleviates the component consumer from having to worry about doing things like (in the case of Windows Forms programming) calling Control.InvokeRequired and Control.BeginInvoke. In the context of an ASP.NET application, this same technique ensures that things like HttpContext.Current, Thread.CurrentCulture, and Thread.CurrentUICulture are all in place when your operation-has-completed event handler is invoked by the component. The result is a unified model for dealing with asynchronous operations in library components that might be used in a variety of diverse application contexts. Components like BackgroundWorker, WebClient, and WSDL.EXE-generated web service proxies (in 2.0) all support this mechanism for asynchronous operations.
Unfortunately, not all library classes that support asynchronous operations follow this new pattern for asynchronous execution. One such example is the SqlCommand class. In version 2.0 of the .NET Framework, SqlCommand sprouted 3 new methods that support the asynchronous execution of (typically long running) queries and stored procedures: BeginExecuteReader, BeginExecuteNonQuery, and BeginExecuteXmlReader. As a result, if/when you use these methods, you need to take whatever steps are appropriate given the application environment you're executing in to deal with the fact that your command-completed callback method is going to be called in the wrong context.
Using a Windows Forms application as an example, this means that if you kick off an async operation like so...
void someButton_Click(object sender, EventArgs ea)
{
SqlConnection conn = new SqlConnection(someConnectionString);
conn.Open();
SqlCommand cmd = new SqlCommand(someQuery, conn);
SqlOperation sqlOp = new SqlOperation(conn, cmd); // SqlOperation is defined at the end of this post.
cmd.BeginExecuteReader(OnReadComplete, sqlOp);
}
...then your OnReadComplete handler had better be written to use InvokeRequired and BeginInvoke to marshal the callback over to the correct thread before you touch any of your controls:
void OnReadComplete(IAsyncResult ar)
{
if( InvokeRequired )
{
BeginInvoke(new AsyncCallback(OnReadComplete), new object[] { ar });
return;
}
SqlOperation sqlOp = (SqlOperation)ar.AsyncState;
using (SqlDataReader rdr = sqlOp.Command.EndExecuteReader(ar))
{
someBindingSource.DataSource = rdr;
}
sqlOp.Complete();
}
That's no so bad - just tedious to do throughout your program. But if you're using something like BeginExecuteReader in the context of an asynchronous ASP.NET page, then you need to resort to one of the techniques for starting and completed asynchronous tasks, which is more cumbersome than the Windows Forms InvokeRequired/BeginInvoke approach.
To facilitate using SqlCommand.BeginExecuteXxx in my own code, I've resorted to using a little helper class that takes care of using the AsyncOperationManager class to “do the right thing“ with respect to execution context. This helper class (which I call SqlAsyncOperation) acts as a trampoline between the callback coming from the SQL provider and my own code, using the AsyncOperationManager to switch between synchronization contexts before making the callback to my own code. In other words, instead of calling something like SqlCommand.BeginExecuteReader directly, I call the helper, which uses the AsyncOperationManager class to note the current synchronization context before forwarding my call to BeginExecuteReader. As part of this, the helper substitutes its own operation-complete callback method for my own. When that method is called later, the helper can make the switch to the correct synchronization context that was noted when the operation was kicked off and then call me back. As a result, all of the context shuffling goo is hidden from my main application code.
I'll show two versions of the helper class. The first, which is the one I use, takes advantage of anonymous methods to provide a nice, compact solution:
public class SqlAsyncOperation
{
public delegate IAsyncResult BeginOperationDelegate(AsyncCallback completedHandler, object state);
public static IAsyncResult Start(BeginOperationDelegate startProc, AsyncCallback completedHandler, object state)
{
AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(null);
return startProc(delegate(IAsyncResult ar) { asyncOp.PostOperationCompleted(delegate { completedHandler(ar); }, null); }, state);
}
}
With the above helper class defined, I can change how I kick off the asynchronous query from this:
cmd.BeginExecuteReader(OnReadComplete, sqlOp);
to this:
SqlAsyncOperation.Start(cmd.BeginExecuteReader, OnReadComplete, sqlOp);
Note that instead of calling cmd.BeginExecuteReader directly, I now pass a delegate to that method to SqlAsyncOperation.Start. Since BeginExecuteReader, BeginExecuteNonQuery, and BeginExecuteXmlReader all support the same method signature via one of their overloads, you can use this approach with any of those methods. That's a fairly decent bang for my buck. It's not much extra typing, but it means that I can count on my OnReadComplete method being called in the “correct“ context automagically without having to worry to much about how it was pulled off. If you're writing a Windows Forms application, you can do this whenever you like. If you're building an asynchronous page in ASP.NET, however, then this call is only valid in the context of Page_Load (due to restrictions ASP.NET places on when async operations can be kicked off). But because SqlAsyncOperation.Start uses the standard AsyncOperationManager class to interact with the current SynchronizationContext, this technique works equally as well in ASP.NET asynchronous pages or Windows Forms applications.
Internally, SqlAsyncOperation.Start uses AsyncOperationManager.CreateOperation to “start“ an async operation (really just making a note of which SynchronizationContext is current), then call cmd.BeginExecuteReader for me. As part of this, SqlAsyncOperation.Start passes a reference to an anonymous method to be invoked when the query completes. This anonymous method takes care of calling AsyncOperation.PostOperationCompleted, which switches synchronization contexts as necessary, calls the specified method (another anonymous method that calls the original caller's operation-completed handler), and then completes the async operation. The net result is that the application level code (whether Windows Forms or ASP.NET applications) can rest assured that the callback they receive when the query/sproc completes will take place in the same synchronization context that was active when the operation was originally started.
For a bit more clarity, here's the same implementation of the SqlAsyncOperation class shown above; annotated to indicate what context each anonymous method is called in (and with some gratuitous whitespace thrown in):
public class SqlAsyncOperation
{
public delegate IAsyncResult BeginOperationDelegate(AsyncCallback completedHandler, object state);
public static IAsyncResult Start(BeginOperationDelegate startProc, AsyncCallback completedHandler, object state)
{
AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(null);
// Context: original/correct context.
//
return
startProc(
delegate(IAsyncResult ar)
{
// Context: unknown/incorrect.
//
asyncOp.PostOperationCompleted(
delegate
{
// Context: original/correct.
//
completedHandler(ar);
},
null
);
},
state
);
}
}
And if the nested anonymous methods approach defined above is still too obfuscated for you even with the annotations and extra whitespace, here's a variation of the SqlAsyncOperation class that doesn't use anonymous methods at all:
public class SqlAsyncOperation
{
public delegate IAsyncResult BeginOperationDelegate(AsyncCallback completedHandler, object state);
public static IAsyncResult Start(BeginOperationDelegate startProc, AsyncCallback completedHandler, object state)
{
// Context: original caller (the “correct“ one).
//
SqlAsyncOperation helper = new SqlAsyncOperation(completedHandler, state);
return startProc(helper.OnOperationCompleted, state);
}
AsyncOperation _asyncOp;
AsyncCallback _completedHandler;
SqlAsyncOperation(AsyncCallback completedHandler, object state)
{
_completedHandler = completedHandler;
_asyncOp = AsyncOperationManager.CreateOperation(this);
}
void OnOperationCompleted(IAsyncResult ar)
{
// Context: arbitrary/the wrong one. Request a
// callback to NotifyComplete in the “correct“ context.
//
_asyncOp.PostOperationCompleted(NotifyComplete, ar);
}
void NotifyComplete(object state)
{
// Context: original caller (the “correct“ one).
//
_completedHandler((IAsyncResult)state);
}
}
At any rate, I've found using this kind of SqlAsyncOperation helper class useful. And being the twisted guy that I am, I loved how anonymous methods and its support for local variable capture saved me from all the extra typing.
P.S.
Here's the SqlOperation helper class that was used in the above code fragments.
class SqlOperation
{
public readonly SqlConnection Connection;
public readonly SqlCommand Command;
public SqlOperation(SqlConnection conn, SqlCommand cmd)
{
Connection = conn;
Command = cmd;
}
public void Complete()
{
Command.Dispose();
Connection.Dispose();
}
}
Posted
Nov 29 2005, 01:56 PM
by
mike-woodring