Click here to Skip to main content
Click here to Skip to main content

A .NET Progress Dialog

By , 26 Aug 2003
Rate this:
Please Sign up or sign in to vote.

Sample Image - Progressdialog.jpg

Introduction

There are many processing operations that will take more than 1/10th of a second to complete, even on today's processors!

To avoid tying up the user interface, a common approach is to spawn a new thread and do the work there, whilst sending updates back to a suitable indicator on the UI thread. This is one such indicator, implemented in C# using the .NET framework.

The first point of interest is actually nothing to do with the progress indicator at all, but is a very useful class that lives in the System.Threading namespace and is an excellent general purpose way of getting your work done on another thread. It is called ThreadPool.

private void SpawnSomeWork()
{
    ThreadPool.QueueUserWorkItem( new WaitCallback( DoSomeWork ) );
}

private void DoSomeWork( object status )
{
    ...
}
        

ThreadPool.QueueUserWorkItem uses a WaitCallback delegate to queue up a request for the DoSomeWork operation to be carried out on one of the worker threads in the runtime thread pool. This is usually neater than managing your own worker threads for most fire-and-forget workers.

There are a couple of important caveats about the threadpool. The most important thing to note is that you do not own the thread. You may not even be the only piece of work executing on that thread. So - you should not abort or interrupt the thread, or otherwise mess about with its priority. This means that you will always be executing as a background thread, and your code will only get executed when a slot in the threadpool comes free. If those things are important to you, you should manage your own thread instead.

You'll notice that the DoSomeWork operation takes an object as a parameter. This allows you to pass some suitable state object to your worker, and we will make use of this later for our progress indicator.

So, having found a way of getting our work done on another thread, we now need to get the progress updates displayed in some sort of UI. To achieve this, I've created an interface called IProgressCallback. This defines the methods that a worker can call on to update a progress indicator as it proceeds, but leaves the implementation of that UI as a separate problem.

Let's take a quick look at that interface.

/// This defines an interface which can be implemented by UI elements
/// which indicate the progress of a long operation.
/// (See ProgressWindow for a typical implementation)
public interface IProgressCallback
{
  /// Call this method from the worker thread to initialize
  /// the progress callback.
  void Begin( int minimum, int maximum );

  /// Call this method from the worker thread to initialize
  /// the progress callback, without setting the range
  void Begin();

  /// Call this method from the worker thread to reset the range in the <BR>  /// progress callback
  void SetRange( int minimum, int maximum );

  /// Call this method from the worker thread to update the progress text.
  void SetText( String text );

  /// Call this method from the worker thread to increase the progress <BR>  /// counter by a specified value.
  void StepTo( int val );

  /// Call this method from the worker thread to step the progress meter to a<BR>  /// particular value.
  void Increment( int val );

  /// If this property is true, then you should abort work
  bool IsAborting
  {
    get;
  }

  /// Call this method from the worker thread to finalize the progress meter
  void End();
}
        

There are operations that allow the worker to indicate the start [Begin()] and end [End()] of the operation, and to advance the progress indicator itself. There is a utility which allows you to change the range of the indicator 'in flight' [SetRange()]. In addition, there is a SetText( String text ) method, which allows you to update a general text prompt associated with the current state of the worker.

Another option would have been to create an event interface that the worker was required to implement. (Concrete progress indicators could then consume these events and update their UI appropriately). The advantage of the event approach is that it would allow you to multicast your progress to several subscribers, without further code. I prefer to use the callback approach, and then potentially multicast back out using events, once it has all been marshalled back onto the UI thread (see below for more information on this...)

We can now write a worker method that uses this interface to update the user on its progress.

private void SpawnSomeWork()
{
    IProgressCallback callback; // = ???
    System.Threading.ThreadPool.QueueUserWorkItem( <BR>                              new System.Threading.WaitCallback( DoSomeWork ),
                              callback );
}

private void DoSomeWork( object status )
{
    IProgressCallback callback = status as IProgressCallback;
            
    try
    {
        callback.Begin( 0, 100 );
        for( int i = 0 ; i < 100 ; ++i )
        {
            callback.SetText( String.Format( "Performing op: {0}", i ) );
            callback.StepTo( i );
            if( callback.IsAborting )
            {
                return;
            }
            // Do a block of work
            if( callback.IsAborting )
            {
                return;
            }
        }
    }
    catch( System.Threading.ThreadAbortException )
    {
        // We want to exit gracefully here (if we're lucky)
    }
    catch( System.Threading.ThreadInterruptedException )
    {
        // And here, if we can
    }
    finally
    {
        if( callback != null )
        {
            callback.End();
        }
    }
}        
        

As you can see above, we have added a callback parameter to our call to QueueUserWorkItem. This gets passed on to our DoSomeWork method, where we cast it back from object to IProgressCallback.

Note that if we hadn't passed an object that implements IProgressCallback, or we'd passed null, then an exception would be thrown. We could, in production code, catch that and clean up nicely after ourselves.

Notice also how we are dealing with terminating the worker. We have reworked our algorithm so that it deals with bite-sized packets of work, and after each packet, we check to see if the IProgressCallback is asking us to abort. If it is, we need to clean up nicely, and return from the method. We've got a couple of catch blocks in there, too, to try to exit cleanly if we are aborted or interrupted, although, in current builds, these are very dangerous operations, so I wouldn't hold out too much hope!

Having dealt with the client end, we can implement a practical progress indicator dialog based on this interface. In this case, it is called ProgressWindow

Essentially, ProgressWindow is a Form that contains a progress indicator, a 'Cancel' button and a text window to display the progress status text. In addition, it takes the Text property of the Window (i.e. its caption text), as it was at Begin() time, and uses this as a stem to update a 'percent complete' indicator in the title bar.

Each of the methods in the public interface is going to be called on from the worker thread. However, there are only a smattering of methods on a window that you can call from the non-owning UI thread (CreateGraphics() is one, for example), so we're going to have to coerce the operation back onto the owning thread to avoid a disaster.

We do this by using the Invoke() method.

Each operation called from the worker thread (e.g. Begin()) has its corresponding DoBegin() method. In the implementation, we call Invoke() and pass it a delegate to the DoBegin() method. We can therefore rely on the fact that the DoBegin() code is being executed back on our UI thread.

Here's an example for the Begin() method.

/// A delegate for methods which take a range
public delegate void RangeInvoker( int minimum, int maximum );

/// Call this method from the worker thread to initialize
/// the progress meter.
public void Begin( int minimum, int maximum )
{
    initEvent.WaitOne();
    Invoke( new RangeInvoker( DoBegin ), new object[] { minimum, maximum } );
}

private void DoBegin( int minimum, int maximum )
{
    DoBegin();
    DoSetRange( minimum, maximum );
}

private void DoBegin()
{
    cancelButton.Enabled = true;
    ControlBox = true;
}

private void DoSetRange( int minimum, int maximum )
{
    progressBar.Minimum = minimum;
    progressBar.Maximum = maximum;
    progressBar.Value = minimum;
    titleRoot = Text;
}
        

There's a couple of things to note about this. Firstly, notice how the parameters are passed to DoBegin() as an array of objects in the call to Invoke(), but then correctly matched to the real method parameters in DoBegin() itself.

We also block the worker thread by waiting for an event that is signalled by the FormLoad handler to ensure that the UI gets displayed (and can therefore be updated or closed) before the worker gets going.

All the remaining methods follow a similar pattern, and the end result is a cross-thread progress indication dialog.

Typically, clients will invoke it either modally (using ShowDialog()) or modelessly (using Show()). In all other respects, it can be treated like a regular form. You can set the background color, or image, and muck around with its size (the default anchoring of the controls will expand the text box and the progress slider to fit).

I hope you find it useful.

History

2 Oct 2001 - updated source files to fix some issues.

24 Aug 2003 - updated source files and article to address ThreadAbort() naughtiness, and to include a suggestion that it should be possible to reset the range 'in flight'.

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

About the Author

Matthew Adams
Chief Technology Officer Ythos Ltd
United Kingdom United Kingdom
I've been developing software on the Microsoft platform for over 15 years. I'm currently focusing on the .NET 3.5 stack, with a particular interest in healthcare and life sciences applications.

Comments and Discussions

 
Generalsome bugs PinmemberLeon Zeng23-Apr-11 16:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140415.2 | Last Updated 27 Aug 2003
Article Copyright 2001 by Matthew Adams
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid