On WinForms and plumbing


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…

Comments (9)

  1. dom says:

    I sympathise.

    I’m currently having enormous problems w/ this kind of stuff.

    🙂

  2. farproc2000 says:

    Have you tried doing a ‘protected override void OnLayout’ instead of using an event handler in your Form? It worked for me but a Toolbar may ‘behave’ differently.