Click here to Skip to main content
15,881,709 members
Articles / Programming Languages / Visual Basic

Progress Bar Best Practices

Rate me:
Please Sign up or sign in to vote.
3.98/5 (30 votes)
11 Mar 2008CPOL5 min read 135.4K   123   20
How to show progress notification for long operations

Proper Progress Notification

This is going to be met with great skepticism but I'm going to say it anyway… There is only one best way to show a progress window for long operations. That answer is to show a modal progress window on the UI thread while the work is done on the worker thread.

First let’s look at why the progress window must be on the UI thread: If you try to show a dialog on a worker thread, that dialog cannot have an owner from the calling thread. Try and you'll get the exception:

Cross-thread operation not valid: Control 'Form1' accessed from a thread 
other than the thread it was created on.

And showing the dialog on another thread without an owner (modal or non) makes the dialog appear non-modal and therefore allows the user to click on your main window and loose the progress window to the depths of the Z-order making it difficult for the user to know if the operation is still in progress or not.

One trick you might be tempted to try is setting the progress window’s TopMost property to force it on top. Unfortunately, this keeps it on top of all other windows and is not ideal for users trying to multi-task while they wait for your application to do its thing.

Next, let’s examine why the work ought to be done in a separate thread at all. If you don't and the application doesn't call DoEvents, the app will quickly become non-responsive as far as Windows is concerned and users will see the “Not Responding” message up in the title bar if a user tries to click on it. And we all know that users feel entitled to tell you your app “freezes” or “hangs” when they see this.

So why not use DoEvents to allow your app to respond? Not only would you need to call frequent DoEvents to give the appearance of a responding app, but DoEvents are evil. Calling DoEvents during your operation makes your code much less reusable. Imagine you have a nice little procedure that is capable of sorting numbers. In this case, you are locking down the UI and showing a progress bar so the DoEvents seems to accomplish the desired effect. But then imagine someone reuses your sort routine in between steps in a wizard. The user clicks Next, the code runs and calls DoEvents, and for whatever reason the user decides to click Next again before the routine has finished. Maybe the user simply double clicked because they're click happy. The button click event will fire AGAIN and potentially get you into all kinds of trouble because your code will run twice. Code should be expected to be reused synchronously without fear of reentry. If we all commit to this, there would be fewer bugs in our apps and our code would be much easier to reuse. This kind of detail in my opinion is simply not something the consumer of a routine should need to worry about.

Lastly, should the progress dialog be modal or modeless? You might think that it doesn't matter. Assuming you have code that needs to run after the worker thread is complete, does it matter if the code is running in some “Thread is complete” event handler or immediately after your progress window closes? In many cases, I admit it doesn't matter. But I assert that it is good practice to do the later. This is because your code is always in some way being called from an event handler. Whether it’s a button click event or some object raising an event, with the exception of Sub Main, you are always running in an event handler of some sort. And if you spawn your worker thread and return immediately, the caller of that event may run some additional code. Now because you're showing a progress bar, you are indicating to the user that this operation did not finish. So why would you return from the event as if the operation did finish? What if there are multiple observers of that event. There’s nothing stopping another module from adding a second event handler to that same event. And if you return immediately, that code will run before your long operation has completed. This may not be a problem, but it could.

  1. It might assume that there is NOT a modal dialog displayed in the case that it needs to show its own, OR
  2. it may be dependent on your code completing before it does its thing.

Also, the event might do its own thing afterwards assuming you are done with the operation. For example, imagine an event BeforeOpen and another AfterOpen. If you do your long operation in BeforeOpen and return immediately, AfterOpen is going to fire before you're done. And consumers of that event are probably assuming you are finished with whatever it is that you are doing. And last: what if the event caller is trapping for exceptions. Waiting for your thread to complete also allows you to trap for exceptions on your thread and bubble them back up to the event issuer or even maybe appropriately set an EventArgs.Cancel flag. So far all these reasons, I assert that it is good practice to show your progress dialogs as modal unless you have some compelling reason not to.

So, how do we do all this? While there is more than one way, the easiest in my opinion is to use the System.ComponentModel.BackgroundWorker object. Here’s how:

VB.NET
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)_
                                                                  Handles Button1.Click
    BackgroundWorker = New System.ComponentModel.BackgroundWorker
    BackgroundWorker.RunWorkerAsync()
    FormProgress = New FormProgress
    FormProgress.ShowDialog()
End Sub

Private Sub BackgroundWorker_DoWork(ByVal sender As Object, _
       ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker.DoWork
    ' do your long operation here
End Sub

Private Sub BackgroundWorker_RunWorkerCompleted(ByVal sender As Object, _
        ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
        Handles BackgroundWorker.RunWorkerCompleted
    FormProgress.Close()
End Sub

Voila! It’s as simple as that. Presumably you'd also report a percent finished back through the BackgroundWorker object and its associated ProgressChanged event but perhaps you'd just have a little repeating animation on your progress form instead in which case this is all you would need.

One caveat: users can still enter Alt+F4 or click the red X in the upper right corner of your progress dialog if it’s there so you'll need to prevent this. An easy way to get around this is to just cancel the Form Close unless it’s coming from our code.

VB.NET
Private AllowClose As Boolean = False

Private Sub FormProgress_FormClosing(ByVal sender As Object, _
            ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
    If e.CloseReason = CloseReason.UserClosing And AllowClose Then
        e.Cancel = True
    End If
End Sub

Public Sub ForceClose()
    AllowClose = True
    Me.Close()
    AllowClose = False
End Sub

Just call FormProgress.ForceClose instead from your BackgroundWorker’s RunWorkerCompleted event and you're in business.

History

  • 11th March, 2008: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
I am a software architect and engineer who has been working in the industry since 1994. Although I officially started writing software many years earlier at age 11 by authoring games, graphics demos, or whatever I found interesting. I even landed my first consulting job at age 16 implementing a newly invented mathimatical algorithm for a team of researchers at the local University.

I've designed and implemented hundreds if not thousands of applications: client, web, and hybrid of all scales and was a contributing author to the Black Belt programming column of Visual Basic Programmer's Journal.

My goals when coding are always to write clean, reusable and optimized code using the latest and technology, tools and techniques.

Comments and Discussions

 
GeneralMy vote of 5 Pin
pistol3503-Jun-13 23:19
pistol3503-Jun-13 23:19 
GeneralThank You Pin
Vasanth.S.R2-Jun-10 22:26
Vasanth.S.R2-Jun-10 22:26 
QuestionHow to use progress bar & Background worker when downloading file from hard drive using C#? Pin
rasingh127-Feb-09 2:07
rasingh127-Feb-09 2:07 
QuestionWhalla? Pin
TimDJones29-Apr-08 9:16
TimDJones29-Apr-08 9:16 
GeneralAbout Best Article Pin
Izzet Kerem Kusmezer28-Apr-08 21:09
Izzet Kerem Kusmezer28-Apr-08 21:09 
GeneralRe: About Best Article Pin
98z2829-Apr-08 19:58
98z2829-Apr-08 19:58 
GeneralStatus dialog box... Pin
Member 42300745-Apr-08 16:24
Member 42300745-Apr-08 16:24 
GeneralYou are right!!!!! Pin
EricF23-Mar-08 12:12
EricF23-Mar-08 12:12 
GeneralCmon Pin
Tim Yen20-Mar-08 16:23
Tim Yen20-Mar-08 16:23 
GeneralRe: Cmon Pin
Tim Greenfield22-Mar-08 15:21
Tim Greenfield22-Mar-08 15:21 
GeneralRe: Cmon Pin
ssm62978-Oct-08 13:26
ssm62978-Oct-08 13:26 
GeneralRe: Cmon Pin
ssm62978-Oct-08 13:29
ssm62978-Oct-08 13:29 
NewsCross-thread not a problem Pin
JoseMenendez12-Mar-08 4:52
JoseMenendez12-Mar-08 4:52 
GeneralRe: Cross-thread not a problem Pin
Kavan Shaban13-Mar-08 4:54
Kavan Shaban13-Mar-08 4:54 
GeneralRe: Cross-thread not a problem Pin
MaxGuernsey17-Mar-08 18:29
MaxGuernsey17-Mar-08 18:29 
GeneralRe: Cross-thread not a problem Pin
Kavan Shaban17-Mar-08 18:31
Kavan Shaban17-Mar-08 18:31 
GeneralRe: Cross-thread not a problem Pin
MaxGuernsey17-Mar-08 18:34
MaxGuernsey17-Mar-08 18:34 
GeneralRe: Cross-thread not a problem Pin
Kavan Shaban17-Mar-08 18:40
Kavan Shaban17-Mar-08 18:40 
GeneralRe: Cross-thread not a problem Pin
supercat924-May-08 12:38
supercat924-May-08 12:38 
MaxGuernsey wrote:
What value does that provide over having the UI thread own all of the UI objects?


From what I can tell, one rather ugly feature of the invocation methods in .net is that methods may only be invoked on a thread when that thread checks for events. While this is effective at ensuring that there won't be any problems with the invoked performing an action which conflicts with something on the parent thread, invoking a function on a thread which doesn't often check for events will be slow.

For this reason, my preferred way to handle a progress dialog (warning: I'm not an expert, and this might be a very bad approach) is to avoid invocation and have the worker thread notify the main thread of progress using shared variables and optionally a semaphore. The UI thread loop can either check the variables within a timer-tick event, or else 'trywait' on the semaphore with a reasonable timeout, then see if the worker thread has updated anything. When the worker thread posts an update (i.e. just after it changes the shared variables), it will signal the semaphore if it isn't already "ready".

There's a slight chance that the UI thread will pass the semaphore just after the worker thread updates its variables, but just before the worker thread tests whether it needs to signal the semaphore. Provided that the worker thread never leaves the variables in an intermediate state that would crash the UI thread, this shouldn't pose a problem. The UI task may end up passing the semaphore once more than would really be necessary, but other than a little wasted CPU time that shouldn't pose a problem. If 'partial updates' must be avoided, a simple approach is to have the worker thread maintain an 'is valid' flag and a 'change' counter. When the UI task passes the semaphore, it should latch the counter, check the is valid flag, latch the variables, check the valid flag again, and finally compare the counter against the latched value. If either check of the valid flag reported 'not valid', or if the counter changed between readings, toss out the read values and wait on the semaphore again.

The important things to note with this technique are:

-1- If the worker thread wants to post two updates before the UI thread has managed to handle the first one, the first update will be lost. For something like a progress bar, this is not a problem; for some other types of indicators (e.g. a "progress log") it may be necessary to implement a thread-safe queue.

-2- The worker thread will not be blocked if the UI thread goes off to do something else. The progress bar may be updated rather sluggishly, but since the worker thread won't care about when the updates are actually performed, it will be unaffected.

-3- If the progress bar is updated via timer tick, the smoothness of updates will be limited by the tick frequency (the higher the frequency, the smoother the updates but the more CPU time will be wasted polling). If the main task uses a semaphore with trywait, then greater responsiveness could be combined with minimal CPU usage, but other events on the main task could be held up.

What's really needed, IMHO, would be a version of things like thread.Sleep and semaphore.trywait which would allow events to occur if/when they're posted, but would not otherwise waste CPU time. Unfortunately, I don't know of any good way to accomplish that.
GeneralRe: Cross-thread not a problem Pin
Fiwel18-Mar-08 6:31
Fiwel18-Mar-08 6:31 

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

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