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.
- It might assume that there is NOT a modal dialog displayed in the case that it needs to show its own, OR
- 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:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)_
BackgroundWorker = New System.ComponentModel.BackgroundWorker
FormProgress = New FormProgress
Private Sub BackgroundWorker_DoWork(ByVal sender As Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker.DoWork
Private Sub BackgroundWorker_RunWorkerCompleted(ByVal sender As Object, _
ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
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.
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
Public Sub ForceClose()
AllowClose = True
AllowClose = False
FormProgress.ForceClose instead from your
RunWorkerCompleted event and you're in business.
- 11th March, 2008: Initial post