A Component for Easilly Executing Async Tasks

An entry about c# 3.0 | windows forms Publication date 14. October 2007 14:23

With the 2.0 version of the.NET Framework, a component aptly named BackgroundWorker was introduced to simplify the execution of asynchronous tasks in Windows Forms development. A joy to use for sure, but you still have to manually manage queueing of tasks, and give the user an indication that something is happening. Wouldn't it be cool if the BackgroundWorker could disable the button that executed the task and automagically show a progress indicator while it is being carried out? In this post I'll be showing you how to develop a custom component that encapsulates these features, and more.

Lets begin at the end, and look at what we want to accomplish.-Basically, we want to be able to write code like this:

IAsyncTask task = _asyncWorker.NewTask();
task.ControlBehaviors.Add(new EnableControlBehavior(_task2Btn, false, ApplyBehaviorWhen.TaskEnqueued));
task.ControlBehaviors.Add(new ProgressBehavior(_progressBar, 10));
task.PerformTask +=
delegate(object obj, AsyncTaskEventArgs args)
{
for (int i = 0; i < 10; i++)
{
args.ReportProgress(i);
Thread.Sleep(150);
}
};
task.Enqueue();

What we see here, is the code for the _task1Btn click event. It creates a new task, says that the button should be disabled until the task completes and a progress bar displayed while executing, and puts the task on the work queue. The Enqueue() method returns immediately, while the task is added to the work queue and executed on a background thread as soon as possible. When clicking the button, the UI looks something like this:

Disabled button and progress bar while an asynchronous task is executing

Interrested in seing how this works? Read on...

The AsyncWorker

What we want to model are two things - a work manager, and the tasks it should perform. A task consists of a work load and optionally a list of control behaviors. When a task has been defined, it is put on the work managers queue, and at some point the work manager then executes it on a background thread. Lets look at the code that accomplishes this.

When an instance of the AsyncWorker component is constructed, it fires up a background worker thread:

public AsyncWorker(IContainer container)
{
container.Add(this);
if (null == this.Site || !this.Site.DesignMode)
{
// fire up the worker thread
        _workQueueThread = new Thread(new ThreadStart(DoWork));
_workQueueThread.Start();
}
}

This thread runs the DoWork method, which loops until the component is disposed, at which time the _alive variable becomes false and the background thread terminates gracefully:

private void DoWork()
{
do
    {
_emptyQueueResetEvent.WaitOne(); // wait until work is added to the queue
        while (_tasks.Count > 0) // while work is in the queue...
        {
AsyncTask task = _tasks.Dequeue(); // ... get the next task
            Invoke(new MethodInvoker(
delegate
                {
foreach (ControlBehavior behavior in task.ControlBehaviors)
{
behavior.TaskBegins();
}
}));
task.RaiseDoWork(); // perform task
            Invoke(new MethodInvoker(
delegate
                {
foreach (ControlBehavior behavior in task.ControlBehaviors)
{
behavior.TaskFinished();
}
}));
}
}
while (_alive);
}

When there is no work on the queue, the _emptyQueueResetEvent is in the non-signaled state, blocking the thread. But when a task is added to the work queue, this reset event gets signalled and the worker thread wakes up again to process the task:

private void Enqueue(AsyncTask task)
{
Invoke(new MethodInvoker(
delegate
        {
foreach (ControlBehavior behavior in task.ControlBehaviors)
{
behavior.TaskEnqueued();
}
}));
// enqueue the task
    _tasks.Enqueue(task);
// signal the worker thread to awaken it, if it is currently idling
    _emptyQueueResetEvent.Set();
}

Notice that throughout the life-time of a task, its behaviors are notified of the state changes (from enqueued to executing to finished). This enables behaviors to do things like disable controls until the task is finished, update a status message label and more. I've implemented a few different kinds of behaviors, and writing custom ones is as easy as inheriting the ControlBehavior class and overriding the appropriate virtual methods. As an example, lets look at the DisableControlBehavior, which we used in the initial example at the top of this post to disable the Execute button until the task completed:

public class EnableControlBehavior : ControlBehavior
{
ApplyBehaviorWhen _when;
bool _enable;
public EnableControlBehavior(Control control, bool enable, ApplyBehaviorWhen when)
: base(control)
{
_when = when;
_enable = enable;
}
protected internal override void TaskEnqueued()
{
if (_when == ApplyBehaviorWhen.TaskEnqueued)
{
this.Control.Enabled = _enable;
}
}
protected internal override void TaskBegins()
{
if (_when == ApplyBehaviorWhen.TaskBegins)
{
this.Control.Enabled = _enable;
}
}
protected internal override void TaskFinished()
{
this.Control.Enabled = !_enable;
}
}

Here I've hooked into the life cycle of the task in order to disable/enable the given control at the appropriate times. The sample code includes three more behaviors - one for toggling the visibility of controls, one for updating a label with state messages and one for displaying a progress bar (either as a marquee or accurately updated with progress).

I Know You Want Me

If what I've shown so far in this post has intrigued you, then feel free to download the source code for the AsyncWorker here and take a closer look. The download includes a few examples, and the code itself is fairly well documented and should be easy to follow.

Although the component seems to be fairly robust, I feel I should advise you to test it thouroughly before considering using it in any production environment. I wrote this over the weekend to try out a few ideas I had, and as with most of the code I post here on this blog, it should be consider a proof-of-concept and not production-ready code :)

Currently rated 5.0 by 1 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Comments

Powered by BlogEngine.NET 1.4.5.0

Welcome!

My name is Fredrik Kalseth, and this is my blog - thanks for visiting! I am fortunate enough to work with what I love for a living, and this blog is essentially the biproduct of that.

I work as a senior consultant for Capgemini, and am also an active participant in the Norwegian .NET community, as an avid attendee but also as a speaker (most recently at NNUG and MSDN Live).

As a developer, I have a wide circle of interest. My primary passion is for agile, test-driven development, with focus on best practices and clean code. That said, I also love to work on the frontend, especially with web development.

On Twitter? My handle is fkalseth. On LinkedIn? I`m there too.


Disclaimer

This is a personal blog; any opinions expressed here are my own and do not necessarily reflect those of my employer. All content herein is my own original creation, and as such is protected by copyright law. Unless otherwise stated, all source code posted on this blog is freely usable under the Microsoft Permissive License.

What Readers Talk About

Comment RSS