Thursday, August 28, 2008
« Using an SGEN.exe MSBuild task in TFS Bu... | Main | Watch How and When You Check For Windows... »

I've been putting together a nice little WinForms Wizard control recently that had a few twist and turns to its development.  In particular, I wanted to make the design-time experience a good one for the end user.  I wanted to support design-time navigation of the wizard pages, drag/drop of control to each child page, reordering of the pages, and much more.  In fact, as soon as I tidy up the code a bit and flesh it out a bit, I'll be posting the component here on my blog for general use.  But enough of that...

The wizard control has a rich design-time experience.  I owe some of the thanks for the functionality to control's ControlDesigner and DesignerActionList.  Among many other things, these classes provides a mechanism for a developer to associate custom verbs and actions with the control at design-time.  These actions are made available to the developer through the smart-tag menu when the control is selected on its design surface.

It is through these custom actions that I've given the programmer the ability to manage pages on the wizard.  Internally, these pages are represented not only as controls on the wizard itself, but also in a collection of pages that the wizard uses to properly navigate the sequence of pages.  It is this collection that gets persisted (serialized) in the form's InitializeComponent() method when saving changes made to the wizard at design-time.

I quickly discovered, however, that all wasn't green grass and blue skies.  I wanted to support reordering the pages through the design-time smart-tags.  While I was able to easily reorder the elements in the page collection and even update the control's display at design-time to reflect the changes, the Visual Studio environment didn't register the change.  Unless I then went ahead and further edited a property through Visual Studio itself (effectively to cause a change in the environment), the reordering of the pages never persisted.  So I had to figure out how to notify Visual Studio that a change had been made.

Fortunately, this isn't difficult, but it's not very intuitive at the same time.

The ControlDesigner's GetService() method provides access to the services available to the control at design time.  Among these is the IComponentChangeService.  It is through this service that you can notify the designer of a change.  To do so, you need a property to update.  I, for one, don't really like the idea of utilizing an existing property for this purpose (though it could be done).

What I did in my solution was create a design-time-only property in the ControlDesigner.  Then, when a change is made that requires the change notification I call a method which 'updates' the property.  The property doesn't really get updated, but at least the code is clearer.

In the control designer:

protected override void PreFilterProperties(IDictionary properties) {
   base.PreFilterProperties(properties);

   Attribute[] attribs = {
                            new BrowsableAttribute(false),
                            new DesignOnlyAttribute(true)
                         };
   PropertyDescriptor prop = TypeDescriptor.CreateProperty(GetType(), "DesignTimeChange", typeof(string), attribs);
   properties.Add("DesignTimeChange", prop);
}

/// <summary>
/// Property created exclusively for the purpose of notifying the
/// designer of changes to properties made through the designer
/// (such as reordering the pages).
/// </summary>
public string DesignTimeChange { get; set; }

Then, also in the Designer, a method that causes a change to be acknowledged:

private void notifyOfChange() {
   PropertyDescriptor prop = TypeDescriptor.GetProperties(this)["DesignTimeChange"];
   getComponentChangeService().OnComponentChanged(this, prop, null, null);
}

That's all there is to it.  When an action occurs in the designer that would not otherwise register a change in the environment, I simply have it call the notifyOfChange() method.

A little more work would be involved if the change were coming from outside the designer.  For instance, if the change was made within the collection or within the control, you'd first have to evaluate whether it's running in design mode, and then retrieve a reference to the ControlDesigner, invoking the method (it'd probably have to be made internal (and it's name appropriately Pascal-cased) rather than private).

Maybe something like this (NOTE: untested, but seemingly approximately accurate):

if ( DesignMode ) {
   IDesignerHost host = ( IDesignerHost )Site.Container;
   ControlDesigner designer = ( ControlDesigner )host.GetDesigner(this);
   designer.NotifyOfChange();
}