Writing Custom Designers for .NET Components

 

Shawn Burke
Microsoft Corporation

June 2001

Summary: This article covers the various features of designers, how to associate them with components, and how to use those features to create great design time user interface. (17 printed pages)

Contents

Introduction What Is a Designer Anyway?
How Does a Component Get a Designer?
Modifying State for Design Time
Defining Component Relationships
Using a Designer to Change a Component's Properties, Attributes, or Events
Simplifying Common Tasks
Playing Nicely With Others
Conclusion

Introduction

The .NET Framework was built with extensibility in mind. Because the same engineers designed and implemented the runtime and design time portions of the .NET Framework, users will be able to exploit much tighter integration than in any other framework or class library.

One of the key aspects of this integration is the interplay between the runtime and design time portions of the code-base. While the runtime code can be kept separate from the design time code, the design time portion can exert a great deal of influence over a component's behavior and appearance at design time.

First, let's discuss the usage of the word "designer" for a moment. Generally, it means any .NET object that is responsible for managing the design time behavior of a component or family of components. But sometimes "designer" refers more globally to the design time UI in Microsoft® Visual Studio® .NET for designing Windows® Forms, Web Forms, or Components. For the purpose of this article "designer" refers to a designer for a specific component rather than the full VS. NET designer unless otherwise noted.

This article will cover the various features of designers, how to associate them with components, and how to use those features to create great design time UI.

What Is a Designer Anyway?

As mentioned above, a designer is an object that is responsible for managing the design time behavior and appearance of components on the design surface. Specifically, designers implement the System.ComponentModel.Design.IDesigner interface.

public interface IDesigner : IDisposable {

        IComponent Component {get;}        
        DesignerVerbCollection Verbs {get;}
        void DoDefaultAction();
        void Initialize(IComponent component);
}

Generally, you will never have to write one of these from scratch. All of the designers that are included in the .NET Framework SDK derive from the default implementation in System.ComponentModel.Design.ComponentDesigner. Any object that implements IComponent (which usually means it derives from Component) will automatically get ComponentDesigner as its designer. There are other types of designers that implement System.ComponentModel.Designer.IRootDesigner, called "root designers" that allow a component to be the "root" object in the Visual Studio .NET design environment. Types such as System.Windows.Forms.Form and System.Windows.Forms.UserControls have root designers that allow them to be shown in design view as well as code view in VS .NET. A given type can have multiple designers associated with it, but only one of each type. For the purpose of this article, the features of standard IDesigner implementations will be explored.

Most designers perform three basic tasks:

  • Creating or modifying design time UI for the component that is being designed
  • Modifying the sets of Properties, Attributes, and Events exposed by the component being designed
  • Adding actions, called Verbs, that can be performed on a component at design time

At design time, the VS .NET designer infrastructure attaches a designer to each component that is "sited." That is, each component that gets a name and connection that allows it to access underlying designer services. Each designer is then given an opportunity to participate in the interaction with the user, the VS .NET designer, and operations like code-generation and persistence.

Figure 1.

How Does a Component Get a Designer?

The power and flexibility of metadata (information attached to classes, properties, methods, or events) is utilized throughout the .NET Framework, and designers are no exception. Designers are associated with components using the System.ComponentModel.DesignerAttribute attribute. This attribute's constructor can take an Assembly-qualified string type name, or an actual type reference. The string form is convenient because it allows the designer portions of a component to be completely separated from the runtime portions. With the runtime and the designer in separate assemblies, component vendors can minimize the size of their runtime redistributables. Conversely, the DesignerAttribute constructor can take an actual Type reference if the designer is contained in the same Assembly or one that will always be available. Using the string format can also help avoid difficult circular build dependency issues, however, since the designer type doesn't have to be available at compile time. The samples here will use the type reference method for simplicity.

For example if the component was called MyCompany.SysComponent in MyCompany.dll, and the designer was called MyCompany.Design.SysComponentDesigner in MyCompany.Design.dll:

namespace MyCompany {
[Designer("MyCompany.Design.SysComponentDesigner, MyCompany.Design")]
public class SysComponent : Component {
}
}

namespace MyCompany.Design {
   internal class SysComponentDesigner : ComponentDesigner {
      // …
}
}

However, the designer can also be included in the assembly with the component itself, such as in a nested class, which can be public, protected, or internal:

namespace MyCompany {
[Designer(typeof(SysComponentDesigner))]
public class SysComponent : Component {
   
internal class SysComponentDesigner : ComponentDesigner{
// …
}
}
}

The benefit of using an actual type reference is that the compiler will complain if the reference is wrong; an assembly-qualified string name does not give you that benefit.

The System.ComponentModel.Design.IDesignerHost interface allows access to designers for components on the design surface. With an IServiceProvider (such as the ISite attached to an IComponent through its Site property at design time), any component's designer can be accessed with the IDesignerHost.GetDesigner method. Here we use Visual Basic .NET code to access the verbs on a given component. Verbs will be discussed later in this article.

Public Function GetComponentVerbs(ByVal comp As IComponent) As DesignerVerbCollection
        If (comp.Site <> Nothing) Then
            Dim host As IDesignerHost
            host = comp.Site.GetService(GetType(IDesignerHost))
            If (host <> Nothing) Then
                Dim designer As IDesigner
                designer = host.GetDesigner(comp)
                If (designer <> Nothing) Then
                    Return designer.Verbs
                End If
            End If
        End If
        Return New DesignerVerbCollection()
End Function

Modifying State for Design Time

In many cases, it is undesirable for a control or a component to act the same on the design surface as in a running application. For example, a timer control should not be processing timer events at design time, or a system monitor control should not be hooking system events. Designers provide a simple way of controlling this.

As an example, imagine a custom control that can accept drag/drop input, such as the RichEdit control. Clearly, on the design surface it would be confusing to be able to drag text or files into this control. To solve this, a designer can be made to disable drag/drop support in the control.

public class DragDropControlDesigner : ControlDesigner {
      public override void Initialize(IComponent c) {
         base.Initialize(c);
         ((Control)c).AllowDrop = false;
      }
   }

In this case, the designer sets the AllowDrop property of the Control that it is associated with false. Notice that Initialize is called on the base class before any actions are performed. This is very important; the call to the base Initialize method allows the base designer classes to set up their state before they are accessed. Also notice that the parameter to Initialize is an IComponent. This is the object instance of the component that is being designed. It is accessible from within all ComponentDesigner-based designers as the ComponentDesigner.Component property. If you are writing a designer based on ControlDesigner, it exposes a property called "Control," which allows you to access the Control that is being designed. In most circumstances, this instance value will be equal to the one returned from ComponentDesigner.Component, yet it saves the overhead and clutter of casting all the time. Theoretically, however, you could use this to write a designer for a non-Control based on ControlDesigner and override this Control property to return the UI for the component being designed.

ControlDesigner performs several operations similar to the above sample. Since the designer manipulates live instances of components, a Control must be both visible and enabled in order to be useful on the design surface. If it is not it would either be invisible or ineligible to receive mouse or keyboard input and therefore could not be moved around. So within ControlDesigner.Initialize, Visible and Enabled are both forced to true.

Defining Component Relationships

Some components have related components that may or may not be on the design surface. For example, ToolBar control buttons are actually components themselves. The same goes for TabPages on a TabControl. If one were to copy a Toolbar from one form and paste it onto another, it would not be very handy to copy only the ToolBar itself and leave the buttons behind. This scenario also applies to a GroupBox with child controls on it, or a MainMenu with submenu items. How can this be handled in a generic way?

On ComponentDesigner, there is a property called AssociatedComponents to solve just this problem. Whenever a drag or cut/copy operation operates on a component, the VS .NET designer recursively walks the AssociatedComponents property of each component's designer to determine the full set of objects to drag, cut, or copy.

In the example below, MainComp returns all of its SubComp members as its AssociatedComponents. SubComp components return only those SubComps that are sited in the VS .NET designer. Components sometimes initialize their state with items in their collections that aren't part of the designer generated elements. If a MainComp is cut and pasted to another form or component designer, it will include all of its SubComps and any sited SubComps of each SubComp. Wow, that's a mouthful!

[Designer(typeof(MainComp.MainCompDesigner))]
public class MainComp : Component {

   public SubCompCollection SubComponents {
      get {   
         return subCollection;
      }
   }

   internal class MainCompDesigner : ComponentDesigner {
      public override ICollection AssociatedComponents{
         get{
            return ((MainComp)base.Component).SubComponents;
         }
      }
   }
}

[DesignTimeVisible(false)]
[Designer(typeof(SubComp.SubCompDesigner))]
public class SubComp : Component {
   public SubCompCollection SubSubComponents {
      get {   
         return subCollection;
      }
   }
   internal class SubCompDesigner : ComponentDesigner {

      public override ICollection AssociatedComponents{
         get{
            // Only return sited subs in this case.  
            // For example, this component could have 
            // a number of sub components that were 
            // added by the component and aren't sited 
            // on the design surface.  We don't want 
            // to move those around.
            //
            ArrayList comps = new ArrayList();
            foreach (SubComp sc in 
               ((SubComp)Component).SubSubComponents) {
               if (sc.Site != null) {
                  comps.Add(sc);
               }
            }
            return comps;
         }
      }
   }
}

Using a Designer to Change a Component's Properties, Attributes, or Events

The most common application of designers is adjusting how a component appears on the design surface. There are many cases in which it is desirable to modify or add properties of a component at design time. The VS .NET designer already adds many properties to each component on the design surface, such as the "(Name)" property, or the "Locked" property. These properties don't actually exist on the component being designed. The attributes on properties can also be modified. Most often, a designer wants to intercept or "shadow" certain properties on components. By shadowing a property, the designer keeps track of the value the user sets and can choose whether to pass that change down to the actual component.

In the case of a Control.Visible and Control.Enabled, this prevents the Control from actually becoming invisible or disabled, or in the case of Timer.Enabled, it prevents it from waking up and beginning to send timer tick events. These properties are available at design time but don't affect the state of the component on the design surface. This "shadowing" can easily be done with any ComponentDesigner-based designer.

First, ComponentDesigner has three sets of methods that allow you to modify the properties exposed by a component that is being designed:

  • PreFilterProperties
  • PostFilterProperties
  • PreFilterAttributes
  • PostFilterAttributes
  • PreFilterEvents
  • PostFilterEvents

The general rule to follow is to add or remove items in the "PreFilter" methods, and modify existing items in the "PostFilter" methods. Always call the base method first in the PreFilter methods and call the base method last in the PostFilter methods. This ensures that all designer classes are given the proper opportunity to apply their changes. ComponentDesigner also has a built-in dictionary for storing and retrieving the shadowed values. This saves the designer author the trouble of creating member variables for all the shadowed properties.

Let's use a simplified version of ControlDesigner as an example. This designer will shadow the Visible and Enabled properties, and add a property called "Locked".

public class SimpleControlDesigner : ComponentDesigner {

   bool locked;

   public bool Enabled {
      get {
         return (bool)ShadowProperties["Enabled"];
      }
      set {
         // note this value is not passed to the actual
         // control
         this.ShadowProperties["Enabled"] = value;
      }
   }
   private bool Locked {
      get {
         return locked;
      }
      set {
         locked = value;            
      }
   }

   public bool Visible {
      get {
         return (bool)ShadowProperties["Visible"];
      }
      set {
         // note this value is not passed to the actual
         // control
         this.ShadowProperties["Visible"] = value;
      }
   }

   public void Initialize(IComponent c) {
      base.Initialize(c);
      Control control = c as Control;

      if (control == null) {
         throw new ArgumentException();
      }

      // pick up the current state and push it
      // into our shadow props to 
// initialize them.
      //
      this.Visible = control.Visible;
      this.Enabled = control.Enabled;

      control.Visible = true;
      control.Enabled = true;
   }

protected override void PreFilterProperties(
IDictionary properties) {
        base.PreFilterProperties(properties);
        
      // replace Visible and enabled with our shadowed versions.
      //
      properties["Visible"] = TypeDescriptor.CreateProperty(
               typeof(SimpleControlDesigner), 
               (PropertyDescriptor)properties["Visible"], 
               new Attribute[0]);
      properties["Enabled"] = TypeDescriptor.CreateProperty(
               typeof(SimpleControlDesigner), 
               (PropertyDescriptor)properties["Enabled"], 
               new Attribute[0]);

      // and add the locked property
      //
      properties["Locked"] = TypeDescriptor.CreateProperty(
               typeof(SimpleControlDesigner), 
               "Locked", 
               typeof(bool), 
               CategoryAttribute.Design, 
               DesignOnlyAttribute.Yes);
      }
    
}

Notice how the Initialize method is used to populate the shadow properties with the values from the components. Also notice that the "Locked" property has the "DesignOnlyAttribute.Yes" added to it, and that it is private. Even though it is marked as private, it can be accessed through reflection since the hookup was done from code that has access to the property. The DesignOnlyAttribute marks this property as valid at design time only so it won't be generated as code for the component (design only properties are persisted in a resource stream).

The TypeDescriptor.CreateProperty call creates a PropertyDescriptor based on either an existing PropertyDescriptor (as is the case with Visible and Enabled) or creates a new one. Because the properties are all defined on the SimpleControlDesigner class, "typeof(SimpleControlDesigner)" is specified as the component type for each property created. This tells the runtime which type of object instance to expect if this property is set or accessed. One caveat: be sure not to use "GetType()" instead of the static typeof expression if your designer class can be derived from. In the derived class, GetType() will return a different value and can cause problems accessing the property.

As far as other objects on the design surface are concerned, the properties defined on the designer are the exposed properties for the object. This shadowing has hidden the original Visible and Enabled properties from other objects in the design environment, including code generation and persistence for these objects. It is for this reason that we are able to provide the values the user intended rather than the values that have been applied to the live object instances living on the designer.

Simplifying Common Tasks

If your component has common actions that are performed on it, it is often useful to expose these actions as a "verb" from the component. To see verbs in action, drop a TabControl on the VS .NET Windows Forms designer and you will see two hyperlinks show up in the property browser. One says "Add Tab" and one says "Remove Tab." As the names suggest, these actions will add or remove a tab from the TabControl. You will also see these verbs on the context menu if you right-click on the TabControl itself.

Adding verbs is simple to do. The IDesigner interface has a "verbs" property that you can override and return a collection of DesignerVerb objects. DesignerVerbs are objects that contain a verb's string name, the delegate to invoke when the verb is invoked, and optionally a command ID if one is to be assigned. Normally, the VS .NET designer architecture dynamically assigns ID's to verbs. As an example, below is a designer for a control that derives from Button. It adds three verbs, "Red," "Green," and "Blue," that change the Control's back color to one of those values. Note ControlDesigner has a property called "Control" that returns the Control that the designer is attached to.

[Designer(typeof(ColorButton.ColorButtonDesigner))]
   public class ColorButton : System.Windows.Forms.Button
   {
       internal class ColorButtonDesigner : ControlDesigner {

            private DesignerVerbCollection verbs = null;

      private void OnVerbRed(object sender, EventArgs e) {
         Control.BackColor = Color.Red;
      }

      private void OnVerbGreen(object sender, EventArgs e){
         Control.BackColor = Color.Green;
      }

      private void OnVerbBlue(object sender, EventArgs e) {
         Control.BackColor = Color.Blue;
      }

      public override DesignerVerbCollection Verbs {
         get {
                   if (verbs == null) {
            verbs = new DesignerVerbCollection();
                       verbs.Add(
                            new DesignerVerb(
                                 "Red", 
                                 new EventHandler(OnVerbRed)));
                   verbs.Add(
                            new DesignerVerb(
                                "Blue", 
                                new EventHandler(OnVerbBlue)));
                   verbs.Add(
                            new DesignerVerb(
                                "Green", 
                                new EventHandler(OnVerbGreen)));
                    }
               return verbs;
         }
      }
      }
   }

Figure 2.

Playing Nicely With Others

When a designer makes modifications to the state of the component it is responsible for, other objects within the VS .NET designer may be interested in that change. For example, if your designer changes the background color of a Control to red, the property browser should show this updated information. Generally, changes in component state are broadcast through the IComponentChangeService. Many other services within the VS .NET designer listen to the IComponentChangeService, including the code persistence engine, undo/redo facilities, and the property browser, and update state if components that they are concerned about are included in the change notifications.

IComponentChangeService works by giving notice that a component is about to change, and then again after a component has changed. There are two primary notifications that can be raised by any client, OnComponentChanging and OnComponentChanged. OnComponentChanging must be fired before an OnComponentChanged, but OnComponentChanged does not need to be called after an IComponentChanging. This may occur if an action is aborted for some reason. IComponentChangeService also has several events for listening to notifications:

  • ComponentChanging
  • ComponentChanged
  • ComponentAdding
  • ComponentAdded
  • ComponentRemoving
  • ComponentRemoved
  • ComponentRename

The service only allows ComponentChanging and ComponentChanged to be fired manually. The other events are fired by the designer infrastructure when components are added, removed, or renamed on the VS .NET designer surface.

When you set a property value through a PropertyDescriptor, component change notifications are automatically sent, so that is the easiest way to do the proper notifications.

In the example above, if we were to use PropertyDescriptors to notify the change (so the property browser updates automatically), we would change the verb handlers to look like the following:

private void OnVerbGreen(object sender, EventArgs e){
PropertyDescriptor backColorProp = 
      TypeDescriptor.GetProperties(Control)["BackColor"];
   
   if (backColorProp != null) {
      backColorProp.SetValue(Control, Color.Green);
}
}

There are sometimes cases where changing the value of one property may affect the other, or cases where many property changes will be done at once. For performance reasons, it is often preferable to act directly on the object (rather than through the PropertyDescriptors), and fire the notifications later. Invoking or setting a property through a PropertyDescriptor is slower than accessing directly.

Think of radio buttons for a moment. When you change the value of a radio button to true, all the other instances of radio buttons on the same parent change their values to false. The runtime code handles this automatically, but it is polite to tell the designers about the change. In this case we shadow the Checked property so we can intercept the value change when it occurs. When the value is set to true, we iterate through the siblings of the radio button, and notify IComponentChangeService for any radio button siblings whose Checked properties have changed. We do this by using the GetService call on ComponentDesigner to get a hold of an instance of the IComponentChangeService and then calling on that instance.

internal class RadioButtonDesigner : ControlDesigner {

   public bool Checked  {
      get {
         // pass the get down to the control.
         //
         return ((RadioButton)Control).Checked;
      }
      set {
         // set the value into the control
         //
         ((RadioButton)Control).Checked = value;

         // if the value for this radio button
         // was set to true, notify that others have changed.
         //
         if (value) {
            IComponentChangeService ccs = 
               (IComponentChangeService)
            GetService(typeof(IComponentChangeService));
            if (ccs != null) {
               PropertyDesciptor checkedProp = 
                   TypeDescriptor.GetProperties(
typeof(RadioButton))["Checked"];
               foreach(RadioButton rb in 
                  Control.Parent.Controls) {
                  if (rb == Control || 
!(rb is RadioButton)) 
continue;
                  
                  ccs.OnComponentChanging(
rb, checkedProp);
                  ccs.OnComponentChanged(rb, 
                     chedkedProp, null, null);
               }
            }
         }
      }
   }

   protected override void PreFilterProperties(
                     IDictionary properties) {
      base.PreFilterProperties(properties);
      // shadow the checked property so we can intercept the set.
      //
      properties["Checked"] = 
TypeDescriptor.CreateProperty(               typeof(RadioButtonDesigner), 
               (PropertyDescriptor)properties["Checked"], 
               new Attribute[0]);
   }
}

Conclusion

Designers can be written for any object that implements IComponent and is sited on the design surface. This goes for Windows Forms controls, Web Forms controls, or any other type of components. Attaching designers allows components to have a different shape presented to the user and allows runtime and design time code to live separately, minimizing redistributable sizes and helping component authors have more control over how their components are used.

Including an extensible, object oriented set of design time interfaces and classes with the .NET Framework gives abilities for control and customization never before available in a framework. Using these capabilities to their fullest extent ensures that components can have usable, rich design time and runtime object models.