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?
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.