TheCaseOfTheInconsistentParent

November 26, 2007

Start with a simple Windows.Forms form and set TopMost to true. Add a button to it and put this code in the click event:

Form form = new Form();
form.ShowDialog();

Run it, and observe: the second form comes up behind the TopMost form.

Now, make a slight change to the ShowDialog call, passing in the calling form (the TopMost form):

Form form = new Form();
form.ShowDialog(this);

Run it, and the second form now comes up in front of the TopMost form.

What's the difference?

MSDN says:
Form.ShowDialog ()Shows the form as a modal dialog box with the currently active window set as its owner.
Form.ShowDialog(IWin32Window)Shows the form as a modal dialog box with the specified owner.


But, in our first example above, surely the TopMost form is the currently active window? The docs aren't much help. Fire up Reflector.

The guts of System.Windows.Forms.Form.ShowDialog() are simple:

public DialogResult ShowDialog()
{
return this.ShowDialog(null);
}

The guts of ShowDialog(IWin32Window) ... not so much. (You can see all 102 lines yourself in Reflector).

What's different if the IWin32Window argument is null? Here seems to be some pertinent code:

public DialogResult ShowDialog(IWin32Window owner);
{
...
IntPtr activeWindow = UnsafeNativeMethods.GetActiveWindow();
IntPtr ptr3 = (owner == null) ? activeWindow : Control.GetSafeHandle(owner);
...
UnsafeNativeMethods.SetWindowLong(new HandleRef(this, base.Handle), -8,
new HandleRef(owner, ptr3));
...
}

If the owner parameter is null, the active window is grabbed, just like the MSDN docs had said. SetWindowLong looks like the important call, doing something with the handles of both forms, presumably setting the calling form (assuming that's what's returned from GetActiveWindow) as the owner of the opening form.

What's the -8 magic number? According to WinUser.h in the Platform SDK, -8 is the GWL_HWNDPARENT constant. Which is interesting, for two reasons. One, the prior MSDN docs had told us the ShowDialogs set the owner of the opening form, not the parent (not to mention the name of the ShowDialog parameter is owner). Two, the MSDN docs on SetWindowLong warn us away from using this constant:
You must not call SetWindowLong with the GWL_HWNDPARENT index to change the parent of a child window. Instead, use the SetParent function.
Why the heck is the .NET Framework doing something the docs say shouldn't be done?

Hmmm ... a Google Groups search on GWL_HWNDPARENT yields this explanation:
(In)famous misleading statement. Almost as misleading as the choice of
GWL_HWNDPARENT as the name. It has nothing to do with a window's
parent. It really changes the Owner...
A more accurate version might be..
“SetWindowLong with the GWL_HWNDPARENT will not change the parent of a
child window. Instead, use the SetParent function. GWL_HWNDPARENT
should have been called GWL_HWNDOWNER, but nobody noticed it until
after a bazillion copies of the SDK had gone out. This is what happens
when the the dev team lives on M&Ms and CocaCola for to long. Too bad.
Live with it.”
Ahh ... while not official documentation, it makes everything else add up.

So. A summary is in order. Calling ShowDialog() should setup the current active window (which should be the calling form) as the owner. Calling ShowDialog(this) should setup the calling form (which should be the active form) as the owner. But in one case the opening form comes up behind the TopMost form, in the other the opening form comes up in front of the TopMost form.

Brainstorm:
- The call inside ShowDialog(IWin32Window) to GetActiveWindow isn't working.
- The call to SetWindowLong isn't working.
-

Ok, braindrizzle.

Something is different about the two opened form instances. Perhaps Spy++ can shed some light.

In both cases, the form has the same styles and ex-styles:

WS_CAPTION
WS_VISIBLE
WS_CLIPSIBLINGS
WS_CLIPCHILDREN
WS_SYSMENU
WS_THICKFRAME
WS_OVERLAPPED
WS_MINIMIZEBOX
WS_MAXIMIZEBOX

WS_EX_LEFT
WS_EX_LTRREADING
WS_EX_RIGHTSCROLLBAR
WS_EX_WINDOWEDGE
WS_EX_CONTROLPARENT
WS_EX_APPWINDOW


One thing that is different. When the opening form comes up on top, its Parent handle is the original TopMost form. When the opening form comes up beneath, its Parent handle is null. In both cases, Owner handle is the TopMost form.

This confirms the SetWindowLong call is working properly, and the GetActiveWindow call is also working, closing those two leads. However, another lead has presented itself: parent setting.

Search the ShowDialog(IWin32Window) code in Reflector for any mention of parent.

Nada.

We know where the owner relationship is setup in the ShowDialog method. Where is the parent relationship setup?

A search for SetParent in Reflector yields nothing too obvious (actually, I believe I spent a bit of time chasing this trail before coming up short. I thought I'd skip over that bit here). More research leads to CreateWindowEx, which takes a parent handle parameter.

But what's the connection between ShowDialog and CreateWindowEx?

Reflector shows CreateWindowEx is called inside Windows.Forms from System.Windows.Forms.UnsafeNativeMethods.CreateWindowEx, which is called only from System.Windows.Forms.NativeWindow.CreateHandle(CreateParams). CreateHandle passes in the Parent property of the CreateParams instance. How does the Parent property get set?

It's set in many methods, but Forms.Form.get_CreateParams seems the most pertinent. It's a long method, but only a few lines devoted to the Parent property of the CreateParams instance it's putting together:

IWin32Window window = (IWin32Window) base.Properties.GetObject(PropDialogOwner);
if (window != null)
{
createParams.Parent = Control.GetSafeHandle(window);
}


Those playing along at home, paying close attention, may now notice a common bit here.

The earlier snippet from ShowDialog omitted one line, done so because it didn't seem relevant to me in real time when I was researching this problem originally. Here's the previous snippet, plus the missing line:

public DialogResult ShowDialog(IWin32Window owner);
{
...
IntPtr activeWindow = UnsafeNativeMethods.GetActiveWindow();
IntPtr ptr3 = (owner == null) ? activeWindow : Control.GetSafeHandle(owner);
base.Properties.SetObject(PropDialogOwner, owner);
...
UnsafeNativeMethods.SetWindowLong(new HandleRef(this, base.Handle), -8,
new HandleRef(owner, ptr3));
...
}


PropDialogOwner is the connection. ShowDialog sets it, get_CreateParams reads it and uses it as the parent property when creating the window handle.

Notice that unlike the code setting up the owner relationship, which uses ptr3, which will be the explicit owner or the currently active window, the call to set the PropDialogOwner, which is used as the parent handle later, ignores ptr3 and uses the owner parameter regardless. If it's null (which it will always be with a plain ShowDialog() call), then the new form gets a null parent and opens up behind the TopMost form who opened it.

We've finally a diagnosis. Now ... what can we do about it?


Fortunately, the Windows.Forms.Form class allows subclasses to intervene in the creation of the CreateParams instance, so we can add some code that will make sure the Parent property of the CreateParams instance is always set the currently active window, like I believe the code inside ShowDialog should do.

Change the sample code we started above to open a subclass of Form instead of just an instance of Form itself:

MyForm form = new MyForm();
form.ShowDialog();


And add the following code somewhere in MyForm :

public class MyForm:Form
{
...
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.Parent = (IntPtr)GetActiveWindow();
return cp;
}
}

[DllImport("USER32", CharSet=CharSet.Auto)]
extern static int GetActiveWindow();
...
}


One of the things I love about this case is it's a perfect example of the ease of the fix when proper time is given to an accurate and thorough diagnosis. If you ever find yourself and/or your colleagues standing around discussing pot-shot solutions to a problem when little to no time has been spent fully understanding the problem itself - stop immediately and go do some more digging. (Of course, now that I've posted this, someone will come along and show me something I missed in my thorough diagnosis that makes most of it moot, a better solution obvious and outs me for the fool that I am.)

It's also a great example of how UI frameworks are pulling off a lot of magic underneath the covers, and why their LeakyAbstractions can go deeper than other types of leaks.


For those of you thinking, “Why the heck go for this obtuse CreateParams override workaround when using ShowDialog(this) appears to work fine?” There's a variant of the original problem in which even the ShowDialog(this) call itself fails to produce the calling form to be on top of the original TopMost form.

Assuming again a call to MyForm:

MyForm form = new MyForm();
form.ShowDialog(this);

And make the constructor of MyForm look like this:

public MyForm()
{
InitializeComponent();
Screen s = Screen.FromControl(this);
}

Now, the ShowDialog(this) fails same as ShowDialog(). Why is that?

I had to pick a rather esoteric line like Screen.FromControl to illustrate the point. It's more direct to recreate the problem by having MyForm do this:

public MyForm()
{
InitializeComponent();
System.IntPtr h = this.Handle;
}

Screen.FromControl(this) will internally use the Handle property. The Handle property will force a handle to be created for the window if one hasn't yet. There are many methods that will also refer to the Handle property from with the Form class itself, but all of them I spot-checked first check the .IsHandleCreated property first before referring to the Handle property, so I had to fall back to Screen.FromControl, because it doesn't do that check (presumably because it must have a handle to do what it needs to do, whereas most internal Form settings can store the values internally until such point the handle is actually needed). Besides, in the real diagnosis, I had a form that had a call to Screen.FromControl in its constructor code which further confused the original diagnosis since most forms worked fine if we simply changed the ShowDialog() calls to ShowDialog(this) calls, but not all.

And as mentioned earlier, creating the window handle has everything to do with what's going on here: “Reflector shows CreateWindowEx is called inside Windows.Forms from System.Windows.Forms.UnsafeNativeMethods.CreateWindowEx, which is called only from System.Windows.Forms.NativeWindow.CreateHandle(CreateParams). CreateHandle passes in the Parent property of the CreateParams instance.”

Since in this odd case, CreateWindowEx is called from the MyForm constructor before ShowDialog has been called, ShowDialog hasn't had a chance to call SetObject(PropDialogOwner, owner). When GetObject(PropDialogOwner, ...) is called, it's 0 and so is the parent value in the CreateParams instance.

Fortunately, the CreateParams override also handles this case, and seems to be the place to effectively workaround this problem.


One caveat about the workaround, though it seems to be a minor one. Inside the get_CreateParams call, you can see in Reflector some code that checks for state 0x80000, and if it's been set, to force the parent property of the CreateParams instance to be 0. Further searching in Reflector shows 0x80000 to be the TopLevel setting, described in MSDN as:
A top-level form is a window that has no parent form, or whose parent form is the desktop window. Top-level windows are typically used as the main form in an application.
My workaround of setting the parent property obviously then will undo the TopLevel setting, so that can't be used in that case. But needing a form opened by a TopMost form to be a TopLevel form and appear in front of the TopMost calling form seems a very weird requirement and not likely to occur.

tags: ComputersAndTechnology
comments powered by Disqus