So far, you have seen how properties are exposed to the developer at design time, and you've seen some of the key infrastructure provided by .NET to improve the property-editing experience, culminating in UITypeEditor. Although the focus has been on properties, they aren't the only aspect of a control that operates differently in design-time mode compared with run-time mode. In some situations, a control's UI might render differently between these modes.
For example, the Splitter control displays a dashed border when its BorderStyle is set to BorderStyle.None. This design makes it easier for developers to find this control on the form's design surface in the absence of a visible border, as illustrated in Figure 9.32.
Figure 9.32. Splitter Dashed Border When BorderStyle Is None
Because BorderStyle.None means "don't render a border at run time," the dashed border is drawn only at design time for the developer's benefit. Of course, if BorderStyle is set to BorderStyle.FixedSingle or BorderStyle.Fixed3D, the dashed border is not necessary, as illustrated by Figure 9.33.
Figure 9.33. Splitter with BorderStyle.Fixed3D
What's interesting about the splitter control is that the dashed border is not actually rendered from the control implementation. Instead, this work is conducted on behalf of them by a custom designer, another .NET design-time feature that follows the tradition, honored by type converters and UI type editors, of separating design-time logic from the control.
Custom designers are not the same as designer hosts or the Windows Forms Designer, although a strong relationship exists between designers and designer hosts. As every component is sited, the designer host creates at least one matching designer for it. As with type converters and UI type editors, the TypeDescriptor class does the work of creating a designer in the CreateDesigner method. Adorning a type with DesignerAttribute ties it to the specified designer. For components and controls that don't possess their own custom designers, .NET provides ComponentDesigner and ControlDesigner, respectively, both of which are base implementations of IDesigner:
public interface IDesigner : IDisposable { public void DoDefaultAction(); public void Initialize(IComponent component); public IComponent Component { get; } public DesignerVerbCollection Verbs { get; } }
For example, the clock face is round at design time when the clock control either is Analog or is Analog and Digital. This makes it difficult to determine where the edges and corners of the control are, particularly when the clock is being positioned against other controls. The dashed border technique used by the splitter would certainly help, looking something like Figure 9.34.
Figure 9.34. Border Displayed from ClockControlDesigner
Because the clock is a custom control, its custom designer will derive from the ControlDesigner base class (from the System.Windows.Forms. Design namespace):
public class ClockControlDesigner : ControlDesigner { ... }
To paint the dashed border, ClockControlDesigner overrides the Initialize and OnPaintAdornments methods:
public class ClockControlDesigner : ControlDesigner { ... public override void Initialize(IComponent component) { ... } protected override void OnPaintAdornments(PaintEventArgs e) { ... } ... }
Initialize is overridden to deploy initialization logic that's executed as the control is being sited. It's also a good location to cache a reference to the control being designed:
public class ClockControlDesigner : ControlDesigner { ClockControl clockControl = null; public override void Initialize(IComponent component) { base.Initialize(component); // Get clock control shortcut reference clockControl = (ClockControl)component; } ... }
You could manually register with Control.OnPaint to add your design-time UI, but you'll find that overriding OnPaintAdornments is a better option because it is called only after the control's design-time or run-time UI is painted, letting you put the icing on the cake:
public class ClockControlDesigner : ControlDesigner { ... protected override void OnPaintAdornments(PaintEventArgs e) { // Let the base class have a crack base.OnPaintAdornments(e); // Don't show border if it does not have an Analog face if( clockControl.Face == ClockFace.Digital ) return; // Draw border Graphics g = e.Graphics; using( Pen pen = new Pen(Color.Gray, 1) ) { pen.DashStyle = DashStyle.Dash; g.DrawRectangle( pen, 0, 0, clockControl.Width - 1, clockControl.Height - 1); } } ... }
Adding DesignerAttribute to the ClockControl class completes the association:
[ DesignerAttribute(typeof(ClockControlDesigner)) ] public class ClockControl : Control { ... }
Design-Time-Only Properties
The clock control is now working as shown in Figure 9.34. One way to improve on this is to make it an option to show the border, because it's a feature that not all developers will like. Adding a design-time-only ShowBorder property will do the trick, because this is not a feature that should be accessible at run time. Implementing a design-time-only property on the control itself is not ideal because the control operates in both design-time and run-time modes. Designers are exactly the right location for design-time properties.
To add a design-time-only property, start by adding the basic property implementation to the custom designer:
public class ClockControlDesigner : ControlDesigner { ... bool showBorder = true; ... protected override void OnPaintAdornments(PaintEventArgs e) { ... // Don't show border if hidden or // does not have an Analog face if( (!showBorder) || (clockControl.Face == ClockFace.Digital) ) return; ... } // Provide implementation of ShowBorder to provide // storage for created ShowBorder property bool ShowBorder { get { return showBorder; } set { showBorder = value; clockControl.Refresh(); } } }
This isn't enough on its own, however, because the Property Browser won't examine a custom designer for properties when the associated component is selected. The Property Browser gets its list of properties from TypeDescriptor's GetProperties method (which, in turn, gets the list of properties using .NET reflection). To augment the properties returned by the TypeDescriptor class, a custom designer can override the PreFilterProperties method:
public class ClockControlDesigner : ControlDesigner { ... protected override void PreFilterProperties( IDictionary properties) { // Let the base have a chance base.PreFilterProperties(properties); // Create design-time-only property entry and add it to // the Property Browser's Design category properties["ShowBorder"] = TypeDescriptor.CreateProperty( typeof(ClockControlDesigner), "ShowBorder", typeof(bool), CategoryAttribute.Design, DesignOnlyAttribute.Yes); } ... }
The properties argument to PreFilterProperties allows you to populate new properties by creating PropertyDescriptor objects using the TypeDescriptor's CreateProperty method, passing the appropriate arguments to describe the new property. One of the parameters to TypeDescriptor. CreateProperty is DesignOnlyAttribute.Yes, which specifies design-time-only usage. It also physically causes the value of ShowBorder to be persisted to the form's resource file rather than to InitializeComponent, as shown in Figure 9.35.
Figure 9.35. ShowBorder Property Value Serialized to the Host Form's Resource File
>
If you need to alter or remove existing properties, you can override PostFilterProperties and act on the list of properties after TypeDescriptor has filled it using reflection. Pre/Post filter pairs can also be overridden for methods and events if necessary. Figure 9.36 shows the result of adding the ShowBorder design-time property.
Figure 9.36. ShowBorder Option in the Property Browser
Design-Time Context Menu Verbs
To take the design-time-only property even further, it's possible to add items to a component's design-time context menu. These items are called verbs, and ShowBorder would make a fine addition to our clock control's verb menu.
Adding to the verb menu requires that we further augment the custom designer class:
public class ClockControlDesigner : ControlDesigner { ... public override DesignerVerbCollection Verbs { get { // Return new list of context menu items DesignerVerbCollection verbs = new DesignerVerbCollection(); showBorderVerb = new DesignerVerb( GetVerbText(), new EventHandler(ShowBorderClicked)); verbs.Add(showBorderVerb); return verbs; } } ... }
The Verbs override is queried by the Designer shell for a list of DesignerVerbs to insert into the component's context menu. Each DesignerVerb in the DesignerVerbCollection takes a string name value plus the event handler that responds to verb selection. In our case, this is ShowBorderClicked:
public class ClockControlDesigner : ControlDesigner { ... void ShowBorderClicked(object sender, EventArgs e) { // Toggle property value ShowBorder = !ShowBorder; } ... }
This handler simply toggles the ShowBorder property. However, because the verb menu for each component is cached, it takes extra code to show the current state of the ShowBorder property in the verb menu:
public class ClockControlDesigner : ControlDesigner { ... bool ShowBorder { get { return showBorder; } set { // Change property value PropertyDescriptor property = TypeDescriptor.GetProperties(typeof(ClockControl))["ShowBorder"]; this.RaiseComponentChanging(property); showBorder = value; this.RaiseComponentChanged(property, !showBorder, showBorder); // Toggle Show/Hide Border verb entry in context menu IMenuCommandService menuService = (IMenuCommandService)this.GetService (typeof(IMenuCommandService)); if( menuService != null ) { // Re-create Show/Hide Border verb if( menuService.Verbs.IndexOf(showBorderVerb) >= 0 ) { menuService.Verbs.Remove(showBorderVerb); showBorderVerb = new DesignerVerb( GetVerbText(), new EventHandler(ShowBorderClicked)); menuService.Verbs.Add(showBorderVerb); } } // Update clock UI clockControl.Invalidate (); } } ... }
ShowBorder now performs two distinct operations. First, the property value is updated between calls to RaiseComponentChanging and RaiseComponentChanged, helper functions that wrap calls to the designer host's IComponentChangeService. The second part of ShowBorder re-creates the Show/Hide Border verb to reflect the new property value. This manual intervention is required because the Verbs property is called only when a component is selected on the form. In our case, "Show/Hide Border" could be toggled any number of times after the control has been selected.
Fortunately, after the Verbs property has delivered its DesignerVerbCollection payload to the Designer, it's possible to update it via the designer host's IMenuCommandService. Unfortunately, because the Text property is read-only, you can't implement a simple property change. Instead, the verb must be re-created and re-associated with ShowBorderClicked every time the ShowBorder property is updated.
On top of adding Show/Hide Border to the context menu, .NET throws in a clickable link for each verb, located on the Property Browser above the property description bar. Figure 9.37 illustrates all three options, including the original editable property.
Figure 9.37. ShowBorder Option in the Property Browser and the Context Menu
Custom designers allow you to augment an application developer's design-time experience even further than simply adding the effects to the Property Browser. Developers can change how a control renders itself, controlling the properties, methods, and events that are available at design time and augmenting a component's verbs.