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

A .NET Progress Dialog

By , 26 Aug 2003
 

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.
Search this forum  
    Spacing  Noise  Layout  Per page   
Generalsome bugsmemberLeon Zeng23 Apr '11 - 16:42 
1. After percent >= 100%, the button title is still "Cancel", I think it should be "OK" because since the operation is completed, nothing to cancel now.
 
2. Click "Cancel" when percent < 100%, usually got exception. This is because the dialog is disposed, but the worker thread is still invoking .Increment() or SetText() etc.
GeneralA tip to avoid SetText errormemberJay Lewis1 Jan '11 - 11:47 
Hi,
 
GREAT work, thanks!
 
I discovered a couple of issues that are really easy to address so I thought I'd share. One issue is that the .Text or SetText or any other method can potentially fail if you don't make sure that the progress window's handle is definitely created before using the properties and methods. I've discovered that it can take a few milliseconds to create the progress window's handle, so it's really easy to handle with something simple like this (VB version):
 
Dim frmProgress As New MWA.Progress.ProgressWindow()
While frmProgress.Handle = Nothing ' wait until window is definitely created
System.Threading.Thread.Sleep(5)
End While
' window handle definitely now exists
frmProgress.Text = "Doing some threaded function..."
System.Threading.ThreadPool.QueueUserWorkItem(New System.Threading.WaitCallback(AddressOf SomeFunction), frmProgress)
If frmProgress.ShowDialog() = DialogResult.Abort Then
' do any cleanup here
MsgBox("Some function aborted.", vbOKOnly + MsgBoxStyle.Exclamation, "App Title")
Exit Sub
End If
frmProgress.Dispose() ' this is not needed, but it prevents a warning from the Code Analysis tool
 
The other issue I found is with the Cancel button. If you change the DialogResult of the Cancel button to "Abort" you can react properly to the Cancel button - otherwise you can't differentiate between OK and Cancel because it was set to return OK in the sample project. I also had to enable the cancel button because it was not enabled in the sample project.
 
The rest of the sample works great, simply follow the sample to make your threaded function and callback progress updates.
 
Thanks again for this great work!
Jay.
GeneralMy vote of 5memberMember 222628719 Jul '10 - 2:06 
help me a lot.thanks.
GeneralHere is a VB port of your great workmemberhjgode1 Jan '10 - 21:41 
Hello
 
Many thanks for this great work.
 
As I needed this in a VB app, I did port your code to VB.
 
    Public Function StartProgress() As Boolean
        ' a progressWindow
        Dim my_progressWindow As New MWA.Progress.ProgressWindow()
        my_progressWindow.Text = "Erstelle Autobackup"
        System.Threading.ThreadPool.QueueUserWorkItem(New System.Threading.WaitCallback(AddressOf DoSomeWork), my_progressWindow)
        my_progressWindow.ShowDialog()
    End Function
 
    Private Sub DoSomeWork(ByVal status As Object)
        Dim callback As MWA.Progress.IProgressCallback = status
        Dim i as Integer
        Try
	    For i=0 to 100
               callback.SetText("Performin Step: " & i
               callback.StepTo(i)
               If (callback.IsAborting()) Then
                   Return
               End If
               System.Threading.Thread.Sleep( 100 )
               If (callback.IsAborting()) Then
                   Return
               End If
            Next i
        Catch abx as System.Threading.ThreadAbortException
            'We want to exit gracefully here (if we're lucky)
        Catch aix System.Threading.ThreadInterruptedException
            'And here, if we can
        Finally
            If (Not IsNothing(callback)) Then
                callback.End()
            End If
        End Try
    End Sub
 
Here is the VB code packed as ZIP:
www.hjgode.de/temp/ProgressWindowVB.zip
 
Maybe you can download it and publish it on your post.
 
Thanks again
GeneralRe: Here is a VB port of your great workmemberJay Lewis8 Dec '10 - 21:56 
Thanks for porting this to VB - the original article and your port are the best I've found anywhere...
 
Maybe you can help with a question, when I reuse the progress window a second time it comes back up and seems to be filled with the last bit of the status from its previous use.
 
Here's an extract of the pertinent code:
 
In my main thread I start the first bit of work:
 
Dim my_progressWindow As New MWA.Progress.ProgressWindow()
my_progressWindow.Text = "Scanning torrent files..."
System.Threading.ThreadPool.QueueUserWorkItem(New System.Threading.WaitCallback(AddressOf ScanFiles), my_progressWindow)
my_progressWindow.ShowDialog()
 
.... some other operations are done here in the main thread after the ScanFiles function ends and the progress dialog closes (all normal so far)...
 
Then when I'm trying to reuse the progressWindow for the next operation it doesn't work correctly:
 
my_progressWindow.Text = "Loading torrent files..."
System.Threading.ThreadPool.QueueUserWorkItem(New System.Threading.WaitCallback(AddressOf LoadList), my_progressWindow)
my_progressWindow.ShowDialog()
 
One note is that the second LoadList function does access user interface elements in the main thread, but they are done correctly with delegates/invokes and they work fine. Things only start to malfunction when showing the progress dialog a second time. I've actually tried re-dimming a new progressWindow2 but it does the same thing - maybe the class isn't releasing something, I don't know...
 
Thanks,
Jay.
GeneralRe: Here is a VB port of your great workmemberJay Lewis1 Jan '11 - 11:26 
I basically decided that you have to declare a new variable name for each progress window. Couldn't find any way to make sure it was clear and ready for another use with the same variable. Oh well - still a GREAT progress window - the best I've found anywhere.
GeneralNot showing real time progress.memberneal12317 Nov '09 - 17:54 
This is incrementing value of progressbar based on for loop iteration. How to show real time progress on binding dataset with grid and then initializing it?
Questionlicense?membertigerharry15 Jul '09 - 3:20 
Hi,
I'd like to use it as base for a progress dialog in commercial software.
I'm I allowed to?
 
Greetings
 
Harry
AnswerRe: license?memberMatthew Adams15 Jul '09 - 3:47 
Sure, that's fine.
 
Matthew Adams
Managing Partner
Ythos Ltd

GeneralIt cannot support asynchronization!memberKevinyou15 Aug '08 - 16:36 
I use the following code:
 
using (ProgressWindow progress = new ProgressWindow())
{
	progress.Text = "Work";
	System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(DoSomeWork), progress);
	progress.ShowDialog();
	using (System.Data.OleDb.OleDbConnection oleconn = new System.Data.OleDb.OleDbConnection(@"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\...\CarsDB.mdb;Persist Security Info=False"))
	{
		using (System.Data.OleDb.OleDbDataAdapter oleadpt = new System.Data.OleDb.OleDbDataAdapter("select * from cars", oleconn))
		{
			using (DataSet ds = new DataSet())
			{
				oleadpt.Fill(ds);
				this.gridControl1.DataSource = ds.Tables[0];
			}
		}
	}
}
The loading data operation doesn't execute until the dialog progress completed!!
GeneralRe: It cannot support asynchronization!memberMatthew Adams15 Jul '09 - 3:47 
I think you'll find that's because you're not executing your data loading operation on a background thread!
 
Matthew Adams
Managing Partner
Ythos Ltd

QuestionMy work takes 10 times longer time with this... why?memberd00_ape13 Nov '07 - 22:36 
I've used your code and encapsulated a funktion that takes very long time. Strange it now takes 10 times mor time to do the opperation (within the DoSomework function).
Does the thread that executes have low priority or what?
 
_____________________________
 
...and justice for all
 
APe

GeneralCancel buttonmemberjabulino6 Feb '06 - 21:19 
My cancel button seems to be disabled.... what i doing wrong?
 
Thanks in advance...
 

GeneralComment of IProgressCallbackmemberBThomee14 Jan '06 - 1:15 
Hello!
 
In the source of IProgressCallback, you swapped the comment
of the Increment and StepTo methods.
 
Nice article by the way!
 
Regards,
Thomas

GeneralGreat!memberilanoire30 Sep '05 - 11:50 
This is a terrific sample - thanks for sharing this work!
QuestionAdopted demo code hangs on large files?memberShrp7720 Sep '05 - 9:21 
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?memberSurutsu3 Oct '05 - 6:03 
I didn't see this pointed out in the demo, but when accessing the UI thread from a worker thread, you CANNOT modify controls directly. You must call the invoke method for a control to do this, else you run into problems the kind of which you encountered.
Invoke needs a delegate to work, which makes it kind of clumsy to use, but it's the only way to make your UI code threadsafe.
Keep me informed whether it makes any difference for you Smile | :)
GeneralRe: Adopted demo code hangs on large files?memberShrp7710 Oct '05 - 4:48 
Thanks, but all the controls are modified through invoke calls.
 
I.e.
 
display.Increment(2,1);
 
calls
 
public void Increment(int display, int val)
{
initEvent.WaitOne();
try
{
Invoke( new IncrementInvoker( DoIncrement ), new object[] {display, val} );
}
catch( ObjectDisposedException )
{

}
catch( Exception e )
{
MessageBox.Show(e.Message);
}
}
 
which then calls the code that modifies the control.
 
Whenever the system gets stuck and I do a break all - the thread is calling the Increment function in the example above (display == 2, val == 1) - but I can't find anything wrong with it...
 
private void DoIncrement(int display, int val)
{
if( isAborting )
return;

switch(display)
{
case 1:
if( progressBar1.Maximum < (progressBar1.Value + val) )
break;
progressBar1.Increment(val);
UpdateCaption();
break;
case 2:
if( progressBar2.Maximum < (progressBar2.Value + val) )
break;
progressBar2.Increment(val);
break;
case 3:
if( progressBar3.Maximum < (progressBar3.Value + val) )
break;
progressBar3.Increment(val);
break;
case 4:
if( progressBar4.Maximum < (progressBar4.Value + val) )
break;
progressBar4.Increment(val);
break;
case 5:
if( progressBar5.Maximum < (progressBar5.Value + val) )
break;
progressBar5.Increment(val);
break;
default:
break;
}
}
 
Confused | :confused:
GeneralRe: Adopted demo code hangs on large files?memberAFSEKI19 May '08 - 1:31 
try BeginInvoke.
GeneralQuestionmemberSk8tz21 Apr '05 - 5:11 
Ever thought of putting this in a control......
 
Sk8tZ
GeneralNewb Questionsussrush2@comcast.net12 Jan '05 - 7:49 
I'm new to threading and was wondering if you could provide a quick code sample in C# if the work was calling on a class that made a DB connection and returned some data, or something along those lines?
GeneralLengthy db operationsussante7420 Dec '04 - 5:02 
Hi.
 
I have a problem. We have a db client and do many operations towards the db that
can take some time. Now we want to be able to cancel these operations if the user
gets tired of waiting. Say that the callback.SetText( String.Format( "Performing op: {0}", i ) );
row is instead a lengthy operation towards the db, then this piece of code will not help, since
the UI gets no time until the request is finished.
 
Do you have a solution for that problem?
Big Grin | :-D
GeneralCross button on title barmemberMukesh12 Dec '04 - 22:10 
Hi,
 
The program seems crash when we clikc on cross button on title bar. Could you pl help?
 
Thanks
GeneralRe: Cross button on title barmemberMr. Rogers5 Jan '05 - 4:10 
You could always disable it by either having the progress guy call this method or the worker
 

using System.Runtime.InteropServices;
.
.
.
private static void RemoveCloseButton(Form frm)
{
IntPtr hMenu;
int n;
hMenu = GetSystemMenu(frm.Handle,false);
if(hMenu != IntPtr.Zero)
{
n = GetMenuItemCount(hMenu);
if(n > 0)
{
RemoveMenu(hMenu, (uint)(n-1), MF_BYPOSITION | MF_REMOVE);
RemoveMenu(hMenu, (uint)(n-2), MF_BYPOSITION | MF_REMOVE);
DrawMenuBar(frm.Handle);
}
}
}
 
[DllImport("user32.dll")]
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
[DllImport("user32.dll")]
private static extern int GetMenuItemCount(IntPtr hMenu);
[DllImport("user32.dll")]
private static extern bool DrawMenuBar(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool RemoveMenu(IntPtr hMenu, uint uPosition, uint uFlags);
private const Int32 MF_BYPOSITION = 0x400;
private const Int32 MF_REMOVE = 0x1000;

GeneralRe: Cross button on title barmemberMatthew Adams20 Jan '05 - 10:56 
Yup...This is because of an unpleasant race condition in the closing code...
 
A considerably improved block of code is coming up at http://spaces.msn.com/members/mwadams which looks at the issues with this implementation, and looks at some strategies for managing worker threads in a WinForms environment.
 
In particular, we can make things a lot simpler by changing the usage model for the progress dialog to place more responsibility for synchronization in the main GUI thread.

 
Matthew Adams
CTO
Digital Healthcare Inc

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

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