When do your forms get repainted? (Or: How does your computer REALLY work?)
Well, the easy answer to this question is: whenever it receives a paint message.
So the real trick then is: when do your forms receive a paint message?
Ah, well, I'm glad you asked!
You see, you know how there's a little tiny man that lives inside your fridge? He generally lives in a little box near the top. Whenever you open the door, he turns the light on for you.
When I first discovered this, I spent hours trying to surprise him, opening the fridge door at odd times, just to make sure he was still awake - maybe I could catch him napping and he'd not turn it on?
But as the more observant of you will have discovered, there's actually a little switch inside the door, so that when you open the fridge door you can the switch move. Tricky huh? When the switch gets moved, the little man gets poked in the head to let him know he's got to turn the light on. This way he can have his little naps and not worry about not turning the light on for you. It's because of this you should be careful not to open and close the fridge door over and over and over again. The little man gets pissed off and leaves.
I also make sure that whenever I go somewhere that has automatically opening doors to wave and smile at the little box above the door. The little man in there that saw you coming always appreciates acknowledgement at a job well done. But the automatic doors don't have a little switch like the fridge door does. Whenever the door doesnt' open for me, I always take a step back and look up at the little man. A small shout of 'Hey, wake up little man!' will normally wake him up and he'll open the door for you straight away after that.
But I digress. The point to that little story is that computers have two little men. One little man sits in side the computer, keeping track of when something needs to be repainted. When it does, he yell down the tube that runs from the back of your computer to the back of your monitor what needs to be repainted, and the other little man, the one inside the monitor, rushes around behind the glass painting the bits that need it as he hears the first little man telling him what to do. There's actually a few little men inside the computer, all making different things happen. This also explains why the faster your computer runs, the hotter it gets. To get more speed, you need to add more little men to get the work done - and with such a lot of little men inside such a small space, are you surprised it doesn't get a bit warm in there? Methane gas is quite hot when it's fresh - and we all know what men like to do best.
Oh, and no, I don't think these little men are Fornits. Fornits only live in typewriters.
Why have I brought this up?
Well, an interesting mail appeared on the aus-dotnet mailing list the other day. Paul Stovell provided a piece of code, wondering what it acted the way it did. Imagine a form with one checkbox and 7 labels. It's nothing special. While he posted it in C#, I'd prefer not to get myself dirty tonight, so here's the english translation:
Private Sub CheckBox1_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CheckBox1.CheckedChanged
Dim value As Boolean = CheckBox1.Checked
Label1.Visible = value
Thread.Sleep(100)
Label2.Visible = value
Thread.Sleep(100)
Label3.Visible = value
Thread.Sleep(100)
Label4.Visible = value
Thread.Sleep(100)
Label5.Visible = value
Thread.Sleep(100)
Label6.Visible = value
Thread.Sleep(100)
Label7.Visible = value
Thread.Sleep(100)
End Sub
If the checkbox is checked, the labels are all visible. If it's not, the labels are hidden. No big deal right?
At first glance, it looks like the code is simple. Make the first one visible, wait a moment, then the next, wait a moment, then the next...etc. Depending on how much you think you know about UI and threading, you'd expect this either to work, or not.
But no. When you make the labels invisible, they disappear one at a time. When you bring them back, there's a long pause (strangely enough, about 700 milliseconds worth) and then they all blink back into existence in one hit.
Whatever the correct outcome, you'd expect it to at least be consistent wouldn't you? Paul also mentioned (and of course I had to double check) that the same thing happens in VS2005.
I thunk about it for a bit. What I realised is that the actual bug is that it worked when the labels were being made invisible.
Why? Well, let's look at our little men.
Windows forms controls are not threadsafe. What this really means that each little man can only do one thing at once, so if you give him too much to do, he gets all flustered. When all the controls on a form are created, the little man responsible for your form has to keep track off all the different controls that you've placed on it. While he's busy watching all these controls, making sure they don't fall asleep and such, if you do something to a control from another 'thread', he loses track of what he was doing. Hence controls are not 'threadsafe'.
When you hook an event on a control, what you've done is asked the little man that's looking after your controls to run some code for you. Running code is pretty hard, and it takes all his thinking power to do. So while he's running your code, he's temporarily ignoring all the other controls that are there - (my wife says like any typical male he can only do one thing at once). If the code that he's running makes any changes to any of the controls he's supposed to be watching, he cant' do anything about it, since he's busy running your code. This is what's known as 'blocking the UI thread' - what you've really done is made that little man too busy to do anything else.
So when does your form get repainted? When this little man isn't running any of your code, he's able to look at his list of things to do and get all his jobs taken care of. People who aren't willing to admit the existence of the little men call this the 'message pump'. When you hook an event, what you're really doing is letting the little man know to call your code when a job appears on his list that you are interested in.
So let's look back at the code from before. All in the one method we're setting the visible property of 7 different controls - well, this shouldn't be done at all, because the little man is busy. Calling Thread.Sleep() when you're within code that's on the UI thread just causes the UI thread to sleep for a little while. Sure, the little man appreciates being able to have a little rest now and again (Thread.Sleep(100) translates to some IL - IL stands for 'Insy-man Language' - that means 'Little man, have yourself 100 beers'. Little men only have little beers, so therefore it doesn't take them very long to drink them. The time it takes to drink about 100 little beers is roughly 100 milliseconds.) but while he's resting, he's still not taking care of his list of things to do.
The discussion we had on the mailing list brought up 3 points that I found interesting.
- There's the old standard from the VB6 days which made it into the .net framework: Application.DoEvents.
- There's a much more focused way of getting things updated on a tighter leash: Control.Refresh/Control.Update
- In the .Net Compact Framework, there's no Control.InvokeRequired property.
Application.DoEvents
You remember the DoEvents keyword from your VB6 days? Well, it's still around (in case you didn't know) as a method on the Application object. A call to DoEvents (if you have a look at it in Reflector) ends up in a call to System.Windows.Forms.Application.ThreadContext.LocalModalMessageLoop(). This function empties the message pump of all queued up messages (the message pump is considered empty when it there's no more messages in the queue _and_ it waits for 100 milliseconds for something new to arrive, and nothing did). In real terms, this tells the little man running your code to go and clear off his list of things to do before coming back and running more of you code. So if we change the above code to be a set of calls like this:
Label1.Visible = value
Application.DoEvents()
Thread.Sleep(100)
The little man gets to run off and clear out his list, and then have a 100 beers for doing such a good job! Changing the visibility state of Label1 caused a new item to be added to his list of things to do - repaint that area. By calling DoEvents he had a chance to shout down the monitor tube that that bit of the screen needed to be redrawn. A side effect of that is that anything else on his list of things to do _also_ got done too.
Control.Refresh/Control.Update
Both of these have the net effect of getting the specific control in question repainted, but nothing else in the list of things to do gets touched.
Label2.Visible = value
Label2.Refresh()
Thread.Sleep(100)
Label2.Refresh here (again looking in reflector) does two things: Label2.Invalidate() followed by Label2.Update().
So what's most important there is what does Control.Invalidate do? Well, let me quote the MSDN doco on the subject: 'Invalidates a specific region of the control and causes a paint message to be sent to the control.' MSDN, of course, stands for 'Minuscule Senors DotNet'. So - all we've really achieved here is to add a 'Hey, repaint Label2' message to be added to the little man's list of things to do. But can he do it? Hell no. He's too busy running our code - or in this specific case moving directly onto Label2.Update().
So all Update does is fall down into the Win32 API User32 method UpdateWindow. Again, the MSDN doco is useful: 'The UpdateWindow function updates the client area of the specified window by sending a WM_PAINT message to the window if the window's update region is not empty. The function sends a WM_PAINT message directly to the window procedure of the specified window, bypassing the application queue. If the update region is empty, no message is sent.'
What this comes down to is a call to UpdateWindow adds a 'repaint this area' job to the list of things to do, but at the same time it's added with about 5 gold stars, which means 'deal with this *** right now, or you get no more beers for a week'.
No Control.InvokeRequired in .Net CF
Well, bummer. This isn't really something I want to talk about, but when Nick Randolph mentioned it, I was a little bit saddened.
The End of the Story
Let's get back on track. So why did it work all the way you'd think one way but not the other? In the end it comes down to the Control.Visible property.
If you fire up Reflector again and check out this one, we end up deep in the bowels of System.Windows.Forms.Control.SetVisibleCore (a protected method that we aren't supposed to play with that much. It's Protected Overridable however, so if you write your own control, you can by all rights override it and really mess with peoples heads!). Reading though this method, I saw that almost every time you it's called, it ends up making a call to SetWindowPos in User32, passing either SWP_SHOWWINDOW or SWP_HIDEWINDOW as one of the flags to the call.
Sidetrack! On the MSDN website I found this handy little article: About Messages and Message Queues. In it, it talks about how messages work (how jobs get on to the little man's list of things to do) and how some some messages are queued and some aren't (some of the messages have 5 gold stars):
'Nonqueued messages are sent immediately to the destination window procedure, bypassing the system message queue and thread message queue. The system typically sends nonqueued messages to notify a window of events that affect it. For example, when the user activates a new application window, the system sends the window a series of messages, including WM_ACTIVATE, WM_SETFOCUS, and WM_SETCURSOR. These messages notify the window that it has been activated, that keyboard input is being directed to the window, and that the mouse cursor has been moved within the borders of the window. Nonqueued messages can also result when an application calls certain system functions. For example, the system sends the WM_WINDOWPOSCHANGED message after an application uses the SetWindowPos function to move a window.'
Ah ha! Like when SetWindowPos is called! So I did some more playing around (man, I really wish Reflector would work with Win32 dlls!) and discovered that when you call SetWindowPos with a flag of SW_HIDEWINDOW (Control.Visible = False), it causes a nonqueueueueued WM_ERASEBKGND message to sent -which causes the little man to immediately drop everything and get the damn thing hidden!
So, to answer some of the people who were wondering, this is not a bug in the framework! The little man isn't to blame either - he was doing exactly what he was told to do by Windows. The SetWindowPos function is to blame, so it's a windows core thing. I guess whoever designed it decided that hiding something was a high priority job, but showing something wasn't.
So in the future - when you open your fridge, walk through a door, or use a computer, don't forget to say hi to your little man once in a while. They really do appreciate it.
Listening to: tiny dancer - ben folds - (5:23)