Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Simple Asynchronous Background Task Processing with Progress Dialog (Silverlight Solution)

0.00/5 (No votes)
21 Apr 2010 1  
Small, generic, reusable infrastructure for running tasks asynchronously and displaying progress information.

Introduction

This article demonstrates a small, generic and reusable infrastructure for running tasks asynchronously and allowing them to update their progress dialogs in a simple way. This solution is for Silverlight but can be used with Winforms or WPF with a very small change.

Background

When doing UI related programming, very often we need to perform background processing that we usually run on worker threads as we don't want to block the UI thread and make our UI nonresponsive for the period of the operation. Probably worker threads or the thread pool is used to perform the task in the background but in many cases we want to show some progress about the task while running.

Problem

As it is well known, we must not call methods or access properties of a UI element from a worker thread, just from the thread that created these elements. This thread is usually called the UI thread. (However an application can have multiple UI threads.) This is the thread that runs the message pump and spends most of its time waiting for different messages and dispatching them to the appropriate UI elements’ handlers. So when user is clicking in the application, messages are generated and posted to the message queue. The UI thread gets these messages and invokes the handlers. This way these handlers are always called from the UI thread. (From .NET 2.0, if we access a value on a control from a worker thread we get an InvalidOperationException: Cross-thread operation not valid: Control 'TextBox1' accessed from a thread other than the thread it was created on.)
So we need to get these UI updates called from the UI thread. (Marshal to UI thread) In WinForms, we can use any control’s BeginInvoke or Invoke method (coming from the ISynchronizableInvoke interface). Simply it takes an arbitrary delegate and an object array for the parameters. That delegate will be somehow wrapped and posted in a message to the message pump and so the UI thread will eventually call it.

Control.BeginInvoke(Delegate del, object[] args);

In Silverlight, we do the same thing using:

Control.Dispatcher.BeginInvoke(Delegate del, object[] args);

I presume (and expect) many of you are familiar with these techniques, but still the problem is that these require additional coding to define delegates, and make these calls properly, even if you make these from the worker thread or a worker thread event, you need to call the BeginInvoke passing a delegate and the arguments. This will also mix with the original logic and scatter all over in the code that makes code less readable, uglier.

Solution

The following solution is made for Silverlight, but very similarly can be ported to .NET/Winforms. The main aim was to somehow hide these message marshallings in the progress dialog box itself. So from the worker threads, we can call directly our UI update methods on the progress dialog as it will in itself marshal the calls invoking the proper event handlers from UI thread.

1. Progress Dialogs

DefaultProgressDlg

First, I created a base dialog class called DefaultProgressDlg deriving from ChildWindow. This will be the ultimate base class providing all the common functionalities for all our descendant progress dialogs. It has a protected PostMessage(Delegate, object[] args) method that takes an arbitrary delegate with parameters and marshal it to the UI thread. (In case it is called from the UI thread it invokes the delegate directly without marshalling.) It also defines two public common operations that all dialog box will have: SetTitle(string) and CloseWindow(). These methods just set up a delegate for their corresponding handler methods and pass it to PostMessage with arguments. So, all in all these defined public operations (and only these) will be safe to call from worker threads. This DefaultProgressDlg has in fact nothing else displayed on it.

Creating Custom Progress Dialogs

If we want (and probably we will) to show additional information, we just derive from this DefaultProgressDlg and place our controls to the dialog (design the XAML). Then we define our public operations that call the inherited PostMessage. We also need to define the private message handlers as well that are passed to the delegates so they will be always called from the UI thread and can make the UI update operations.

The public operation (Post the delegate):

public void SetTitle (string text)
{
   Action<string> a = new Action<string>(OnSetTitle);
   PostMessage(a, text);
}

The corresponding private message handler (called on the UI thread):

private void OnSetTitle (string text)
{
   this.txTitle.Text = text;
}

If we use from zero or maximum four parameter methods, the easiest is to use Action() or Action<t>(T t) delegate overloads defined in the .NET base class library, but here we can use any kind of delegate as PostMessage accepts a Delegate type that is base for all delegates.

2. Tasks

The AsyncProcess assembly also defines an abstract TaskBase<t> class to write your tasks. All you need when writing your custom task is to derive from it, set the appropriate ProgressDialog class type as the T Generic parameter, and override the Run abstract method with your custom logic. The TaskBase class has a Start() method and when that gets called it creates the specified Progress Dialog, displays it, and launches the Run method asynchronously on a worker thread, and as the progress dialog instance is a protected property inside TaskBase, your custom task will always access it, and can directly call the defined public operations on the progress dialog.

** Note: Only the defined custom operations are safe to call from non UI thread! ***

Creating a custom task:

public class TestTask1 : TaskBase<mycustomdialog>
{
   //note this method is running on worker thread
   protected override void Run()
   {
      ProgressDialog.SetTitle("Test1 running..");
       
      //your logic comes here..

      ProgressDialog.SetTitle("Test1 finished..");
   }

   protected override Dispose(bool disposing)
   { 
       //put your cleanup code here

       base.Dispose(disposing);
   {
}

Note: You don’t need to call ProgressDialog.CloseWindow() at the end, as the base class will call it for you. It will also invoke completion callback if specified (see later), and call dispose on the task.

AsyncController – Starting the Task

The AsyncController is a simple static class having few static methods where you can start your tasks. The TaskBase Start() method is in fact set to internal so it can only be started using the AsyncController. 3 overloads defined:

Starts the task and returns immediately. No callback assigned.

public static void Queue(TaskBase task)

Starts the task and assigns a callback that is called when task finished. Returns immediately.

public static void Queue(TaskBase task, Action<taskbase> completionCallback)

Starts the task, assigns a callback, if wait is true the calling thread is blocked until task finished.

public static void Queue(TaskBase task, Action<taskbase> completionCallback, bool wait)

The following overloads get delegates that will be called on running. This is handy if you don't have custom task just want to use a method as the task. The delegate is of type Action<TP> (where TP is the Dialog being used). These overloads build an instance of the internal shipped DelegateTask.

public static void Queue<TP>(Action<TP> taskRun)

public static void Queue<TP>(Action<TP> taskRun, bool IsCancellable)

//...

Also it is very important that the you can start tasks from any thread!
Dialog boxes are always created on the UI thread, the TaskBase class makes sure of it! .

Summary

As you can see this is really a compact solution for the outlined problem, you just need to define your custom progress dialog (writing public operations and private handlers), and the custom tasks (overriding Run() method) and that’s it. The dialog creation, launching worker thread, marshalling the delegates, and some other functionality are encapsulated in the base classes.

Additional Features

Cancelling Operation

There is also a built in way to allow the operations to be cancelled. Both the Dialog being used and the actual Task should support it.

To make your task cancellable you should override the IsCancellable property and return true. In this case when the user cancels the operation on the Dialog box, your task is notified in the OnCancelled() method. So you should also override this and put your cancellation logic here.

public class TestTask1 : TaskBase
{
   private bool _stop = false;

   protected override bool IsCancellable
   {
       get {return true;} 
   }

   //note this method is running on worker thread
   protected override void Run()
   {
      ProgressDialog.SetTitle("Test1 running..");
       
      //your logic comes here..
      while(!_stop)
      {
         //do something
      }

      ProgressDialog.SetTitle("Test1 finished..");
   }

   protected override OnCancelled()
   { 
       ProgressDialog.SetTitle("Cancelling..");

       _stop = true;
   {
}

To make a custom Dialog box to support cancellation, all you have to do is to override the CancelButton property and return an instance of a System.Windows.Controls.Button type. Just place a Button anywhere in the XAML and return this button in the CancelButton property.

Note: If a task’s IsCancellable is set to false even the Dialog box supports it, the cancel button will be set to invisible. The other way, even if the task supports cancellation, but no CancelButton is specified in Dialog Box being used, the user will never be able to cancel the operation.

Wait for Operation to Complete

As you could see, when you launch the task in AsyncController you can set wait flag to true, which will cause the calling thread to wait (block) until the operation has completed.

** Note: You should never use it when on UI thread as it would block it. It can only be used when launching task from a worker thread. (For example a running task can launch a child task and wait until that finished.) **

Task Completion Callback

You can also specify a completion callback when launching a task in AsyncController. This way you can be notified when background task finished.

** Note: Completion callback is called from the worker thread! **

3 Shipped Progress Dialogs

Apart from the DefaultProgressDlg which can display a title, 3 other pre-built Dialog boxes are shipped in the AsyncOperation.dll.

  • DialogOnlyStatus

    It can display a main status message on the top of the window client area

    DialogOnlyStatus.png

  • DialogOneProgress

    The same as DialogOnlyStatus but can also display a progress bar with a status text for the progress bar.

    DialogOneProgress.png

  • DialogTwoProgress

    The same as DialogOnlyStatus but can also display two progress bars with two status texts for each the progress bars.

    DialogTwoProgress.png

Question

Here, I also have a question.

As you could see, theoretically the DialogOneProgress should derive from DialogOnlyStatus as it has the same main status text plus the progress bar and progress status.

The DialogTwoProgress could be derived from the DialogOneProgress as it has one additional progress bar. Currently as seen on the class diagram, all 3 Dialogs derive from the basic DefaultProgressDlg, so some functionality is repeated.

The problem is that I could not find a way to inherit a control or window from one another that also has XAML. Should you know a way, please let me know! Thanks.

Attached Source Code

In the source code, you can find the project for AsyncOperation.dll, and also a test Silverlight application that defines some custom tasks that use the pre-built dialogs.

classdiagram.png

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here