Click here to Skip to main content
15,879,239 members
Articles / Desktop Programming / Windows Forms

'Inheriting' from an Internal WinForms Designer

Rate me:
Please Sign up or sign in to vote.
4.91/5 (16 votes)
26 Jan 2011CPOL5 min read 33K   934   22   3
Customize an internal designer by encapsulation in a custom component designer

Introduction

A designer is often the best choice to extend the behavior of an associated component in design mode. While there exist other means like TypeDescriptionProvider, ITypeDescriptorFilterService or overriding the Component.Site property; a designer remains the easiest and most concise way. As most components rely on the public framework ComponentDesigner or ControlDesigner, using a derived custom designer poses no problem.

Trouble starts when the designer is marked internal for the System.Design assembly and is non trivial to reimplement by 'borrowing' code from Microsoft. Customizing a smart tag is one the features, which is nearly impossible without using a custom designer. An example of problematic hidden designers are ToolStrip / ToolStripItem components, with their lot of interdepending internal classes enhancing our IDE experience.

I propose the simple idea of using the internal default designer, by encapsulating it in a suitable ComponentDesigner or ControlDesigner and delegating member calls to the internal designer. This article highlights some not too obvious issues involved to make it work.The demo project uses a ContextMenuStrip and a TreeView control without any added real functionality as proof of concept.

Custom Designer Skeleton

The framework ContextMenuStrip is a Control, yet it's associated ToolStripDropDownDesigner derives from the ComponentDesigner. So our custom designer will too:

C#
internal abstract class ToolStripDropDownDesigner : ComponentDesigner
{
    protected ComponentDesigner defaultDesigner;

    public override void Initialize(IComponent component)
    {
        // internal class ToolStripDropDownDesigner : ComponentDesigner
        // Name: System.Windows.Forms.Design.ToolStripDropDownDesigner ,
        // Assembly: System.Design, Version=4.0.0.0
        Type tDesigner = Type.GetType
        ("System.Windows.Forms.Design.ToolStripDropDownDesigner, System.Design");
        defaultDesigner = (ComponentDesigner)Activator.CreateInstance
        (tDesigner, BindingFlags.Instance | BindingFlags.Public, null, null, null);

        defaultDesigner.Initialize(component);
        base.Initialize(component);
    }

    public override void InitializeNewComponent(IDictionary defaultValues)
    {
        base.InitializeNewComponent(defaultValues);
        defaultDesigner.InitializeNewComponent(defaultValues);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (defaultDesigner != null)
            {
                defaultDesigner.Dispose();
            }
        }

        base.Dispose(disposing);
    }
}

Designer Properties

A designer may expose design time only properties, adding new ones or ones that shadow existing control properties.These can be marked private, as design time environment uses Reflection to read and set values. Now our custom designer was specified as the principal designer by the DesignerAttribute and our designer instead of the default designer will be queried for properties. Does this mean we have to reimplement all designer properties on our custom designer and fidget with Reflection to delegate all calls?
Luckily inserting a single line will save us from the trouble:

C#
public override void Initialize(IComponent component)
{
    ...
    // use Designer properties of nested designer ( do before base.Initialize ! )
    TypeDescriptor.CreateAssociation(component, defaultDesigner);

    defaultDesigner.Initialize(component);
    base.Initialize(component);
}

Quoted from MSDN:

"The CreateAssociation method creates an association between a primary and a secondary object. Once an association is created, a designer or other filtering mechanism can add properties that route to either object into the primary object's property set. When a property invocation is made against the primary object, the GetAssociation method will be called to resolve the actual object instance that is related to its type parameter."

For clarity: Any defined properties on our designer will be queried as well, we just created an additional target. BTW, CreateAssociation() was the missing piece, when I first failed at encapsulation some years ago.

IDesignerFilter Methods

ComponentDesigner inherits from IDesignerFilter interface and we must override its methods to delegate the calls to the default designer. As the methods are marked protected, we cast the designer to the interface, in order to access them:

C#
protected IDesignerFilter designerFilter;
designerFilter = defaultDesigner;

ComponentDesigner's PreFilterAttributes() and PreFilterEvents() implementations are empty, and PostFilterProperties() only deals with the seldom case, that component inherits from IPersistComponentSettings. We won't bother overriding these methods.

C#
protected override void PreFilterProperties(IDictionary properties)
{
    // base.PreFilterProperties(properties) omitted, as designerFilter calls it as well
    designerFilter.PreFilterProperties(properties);
}

Inherited components act differently by inheriting most property values from their base class instance. Component inheritance is identified by the protected 'Inherited' (bool) and 'InheritanceAttribute' properties on the designer. Somehow only the default designer is decorated with the InheritanceAttribute, our designer never and always reports: not inherited. When we know that ComponentDesigner's PostFilterAttributes() implementation synchronizes the attribute existence with the 'InheritanceAttribute' property value, the fix becomes easy:

C#
protected override void PostFilterAttributes(IDictionary attributes)
{
    designerFilter.PostFilterAttributes(attributes);
    // will set this.InheritanceAttribute property from
    // nested designer attributes, if control is inherited
    base.PostFilterAttributes(attributes);

#if DEBUG
    if (attributes.Contains(typeof(InheritanceAttribute)))
    {
        Debug.Assert(base.InheritanceAttribute ==
        	attributes[typeof(InheritanceAttribute)]);
    }
    else
    {
        Debug.Assert(base.InheritanceAttribute == InheritanceAttribute.NotInherited);
    }
#endif
}

protected override void PostFilterEvents(IDictionary events)
{
    // filters events based on InheritanceAttribute
    designerFilter.PostFilterEvents(events);
}

Now our designed inherited control behaves correctly, all properties are rendered readonly, the rather annoying way all ToolStrips behave when inherited.

DesignerActionList and Verbs

My original goal was to customize the smart tag by overriding the 'ActionLists' property, yet it turned out that only the property on the default designer was queried. In case of the 'Verbs', it was the opposite, only my designer was invoked. Closer inspection revealed, that ComponentDesigner.Initialize() registers a DesignerCommandSet instance as a site-specific service. This service is then queried from the DesignerActionService, which manages smart tag and designer verb capabilities.

DesignerCommandSet is public and ComponentDesigner.Initialize() only registers it's version, when the service is not already present. So the fix is easily accomplished by adding our own version, that routes calls to our designer properties and registering it before initializing the two designers.

C#
private class CDDesignerCommandSet : DesignerCommandSet
{
    private readonly ComponentDesigner componentDesigner;

    public CDDesignerCommandSet(ComponentDesigner componentDesigner)
    {
        this.componentDesigner = componentDesigner;
    }

    public override ICollection GetCommands(string name)
    {
        if (name.Equals("Verbs"))
        {
            // componentDesigner.Verbs & defaultDesigner.Verbs are empty
            return null;
        }
        if (name.Equals("ActionLists"))
        {
            return componentDesigner.ActionLists;
        }
        return base.GetCommands(name);
    }
}
C#
public override void Initialize(IComponent component)
{
    ...
    IServiceContainer site = (IServiceContainer)component.Site;
    site.AddService(typeof(DesignerCommandSet), new CDDesignerCommandSet(this));

    defaultDesigner.Initialize(component);
    base.Initialize(component);
}
C#
public override DesignerActionListCollection ActionLists
{
    get { return defaultDesigner.ActionLists; }
}

If you do not require a customized smart tag, you can strip the above code.

Other Overrides

You must analyze, what other members your default designer overrides and reimplement them on the custom designer. Here for the ContextMenuStrip, only one property proved necessary, returning the contained ToolStripItem's:

C#
public override ICollection AssociatedComponents
{
    get { return defaultDesigner.AssociatedComponents; }
}

A part from static analysis you must test, whether overridden and internal members are invoked correctly.TreeView's default designer overrides ControlDesigner.OnPaintAdornments(), but to my surprise the method was invoked on both designers and worked properly in conjunction.

DemoContextStripDesigner

You may have noticed that I declared the custom ToolStripDropDownDesigner as an abstract class, it is reusable for all ToolStripDropDown components. The demo designer as proof of concept just adds a new designer property and removes all standard entries from the smart tag, except the 'Edit Items' verb.

C#
internal class DemoContextStripDesigner : ToolStripDropDownDesigner
{
    private bool myVar;

    public bool MyProperty
    {
        get { return myVar; }
        set { myVar = value; }
    }

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

        PropertyDescriptor pd = TypeDescriptor.CreateProperty(
            GetType(), "MyProperty", typeof(bool),
            new DescriptionAttribute("Designer Property"),
            new DesignerSerializationVisibilityAttribute
		(DesignerSerializationVisibility.Hidden));

        properties.Add(pd.Name, pd);
    }

    public override DesignerActionListCollection ActionLists
    {
        get
        {
            DesignerActionListCollection actionLists = base.ActionLists;
            actionLists.RemoveAt(0);
            return actionLists;
        }
    }
}

ControlDesigner's Peculiarity

ControlDesigner.Initialize() adds a private DockingActionList to the DesignerActionService for dockable controls, and we end up showing twice the 'Dock/Undock in Parent Container' verb. I found no other way, than to use Reflection's 'black magic' to remove one list after initializing both designers.

C#
private void removeDuplicateDockingActionList()
{
    // ControlDesigner field : private DockingActionList dockingAction;
    FieldInfo fi = typeof(ControlDesigner).GetField("dockingAction",
    BindingFlags.Instance | BindingFlags.NonPublic);
    if (fi != null)
    {
        DesignerActionList dockingAction = (DesignerActionList)fi.GetValue(this);
        if (dockingAction != null)
        {
            DesignerActionService service = (DesignerActionService)
            	GetService(typeof(DesignerActionService));
            if (service != null)
            {
                service.Remove(Control, dockingAction);
            }
        }
    }
}

Points of Interest

Sadly, the System.Design assembly is not part of Microsoft Reference Source. If you are serious into design time or package development, than get the .NET Reflector Pro addin. In a temporary project reference, System.Design.dll and other Microsoft.VisualStudio.* assemblies, ensure they get loaded and use the trial period to decompile them, allowing source stepping while debugging. Do this twice targeting for both .NET 2.0 and .NET 4.0 frameworks. Take care not to decompile assemblies already contained in Microsoft Reference Source, as they provide the better commented source.

To debug the demo solution at design time, use the control library 'EncapsulatedDesigner' as Startup project. On its project Properties page, point at your devenv.exe installation and specify the path to the 'TestDesigner' project as command line argument.

I have used the presented encapsulation technique so far for two different controls, which tap deeply into design time infrastructure and everything works well, yet "your mileage may vary".

History

  • 24th January, 2011: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Phillip Piper26-Apr-12 2:30
Phillip Piper26-Apr-12 2:30 
GeneralMy vote of 4 Pin
I'm Chris31-Jan-11 21:33
professionalI'm Chris31-Jan-11 21:33 
Generalnice - have 5 Pin
Pranay Rana26-Jan-11 17:25
professionalPranay Rana26-Jan-11 17:25 
thanks for sharing

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.