DekGenius.com
[ Team LiB ] Previous Section Next Section

9.2 Custom Component Designers

Visual Studio .NET enables controls to customize the Forms Designer. New commands can be added in the property grid. Resize and move drag operations can be altered, and mouse handling within the control can be customized. Design-time adornments may be added (adornments are editing features such as resize handles). Hit testing can be provided for nonrectangular controls. Controls may elect to act as containers of child controls. Controls may even modify the way that properties are presented in the property panel. All these facilities revolve around supplying a custom component designer.

A component designer is a class associated with the control class, whose job is to handle design-time interaction with the development environment. It must derive from the ControlDesigner class, which is defined in the System.Windows.Forms.Design namespace. (In fact, any component may have a designer, not just a control. A non-visual component's designer derives from the ComponentDesigner class, which is defined in the System.ComponentModel.Design namespace.)

The terminology is unfortunate—there are three designers involved at design time:

  • The Forms Designer (the visual editing environment provided by Visual Studio .NET for building Windows Forms applications)

  • The control's custom designer class

  • The developer who is designing the form

To avoid ambiguity, in this book, the Forms Designer is always referred to with a capital D. A designer with a lower case D refers to the custom designer class. We avoid using the term designer to refer to the developer.


All controls are required to have an associated designer. If you do not specify one, your control will just use the default, ControlDesigner. But you can choose a custom designer by applying the DesignerAttribute custom attribute to your class as Example 9-4 shows. (DesignerAttribute is defined in the System.ComponentModel namespace. It is not entirely clear what it is doing in there when the System.ComponentModel.Design namespace would have been the more obvious choice.)

Example 9-4. Specifying a custom designer for a class
// C# code
[DesignerAttribute(typeof(MyControlDesigner))]
public class MyControl :
   System.Windows.Forms.Control
{
    . . .
}

public class MyControlDesigner :
  System.Windows.Forms.Design.ControlDesigner
{
    . . .
}

' VB code
<Designer(GetType(MyControlDesigner))> _
Public Class MyControl 
   Inherits System.Windows.Forms.Control
   . . .
End Class

Public Class MyControlDesigner
   Inherits System.Windows.Forms.Design.ControlDesigner
   . . .
End Class

Design-time behavior is customized by overriding various methods or properties of the ControlDesigner class. We will now examine each customizable aspect in turn.

9.2.1 Designer Verbs

Visual Studio .NET provides extensive support for modifying control properties of all kinds. It has built-in editors for a wide range of standard property types, and because it uses the PropertyGrid, it is easy to provide custom editors for new types—all the techniques discussed in Chapter 8 for customizing the PropertyGrid work just fine for properties of a control. However, for certain kinds of properties, it can still be cumbersome to use the PropertyGrid to perform frequently used operations. So the Forms Editor allows a component designer to add verbs—custom operations available through a single click in the editor.

Consider the built-in TabControl class, which allows several user interface panels to be contained inside a standard tabbed view, such as that used by Windows Explorer's file properties windows. The first thing that a developer is likely to want to do with a newly created TabControl is to add some new panels. This can be done by adding new TabPage objects to the control's TabPages property. TabPages is a collection property, so editing this property displays the standard collection editor dialog, which can be used to add new TabPage objects.

The TabControl component's requirements are met by the PropertyGrid. However, it is all rather cumbersome—the developer must first locate the TabPages property in the property grid, then two clicks are required to bring up the collection editor, which is a modal dialog and hence must be dismissed once the new pages have been added. This is somewhat inconvenient, given that developers will almost always need to add new pages whenever they use the TabControl.

To make life easier for developers, the TabControl therefore defines two designer verbs, one for adding new panels and one for removing existing panels. Figure 9-1 shows how the property grid displays the verbs for a TabControl.

Figure 9-1. A property grid with a verbs panel
figs/winf_0901.gif

The Add Tab and Remove Tab verbs in the central panel in Figure 9-1 are also available through the context menu, as shown in Figure 9-2.

Figure 9-2. A context menu with verbs
figs/winf_0902.gif

If your custom control could benefit from a similar one-click interface for common but cumbersome operations, it is easy to add designer verbs. Simply override the Verbs property in your custom designer class, as shown in Examples Example 9-5 and Example 9-6. (Note that this property's type, DesignerVerbCollection, is defined in the System.ComponentModel.Designer namespace.)

Example 9-5. Adding designer verbs using C#
public class MyControlDesigner : ControlDesigner
{
    public override DesignerVerbCollection Verbs 
    {
        get
        {
  DesignerVerb[] verbs = new DesignerVerb[]
  {
      new DesignerVerb("Add Panel",
         new EventHandler(OnAddPanelVerb)),
      new DesignerVerb("Remove Panel",
         new EventHandler(OnRemovePanelVerb))
  };
  return new DesignerVerbCollection(verbs);

        }
    }

    private void OnAddPanelVerb(object sender, EventArgs e)
    {
        MyControl ctl = (MyControl) this.Control;
        . . .
    }

    private void OnRemovePanelVerb(object sender, EventArgs e)
    {
        MyControl ctl = (MyControl) this.Control;
        . . .
    }
}
Example 9-6. Adding designer verbs using VB
Public Class MyControlDesigner
   Inherits System.Windows.Forms.Design.ControlDesigner

   Public Overrides ReadOnly Property Verbs() As DesignerVerbCollection
      Get
         Dim vrbs(1) As DesignerVerb
         vrbs(0) = New DesignerVerb("Add Panel", _
         AddressOf OnAddPanelVerb)
         vrbs(1) = New DesignerVerb("Remove Panel", _
         AddressOf OnRemovePanelVerb)
         Return New DesignerVerbCollection(vrbs)
      End Get
   End Property
   
   Private Sub OnAddPanelVerb(sender As Object, e As EventArgs)
       Dim ctl As MyControl = DirectCast(Me.Control, MyControl)
       . . .
   End Sub

   Private Sub OnRemovePanelVerb(sender As Object, e As EventArgs)
       Dim ctl As MyControl = DirectCast(Me.Control, MyControl)
       . . .
   End Sub

End Class

The Verbs property must return a collection of DesignerVerb objects, one for each verb that is to appear on the property page. (The easiest way to create a DesignerVerbCollection is to use the constructor that takes a DesignerVerb[] array, as shown here.) Each DesignerVerb object contains two pieces of information: the name of the verb (as it will appear on the property panel and in the context menu) and the method of the component designer that should be called if the verb is invoked. The method to be invoked must be referred to with an EventHandler delegate.

When the Forms Designer calls the method for your verb (i.e., when the user clicks on the verb), the object passed as the sender parameter is not the control, as you might have expected. It is a reference to the DesignerVerb object in the collection that you returned in the Verbs property. This is not usually particularly useful.To access the control that your custom designer is editing, simply use the Control property defined by the ControlDesigner class (i.e., your designer's base class), as illustrated in Examples Example 9-5 and Example 9-6.


9.2.1.1 DesignerVerb properties

The DesignerVerb object has properties that allow you to modify the appearance of your verbs. For example, if you set the Enabled property to false, the verb will be grayed out. Setting the Checked property to true will cause a tick to appear beside the verb on the context menu. (It has no effect on its appearance in the property grid.)

You can set these properties when you create the verbs in your Verbs property. You can also modify the properties later on, and the Forms Designer will track these changes. For example, if you change the Enabled property of a verb while your control is selected, the verb's appearance will change appropriately in the property panel. However, every time your control is deselected and reselected, the Designer will read your Verbs property again—it does not cache verbs between selections. This means that any changes you make to your verbs' properties will be lost when the selection changes unless you make your Verbs property return the same objects every time.

Although it is possible to write a Verbs property that returns the same set of verbs every time, you should not rely on this technique. The Forms Designer reserves the right to destroy your component designer object at any time and create a new one as needed. For example, if the user closes the Designer window for the form that contains your control and then reopens it, a new instance of your designer class will be created. You should therefore make sure that your Verbs property always creates DesignerVerb objects that are appropriately initialized to be consistent with the control's state. And in general, your designer should be written to assume that it might be destroyed and recreated at any time, and should therefore not rely on being able to store state between operations.

9.2.2 Selection and Resizing

When writing a custom component designer for a control, your class will normally derive from the ControlDesigner class. This class provides the standard support for selecting, moving, and resizing controls with the mouse. You can influence the way in which these operations work by overriding certain properties and methods.

9.2.2.1 Hit testing

By default, a control can be selected by clicking anywhere inside its bounding rectangle. Because most controls are rectangular, this is reasonable behavior, but for controls with a more unusual shape, it can be confusing. So the Designer lets you modify this behavior by overriding the GetHitTest method in your control designer class.

The Designer will call the GetHitTest method repeatedly whenever the mouse pointer is over your control. It uses the return value to decide what kind of mouse cursor to display—if the method returns true, the Designer will display the four-way cursor to indicate that the control can be selected and moved. If the method returns false, the default mouse cursor will be displayed to indicate that the pointer is not considered to be over any control right now. GetHitTest will also be called when the mouse is clicked on your control to determine whether to select the control or not. Examples Example 9-7 and Example 9-8 show a simple custom control that draws an ellipse in its client area, and a corresponding designer with a GetHitTest method that only returns true when the mouse pointer is over the ellipse.

Example 9-7. A control designer with hit testing using C#
[Designer(typeof(EllipseDesigner))]
public class EllipseControl : System.Windows.Forms.Control
{
    public EllipseControl()
    {
        SetStyle(ControlStyles.ResizeRedraw, true);
    }

    protected override void OnPaint(PaintEventArgs pe)
    {
        using (Brush b = new SolidBrush(ForeColor))
        {
  pe.Graphics.FillEllipse(b, ClientRectangle);
        }
        base.OnPaint(pe);
    }
}

public class EllipseDesigner : ControlDesigner
{
    protected override bool GetHitTest(System.Drawing.Point point)
    {
        // Avoid divide-by-zero problems with zero-sized controls

        if (this.Control.Width == 0 || this.Control.Height == 0)
  return true;


        // Map point from screen to client coordinates.

        PointF p = this.Control.PointToClient(point);


        // Test for containment by ellipse.

        double w = this.Control.Width;
        double h = this.Control.Height;

        double ratio = w / h;

        double sx = p.X - w/2;
        double sy = (p.Y - h/2)*ratio;

        return (sx*sx + sy*sy) >= w*w/4.0;
    }
}
Example 9-8. A control designer with hit testing using VB
Imports System
Imports System.ComponentModel
Imports System.Drawing
Imports System.Windows.Forms
Imports System.Windows.Forms.Design

<Designer(GetType(EllipseDesigner))> _
Public Class EllipseControl 
       Inherits System.Windows.Forms.Control

    Public Sub New()
        SetStyle(ControlStyles.ResizeRedraw, True) 
    End Sub

    Protected Overrides Sub OnPaint(pe As PaintEventArgs)
        Dim b As Brush = New SolidBrush(ForeColor)
        Try
  pe.Graphics.FillEllipse(b, ClientRectangle)
        Finally
  Dim disp As IDisposable
  If TypeOf b Is IDisposable Then
     disp =  b
     disp.Dispose()
  End If
        End Try
        MyBase.OnPaint(pe) 
    End Sub
End Class

Public class EllipseDesigner : Inherits ControlDesigner

    Protected Overrides Function GetHitTest( _
    point As System.Drawing.Point) As Boolean

        ' Avoid divide-by-zero problems with 0-sized controls
        If Me.Control.Width = 0 Or Me.Control.Height = 0 Then
  Return True 
        End If

        ' Map point from screen to client coordinates.
        Dim p As PointF = _
 Point.op_Implicit(Me.Control.PointToClient(point))

        ' Test for containment by ellipse.
        Dim w As Double = Me.Control.Width 
        Dim h As Double = Me.Control.Height 

        Dim ratio As Double = w / h 

        Dim sx As Double = p.X - w/2 
        Dim sy As Double = (p.Y - h/2)*ratio 

        Return (sx*sx + sy*sy) >= w*w/4.0 
    End Function
End Class

Examples Example 9-7 and Example 9-8 illustrate a curious feature of GetHitTest. The Point that is passed as a parameter is relative to the top-left corner of the screen. (This is at odds with what the documentation claims at the time this book went to press—it says that the Point will be relative to the top-left corner of the control.) This means that the first thing we must do is convert the Point from screen coordinates to control coordinates. Fortunately, the Control class has a built-in method for doing this: PointToClient. The remainder of the method simply calculates whether the point is contained within the ellipse.

If your control sets its shape by modifying its Region property, you do not need to supply your own GetHitTest implementation. The default ControlDesigner class automatically manages hit testing for such shaped controls.


9.2.2.2 Resizing and moving

The ControlDesigner class provides automatic support for resizing and moving controls. You can control this facility by overriding the SelectionRules property in your own designer class. You must return some combination of the values defined in the SelectionRules enumeration, which is defined in the System.Windows.Forms.Design namespace.

There is no way to take complete control of the resizing and moving process unless you are prepared to disable the built-in support completely and draw your own adornments. (Returning SelectionRules.None from the SelectionRules property will turn off the standard support, and the next section describes how to add your own adornments. But even then, you will be restricted to drawing adornments that lie within the control's client rectangle.) However, you will normally be able to achieve what you require just by choosing a more restrictive set of selection rules than the default of SelectionRules.AllSizeable | SelectionRules.Moveable (SelectionRules.AllSizeable Or SelectionRules.Moveable in VB), which allows the control to be moved and to be resized in all directions.

If you want the designer to draw a border on your control, then no matter what other values you choose from the SelectionRules enumeration, you must include SelectionRules.Visible. If you just specify this in conjunction with SelectionRules.Moveable, your control will have a fixed size, but will be able to be moved around the form. You can also selectively enable sizing of individual edges using the TopSizeable, BottomSizeable, LeftSizeable, and RightSizeable enumeration members. Example 9-9 shows an example SelectionRules property implementation that does not allow the control to be moved, and only allows its left edge to be resized.

Example 9-9. Modifying support for moving and resizing
// C# code
public override SelectionRules SelectionRules
{
    get
    {
        return SelectionRules.Visible |
  SelectionRules.LeftSizeable;
    }
}

' VB code
Public Overrides ReadOnly Property SelectionRules() _
       As SelectionRules
    Get
        Return SelectionRules.Visible Or _
     SelectionRules.LeftSizable
    End Get
End Property

Figure 9-3 shows the result of Example 9-9. This is how the Forms Designer displays such a control when it is selected. The visual cue is not especially obvious. In case you missed it, the Designer indicates the resizable edge by coloring its center handle white, while coloring the handles that cannot be dragged pale gray. (It is slightly more obvious when using the control in the Designer, because the mouse cursor only changes into a resize cursor when it is over that handle.)

Figure 9-3. A control with one resizable edge
figs/winf_0903.gif

9.2.3 Adornments

An adornment is a user interface feature that is only painted on a control at design time. (Selection outlines and resize handles are examples of built-in adornments.) Adornments are not drawn by the control—they are supplied by the control designer class, enabling you to add extra design-time handles appropriate to your class.

To show how to display adornments, we need a control that can make use of extra resize handles beyond the standard ones. We will use a control that displays a box with rounded edges. Examples Example 9-10 and Example 9-11 show the complete source for such a control, and it is shown in action in Figure 9-4.

Figure 9-4. A rounded box control
figs/winf_0904.gif

There is nothing unusual about this control—it uses standard techniques already discussed in previous chapters. It provides a single property called CornerSize (along with the usual associated change notification event and overridable OnCornerSizeChanged method). This property determines how large the curved corners are. This property can be edited using the property grid in the normal way, but we will provide a custom designer that draws a drag handle adornment to allow the corner size to be modified by dragging with the mouse.

Example 9-10. A rounded box control using C#
[Designer(typeof(RoundedBoxDesigner))]
public class RoundedBoxControl : System.Windows.Forms.Control
{
    public RoundedBoxControl()
    {
        SetStyle(ControlStyles.ResizeRedraw, true);
    }

    [Category("Appearance")]
    [DefaultValue(10)]
    public int CornerSize
    {
        get
        {
  return cornerSizeVal;
        }
        set
        {
  if (cornerSizeVal != value)
  {
      cornerSizeVal = value;
      OnCornerSizeChanged(EventArgs.Empty);
      Refresh();
  }
        }
    }
    private int cornerSizeVal = 10;

    [Category("Property Changed")]
    public event EventHandler CornerSizeChanged;

    protected virtual void OnCornerSizeChanged(EventArgs e)
    {
        if (CornerSizeChanged != null)
  CornerSizeChanged(this, e);
    }


    protected override void OnPaint(PaintEventArgs pe)
    {
        // Truncate rounded corner sizes so that the
        // corners aren't larger than the box.
        int cwidth = cornerSizeVal*2 > Width ? Width : cornerSizeVal*2;
        int cheight = cornerSizeVal*2 > Height ? Height : cornerSizeVal*2;
        Rectangle corner = new Rectangle(0, 0, cwidth, cheight);

        using (GraphicsPath gp = new GraphicsPath())
        {
  // GraphicsPath.AddArc complains about
  // zero-sized arcs, so just use a
  // standard Rectangle in that case.
  if (cwidth == 0 || cheight == 0)
  {
      Rectangle cr = ClientRectangle;
      if (cr.Width != 0) cr.Width -= 1;
      if (cr.Height != 0) cr.Height -= 1;
      gp.AddRectangle(cr);
  }
  else
  {
      gp.AddArc(corner, 180, 90);
      corner.X = Width - 1 - cwidth;
      gp.AddArc(corner, 270, 90);
      corner.Y = Height - 1 - cheight;
      gp.AddArc(corner, 0, 90);
      corner.X = 0;
      gp.AddArc(corner, 90, 90);
      gp.CloseFigure();
  }

  using (Pen p = new Pen(ForeColor, 1))
  {
      pe.Graphics.DrawPath(p, gp);
  }
        }

        // Calling the base class OnPaint
        base.OnPaint(pe);
    }
}
Example 9-11. A rounded box control using VB
<Designer(GetType(RoundedBoxDesigner))> _
Public Class RoundedBoxControl 
       Inherits System.Windows.Forms.Control

    Private cornerSizeVal As Integer = 10 

    Public Sub New()
        SetStyle(ControlStyles.ResizeRedraw, True)
    End Sub

    <Category("Appearance"), _
     DefaultValue(10)> _
    Public Property CornerSize() As Integer
        Get
  Return cornerSizeVal 
        End Get
        Set
  If cornerSizeVal <> Value Then
      cornerSizeVal = Value 
      OnCornerSizeChanged(EventArgs.Empty) 
      Refresh() 
  End If
        End Set
    End Property

    <Category("Property Changed")> _
    Public Event CornerSizeChanged(sender As Object, _
                         e As EventArgs)

    Protected Overridable Sub OnCornerSizeChanged( _
    e As EventArgs)
        RaiseEvent CornerSizeChanged(Me, e) 
    End Sub

    Protected Overrides Sub OnPaint(pe As PaintEventArgs)
        ' Truncate rounded corner sizes so that the
        ' corners aren't larger than the box.
        Dim cwidth As Integer
        If cornerSizeVal*2 > Width Then
 cwidth = Width
        Else
 cwidth = cornerSizeVal*2
        End If
        Dim cheight As Integer
        If cornerSizeVal*2 > Height Then
  cheight = Height
        Else
  cheight = cornerSizeVal*2
        End If
        Dim corner As New Rectangle(0, 0, cwidth, cheight) 

        Dim gp As New GraphicsPath()
        Try
  ' GraphicsPath.AddArc complains about
  ' zero-sized arcs, so just use a
  ' standard Rectangle in that case.
  If cwidth = 0 Or cheight = 0 Then
      Dim cr As Rectangle = ClientRectangle 
      If cr.Width <> 0 Then cr.Width -= 1 
      If cr.Height <> 0 Then cr.Height -= 1 
      gp.AddRectangle(cr) 
  Else
      gp.AddArc(corner, 180, 90) 
      corner.X = Width - 1 - cwidth 
      gp.AddArc(corner, 270, 90) 
      corner.Y = Height - 1 - cheight 
      gp.AddArc(corner, 0, 90) 
      corner.X = 0 
      gp.AddArc(corner, 90, 90) 
      gp.CloseFigure() 
  End If

  Dim p As New Pen(ForeColor, 1)
  Try
      pe.Graphics.DrawPath(p, gp) 
  Finally
      Dim disp As IDisposable
      If TypeOf p Is IDisposable Then
          disp = p
          disp.Dispose()
      End If
  End Try
        Finally
  Dim disp As IDisposable
  If TypeOf disp Is IDisposable Then
      disp = gp
      disp.Dispose()
  End If
        End Try

        ' Calling the base class OnPaint
        MyBase.OnPaint(pe) 
    End Sub
End Class

To draw a drag handle, we simply provide an appropriate designer that overrides the OnPaintAdornments method. This designer class is shown in Examples Example 9-12 and Example 9-13. (Note that the class definition in Examples Example 9-10 and Example 9-11 is marked with the Designer custom attribute. This is how Visual Studio .NET knows to use our RoundedBoxDesigner class.)

Example 9-12. Drawing grab handle adornments using C#
public class RoundedBoxDesigner : ControlDesigner
{
    private const int grabSize = 7;

    protected override void OnPaintAdornments(PaintEventArgs pe)
    {
        Rectangle grabRect = GetGrabRectangle();
        ControlPaint.DrawGrabHandle(pe.Graphics, grabRect,
                          true, true);
    }

    private Rectangle GetGrabRectangle()
    {
        RoundedBoxControl ctl = (RoundedBoxControl) Control;
        return new Rectangle(ctl.CornerSize - grabSize/2, 0,
                   grabSize, grabSize);
    }

    . . .
}
Example 9-13. Drawing grab handle adornments using VB
Public Class RoundedBoxDesigner : Inherits ControlDesigner

    Private Const grabSize As Integer = 7

    Protected Overrides Sub OnPaintAdornments( _
                  pe As PaintEventArgs)
        Dim grabRect As Rectangle = GetGrabRectangle()
        ControlPaint.DrawGrabHandle(pe.Graphics, grabRect, _
                          True, True)
    End Sub

    Private Function GetGrabRectangle() As Rectangle
        Dim ctl As RoundedBoxControl = DirectCast(Control, RoundedBoxControl)
        Return New Rectangle(CInt(ctl.CornerSize - grabSize/2), _
                   0, grabSize, grabSize)
    End Function

    . . .
End Class

With this designer in place, the control will now have an extra grab handle at design time, as shown in Figure 9-5.

Figure 9-5. The RoundedBoxControl at design time
figs/winf_0905.gif

The designer class in Examples Example 9-12 and Example 9-13 is not complete. As it stands, it only draws the grab handle. It does nothing to manage clicks or drags on the handle. To make the handle useful, we must override more methods. First we will want to provide feedback with the mouse cursor—we should display the left-right resize cursor when the mouse is over our drag handle to indicate how it can be moved. This is done by overriding the designer class's OnSetCursor method, as shown in Examples Example 9-14 and Example 9-15.

Example 9-14. Modifying the mouse cursor at design time using C#
protected override void OnSetCursor()
{
    Point cp = Control.PointToClient(Cursor.Position);
    if (GrabHitTest(cp))
    {
        Cursor.Current = Cursors.SizeWE;
    }
    else
        base.OnSetCursor();
}

private bool GrabHitTest(Point p)
{
    Rectangle grabRect = GetGrabRectangle();
    return grabRect.Contains(p);
}
Example 9-15. Modifying the mouse cursor at design time using VB
Protected Overrides Sub OnSetCursor()
    Dim cp As Point = Control.PointToClient(Cursor.Position)
    If GrabHitTest(cp) Then
        Cursor.Current = Cursors.SizeWE
    Else
        MyBase.OnSetCursor()
    End If
End Sub

Private Function GrabHitTest(p As Point) As Boolean
    Dim grabRect As Rectangle = GetGrabRectangle()
    Return grabRect.Contains(p)
End Function

Note that OnSetCursor does not pass the mouse cursor's position. We must therefore retrieve it from the Cursor class directly. The Cursor class's Position property returns the mouse position in screen coordinates, so we need to translate these into control coordinates using the PointToClient method. Then we test to see if the mouse is over the handle. (The hit test logic has been factored out into a separate method, GrabHitTest, because we will also need to perform hit testing in another method shortly. This in turn uses the GetGrabRectangle method we defined earlier, which is also used to determine where the grab handle is drawn.)

Our control will now provide feedback at design time when the user moves the mouse over our drag handle. But we still need to handle the drag operation itself. To do this, we must override three methods: OnMouseDragBegin, OnMouseDragMove, and OnMouseDragEnd. The code is fairly straightforward, with only two minor complications. First, we need to be able to reset the property to its original value if the drag is cancelled. Second, it is good practice to make sure that if the user doesn't click dead in the center of the drag handle, we don't end up making the handle leap to the clicked location. (Naïve handling of the OnMouseDragMove event would cause this to happen—we are using the offset field to avoid this.) Examples Example 9-16 and Example 9-17 show the drag handling code.

Example 9-16. Handling adornment mouse events using C#
private int oldCornerSize;  // Used for handling cancellation
private int offset;
private bool dragging;

protected override void OnMouseDragBegin(int x, int y)
{
    Point dp = Control.PointToClient(new Point(x, y));
    if (GrabHitTest(dp))
    {
        RoundedBoxControl ctl = (RoundedBoxControl) Control;
        oldCornerSize = ctl.CornerSize;
        offset = oldCornerSize - dp.X;
        dragging = true;
    }
    else
        base.OnMouseDragBegin(x, y);
}

protected override void OnMouseDragMove(int x, int y)
{
    if (dragging)
    {
        Point dp = Control.PointToClient(new Point(x, y));

        int newCornerSize = dp.X - offset;
        if (newCornerSize < 0) newCornerSize = 0;

        RoundedBoxControl ctl = (RoundedBoxControl) Control;
        ctl.CornerSize = newCornerSize;
    }
    else
        base.OnMouseDragMove(x, y);

}

protected override void OnMouseDragEnd(bool cancel)
{
    if (dragging)
    {
        RoundedBoxControl ctl = (RoundedBoxControl) Control;
        if (cancel)
        {
  ctl.CornerSize = oldCornerSize;
        }
        else
        {
  // Update property in property grid
  PropertyDescriptor pd =
      TypeDescriptor.GetProperties(typeof(RoundedBoxControl))
        ["CornerSize"];
  pd.SetValue(ctl, ctl.CornerSize);
        }

    }

    dragging = false;


    // Always call base class.
    base.OnMouseDragEnd(cancel);
}
Example 9-17. Handling adornment mouse events using VB
Private oldCornerSize As Integer  ' Used for handling cancellation
Private offset As  Integer
Private dragging As Boolean

Protected Overrides Sub OnMouseDragBegin(x As Integer, y As Integer)
    Dim dp As Point = Control.PointToClient(New Point(x, y))
    If GrabHitTest(dp) Then
        Dim ctl As RoundedBoxControl = DirectCast(Control, RoundedBoxControl)
        oldCornerSize = ctl.CornerSize
        offset = oldCornerSize - dp.X
        dragging = True
    Else
        MyBase.OnMouseDragBegin(x, y)
    End If
End Sub

Protected Overrides Sub OnMouseDragMove(x As Integer, y As Integer)
    If dragging Then
        Dim dp As Point = Control.PointToClient(New Point(x, y))

        Dim newCornerSize As Integer = dp.X - offset
        If newCornerSize < 0 Then newCornerSize = 0

        Dim ctl As RoundedBoxControl = DirectCast(Control, RoundedBoxControl)
        ctl.CornerSize = newCornerSize
    Else
        MyBase.OnMouseDragMove(x, y)
    End If
End Sub

Protected Overrides Sub OnMouseDragEnd(cancel As Boolean)
    If dragging Then
        Dim ctl As RoundedBoxControl = DirectCast(Control, _
                             RoundedBoxControl)
        If cancel Then
  ctl.CornerSize = oldCornerSize
        Else
  ' Update property in property grid
  Dim pd As PropertyDescriptor = _
      TypeDescriptor.GetProperties( _
      GetType(RoundedBoxControl))("CornerSize")
  pd.SetValue(ctl, ctl.CornerSize)
        End If
    End If

    dragging = False

    ' Always call base class.
    MyBase.OnMouseDragEnd(cancel)
End Sub

Note that all three methods defer to the base class implementation when they are not handling the drag operation (i.e., when the user clicks somewhere other than on the drag handle). This is necessary to make sure that the control can still be moved in the Designer using normal drag and drop. Also note that the OnMouseDragEnd method always calls the base class method, regardless of whether the handle was being dragged or not. This is necessary because otherwise the drag operation will not be completed correctly, and the Forms Designer will start to malfunction.

The OnMouseDragEnd method has some slightly strange-looking code that runs when the drag is not cancelled. It obtains a PropertyDescriptor object for the RoundedBoxControl class's CornerSize property, and then uses this to set that property to the value it is already set to. On the face of it, this may seem pointless. However, despite the fact that the CornerSize property raises property change notifications, the property grid appears not to detect the change. Pushing the update through the PropertyDescriptor causes the property grid to refresh its display of the CornerSize property correctly.

9.2.4 Containment

As we saw in Chapter 3, all controls are able to contain child controls. But it doesn't always make sense for a control to act as a parent. For example, although you can write code that puts child controls inside a Button, the results are not helpful. Fortunately, the Forms Designer prevents you from placing child controls inside controls that are not designed to act as parents. It only allows children to be added to controls for which it is appropriate, such as Panel or GroupBox.

By default, any controls we write will not be treated as containers by the Forms Designer. If you try to drop a control inside one of your custom controls, the new control's parent will be the form, not your control. However, it is easy to make your control behave like a Panel: simply give it a designer that derives from the ParentControlDesigner class. ParentControlDesigner derives from ControlDesigner and provides all the same functionality, and it also signals to the Forms Designer that the control can act as a container.

If you do not require any special design-time behavior other than the ability to act as a parent, it is sufficient to choose the ParentControlDesigner class itself as your designer. As Example 9-18 illustrates, there is no need to derive your own designer class.

Example 9-18. A simple parent control
// C# code
[Designer(typeof(ParentControlDesigner))]
public class MyParentControl : Control
{
    . . .
}

' VB code
<Designer(GetType(ParentControlDesigner))> _
Public Class MyParentControl : Inherits Control
   . . .
End Class

If you need to supply a designer class for other design-time features, such as painting adornments, you can simply change its base class to be ParentControlDesigner. Example 9-19 shows a modified version of the designer class for our RoundedBoxControl, previously shown in Examples Example 9-12 and Example 9-13. We have changed it to inherit from ParentControlDesigner, which will cause the Forms Designer to allow child controls to be added to it.

Example 9-19. A parent control designer
// C# code
public class RoundedBoxDesigner : ParentControlDesigner
{
    As before
    . . .
}

' VB code
Public Class RoundedBoxDesigner 
       Inherits ParentControlDesigner
   . . . As before
End Class

You can be selective about which controls you contain. Your designer class can override the CanParent method. The Forms Designer will call this when the user drags a control over your control. If this method returns false, the Designer will display the no entry mouse cursor to indicate that the control being dragged cannot be dropped into your control.

CanParent is an overloaded method. You must override the overload that takes a Control as a parameter. This will be called when a control that is already on the form is being dragged around. (It is not clear when the other overload, which takes a ControlDesigner, is called. You might expect it to be called when a new instance is dragged from the Toolbox. But this is not the case; there appears to be no way of controlling which types of new instances can be added to your control.)

9.2.5 Metadata Filtering

In the previous chapter, we saw how the PropertyGrid control provides a virtual view of an object's properties: although it relies on reflection to determine what properties are present, it has extensibility hooks that allow us to modify what the user sees. We exploited this by writing a type converter to support localization of property names. Because Visual Studio .NET uses the PropertyGrid to edit control properties, we have the same flexibility for the way our controls are presented at design time. On top of this, the Forms Designer provides us with some extra support for common ways of modifying and filtering properties without having to go to the trouble of writing a type converter.

If your control already has a designer class associated with it, you can use this to perform many of the tricks that would otherwise require a type converter. In particular, you can hide certain properties, rename them, intercept reads and writes, and even add new properties.

Because there is some overlap in what can be achieved by writing a custom type converter and by writing a custom designer, you will sometimes have requirements that could be met by writing either. It doesn't matter which you choose. The main restriction to be aware of is that designer classes can only be used in Visual Studio .NET, and only at design time. So if you need metadata filtering at runtime (usually because you are using the PropertyGrid control) a type converter will be the right solution.


At the center of this mechanism are six methods. Three of these are intended to let you add new properties, events and attributes, and they are, respectively, PreFilterProperties , PreFilterEvents, and PreFilterAttributes. Each is passed a dictionary into which it can add descriptors. (The descriptors you add should be of type PropertyDescriptor, EventDescriptor, and AttributeDescriptor, respectively. These are all defined in the System.ComponentModel namespace.) This dictionary will already contain entries for all the component's real properties, events, or attributes. (Or if the component has an associated type converter, the dictionary will contain whatever that returned.) But the designer class has the option to add to these.

The remaining three methods are PostFilterProperties , PostFilterEvents, and PostFilterAttributes. These are passed the same dictionary as before, but this time the method is allowed to remove or modify entries. (In practice, there is currently not much difference between the Pre... and Post... methods—the Post... methods are called directly after the Pre... methods, and you can do whatever you like to the dictionary in either. But you should stick to the rule of only adding entries in the Pre... methods, and performing any modifications or removals in the Post... methods, just in case a future version of the Forms Designer decides to enforce this.)

Examples Example 9-20 and Example 9-21 show an implementation of PreFilterProperties that adds an extra property, Fooness, to the control at design time. Although the underlying control will not have such a property, it will still appear in the property panel for the control because the designer class has added it to the dictionary of property descriptors. Note that the designer class itself has provided the implementation of the Fooness property to which the descriptor refers. (The designer class might then use this property to modify its design-time behavior.)

Example 9-20. Adding a design-time property using C#
protected override void PreFilterProperties(IDictionary properties)
{
    base.PreFilterProperties(properties);

    properties["Fooness"] = TypeDescriptor.CreateProperty(
        typeof(MyDesigner),
        "Fooness",
        typeof(bool),
        CategoryAttribute.Design,
        DesignOnlyAttribute.Yes);
}

public bool Fooness
{
    get { return fooVal; }
    set { fooVal = value; }
}
private bool fooVal;
Example 9-21. Adding a design-time property using VB
Protected Overrides Sub PreFilterProperties(properties As IDictionary)
   MyBase.PreFilterProperties(properties)

   properties("Fooness") = TypeDescriptor.CreateProperty( _
       GetType(MyDesigner), _
       "Fooness", _
       GetType(Boolean), _
       CategoryAttribute.Design, _
       DesignOnlyAttribute.Yes)
End Sub

Public Property Fooness As Boolean
   Get
      Return fooVal
   End Get
   Set
      fooVal = Value
   End Set
End Property

The PropertyDescriptor is created using the CreateProperty factory method supplied by the TypeDescriptor class. As well as specifying the type that really implements the property (the designer class, in this case) along with the name and type of the property, we can also optionally specify a list of custom attributes. (CreateProperty takes a variable length argument list, so you can supply as many attributes as you like.) In this case, we have specified a CategoryAttribute (this will determine which category the property appears under in the PropertyGrid) and the DesignOnlyAttribute, which informs the development environment that this is a design-time property, which it should not attempt to set at runtime. All properties added this way should specify DesignOnlyAttribute.Yes, because otherwise, the Forms Designer will generate code that attempts to set these properties at runtime. Such code will not compile, because these design-time properties are not available at runtime.

This technique of adding extra properties at design time is used by Visual Studio .NET itself. It adds a Locked property to every control, which can be used to prevent accidental editing. The Control class does not define a Locked property—this is a design-time property automatically added by the ControlDesigner class's PreFilterProperties method.


9.2.5.1 Shadow properties

A common design-time requirement is to prevent certain properties from being set at design time. The most obvious example is the Visible property. If the user sets a control's Visible property to false, we don't really want to make the control invisible in the Designer, because this would make it hard to edit. We want the control to remain visible in the Designer, but for the Visible property to be honored at runtime. One solution would be for controls to ignore their Visible property at design time. However, the ControlDesigner class provides a more elegant solution than this, known as shadow properties, that doesn't require special design-time behavior from the control class.

The technique for making sure that the underlying property is only set to the specified value at runtime is to replace its PropertyDescriptor in the PostFilterProperties method with one that refers to a property supplied by the designer class. (So it is very similar to the technique of adding a design-time property shown in Examples Example 9-20 and Example 9-21.) This allows the designer class to remember the user's intended setting for the property without having to set that property on the control itself. The property will be set to the intended value at runtime, but not at design time.

To save you from having to declare fields for each of the properties you wish to shadow in your designer class, the base ControlDesigner class provides a protected property called ShadowProperties. This is a collection class that holds name/value pairs. As a convenience, if you try to retrieve a value for a name that is not in the collection, it will retrieve the property of the same name from the control itself, which means you don't need to initialize any of your shadow properties either. This makes the code for adding a shadow property relatively simple, as Examples Example 9-22 and Example 9-23 show.

Example 9-22. Shadowing the Anchor property using C#
protected override void PostFilterProperties(
    System.Collections.IDictionary properties)
{
    base.PostFilterProperties(properties);
    properties["Anchor"] = TypeDescriptor.CreateProperty(
        typeof(RoundedBoxDesigner),
        (PropertyDescriptor) properties["Anchor"]);
}

public AnchorStyles Anchor
{
    get { return (AnchorStyles) ShadowProperties["Anchor"]; }
    set { ShadowProperties["Anchor"] = value; }
}
Example 9-23. Shadowing the Anchor property using VB
Protected Overrides Sub PostFilterProperties( _
    properties As System.Collections.IDictionary)

    MyBase.PostFilterProperties(properties)
    properties("Anchor") = TypeDescriptor.CreateProperty( _
        GetType(RoundedBoxDesigner), _
        CType(properties("Anchor"), PropertyDescriptor))
End Sub

Public Property Anchor() As AnchorStyles
    Get 
        Return CType(ShadowProperties("Anchor"), AnchorStyles)
    End Get
    Set 
        ShadowProperties("Anchor") = Value
    End Set
End Property

This will have the effect of disabling anchoring behavior at design time, but leaving it enabled at runtime. Note that we are using a different version of the CreateProperty factory function. This one takes an existing PropertyDescriptor (the one already in the properties dictionary) and builds a new one based on that. This preserves all the attributes of the original, and merely has the effect of redirecting to our designer class's property.

The ControlDesigner automatically shadows certain standard properties for you. Visible and Enabled are shadowed because it would difficult to edit invisible or disabled controls. Controls are therefore always visible and enabled at design time, but will honor their settings at runtime. ContextMenu and AllowDrop are also shadowed because the Forms Designer provides its own context menus and uses drag and drop for editing the contents of the form.


9.2.6 Designer Host Interfaces

The custom designer classes we have seen so far have been essentially passive. They rely on the Forms Designer to create designer instances when needed, and to call their methods only when particular services are required. But sometimes your designer class will need to be a bit more proactive, explicitly requesting certain services from the Forms Designer. There is a generic mechanism to enable any container to provide arbitrary services on demand to the contained component.

A component can request a service from its container by calling the GetService method. (This method is available in both the Component class and the ComponentDesigner class, so both controls and their designer classes can request services.) GetService takes a single parameter, a Type object. This should be the type of an interface, indicating which service is required. If the container does not provide the requested service (or if there is no container, which is typically the case at runtime) GetService will return null or Nothing. But if the service is available, it will return an object that implements the requested service.

One of the standard services provided by Visual Studio .NET is ISelectionService. This allows designer classes and controls to find out which items have been selected by the user. Examples Example 9-24 and Example 9-25 illustrate how to use this service to modify the design-time appearance of a control according to whether it is selected or not. The example shows modifications to the RoundedBoxDesigner class we first saw in the C# code in Examples Example 9-12, Example 9-14, and Example 9-16, and in the VB code in Examples Example 9-13, Example 9-15, and Example 9-17. Here we override the Initialize method (which will be called when the designer class is instantiated), and ask the container for the ISelectionService interface. If this service is present (it always will be in Visual Studio .NET), our designer class attaches a handler to the SelectionChanged event. Our designer can now track the selection status of the control, causing the control to be redrawn each time it is selected or deselected. The OnPaintAdornments method has been modified so that it only paints the grab handle when the control is selected. (This is more consistent with the behavior of the standard resize handles and selection outline, which are only drawn when the control is selected.)

Example 9-24. Drawing adornments only when selected using C#
private ISelectionService iss;

public override void Initialize(IComponent component)
{
    // Call base class so that it can initialize itself

    base.Initialize(component);


    iss = GetService(typeof(ISelectionService)) as ISelectionService;
    if (iss != null)
    {
        iss.SelectionChanged += new EventHandler(OnSelectionChanged);
    }
    else
    {
        // ISelectionService is always available in VS.NET, but
        // if we're being hosted somewhere odd that doesn't have
        // it, just display adornments at all times.
    }
}

protected override void Dispose(bool disposing)
{
    if (disposing && iss != null)
    {
        iss.SelectionChanged -= new EventHandler(OnSelectionChanged);
    }

    base.Dispose(disposing);
}


private bool selected = false;

private void OnSelectionChanged(object sender, EventArgs e)
{
    bool previouslySelected = selected;
    selected = iss.GetComponentSelected(this.Control);
    if (selected != previouslySelected)
        this.Control.Invalidate();
}

protected override void OnPaintAdornments(PaintEventArgs pe)
{
    if (selected)
    {
        Rectangle grabRect = GetGrabRectangle();
        ControlPaint.DrawGrabHandle(pe.Graphics, grabRect, true, true);
    }
}
Example 9-25. Drawing adornments only when selected using VB
Private iss As ISelectionService
Private selected As Boolean = False 

Public Overrides Sub Initialize(component As IComponent)

    ' Call base class so that it can initialize itself
    MyBase.Initialize(component) 


    iss = CType(GetService(GetType(ISelectionService)), _
      ISelectionService)
    If Not iss Is Nothing Then
        AddHandler iss.SelectionChanged, _
         AddressOf OnSelectionChanged
    Else
        ' ISelectionService is always available in VS.NET, but
        ' if we're being hosted somewhere odd that doesn't have
        ' it, just display adornments at all times.
    End If
End Sub

Protected Overrides Overloads Sub Dispose(disposing As Boolean)
    If disposing And Not iss Is Nothing Then
        RemoveHandler iss.SelectionChanged, _
            AddressOf OnSelectionChanged
    End If

    MyBase.Dispose(disposing) 
End Sub

Private Sub OnSelectionChanged(sender As Object, e As EventArgs)
    Dim previouslySelected As Boolean = selected 
    selected = iss.GetComponentSelected(Me.Control) 
    If selected <> previouslySelected Then
        Me.Control.Invalidate() 
    End If
End Sub

Protected Overrides Sub OnPaintAdornments(pe As PaintEventArgs)
    If selected Then
        Dim grabRect As Rectangle = GetGrabRectangle() 
        ControlPaint.DrawGrabHandle(pe.Graphics, grabRect, _
                          True, True) 
    End If
End Sub

ISelectionService is just one of the many different services available to components and designer classes running in the Forms Designer. Table 9-1 lists the available services and provides a brief description of each service's purpose. These interfaces, which are all defined in the System.ComponentModel.Design namespace, are described in more detail in the reference section.

Table 9-1. Designer services

Service type

Purpose

IComponentChangeService

Provides notifications when components are added to and removed from the form, and when existing components are modified.

IDesignerEventService

Provides access to all the designers active on the form, and provides notifications when designers come and go.

IDesignerHost

Provides notification as modifications to a form progress through their various phases. (It supports a lightweight transaction model for these modifications.)

Also provides access to the root component—i.e., the Form or the UserControl that contains the control.

IDesignerOptionService

Provides access to the designer options chosen by the user. (Currently only used for grid settings such as snap-to-grid.)

IDictionaryService

A generic name/value store, allowing the designer class to store arbitrary data.

IEventBindingService

Used to make events appear in the property grid.

IExtenderListService

Allows a list of extender providers present on the form to be retrieved.

IExtenderProviderService

Allows extender providers to be added to or removed from the form.

IHelpService

Allows extra hints to be passed to the IDE to improve the suggestions offered by the context-sensitive help system.

IInheritanceService

Allows components that inherit from particular base classes to be located.

IMenuCommandService

Allows extra menu commands to be added to the development environment.

IReferenceService

Provides a way of locating references to a particular component within a project.

IResourceService

Allows access to culture-specific resources at design time.

IRootDesigner

Retrieves the root designer in environments that support nested designers.

ISelectionService

Provides notifications when the selection changes, and allows the list of selected controls to be retrieved.

ITypeDescriptorFilterService

Allows metadata filtering.

ITypeResolutionService

Allows specified assemblies or types to be found and loaded.

    [ Team LiB ] Previous Section Next Section