Monday, August 07, 2006

Make Asynchronous User Interface Programming Easier

So it's becoming quite popular to write 'Asynchronous user interface code' these days. Particularly in 'Smart Clients', where, often a Windows Forms application, calls a web service that may take a noticeable amount of time to return it's result.

This is great from the user's perspective, the user interface remains responsive, the application won't be reported as 'not responding' by Windows or Task Manager, and the user may be able to perform other operations in the same application while waiting.

The problem is while it's simple, it's also quite tedious, to write multi-threaded event handlers for buttons. First you have to start your thread, using either a new Thread object, the ThreadPool object or a BackgroundWorker component. Then you have to perform your long-running operation on that other thread, using a delegate. When that's over, if you want to display the results, you need to use Control/Form.Invoke passing another delegate to a function that will load the results back into the user interface.

Ok, so as I said this isn't hard, but creating (probably) two methods per button, creating and starting the other thread , and then invoking the second method is a lot of work if you have a lot of buttons you want to run Asynchronously.

The good news is, you can simplify this by writing your own Asynchronous Button control (or you can use mine). Better yet, writing the control isn't hard at all.

First, start a new Windows Control Library project in Visual Studio. Setup the project properties (company name, assembly title etc.) the way you want, and delete the user control VS added automatically for you.

Next, add a Custom Control to the project. Open the code for the control and change it's class declaration so it inherits from Button, like this;


[DefaultEvent("AsyncClick")]
public partial class AsynchronousButton : Button

Notice the use of the DefaultEvent attribute, this tells Visual Studio which event to create a handler for when you double click a control in the designer. We're going to create an AsyncClick event that runs on a separate thread, so we've declared that as our default.

Also, don't forget to rename the default constructor VS created for us, so it's name matches the control name - AsynchronousButton. While we're playing the default code, remove the override for OnPaint too, we don't need it since we've inherited from Button and we're not doing any custom drawing.

Now add a new event called AsyncClick, and replace the inherited Click event like so;

public event
EventHandler<AsynchronousClickEventArgs> AsyncClick;
public new event
EventHandler<AsynchronousClickEventArgs> Click;
Next, override the OnClick method of the parent
class. This method usually raises the Click event.
We're going to change it so that it raises our
AsyncClick event.
protected override void OnClick(EventArgs e)
{
//Check to see if anyone is subscribed to our
//event.

if (AsyncClick != null)
{
//Start a new thread, and use it to run a method
//that actually raises the event. Also note,
//we're using the thread pool here but you might
//want to use some other mechanism for
starting
//the thread.

System.Threading.ThreadPool.QueueUserWorkItem(
new System.Threading.WaitCallback(
this.RaiseAsyncClickEvent));
}
}



Ok, now that's done we need a new private method called RaiseAsyncClickEvent. Add one that looks like this;


private void RaiseAsyncClickEvent(object state)
{
AsynchronousClickEventArgs e =
new AsynchronousClickEventArgs(
System.Threading.Thread.CurrentThread);

//Ensure there's still at least one event handler
//connected to our event.

if (AsyncClick != null)
{
//Actually raise the event.
AsyncClick(this, e);

//Ok, so now we've performed our long-running
//operation,
let's raise the normal click
//event on the same thread the button was
//created on. This gives us the opportunity
//to update the UI after our async
operation.
if (Click != null)
this.Invoke(new
System.Threading.ParameterizedThreadStart(
RaiseClickEvent), e);
}
}



And finally, we need another private method called RaiseClickEvent which actually raises the click event.


private void RaiseClickEvent(object state)
{
if (Click != null)
Click(this, (AsynchronousClickEventArgs)state);
}



That's the control complete ! But we're not done yet, one last step. Add the following class to your project;


public class AsynchronousClickEventArgs
: EventArgs
{

private System.Threading.Thread thread;
private object state;

public AsynchronousClickEventArgs()
{
}


public AsynchronousClickEventArgs(
System.Threading.Thread thread)
{
this.thread = thread;
}

public object State
{
get { return state; }
set { state = value; }
}

public System.Threading.Thread Thread
{
get { return thread; }
set { thread = value; }
}
}



The above class is used to provide parameters to the event handlers for both the AsyncClick and Click events. Note the 'State' property. This can be set to any value (or array/collection of values) you like during your AsyncClick event handler. It can then be read by your Click event handler. For example, suppose your AsyncClick event handler executes a SQL query and fills a dataset with your result. You can set e.State = myDataSet during your AsyncClick event handler, then in your Click event handler you can say DataSet myDataSet = (DataSet)e.State which gives you a strongly typed reference to your data set again. You can then data bind the DataSet to a grid or whatever to update the user interface.


Once you've built your control, all you have to do to use it is;


  • Compile the control library project.
  • Open your applications project, right-click the toolbox and select 'Choose Items'.
  • Browse to the control library project assembly and add it's controls.
  • Add your AsynchronousButton control to your form and set it's properties appropriate.
  • Double click the button and write code in the AsyncClick event handler.
  • If you want to update the user interface after the Asynchronous operation then also attach an event handler to the Click event.
Of course there's a number of other things you can do. My own version of this control has the following additional features;



  • Specify how the thread should be created, New Thread, Thread Pool or Background Worker. The thread or BackgroundWorker component used available as a property on the AsynchronousClickEventArgs class.
  • If the thread is started using a new Thread object, you can specify a name and priority to be assigned to the new thread, using properties on the control.
  • Specify the control should automatically disable itself when clicked (to prevent multiple clicks during an existing operation). A separate property specifies whether or not the control automatically re-enables itself after the Click event completes successfully.
  • Naturally, the control has it's own toolbox bitmap.
  • XML documentation for the properties, methods and events of the control and it's event arguments class.
  • After raising the AsyncClick event, invoke another method on the buttons thread that raises the original Click event. This allows you to place non-UI code (such as database or webservice calls) in the AsyncClick event and then code that updates the UI with the result in the normal Click event.

No comments:

Post a Comment