Click here to Skip to main content
Licence 
First Posted 26 Sep 2001
Views 300,173
Bookmarked 169 times

A .NET Progress Dialog

By | 26 Aug 2003 | Article
A progress dialog for asynchronous workers

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

Member

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.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
Generalsome bugs PinmemberLeon Zeng16:42 23 Apr '11  
GeneralA tip to avoid SetText error PinmemberJay Lewis11:47 1 Jan '11  
GeneralMy vote of 5 PinmemberMember 22262872:06 19 Jul '10  
GeneralHere is a VB port of your great work Pinmemberhjgode21:41 1 Jan '10  
GeneralRe: Here is a VB port of your great work PinmemberJay Lewis21:56 8 Dec '10  
GeneralRe: Here is a VB port of your great work PinmemberJay Lewis11:26 1 Jan '11  
GeneralNot showing real time progress. Pinmemberneal12317:54 17 Nov '09  
Questionlicense? Pinmembertigerharry3:20 15 Jul '09  
AnswerRe: license? PinmemberMatthew Adams3:47 15 Jul '09  
GeneralIt cannot support asynchronization! PinmemberKevinyou16:36 15 Aug '08  
GeneralRe: It cannot support asynchronization! PinmemberMatthew Adams3:47 15 Jul '09  
QuestionMy work takes 10 times longer time with this... why? Pinmemberd00_ape22:36 13 Nov '07  
GeneralCancel button Pinmemberjabulino21:19 6 Feb '06  
GeneralComment of IProgressCallback PinmemberBThomee1:15 14 Jan '06  
GeneralGreat! Pinmemberilanoire11:50 30 Sep '05  
QuestionAdopted demo code hangs on large files? PinmemberShrp779:21 20 Sep '05  
Hi
 
I've been searching for something like this for a while and when I finally got it working I was extatic Big Grin | :-D . I decided to write a little application based on the demo code, which counts how many lines in a text file begin with each letter in the alphabet (e.g. 362 lines start with the letter 'A') etc.
 
I purposely wrote the counting function to be as long and drawn out as possible in order to simulate a drawn-out process Sleepy | :zzz: , and that's when I stumbled across the problem WTF | :WTF: .
 
At random times - the progress window will simply freeze Dead | X| . Any time I run the test application, it will freeze at a different point. I can still cancel and x-out of the progress window and the application is still working fine (except that it didn't complete the task).
 
The code I'm using to run through the files is this:
 

public class ScanEngine
{
private ArrayList lines = new ArrayList();
private string fileName;
private int[] alpha = new int[26];
private int size;
 
public int[] Alpha
{
get{ return alpha; }
}

public ScanEngine(string filename)
{
fileName = filename;
ProgressDisplay.ProgressDisplay display = new ProgressDisplay.ProgressDisplay();
if( !ThreadPool.QueueUserWorkItem( new WaitCallback(ScanFile), display ) )
MessageBox.Show("Could not queue worker thread!");
else
display.ShowDialog();
}
 
private void ScanFile( object disp )
{
IProgressDisplay display = disp as IProgressDisplay;
 
try
{
display.Begin();
 
CountLines(display);
if( display.isAborting )
return;
 
ProcessLinesByAlphabet(display);
if( display.isAborting )
return;
 
}
catch( ThreadAbortException e )
{
MessageBox.Show(e.Message);
}
catch( ThreadInterruptedException e )
{
MessageBox.Show(e.Message);
}
finally
{
if( display != null )
display.End();
}
}
 
private void CountLines(IProgressDisplay display)
{
StreamReader sr = new StreamReader(fileName);
string line;
 
display.SetCaption("Scanning File");
display.SetText(1, "Counting Lines", "0");
 
while( (line=sr.ReadLine()) != null )
{
lines.Add( line );
if( lines.Count % 50 == 0 )
display.SetInfo(1, lines.Count.ToString());
 
if( display.isAborting )
return;
}
sr.Close();
 
size = lines.Count;
 
display.SetInfo(1, size.ToString());
}
 
private void ProcessLinesByAlphabet(IProgressDisplay display)
{
display.SetRange(1, 0, 26);
display.SetRange(2, 0, lines.Count);
display.SetText(1,"Process lines", "");
display.SetText(2,"Lines in this group","0");
string comp;
int ln;
int steps = Convert.ToInt32(Math.Ceiling(size / 30));
 
for( int i=0; i

The progress window is a little different from the one you have and features 5 progress indicators and 5 groups of 2 labels (one for task description and the other for task information). Very much overkill for any progress window - but I just wanted to test it out and play around with it.
 
I can't figure out why the line counting code freezes up... Confused | :confused:
 
Any help - GREATLY appreciated Blush | :O
 
-- modified at 15:30 Tuesday 20th September, 2005
AnswerRe: Adopted demo code hangs on large files? PinmemberSurutsu6:03 3 Oct '05  
GeneralRe: Adopted demo code hangs on large files? PinmemberShrp774:48 10 Oct '05  
GeneralRe: Adopted demo code hangs on large files? PinmemberAFSEKI1:31 19 May '08  
GeneralQuestion PinmemberSk8tz5:11 21 Apr '05  
GeneralNewb Question Pinsussrush2@comcast.net7:49 12 Jan '05  
GeneralLengthy db operation Pinsussante745:02 20 Dec '04  
GeneralCross button on title bar PinmemberMukesh22:10 12 Dec '04  
GeneralRe: Cross button on title bar PinmemberMr. Rogers4:10 5 Jan '05  
GeneralRe: Cross button on title bar PinmemberMatthew Adams10:56 20 Jan '05  

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.

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