Tuesday, May 22, 2012

Doing Progress Right

Introduction

The ProgressBar control and loops go hand-in-hand. If there is a ProgressBar on a Window, you can bet a For loop is close by. But there are right and wrong ways to do this, and most of the time I see progress code, I see the wrong way. It'll look something like the following, inside the action event of a PushButton:

Dim I As Integer
For I = 0 To 100
ProgressBar1.Value = I
ProgressBar1.Refresh
Next

But this is very, very bad code.

Why is it so bad?

In short, your app locks up entirely while the loop is doing its work. The user interface only updates once all your code is finished, so buttons will not be clickable, windows will not be movable, menus not be usable, etc. The OS will probably even list your app as "not responding" if your code takes more than a few seconds to complete. So your user experience suffers.

It's also slower. The Refresh call demands the ProgressBar be redrawn before the next line of code is triggered. This is wasted effort as the ProgressBar doesn't need to update so frequently.

Experienced users may have heard all this before. But there are also more subtle reasons this code is awful. On Windows 7 and Vista, ProgressBars now animate from their previous value to the new value. Since you're locking up your app while processing however, the ProgressBar can never animate, so it will always appear to be one "step" behind.

And in Web projects, the situation is even worse. WebControls have no Refresh method. All user actions, such as a push of a button, get a single "payload" of data to send back to the browser. If we included such a method, the first call to Refresh would work, but then we use up our one payload, and all following UI updates will never reach the browser.

Doing it right

The answer is the Thread class. To oversimplify, Threads allow you to execute code that does not get in the way of the UI update code. However, you absolutely must not interact with any controls from within the Thread. This poses an obvious problem for updating a ProgressBar. That problem is solved by a Timer. The setup is a little on the tedious side, but it is necessary.

Both objects can be placed on any Window, WebPage, or Container. Create a property on your Window called Progress As Integer. In your Thread1.Run event, put the following code:

Dim I As Integer
For I = 0 To 100
// Do your work here
Self.Progress = I
Next

In this sample code, we're not doing any actual work. Yours will of course. Rather than attempting to update the ProgressBar directly, we update the Progress property so the timer can know how much work has completed. And speaking of your timer, use this code for its Action event:

ProgressBar1.Value = Self.Progress
If Self.Progress >= ProgressBar1.Maximum Then
Me.Mode = Timer.ModeOff
End If

We use the timer to update the ProgressBar. There is no need for a Refresh call, because the ProgressBar will redraw automatically when the Timer Action event completes. If the ProgressBar is full, the work is done, so we turn off the Timer. And lastly, to start the work, use this code:

Self.Progress = 0
Timer1.Mode = Timer.ModeMultiple
Thread1.Run

We first reset the Progress property, just in case. Then "turn on" the Timer so it updates the ProgressBar frequently while the work is being done, and finally actually start the work by calling Thread1.Run. Now all you need to do is update Timer1's properties to adjust the rate you want to ProgressBar to update. On a Desktop app, you can easily get away with a Period of 250. On a Web app, you'll want to slow it down though. A minimum of Period of 1000 is strongly recommended, though I would use 2000 personally. Set Timer1.Mode to Off, there's no need to update the ProgressBar if nothing is happening.

You could also turn all this into a handy reusable class.

That's it. Your ProgressBar will update nicely, your app will be a "good citizen" and your users will be happier.

8 comments:

Gerrut said...

Very informative. I was already wondering why the progress bar did not update in some of my experiments, but of course it only gets its information after the code is finished :$

anic297 said...

While I also think the thread way is better, the arguments mentioned here are perhaps discussable.

“your app locks up entirely while the loop is doing its work. The user interface only updates once all your code is finished, so buttons will not be clickable, windows will not be movable, menus not be usable, etc”
Ok, I knew that. However, under certain circumstances, I don't plan to allow the user to be able to use the UI at all (while an operation that shouldn't be cancellable occurs, and the user shouldn't have any interaction).
Sometimes, I don't expect nor care that the user wants to be able to move the modal dialog showing the progress.

“It's also slower. The Refresh call demands the ProgressBar be redrawn before the next line of code is triggered. This is wasted effort as the ProgressBar doesn't need to update so frequently.”.
Ok, but, for the rest of the code, using a thread is slower than using the main thread (this is from Mac OS 9 days; perhaps it's no longer true?).
As for being updated too frequently, doesn't it update once in both cases? (either with .refresh or, in a threading model, when you change the value)

Some questions that came in mind…

Thom McGrath said...

@anic297

It's more than the user interacting with the UI. On Mac, your app will appear as "Not Responding". On Windows, the same is true, but Windows will actually display a "This program has stopped responding" message. To the OS, your app is stuck in an infinite loop.

Threads don't inherently run any faster or slower than the main thread. They're all basically just threads.

As for updating, the progress bar will be forced to refresh when you call .refresh. Under the threading model, the progress bar is updated only as frequently as your timer fires. Imagine a progressbar update takes 1ms. In a 1000 iteration "forced" loop, you'll have wasted an entire second. In a threaded loop, you'll probably not even waste a hundredth of a second.

michaeljk said...

What about updating a Listbox instead of a ProgressBar ? How would you transfer the data from the thread to the main window / the timer? I have a SQL Query which executes fast in a thread, but the painting of the listbox is really slow.

Thom McGrath said...

@michaeljk I would load the data into an array, array of dictionaries, array of classes, something like that.

markusandbianca said...

I tried this and it seems to be much better to use the value only to update the progress bar, but not to rely on the progress bar for updating the user interface (it can cause timing problems which have weird effects - see http://forums.realsoftware.com/viewtopic.php?f=10&t=44693)

So do

If NOT Thread_Update.State=Thread.Running Then

instead of

If Self.Progress >= ProgressBar1.Maximum Then

(solution thanks to Jim)

Markus Winter said...

> You could also turn all this into a handy reusable class.
How? There was some discussion on the newsgroug but it seemed to hit a snag, so I'm curious how you would do it.

thehman said...

I'm able to use the format: If I mod 70 = 0 (where 70 is 1% of total items) then I'm updating the progress bar only 100 times. If I use your method and put my code that does the work in the thread I would have to have 10 threads for 10 separate routines, correct?