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:
internal abstract class ToolStripDropDownDesigner : ComponentDesigner
{
protected ComponentDesigner defaultDesigner;
public override void Initialize(IComponent component)
{
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:
public override void Initialize(IComponent component)
{
...
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:
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.
protected override void PreFilterProperties(IDictionary properties)
{
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:
protected override void PostFilterAttributes(IDictionary attributes)
{
designerFilter.PostFilterAttributes(attributes);
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)
{
designerFilter.PostFilterEvents(events);
}
Now our designed inherited control behaves correctly, all properties are rendered readonly, the rather annoying way all ToolStrip
s 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.
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"))
{
return null;
}
if (name.Equals("ActionLists"))
{
return componentDesigner.ActionLists;
}
return base.GetCommands(name);
}
}
public override void Initialize(IComponent component)
{
...
IServiceContainer site = (IServiceContainer)component.Site;
site.AddService(typeof(DesignerCommandSet), new CDDesignerCommandSet(this));
defaultDesigner.Initialize(component);
base.Initialize(component);
}
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:
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.
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.
private void removeDuplicateDockingActionList()
{
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