When you're writing Windows Forms applications it's always nice to be able to keep the UI responsive in order to avoid frustrating the user. I seem to remember reading a UI study ages back which said that if the UI hangs for about 1/10th of a second the user can detect it so you don't get a lot of time to play with.
There's a lot of asynchronous support in the .NET framework that helps you do work "on other threads". Some of that support is done intrinsically by classes such as the FileStream
or HttpWebRequest
and othertimes you get given something that's synchronous in its operation but you make it asynchronous by using a delegate and invoking the BeginXXX operation which will move the work to the managed ThreadPool.
Either way, at this point with Windows Forms it gets a bit trickier because the callback that you'll get is going to happen on another thread and UI controls have thread affinity which means you can't touch them from that other thread. This leads into the whole Control.Invoke
pattern for making sure that what you do happens on the UI thread but it complicates programming quite a bit. The documentation for that is over here.
However, something that you don't see flagged very often is the possibility of completing your asynchronous work on the Application
Idle loop rather than allowing another thread to wander into your Windows Forms code and then trying to deal with it by Control.Invoke
I wrote this little class to manage a list of work that needs to be completed and execute the callbacks on the Idle loop of the Windows Forms application. This means that callbacks would occur on your main thread rather than some other thread and hence, no need for locking or Control.Invoke. Here's the class (didn't spend a long time on this so it might not be exactly right so beware of that);
public class AsyncIdleQueue
{
private AsyncIdleQueue()
{
}
private class QueuedDetails
{
public QueuedDetails(IAsyncResult result, AsyncCallback callback)
{
_result = result;
_callback = callback;
}
public bool InvokeIfComplete()
{
bool done = _result.IsCompleted;
if (done)
{
_callback(_result);
}
return(done);
}
private IAsyncResult _result;
private AsyncCallback _callback;
}
static AsyncIdleQueue()
{
Application.Idle += new EventHandler(OnApplicationIdle);
}
public static void AddEntry(IAsyncResult result, AsyncCallback callback)
{
Entries.Add(new QueuedDetails(result, callback));
}
private static ArrayList Entries
{
get
{
if (_entries == null)
{
_entries = new ArrayList();
}
return(_entries);
}
}
private static void OnApplicationIdle(object sender, EventArgs e)
{
for (int i = 0; i < Entries.Count; ++i)
{
QueuedDetails d = (QueuedDetails)Entries[i];
if (d.InvokeIfComplete())
{
Entries.Remove(d);
--i;
}
}
}
private static ArrayList _entries;
}
I can make use of this with code such as;
FileStream fs = new FileStream(d.FileName, FileMode.Open, FileAccess.Read);
byte[] array = new byte[fs.Length];
IAsyncResult r = fs.BeginRead(array, 0, array.Length, null,
new object[] { fs, array} );
AsyncIdleQueue.AddEntry(r, new AsyncCallback(OnReadComplete));
Note that here we don't supply a callback directly to the BeginRead operation, we instead supply one when we call AddEntry
on the AsyncIdleQueue
and when we get called back we can do something like;
private void OnReadComplete(IAsyncResult result)
{
object[] state = (object[])result.AsyncState;
FileStream fs = (FileStream)state[0];
byte[] array = (byte[])state[1];
fs.EndRead(result);
}
to complete our asynchronous operation. In doing that we haven't directly been involved with any threading issues whatsoever and there's no need to call Control.Invoke
or anything like that.
Comments