software greenhouses
nuturing the growth of living abstractions…

October 20, 2007

Windows Controls Extensibility with .NET 3.5 CLR Add-Ins Using a “Leased-Space” Model

Filed under: .NET 3.5 CLR AddIn — Marty Nelson @ 3:52 pm

Please Read

I wrote this initial example before Jesse Kaplan suggested using WPF and Crossbow to implement Windows Control Add-in integration.  My newer post here describes this technique.  The approach below may still be valid if seen as a *lightweight* technique if the overhead of loading WPF is too great and Hot Key integration is not important.

UI Integration for Add-Ins 

With the release of .NET 3.5 Beta 2, Jack Gudenkauf touted the inclusion of the highly requested UI integration feature for System.AddIns, for WPF only.  While from a .NET 3.5 perspective it looks attractive to leave Windows Forms behind, the reality is that:

  1. Microsoft has done an excellent job making new .NET Framework versions additive and pseudo-backwards compatible, thus it has become easier to use the best of the new without needing to fully migrate to the bleeding edge.
  2. Windows Forms has been around a long time.  There’s plenty of legacy code, and perhaps more importantly, legacy developer skills.

Even with an eventually migration to WPF in the near-term future, I needed a Windows Form extensibility story now.  

Initially, I had conceived of creating a shadow control on the host-side of the add-in boundary, forwarding calls and events to the “real” add-in control and visa-versa.  I quickly became overwhelmed with the number of Control methods, properties, events and had doubts about the threading feasibility of this approach. 

Leased-Space Model

Then I realized that for add-in UI stories, I wasn’t really looking for control level integration.  UI add-ins are more feature based, serving as small components of functionality with often clearly defined input/output contract expectations.  The host-form doesn’t really care how the add-in goes about its control business, as long as it does it in the right place.  A designated control on the host form becomes the marker for the leased space, and any changes to its size and position are tracked. 

Here’s the critical code in the host side adapter:

public void Activate(Control leasedSpace, Form hostForm)

{

    leasedSpace_ = leasedSpace;

 

    //some of these events may be redundant…

    leasedSpace_.Move += LeasedSpaceChanged;

    leasedSpace_.Resize += LeasedSpaceChanged;

    leasedSpace_.SizeChanged += LeasedSpaceChanged;

    leasedSpace_.VisibleChanged += LeasedSpaceChanged;

    leasedSpace_.Parent.Move += LeasedSpaceChanged;

    if (!leasedSpace_.Parent.Equals(leasedSpace_.TopLevelControl))

    {

        leasedSpace_.TopLevelControl.Move += LeasedSpaceChanged;

    }

 

    addInFormHandle_ = addInControl_.Activate(GetPanelRectangle(), new HostFormVca(hostForm));

 

    leasedSpace_.Invalidate();

 

    leasedSpace_.GotFocus += LeasedSpaceFocused;

}

 

private void LeasedSpaceChanged(object sender, EventArgs e)

{

    Rectangle rectangle = GetPanelRectangle();

    if (rectangle != lastUpdate_)

    {

        addInControl_.UpdateLocation(rectangle);

    }

    lastUpdate_ = rectangle;

}

 

private Rectangle GetPanelRectangle()

{

    return leasedSpace_.RectangleToScreen(leasedSpace_.ClientRectangle);

}

 

private void LeasedSpaceFocused(object sender, EventArgs e)

{

    addInControl_.Focus();

}

Dealing with a Rectangle and contract representing the needed information from the host form (more on this later) are easy enough to pass across the add-in boundary:

[AddInContract]

public interface IAddInControlContract : IContract

{

    IntPtr Activate(Rectangle rectangle, IHostFormContract hostForm);

    void UpdateLocation(Rectangle rectangle);

    void Focus();

}

The add-in side adapter creates an independent host form for the control, with all the form borders and trimmings turned off.

public IntPtr Activate(Rectangle rectangle, IHostFormContract hostFormContract)

{

    Application.EnableVisualStyles();

 

    HostFormCva hostForm = new HostFormCva(hostFormContract);

 

    form_ = new AddInControlHostingForm(hostForm);

    form_.FormBorderStyle = FormBorderStyle.None;

    form_.ShowInTaskbar = false;

    form_.Controls.Add(control_);

    control_.Anchor = AnchorStyles.Bottom | AnchorStyles.Left |

        AnchorStyles.Right | AnchorStyles.Top;

 

    form_.Size = rectangle.Size;

    form_.Location = rectangle.Location;

 

    form_.Show(hostForm);

 

    return form_.Handle;

}

 

public void UpdateLocation(Rectangle rectangle)

{

    form_.Size = rectangle.Size;

    form_.Location = rectangle.Location;

}

Simple enough.  Hook up the pipeline, create a test control and away we go.  The add-in form dutifully adheres to the size and location of its associated host control.

Fixing the Bugs

Well, not quite.  There are four UI inconsistencies.  Two of which won’t be relevant until we add menus and toolbars.  The other two are:

  1. Focus.  The title bar on the host form loses focus whenever the add-in’s form is active.  It doesn’t look like a single integrated form.
  2. Layering.  The host form covers the add-in’s form once it regains the focus.  A quick hack is to set the addInForm.TopMost = true.  Nice for a proof-of-concept demo, but not a workable solution.

We’re going to need to deal with WndPrc to sort this mess out.  And there’s an architectural impact:  the host form is going to be customized, meaning we won’t be able to use any Form as our host view.  It will need to be used as the replacement base class for any form that we want to make add-in capable.  At this point, this limitation seems acceptable.

Help From the Internet

While I did spend a few days looking at Console.WriteLine(m) in WndPrc, it was mostly to know enough to know what question to ask to find the right answers.  The maturity of Winows Forms means someone else has mostly likely already been there.  James Brown’s post on creating Docking Toolbars provided the answer to both the focus and layering issue.  It is also the reason that the form handles are passed in both directions.  In the host form, we ignore any deactivation messages if one of the hosted add-in’s form is being activated:

protected override void WndProc(ref Message m)

{

    if (IsDeactivateMessage(ref m) && AddInWindowBeingActivated(ref m))

    {

        m.Result = new IntPtr(1);

    }

    else

    {

        base.WndProc(ref m);

    }

}

So far, there hasn’t been then need to broadcast an activation message to the add-ins’ forms, as they don’t have a displayed title bar.  The add-in’s form merely needs the owner form handle in the show method, and Windows takes care of all the layering issues.  The add-in side adapater for the host form essentially acts as a wrapper to expose IWin32Window.

Menus and Toolbars

MenuStrip and ToolStrip add two more issues to resolve:  click-through (it takes two clicks on a menu or toolbar if the form does not have the active focus) and command keys.  Rick Brewster had already tackled the first issue for Paint.NET (another Toolbar, multi-form environment).  The solution introduces two more derivative classes that must be used on any host form instead of the “normal” .NET controls.

Not Yet Done: Command Keys

Ideally we would have a communication mechanism from the add-in’s form to report Command Keys (Alt, Ctl, etc. combinations) back to the host form for processing.  There’s also some preventative code needed to prevent shenanigans like using ALT+F4 on the add-in’s form.

For some reason, ProcessCmdKey does not fire in the add-in form. Paint.NET has a command key registration implementation that I am looking at.  The pipeline is in place through the HostFormCva class to pass any needed information back to the host form.

One of the reasons I wanted to post this solution is to see if anyone else already had the Windows knowledge to help solve this last problem.

Also, please let me know if you come up with any other UI scenario that need to be mitigated.  I’m curious to know if there are certain controls or hosting situations that break the appearance of a single integrated UI form.

And a Caveat…

If you try and launch the AddIn in a separate process, it hangs when you call Activate().  It does work cross AppDomain, which is the big win for the .NET assembly versioning and unload stories.

Also, the solution is for VS2005, with .NET 3.5 installed of course.

In the code, the sample host form implementation manages the add-in AppDomains and handles AddInToken finding and activation.  The main base class (AddInCapableForm) just takes the leased-space control and an IAddInControl.  It will be up to your process to figure out the best way to collocate or segregate add-ins into AppDomains (a topic worthy of its own blog entry).

Download solution

2 Comments »

  1. […] In an earlier post, I described a method of hosting windows controls that relied on a hidden form on the add-in side that hosted the add-in control, but appeared in a designated area of the host form. […]

    Pingback by software greenhouses » Part II - Windows Controls Extensibility with .NET 3.5 CLR Add-Ins using WPF and Crossbow — January 13, 2008 @ 8:59 pm

  2. Good for people to know.

    Comment by Michelle — October 29, 2008 @ 5:32 am

RSS feed for comments on this post. TrackBack URI

Leave a comment

Powered by WordPress