0

I am trying to understand a certain longstanding concept in Windows Forms re: UI programming; following code is from Chris Sells' Windows Forms Programming book (2nd Ed., 2006):

void ShowProgress(string pi, int totalDigits, int digitsSoFar) {
  // Display progress in UI
  this.resultsTextBox.Text = pi;
  this.calcToolStripProgressBar.Maximum = totalDigits;
  this.calcToolStripProgressBar.Value = digitsSoFar;

  if( digitsSoFar == totalDigits ) {
    // Reset UI
    this.calcToolStripStatusLabel.Text = "Ready";
    this.calcToolStripProgressBar.Visible = false;
  }

  // Force UI update to reflect calculation progress
  this.Refresh();
}

This method is part of small sample application that has another long-running method which calculates Pi. Each time a cluster of digits are calculated, ShowProgress() is called to update the UI. As explained in the book, this code is the "wrong" way of doing things, and causes the UI to freeze when the application is minimized and then brought into the foreground again, causing the system to ask the application to repaint itself.

What I don't understand: Since this.Refresh() is being called repeatedly, why doesn't it process any system repaint event that is waiting for attention?

And a follow-up question: When I add Application.DoEvents() immediately following this.Refresh(), the freeze-up problem disappears. This is without having to resort to Invoke/BeginInvoke, etc. Any comments?

Sabuncu
  • 5,095
  • 5
  • 55
  • 89
  • DoEvents tells the system to ignore events? It doesn't process events? Then why does the UI start becoming responsive after DoEvents is used? – Sabuncu Jan 04 '12 at 19:01
  • 1
    Here's some more background information from our own Jeff Atwood: http://www.codinghorror.com/blog/2005/08/is-doevents-evil-revisited.html – 500 - Internal Server Error Jan 04 '12 at 19:11
  • @DJ KRAZE: Appreciate your comments but can you be more specific? When you say "this just skips them and refreshes your stuff", do you refer to Refresh or DoEvents? Also, my question is not about what to do, but rather what goes on behind the scenes wrt Refresh and DoEvents. I am familiar about the requirement that cross-thread calls need to follow a protocol when controls on the UI thread are being updated. – Sabuncu Jan 04 '12 at 19:12
  • 2
    @DJKRAZE: Wow, your comments are not very coherent. But if I understand you correctly, you say "if there are events waiting to be processed" then `DoEvents()` "just skips them and refreshes your stuff". That is **completely wrong** -- in fact, it is the **opposite** of what `DoEvents()` actually does. Wiktor Zychla describes the behavior in his answer, but `DoEvents()` essentially sets up a second message pump loop, and pumps messages until the queue is empty. This is very dangerous precisely because it *doesn't* "skip events" -- it handles whatever's in the queue, not just "your stuff". – Daniel Pryden Jan 04 '12 at 19:36
  • I mixed the wording becasue I got caught up in trying to formulate my initial answer in regards to referencing what I use to do in Delphi explanation.. sorry for the confusion – MethodMan Jan 04 '12 at 20:42
  • Sorry, had to mention that the title of this question reminds me of a classic Simpsons quote: "Don't do what Donny Don't does". – Kevin McCormick Jan 04 '12 at 23:48

1 Answers1

5

Basically, the reason for this is the way Windows handles messages - it does this in a synchronous way in an internal message loop.

The point is that there was a message that triggered your code. For example a button click. Your application is in the middle of handling the message. From within this handler, you force the refresh which puts another WM_PAINT in the message queue. When your handler finishes, the message loop will surely pick it up and dispatch, thus repainting the control. But your code is not finished, in fact it loops calling your ShowProgress, causing WM_PAINT being queued forever.

On the other hand, the DoEvents() causes an independent instance of the message loop to fire. It's fired from within your code which means that the call stack looks like this:

outer message loop -> your code -> inner message loop.

The inner message loop processes all pending messages, including the WM_PAINT (thus the control is redrawn) but it is dangerous - as it will dispatch all other pending messages, including button clicks, menu clicks or event closing your application with the X at the top-right corner. Unfortunately, there's no easy way to make the loop to process the WM_PAINT only which means that calling DoEvents() exposes your application to subtle potential problems involving unexpected user activity during the execution of your code which triggers the DoEvents.

Wiktor Zychla
  • 47,367
  • 6
  • 74
  • 106
  • 2
    Well, there *is* a way to exclusively get WM_PAINT to dispatch: the form's Update() method. Or Refresh(), unnecessary here. It is not dispatching the *other* messages that convinces Windows that the UI thread isn't pumping messages and to display the ghost window after 3 seconds. Which is accurate, it isn't. More about DoEvents here: http://stackoverflow.com/questions/5181777/use-of-application-doevents/5183623#5183623 – Hans Passant Jan 04 '12 at 19:31
  • @Wiktor: If we consider the case where DoEvents is NOT used: Refresh requests by the application itself are honored. But when the application is forced into background and then brought back to foreground, the UI freezes up. Prior to this, even though the application was looping, the Refresh requests were being honored, but now they are not? – Sabuncu Jan 04 '12 at 19:34
  • I guess this is the part that I don't understand: you say "When your handler finishes, the message loop will surely pick it up and dispatch, thus repainting the control." But why? The application is busy, although it calls Refresh once in a while. It seems that responding to the repaint requests is the ONLY thing that the dispatcher does. It selectively answers repaint requests, and nothing else. Is this correct? – Sabuncu Jan 04 '12 at 20:14
  • @Sabuncu: without the rest of the code it's impossible to say what's the cause of freezing when brought back to foreground. When you bring your application back, it also repaints the window. For some reason it makes your repainting to fail. As for the message loop (dispatcher) - it dispatches all incoming messages, not only the WM_PAINT. – Wiktor Zychla Jan 04 '12 at 20:22