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.
public interface IProgressCallback
{
void Begin( int minimum, int maximum );
void Begin();
void SetRange( int minimum, int maximum );
void SetText( String text );
void StepTo( int val );
void Increment( int val );
bool IsAborting
{
get;
}
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(
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;
}
if( callback.IsAborting )
{
return;
}
}
}
catch( System.Threading.ThreadAbortException )
{
}
catch( System.Threading.ThreadInterruptedException )
{
}
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.
public delegate void RangeInvoker( int minimum, int maximum );
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 object
s 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'.