Re: ParkingWindow revisited by Clive
Clive
Thu Jun 24 04:55:40 CDT 2004
Benzi,
I found a workaround for my particular circumstance. Here I reproduce the
report I produced for circulation within my company. Hope it helps for you
too.
Ying,
Also as part of this is a description of how to recreate the problem with a
very simple project, with C# code included. I hope that you can take this
and find the exact nature of the problem.
The problem in its barest form
The problem can be easily recreated as follows (example C# code attached):
Create a .NET windows control library project.
Add an owner draw control and a button to the user control.
Add a derived control class to handle the owner draw, modify the IDE
generated code to use this class.
Explicitly create the owner draw control in InitializeComponent using
CreateControl. (This part is the crux - in our company's product, there are
components where InitializeComponent is setting certain properties of child
owner draw controls which cause the child window handle to be created.)
Add button click handler that shows a message box.
Add COM attributes to user control class.
Add ComRegister function to register component as ActiveX control.
Set project to register for interop.
Compile.
Add control to tstcon32.exe (or add to MFC app dialog).
Press button on user control to show message box.
Owner draw control is now broken.
Code for UserControl.cs:
using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Microsoft.Win32;
namespace WindowsControlLibrary
{
[Guid("BDD17877-B805-4970-B3BB-2F1A2C1E5A38")]
[ProgId("WindowsControlLibrary.UserControl")]
[ClassInterface(ClassInterfaceType.AutoDual)]
public class UserControl : System.Windows.Forms.UserControl
{
private WindowsControlLibrary.OwnerDrawCombo comboBox1;
private System.Windows.Forms.Button button1;
private System.ComponentModel.Container components = null;
[ComRegisterFunction]
static void ComRegister(Type t)
{
string keyName = @"CLSID\" + t.GUID.ToString("B");
using (RegistryKey key = Registry.ClassesRoot.OpenSubKey(keyName,true) )
{
key.CreateSubKey("Control").Close();
}
}
[ComUnregisterFunction]
static void ComUnregister(Type t)
{
string keyName = @"CLSID\" + t.GUID.ToString("B");
Registry.ClassesRoot.DeleteSubKeyTree(keyName);
}
public UserControl()
{
InitializeComponent();
}
protected override void Dispose( bool disposing )
{
if( disposing )
{
if( components != null )
components.Dispose();
}
base.Dispose( disposing );
}
#region Component Designer generated code
private void InitializeComponent()
{
this.comboBox1 = new WindowsControlLibrary.OwnerDrawCombo();
this.button1 = new System.Windows.Forms.Button();
this.SuspendLayout();
/*
Workaround #1
this.comboBox1.Parent = this;
this.CreateControl();
*/
this.comboBox1.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawFixed;
this.comboBox1.DropDownStyle =
System.Windows.Forms.ComboBoxStyle.DropDownList;
this.comboBox1.Items.AddRange(new object[] {
"Owner Draw Combo",
"One",
"Two",
"Three"});
this.comboBox1.Location = new System.Drawing.Point(16, 32);
this.comboBox1.Name = "comboBox1";
this.comboBox1.Size = new System.Drawing.Size(121, 21);
this.comboBox1.TabIndex = 0;
this.comboBox1.SelectedIndex = 0;
// Creating the control here causes the combo box
// draw item messaging to go through the parking window,
// and a message box invocation will break the combo.
this.comboBox1.CreateControl();
this.button1.Location = new System.Drawing.Point(32, 80);
this.button1.Name = "button1";
this.button1.TabIndex = 1;
this.button1.Text = "Click Me!";
this.button1.Click += new EventHandler(button1_Click);
this.Controls.Add(this.button1);
this.Controls.Add(this.comboBox1);
this.Name = "UserControl";
this.ResumeLayout(false);
}
#endregion
void button1_Click(object sender, System.EventArgs e)
{
MessageBox.Show("The owner draw combo box is now broken!\n" +
"Click on the drop down to see.");
}
}
}
Code for OwnerDrawCombo.cs:
using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using Microsoft.Win32;
namespace WindowsControlLibrary
{
public class OwnerDrawCombo : System.Windows.Forms.ComboBox
{
protected override void OnDrawItem(DrawItemEventArgs e)
{
e.DrawBackground();
if ( e.Index >= 0 && e.Index < this.Items.Count )
{
e.Graphics.DrawString(this.Items[e.Index].ToString(),
e.Font,Brushes.Black,
e.Bounds.Left,e.Bounds.Top);
}
e.DrawFocusRectangle();
}
// Workaround #2
/*
protected override void OnParentChanged(EventArgs e)
{
if ( this.Parent != null )
{
this.CreateParams.Parent = this.Parent.Handle;
this.RecreateHandle();
}
base.OnParentChanged (e);
}
*/
}
}
What is going wrong
If a child control window handle is created without a parent (i.e. before
the parent has been created), then the child is assigned a parent window of
class System.Windows.Forms.Application.ParkingWindow, of which there is at
most 1 per thread and which is created if necessary.
If the parent control is used by another .NET component, then when the
parent control is created, the .NET framework correctly reassigns the parent
and the internal objects used for message routing appropriately.
However if the parent control is being created as a result of ActiveX
activation, then the .NET framework does not correctly reassign the internal
objects, and messages such as owner draw messages to the parent continue to
be routed through the ParkingWindow object. By good fortune, message
reflection ensures that these messages get to the child which handles them
correctly. But as soon as a message box is invoked, .NET destroys the
parking window. At best, the child control will stop drawing itself because
the owner draw messages never get processed, at worst the CLR will throw an
exception because of null references.
Ways to get around the problem
1. Ensure that nothing happens in the parent control's InitializeComponent
or constructor to cause child controls to have their window handle created
before the parent window handle is created by the ActiveX activation. (Note
that having trawled through .NET framework code, it appears entirely
possible that setting some standard framework control object properties
could cause the underlying window handle to be created.)
2. Failing that, call the parent window's CreateControl and set the child
control's parent object before the thing happens which causes the child
control window to be created (see comment Workaround #1 in file
UserControl.cs above).
3. Better still, add the following code within the child control itself so
that it is guaranteed that the parent and child controls will be created and
parent object set in the right order irrespective of what happens in
InitializeComponent (see comment Workaround #2 in file OwnerDrawCombo.cs
above):
protected override void OnParentChanged(EventArgs e)
{
if ( this.Parent != null )
{
this.CreateParams.Parent = this.Parent.Handle;
this.RecreateHandle();
}
base.OnParentChanged (e);
}
What I have not been able to fully ascertain is quite how in the case where
the parent control is used 'normally' by another .NET component (i.e. as
opposed to used as an ActiveX control by unmanaged code), the .NET framework
correctly reassigns the message routing. Knowing this might offer a better
solution in some sense (having said that - much of what goes on here
involves private and internal methods and properties and thus stuff we
cannot use).