A short digression from multimethods which the next article will continue to focus on. This article examines different ways for event handlers to run lengthy tasks asynchronously which may be updating GUI controls while running.
There are quite a few ways to call tasks asycnrhonously in .NET 3.x:
1) Do it in GUI Thread – ok, this is a cop out
2) Create thread explicitly
3) Create thread implicitly using thread pool
4) Create thread implicitly using delegate BeginInvoke
5) Use Background worker
To download source, go here
We create a task runner interface to facilitate for different implementations:
public interface ITaskRunner
{
ContentControl DisplayArea { get; set; } // DisplayArea.Content will be set while task is running.
Action DoneCB { get; set; } // Called when task is done
string Name { get; set; } // Name of task runner
int LoopCount { get; set; } // Loop countvoid Run();
}
The GUI looks like this:
Combo box offers a choice of the task runner implementation. Pressing start will start task runner and button becomes disabled until task is finished.
The start button handler:
private void btnStart_Click(object sender, RoutedEventArgs e)
{
ITaskRunner runner = GetTaskRunner();
runner.DisplayArea = lblOutput;
runner.LoopCount = 3; // 3 seconds
runner.DoneCB = EnableButton;
btnStart.IsEnabled = false;
runner.Run();
}
The EnableButton function may be called outside of GUI thread, so it uses Dispatcher to invoke enabling of button:
private void EnableButton()
{
Dispatcher.Invoke(new Action(() => { btnStart.IsEnabled = true; }));
}
To promote some code sharing, we create an abstract class DefaultTaskRunner which has common code needed by all implementations, such as definitions of the properties:
public abstract class DefaultTaskRunner : ITaskRunner
{
protected void CallDone() { … }public System.Windows.Controls.ContentControl DisplayArea { get; set; }
public int LoopCount { get; set; }
public string Name { get; set; }
public Action DoneCB { get; set; }
public abstract void Run();
DefaultTaskRunner also has a static function to return a list of task runners. Instead of hardcoding, we will use reflection to discover all concrete classes which implement the ITaskRunner interface:
public static IEnumerable<ITaskRunner> GetRunners()
{
var q = from t
in Assembly.GetAssembly(typeof(DefaultTaskRunner)).GetTypes()
where typeof(ITaskRunner).IsAssignableFrom(t) && !t.IsAbstract
select Activator.CreateInstance(t) as ITaskRunner;
return q;
}
Let’s look at some of the implementations. For example, the Delegate approach:
public override void Run()
{
Action act = RealRun;
act.BeginInvoke(null, null);
}
private void RealRun()
{
Dispatcher dispatch = DisplayArea.Dispatcher;dispatch.Invoke(new Action(
() => { DisplayArea.Content = "Running"; }));for (int i = 0; i < LoopCount; ++i) { … }
dispatch.Invoke(new Action(
() => { DisplayArea.Content = "Done"; }));CallDone();
}
ThreadPool approach is very similiar:
public override void Run()
{
ThreadPool.QueueUserWorkItem(new WaitCallback(RealRun));
}
Backgroundworker approach is the most complicated:
public override void Run()
{
m_bg.RunWorkerAsync();
}
private void m_bg_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
CallDone();
}private void m_bg_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
if (e.ProgressPercentage == 0)
{
DisplayArea.Content = "Running";
}
else if (e.ProgressPercentage == 100)
{
DisplayArea.Content = "Done";
}
}private void m_bg_DoWork(object sender, DoWorkEventArgs e)
{
m_bg.ReportProgress(0);for (int i = 0; i < LoopCount; ++i)
{
Thread.Sleep(1000);
}m_bg.ReportProgress(100);
}
I like delegate approach the best. It requires the least amount of code, and the code is also the simplest.
