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.
Just a thought!
Posted
Wed, Mar 2 2005 1:31 AM
by
mtaulty