Custom Designers


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 either 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   Inherits IDisposable   Public Sub DoDefaultAction()   Public Sub Initialize(component As IComponent)   Public ReadOnly Property Component() As IComponent   Public ReadOnly Property Verbs() As DeisgnerVerbCollection End Interface 

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   Inherits ControlDesigner   ... End Class 

To paint the dashed border, ClockControlDesigner overrides the Initialize and OnPaintAdornments methods :

 
 Public Class ClockControlDesigner   Inherits ControlDesigner   ...   Public Overloads Overrides Sub Initialize(component As IComponent) ...   Public Overloads Overrides Sub OnPaintAdornments(e As PaintEventArgs)   ...   ... End Class 

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   Inherits ControlDesigner   Dim myClockControl As ClockControl = Nothing   Public Overrides Void Initialize(component As IComponent)       MyBase.Initialize(component)       ' Get clock control shortcut reference       myClockControl = CType(component, ClockControl)   End Sub   ... End Class 

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   Inherits ControlDesigner   ...   Protected Overloads Overrides Sub OnPaintAdornments(_     e As PaintEventArgs)       ' Let the base class have a crack       MyBase.OnPaintAdornments(e)       ' Don't show border if it does not have an Analog face       If myClockControl.Face = ClockFace.Digital Then Exit Sub       ' Draw border       Dim g As Graphics = e.Graphics       Dim myPen as Pen = New Pen(Color.Gray, 1)       myPen.DashStyle = DashStyle.Dash       g.DrawRectangle(myPen, 0, 0, _         myClockControl.Width  1, myClockControl.Height  1)       myPen.Dispose()   End Sub   ... End Class 

Adding DesignerAttribute to the ClockControl class completes the association:

 
 <Designer(GetType(ClockControlDesigner))> _ Public Class ClockControl   Inherits Control   ... End Class 

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   Inherits ControlDesigner   ...   Dim myShowBorder As Boolean = True   ...   Protected Overrides Sub OnPaintAdornments(e As PaintEventArgs)       ...       ' Don't show border if hidden or does not have an Analog face       If Not(myShowBorder) Or myClockControl.Face = ClockFace.Digital Then           Exit Sub       ...   End Sub   ' Provide implementation of ShowBorder to provide   ' storage for created myShowBorder property   Property ShowBorder() As Boolean       Get           Return myShowBorder       End Get       Set           myShowBorder = value           myClockControl.Refresh()       End Set   End Property End Class 

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   Inherits ControlDesigner   ...   Protected Overrides Sub PreFilterProperties(_       properties As IDictionary)       ' Let the base have a chance       MyBase.PreFilterProperties(properties)       ' Create design-time-only property entry and add it to the       ' Property Browser's Design category       properties("ShowBorder") = TypeDescriptor.CreateProperty(_           GetType(ClockControlDesigner), _           "ShowBorder", GetType(Boolean), _             CategoryAttribute.Design, _           DesignOnlyAttribute.Yes)   End Sub   ... End Class 

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   Inherits ControlDesigner   ...   Public Overloads Overrides Property Verbs() _ As DesignerVerbCollection Get           ' Return new list of context menu items           Dim myVerbs As DesignerVerbsCollection = _             New DesignerVerbsCollection()           showBorderVerb = New DesignerVerb(GetVerbText(), _               New EventHandler(AddressOf ShowBorderClicked))           myVerbs.Add(showBorderVerb)           Return myVerbs   End Property   ... End Class 

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   Inherits ControlDesigner   ...   Sub ShowBorderClicked(sender As Object, e As EventArgs)       ' Toggle property value       ShowBorder = Not(ShowBorder)   End Sub   ... End Class 

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   Inherits ControlDesigner   ...   Property ShowBorder() As Boolean       Get           Return myShowBorder       End Get       Set(ByVal Value As Boolean)           ' Change property value           Dim property As PropertyDescriptor = _               TypeDescriptor.GetProperties(GetType(ClockControl))               ("ShowBorder")           Me.RaiseComponentChanging(property)           myShowBorder = value           Me.RaiseComponentChanged(property, Not(myShowBorder),           myShowBorder)           ' Toggle Show/Hide Border verb entry in context menu           Dim menuService As IMenuCommandsService = _               CType(Me.GetService(GetType(IMenuCommandsService), _                   IMenuCommandsService)           If Not (menuService Is Nothing) Then               ' Recreate Show/Hide Border verb               If menuService.Verbs.IndexOf(showBorderVerb) >= 0 Then                 menuService.Verbs.Remove(showBorderVerb)                 showBorderVerb = New DesignerVerb(_                     GetVerbText(), _                       New EventHandler(AddressOf ShowBorderClicked)                 menuService.Verbs.Add(showBorderVerb)               End If           End If           ' Update clock UI           clockControl.Invalidate()       End Set   End Property   ... End Class 

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.



Windows Forms Programming in Visual Basic .NET
Windows Forms Programming in Visual Basic .NET
ISBN: 0321125193
EAN: 2147483647
Year: 2003
Pages: 139

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net