It's late. I'm very hungry. I really want to go home. In fact, I almost left 3 hours ago, except something very strange started happening with our UI bits and I didn't know why. Turns out we had a plumbing problem. And now I need to vent.

Plumbing is great. In fact it's so great that you don't even know it exists until your lawn starts smelling funny or your toilets stop flushing. And then you're forced to remember.

Windows Forms is a fantastic library is a sense that it tries to isolate you from a lot of yucky stuff that you don't want to deal with - handles, message pumping, lovely functions such as CreateWindowEx  that take a boatload of arguments etc.(that is on current version Windows of course. On other platforms - or maybe future Windows platforms - the "yucky stuff" may be entirely different. But there will be  some). Basically WinForms hide the plumbing, and it feels great... and then you pipe bursts. Bizarrely, sometimes you didn't even know there was a pipe. Worse yet, something you don't even know what the heck is happening, except there's a whole lot of water on your carpet.

Enough of this - here's what happened. Pop-quiz: what's wrong with this code?

 

                public class Form1 : System.Windows.Forms.Form

                {

                                private ToolBar toolBar1;

                                private ToolBarButton toolBarButton1;

 

                                public Form1()

                                {

                                                InitializeComponent();

                                }

                                private void InitializeComponent()

                                {

                                                this.toolBar1 = new System.Windows.Forms.ToolBar();

                                                this.toolBarButton1 = new System.Windows.Forms.ToolBarButton();

                                                this.SuspendLayout();

                                                this.toolBar1.Buttons.AddRange(new System.Windows.Forms.ToolBarButton[] { this.toolBarButton1 });

                                                this.toolBar1.ButtonSize = new System.Drawing.Size(36, 36);

                                                this.toolBar1.Name = "toolBar1";

                                                this.toolBar1.TabIndex = 0;

                                                this.toolBarButton1.Text = "A";

                                                this.Controls.Add(this.toolBar1);

                                                this.Text = "Form1";

                                                this.Layout += new System.Windows.Forms.LayoutEventHandler(this.Form1_Layout);

                                                this.ResumeLayout(false);

                                }

 

                                [STAThread]

                                static void Main ()

                                {

                                                Application.Run(new Form1());

                                }

 

                                private void Form1_Layout(object sender, System.Windows.Forms.LayoutEventArgs e)

                                {

                                                this.toolBar1.ButtonSize = new Size( this.Width/10, this.Height/10 );                               

}

}

 

 

This is all pretty straight-forward, huh? You have your form, which has a toolbox with one button. You then want your toolbar buttons to resize appropriately, based on the size of the form, so you start handling Layout event which, according to MSDN, is called "when a form or a control should reposition its child controls" (yes, I realize that the handler sucks - this.Width/10 may turn in 0 and then things will go bad... just bear with me people).

Looks good? Good. Now run it. Seriously, copy/paste this thing into a small C# project and run it. I'll wait.

I know. Such innocuous code and such nasty error. If case you haven't gone through this little exercise, Application.Run() throws Win32Exception saying "Error creating window handle". So now you have water on your carpet and you don't know why.

There are three factors here

·         Form.Layout gets fired from ToolBar.OnHandleCreated(). Yup. It does. If you don't believe me, just set a breakpoint and look at the stack.
Why is that? I'm not certain. This is definitely a little excessive, but hardly is a crime. All handles should be created by then, no reason to worry... or is there?

·         In Windows, toolbar button size can be set through TB_SETBUTTONSIZE message. That's all great, but if you read MSDN carefully, you'll see that "The size can be set only before adding any buttons to the toolbar".
Why is that? I really have no idea. Maybe it's just hard to resize things dynamically. Again, a little too strict, but hardly a crime.

·         Windows Forms try hard to hide all yucky stuff from you. So you add some buttons, call ToolBar.ButtonSize... what a poor ToolBar to do? Easy - simply recreate the handle, and set the new size. Again, a little excessive, but there isn't much that can be done here. OK, so it could have thrown an obscure exception saying that you can't change button size, but that would be too intimidating and confusing.

 

So far, so good. Let's put this together(I haven't really seen source code for this, so this is all based on common sense and debugging)

1.       Your run your app and the loop starts pumpin'

2.       Your form gets WM_SHOWWINDOW message and realizes and needs to show its children, so it starts creating "real" Win32 windows and calls CreateWindowEx() for everything, including the toolbar.

3.       "Real" windows toolbat gets created, WM_CREATE is sent to it and OnHandleCreated() gets called. Toolbar then fires up Parent.Layout. Keep in mind we are still inside CreateWindowEx call - it doesn't return until WM_CREATE is fully handled.

4.       In Form1_Layout handler, we attempt to change toolbar button size, which - as we know - can only be done via handle recreation - so the toolbar calls DestroyWindow.

 

What does this mean? We destroy the handle that was created while still in CreateWindowEx that creates the very handle! Needless to say, Win32 gets upset, and CreateWindowEx fails.

Caboom.

So, is this a bug? I'd say Toolbox should be much more careful with what it does - especially when that comes from OnHandleCreated. Or make handle re-creation conditional based on whether buttons have been created or not. Or both - that way Parent.Layout gets fired first, then ButtonSize sees that there are no buttons yet, so no need re-create the handle. Yup, that'll do it.

If only it was so easy. The truth is, as much as you can try, when you attempt to create a simple and consistent model on top of something that is not quite to straight-forward(Win32 is pretty old and inconsistent in places, which is the very reason Longhorn API gets introduced), you will always run into something like this.

 [Sigh] Anyway, all of this is fixable, but in a rather ugly way. This is what I came up with:

            public class SafeToolBar : ToolBar

                {

                                private bool fullyCreated = false;

                                internal bool FullyCreated

                                {

                                                get { return this.fullyCreated; }

                                }

                                protected override void OnHandleCreated(EventArgs e)

                                {

                                                base.OnHandleCreated (e);

                                                fullyCreated = true;

                                }

                }

 

I then use this class instead of ToolBar, and modify my OnLayout as follows :

 

                private void Form1_Layout(object sender, System.Windows.Forms.LayoutEventArgs e)

                {

                                if ( !toolBar1.FullyCreated )

                                                return;

                                this.toolBar1.ButtonSize = new Size( this.Width/10, this.Height/10 );

}

 

This way the "dangerous" part of Form1_Layout will only be called after the toolbar is completely done creating.

I'll talk to WinForm folks and see if we can permanently fix this...

[Sigh...again]Moral? The plumbing is there. Even if your pipes are all neatly tucked away into white closets, there's still water in them.  It helps to understand how things really work... although in most cases it is convenient to forget the plumbing exists - if it works well. And I definitely think that Windows Forms team has done a pretty great job hiding it :)

P.S. I have discussed this with Michael Entin - a colleague of mine (who really ought to start blogging... but I digress) - and he went "Yeah, leaky abstractions everywhere...". Read the link, it's nothing revolutionary, but very insightful...