Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Rich Design Time Editing with UITypeEditors (VB.NET)

0.00/5 (No votes)
18 Jan 2012 2  
How to use UITypeEditors, Smart Tags, ControlDesigner Verbs, and Expandable Properties to make design-time editing easier.
UITypeEditorDemo.jpg

Introduction

This article assumes you have basic knowledge on creating a simple UserControl, so I am not going to go into much detail on creating one here. To demonstrate the Property Editors, I decided to make a control that draws itself in a selected shape. This gave me lots of options to demonstrate different design time editors. This demo uses UITypeEditors, Smart Tags, ControlDesigner Verbs, and Expandable Properties.

Property Value versus Displayed Value

The property value displayed in the property grid is a string representation of the actual property value. For example, a control's Visible property is type Boolean. The value is either 0 or -1, but the property grid doesn't display this. It converts the value 0 to the string "False" and the value -1 to the string "True". Different property value types have different interface types to edit their value.

Property Types

These are the basic property types and a common example:

  • TextBox

    TabIndex -> Edit the value directly in the display textbox.

  • Listbox

    BackgroundImageLayout -> Dropdown list of enumerated string values.

  • UI Dropdown

    ForeColor -> Dropdown area with graphical selection interface.

  • UI Modal

    BackgroundImage -> Separate dialog form opens to select the property value.

  • Expandable

    Font -> Property with plus sign next to it to expand out all the child properties. (Note: In addition, Font also has a Modal option.)

Textbox

A simple property type like Single:

Private _BorderWidth As Single = 2
<Description("Get or Set the Width of the border around the shape")> _
<Category("Shape")> _
<DefaultValue(GetType(Single), "2")> _
    Public Property BorderWidth() As Single
        Get
            Return _BorderWidth
        End Get
        Set(ByVal value As Single)
            _BorderWidth = value
            Invalidate()
        End Set
    End Property

Listbox

A property type that has enumerated values like DashStyle. This property will automatically give a list of the DashStyle text values to choose from, or create your own type with an ENUM list.

Enum eFillType
    Solid
    GradientLinear
    GradientPath
    Hatch
End Enum

Private _FillType As eFillType = eFillType.Solid
<Description("The Fill Type to apply to the Shape")> _
<Category("Shape")> _
<DefaultValue(GetType(eFillType), "Solid")> _
Public Property FillType() As eFillType
    Get
        Return _FillType
    End Get
    Set(ByVal value As eFillType)
        _FillType = value
        Me.Invalidate()
    End Set
End Property

UITypeEditor

What if you want a more visual way of choosing property values? UITypeEditors is the answer. There are two types: Dropdown and Modal. Each has the same overrideable methods.

  • GetEditStyle - Gets the editor style used by the EditValue method
  • EditValue - Edits the property value using the UITypeEditor

Optional if the following features are wanted. Sometimes, you want to have a small graphic representation displayed next to the text value, as in the color box next to the color name in a color type property like ForeColor.

  • GetPaintValueSupported - Returns True to allow overriding the PaintValue method
  • PaintValue - Paints the graphic in the display value

DropDown mode has one extra method:

  • IsDropDownResizable - Indicates if a grip handle is visible to allow the user to resize the dropdown.

DropDown

The DropDown mode creates an area that drops down below the property value. Here is where you place a UserControl that edits and returns the property value. I have two kinds of DropDowns demonstrated. One uses an owner-drawn listbox (BorderStyleEditor) and the other uses a separate UserControl, kind of like a mini-form. The BorderStyleEditor uses a simple CustomControl class (LineStyleListBox) that inherits the ListBox and draws its own items in the DrawItem event. Instead of just listing the text values, each DashStyle is drawn in that style. Now that you have a control to visually select the DashStyle, you need to add an IWindowsFormsEditorService to make it work in the editor. Also, if you don't want the custom control to show in the ToolBox, add <ToolboxItem(False)>.

<ToolboxItem(False)> _
Public Class LineStyleListBox
    Inherits ListBox

    Private _lineColor As Color = Color.Black
    Public Property LineColor() As Color
        Get
            Return _lineColor
        End Get
        Set(ByVal Value As Color)
            _lineColor = Value
        End Set
    End Property

    ' The editor service displaying this control.
    Private m_EditorService As IWindowsFormsEditorService

    Public Sub New(ByVal line_style As DashStyle, _
     ByVal editor_service As IWindowsFormsEditorService, _
    ByVal Line_Color As Color)
        MyBase.New()

        m_EditorService = editor_service

        ' Make items for each LineStyles value.
        For i As Integer = 0 To 4
            Items.Add(i)
        Next i
        LineColor = Line_Color
        ' Select the current line style.
        SelectedIndex = DirectCast(line_style, Integer)

        DrawMode = Windows.Forms.DrawMode.OwnerDrawFixed
        ItemHeight = 18
    End Sub
.
.
.
End Class

To make the editor display the new control, you need a class that inherits the UITypeEditor.

Public Class BorderStyleEditor
    Inherits UITypeEditor

    ' Indicate that we display a dropdown.
    Public Overrides Function GetEditStyle(ByVal context As ITypeDescriptorContext) _
        As UITypeEditorEditStyle
      
      Return UITypeEditorEditStyle.DropDown
    
    End Function

    ' Edit a line style
    Public Overrides Function EditValue(ByVal context As ITypeDescriptorContext, _
        ByVal provider As IServiceProvider, _
        ByVal value As Object) As Object
        ' Get an IWindowsFormsEditorService object.
        Dim editor_service As IWindowsFormsEditorService = _
            CType(provider.GetService(GetType(IWindowsFormsEditorService)), _
            IWindowsFormsEditorService)
        If editor_service Is Nothing Then
            Return MyBase.EditValue(context, provider, value)
        End If

        ' Convert the value into a BorderStyles value.
        Dim line_style As DashStyle = DirectCast(value, DashStyle)

        ' Make the editing control.
        Using editor_control As New LineStyleListBox(line_style, _
                                    editor_service, _
                                    CType(context.Instance, Shape).BorderColor)
            ' Display the editing control.
            editor_service.DropDownControl(editor_control)
            ' Save the new results.
            Return CType(editor_control.SelectedIndex, DashStyle)
        End Using
    End Function

    ' Indicate that we draw values in the Properties window.
    Public Overrides Function GetPaintValueSupported( _
        ByVal context As ITypeDescriptorContext) As Boolean

        Return True

    End Function

    ' Draw a BorderStyles value.
    Public Overrides Sub PaintValue(ByVal e As PaintValueEventArgs)
        ' Erase the area.
        e.Graphics.FillRectangle(Brushes.White, e.Bounds)

        Dim LnColor As Color
        If IsNothing(e.Context) Then
            LnColor = Color.Black
        Else
            LnColor = CType(e.Context.Instance, Shape).BorderColor
        End If
        ' Draw the sample.
        DrawSamplePen( _
                e.Graphics, _
                e.Bounds, _
                LnColor, _
                DirectCast(e.Value, DashStyle))
    End Sub
End Class

To make the whole thing come together, apply the editor to the property declaration:

Private _BorderStyle As DashStyle = DashStyle.Solid
<Category("Shape")> _
<Description("The line dash style used to draw state borders.")> _
<Editor(GetType(BorderStyleEditor), GetType(UITypeEditor))> _
<DefaultValue(GetType(DashStyle), "Solid")> _
Public Property BorderStyle() As DashStyle
    Get
        Return _BorderStyle
    End Get
    Set(ByVal value As DashStyle)
        _BorderStyle = value
        Me.Invalidate()
    End Set
End Property

What Does the UITypeEditor Do?

It gets the style using GetEditStyle:

Public Overrides Function GetEditStyle( _
  ByVal context As System.ComponentModel.ITypeDescriptorContext) _
  As System.Drawing.Design.UITypeEditorEditStyle

    Return UITypeEditorEditStyle.DropDown
    
End Function

To edit the value in the EditValue method:

  • Create an EditorService.
  • Create a new LineStyleListBox control.
  • Using EditorService.DropDownControl (control reference), it opens the dropdown with the control in it. After the item is selected, the CloseDropDown is called to return the value back to EditValue to be displayed in the PropertyGrid.
    Public Overrides Function EditValue(ByVal context As ITypeDescriptorContext, _
        ByVal provider As IServiceProvider, _
        ByVal value As Object) As Object
        ' Get an IWindowsFormsEditorService object.
        Dim editor_service As IWindowsFormsEditorService = _
            CType(provider.GetService(GetType(IWindowsFormsEditorService)), _
            IWindowsFormsEditorService)
        If editor_service Is Nothing Then
            Return MyBase.EditValue(context, provider, value)
        End If

        ' Convert the value into a BorderStyles value.
        Dim line_style As DashStyle = DirectCast(value, DashStyle)

        ' Make the editing control.
        Using editor_control As New LineStyleListBox(line_style, _
                                    editor_service, _
                                    CType(context.Instance, Shape).BorderColor)
            ' Display the editing control.
            editor_service.DropDownControl(editor_control)
            ' Save the new results.
            Return CType(editor_control.SelectedIndex, DashStyle)
        End Using
    End Function

Add a graphic to the display value:

  • Using the PaintValue method, a small graphic of the DashStyle is drawn next to the DashStyle name.
    Public Overrides Sub PaintValue(ByVal e As PaintValueEventArgs)
        ' Erase the area.
        e.Graphics.FillRectangle(Brushes.White, e.Bounds)

        Dim LnColor As Color
        If IsNothing(e.Context) Then
            LnColor = Color.Black
        Else
            LnColor = CType(e.Context.Instance, Shape).BorderColor
        End If
        ' Draw the sample.
        DrawSamplePen( _
                e.Graphics, _
                e.Bounds, _
                LnColor, _
                DirectCast(e.Value, DashStyle))
    End Sub

    BorderStyleEditor.jpg

The ShapeTypeEditor is another way of using the DropDown UITypeEditor. It uses a UserControl (DropdownShapeEditor) that lets you select the shape by clicking on a graphical representation of the shape. In the EditValue method, after creating a new DropdownShapeEditor UserControl, you could simply highlight the shape the mouse is over, but I wanted it to reflect what the original shape looks like, so the properties of the original Shape control are passed over to the Shape control in the DropdownShapeEditor, so when the mouse passes over the shape, it takes on the visual properties of the original Shape control. Selecting the shape calls CloseDropDown.

    ShapeTypeEditor.jpg

Another example of the DropDown is the RadiusInnerTypeEditor. It uses the DropdownRadiusInner UserControl, which contains a TrackBar and a Shape control. If the Shape is a Star, sliding the TrackBar back and forth changes the RadiusInner property value on the Star shape. When you get it the way you want, check the Apply button to call CloseDropDown.

    RadiusInnerTypeEditor.jpg

The last Dropdown is the BlendTypeEditor. It uses the DropdownColorBlender UserControl, which is a slight variation on my ColorBlender UserControl[^] The UserControl may be more complex, but there is very little difference in the EditValue method.

    BlendTypeEditor.jpg

There are two examples of editors for HatchStyle. For a quick and dirty editor, there is the HatchStyleEditorEasy.

Public Class HatchStyleEditorEasy
    Inherits UITypeEditor

    Public Overrides Function GetPaintValueSupported(
        ByVal context As ITypeDescriptorContext) As Boolean
        Return True
    End Function

    Public Overrides Sub PaintValue(ByVal e As PaintValueEventArgs)
        Dim hatch As HatchStyle = CType(e.Value, HatchStyle)
        Using br As Brush = New HatchBrush(hatch, SystemColors.WindowText, SystemColors.Window)
            e.Graphics.FillRectangle(br, e.Bounds)
        End Using
    End Sub
End Class

HatchStyleEditor is a better looking property editor. It is very similar to the BorderStyleEditor, however to reflect the current colors in the property display you need to get a current instance of the control to retrieve those properties. One major hitch I discovered is that for some unknown reason when this editor is used in the SmartTag. The Context value becomes Null in the PaintValue Sub which will cause a fatal error and crash the IDE. To work around this, I saw that the context value is OK in the GetPaintValueSupported function so I grab a reference to it there and store it in the SmartContext variable, and in the PaintValue sub check for Null and use then use the SmartContext variable instead.

    Private SmartContext As ITypeDescriptorContext 'SmartTag Workaround

    Public Overrides Function GetPaintValueSupported(
         ByVal context As ITypeDescriptorContext) As Boolean
        SmartContext = context 'store reference for use in PaintValue
        Return True
    End Function

    Public Overrides Sub PaintValue(ByVal e As PaintValueEventArgs)
        Dim hatch As HatchStyle = CType(e.Value, HatchStyle)
        ' Pass the UI editor the current property value
        Dim Instance As New Shape
        'e.context only works properly in the Propertygrid.
        'When coming from the SmartTag e.context becomes null and 
        'will cause a fatal crash of the IDE.
        'So to get around the null value error I captured a reference to the context
        'in the SmartContext variable in the GetPaintValueSupported function 
        If e.Context IsNot Nothing Then
            Instance = CType(e.Context.Instance, Shape)
        Else
            Instance = CType(SmartContext.Instance, ShapeActionList).CurrShape
        End If

        Using br As Brush = New HatchBrush(hatch, Instance.ColorFillSolid,
            Instance.ColorFillSolidB)
            e.Graphics.FillRectangle(br, e.Bounds)
        End Using

    End Sub

Here you can see the difference between the two representations of the same property type, and which is obviously better.

    HatchTypeEditors.jpg

Modal

Modal is the other type of UITypeEditor. Instead of a dropdown, a separate dialog form opens up, just like Font or BackgroundImage does. The FocalTypeEditor first creates a new dialog form (dlgFocalPoints) that lets you adjust the property values the way you want.

The GetEditValue returns UITypeEditorEditStyle.Modal.

With Modal, the EditValue's EditorService will use EditorService.ShowDialog (dialog reference). There is no CloseDropdown needed, just closing the dialog does the same thing. Using a dialog is nice when you want a little more real estate. I used this for a property I called FocalPoints. Changing the CenterPoint and FocusScales properties of a ColorBlend made more visual sense when I could work with them together. I made a form that would allow the tweaking of these values visually, but because the editors only return one property value, I created a cFocalPoints class that contains two PointF values that represent the CenterPoint and FocusScales as one.

    FocalTypeEditor.jpg

ControlDesigner

Alot of cool DesignMode additions can be added here to make the design time experience even more rich.

  • Mouse interaction beyond selection and resizing
  • Extra Design time only painting
  • Resize restrictions
  • Smart Tags
  • Verbs

Let's take another look at how the FocalPoints can be handled in a different way. It would be better if you could just click on the control directly and move the points around. Normally mouse events are ignored in the Designer. The control can only be selected or resized with the mouse, but in the ControlDesigner you can override the GetHitTest function to tell the designer to process the mouse events. There is also OnMouseEnter, OnMouseHover, and OnMouseLeave which are located in the ControlDesigner. With the GetHitTest function the control can be told to process the control's Mouse Events by checking DesignMode property and then handle the Design time routines. Note because you override the GetHitTest the control won't select properly so you need to set it manually select it with the ISelectionService.

After the control paints itself, the OnPaintAdornments sub lets you paint additional things on the control only at design time like a selection rectangle. When the control has a FillType of GradientPath a circle is drawn around where the CenterPoint is and a square where the FocusPoint is. Then Click on that spot to drag the point around. or Right-Click to Reset the Point

FocalTypeEditor.jpg

In the ControlDesigner class:

    Protected Overrides Function GetHitTest( _
      ByVal point As System.Drawing.Point) As Boolean
        point = _Shape.PointToClient(point)
        _Shape.CenterPtTracker.IsActive = _
            _Shape.CenterPtTracker.TrackerRectangle.Contains(point)
        _Shape.FocusPtTracker.IsActive = _
            _Shape.FocusPtTracker.TrackerRectangle.Contains(point)

        Return _Shape.CenterPtTracker.IsActive Or _Shape.FocusPtTracker.IsActive
    End Function

    Protected Overrides Sub OnMouseEnter()
        MyBase.OnMouseEnter()
        TheBox = True
        _Shape.Invalidate()
    End Sub

    Protected Overrides Sub OnMouseLeave()
        MyBase.OnMouseLeave()
        TheBox = False
        _Shape.Invalidate()
    End Sub

    Protected Overrides Sub OnPaintAdornments _
      (ByVal pe As System.Windows.Forms.PaintEventArgs)
        MyBase.OnPaintAdornments(pe)

        If _Shape.FillType = Shape.eFillType.GradientPath And TheBox Then
            Using g As Graphics = pe.Graphics
                Using pn As Pen = New Pen(Color.Gray, 1)
                    pn.DashStyle = DashStyle.Dot
                    g.FillEllipse( _
                        New SolidBrush(Color.FromArgb(100, 255, 255, 255)), _
                        _Shape.CenterPtTracker.TrackerRectangle)
                    g.DrawEllipse(pn, _Shape.CenterPtTracker.TrackerRectangle)

                    g.FillRectangle( _
                        New SolidBrush(Color.FromArgb(100, 255, 255, 255)), _
                        _Shape.FocusPtTracker.TrackerRectangle)
                    g.DrawRectangle(pn, _Shape.FocusPtTracker.TrackerRectangle)
                End Using
            End Using
        End If
    End Sub

In the Control Class:

    Private Sub Shape_MouseDown(ByVal sender As Object, _
      ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseDown
        If DesignMode Then
            'Because of the GetHitTest Override in the
            'Designer Manual Selection is needed
            Dim selectservice As ISelectionService = _
                 CType(GetService(GetType(ISelectionService)), ISelectionService)
            Dim selection As New ArrayList
            selection.Clear()
            selectservice.SetSelectedComponents(selection, SelectionTypes.Replace)
            selection.Add(Me)
            selectservice.SetSelectedComponents(selection, SelectionTypes.Add)

            'FocusPoints Reset
            If e.Button = Windows.Forms.MouseButtons.Right Then
                If Me.CenterPtTracker.IsActive Then
                    Me.FocalPoints = New cFocalPoints( _
                        New PointF(0.5, 0.5), _
                        Me.FocalPoints.FocusScales)
                    Me.Invalidate()
                ElseIf Me.FocusPtTracker.IsActive Then
                    Me.FocalPoints = New cFocalPoints( _
                        Me.FocalPoints.CenterPoint, _
                        New PointF(0, 0))
                    Me.Invalidate()
                End If
            End If
        End If
    End Sub

    Private Sub Shape_MouseMove(ByVal sender As Object, _
      ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseMove
        If DesignMode Then
            If e.Button = Windows.Forms.MouseButtons.Left Then
                If Me.CenterPtTracker.IsActive Then
                    Me.FocalPoints = New cFocalPoints( _
                        New PointF(e.X / Me.Width, e.Y / Me.Height), _
                        Me.FocalPoints.FocusScales)
                    Me.Invalidate()
                ElseIf Me.FocusPtTracker.IsActive Then
                    Me.FocalPoints = New cFocalPoints( _
                        Me.FocalPoints.CenterPoint, _
                        New PointF(e.X / Me.Width, e.Y / Me.Height))
                    Me.Invalidate()
                End If
            End If
        End If
    End Sub

Another feature you can apply (I'll explain but I didn't put a functioning example in the code) is changing the SelectionRules property. If you want to restrict sizing or moving or visibility this is the one. A normal TextBox can only be sized horizontally unless you make the Multiline property True, then you can resize vertically too. The below example will give your control the horizontal only effect.

    Public Overrides ReadOnly Property SelectionRules() _
      As System.Windows.Forms.Design.SelectionRules
        Get
            Return SelectionRules.LeftSizeable _
                   Or SelectionRules.RightSizeable _
                   Or Windows.Forms.Design.SelectionRules.Visible _
                   Or Windows.Forms.Design.SelectionRules.Moveable
        End Get
    End Property

Smart Tags

Now that you have made these cool property editors, it would be nice to access them in an organized way. The PropertyGrid lists the properties alphabetically in each category, and with a careful naming structure, you can get some organization. A Smart Tag will give you an independent area to organize select properties the way you want and label them differently than the sometimes cryptic property names. Click on the little arrow that appears in the upper right corner of some controls to open up the Smart Tag.

SmartTag_Arrow.jpg

To make Smart Tags, you need two classes. First, create a class that inherits ControlDesigner and override the ActionLists property to add the second class's DesignerActionListCollection. The second class inherits DesignerActionList. In the New event, add a reference to the component being designed and to the DesignerActionUIService, and if you want the Smart Tag to open automatically, add Me.AutoShow = True.

Make a list of properties you want to appear in the Smart Tag (order does not matter here). The property is like the property in the component. except you get the property value from the component reference and set it using the TypeDescriptor SetValue to keep it in sync with the IDE.

Public Class ShapeActionList
    Inherits DesignerActionList

    ' The Shape we are designing.
    Private _ShapeSelector As Shape

    Private _DesignerService As DesignerActionUIService = Nothing

    Public Sub New(ByVal component As IComponent)
        MyBase.New(component)

        ' Save a reference to the control we are designing.
        _ShapeSelector = DirectCast(component, Shape)

        ' Save a reference to the DesignerActionUIService
        _DesignerService = _
            CType(GetService(GetType(DesignerActionUIService)), _
            DesignerActionUIService)

        'Makes the Smart Tags open automatically 
        Me.AutoShow = True
    End Sub

    <Editor(GetType(ShapeTypeEditor), GetType(UITypeEditor))> _
    Public Property Shape() As Shape.eShape
        Get
            Return _ShapeSelector.Shape
        End Get
        Set(ByVal value As Shape.eShape)
            SetControlProperty("Shape", value)
        End Set
    End Property

    Private Sub SetControlProperty(ByVal property_name As String, ByVal value As Object)
        TypeDescriptor.GetProperties(_ShapeSelector) _
            (property_name).SetValue(_ShapeSelector, value)
    End Sub
.
.
.

End Class

ActionList Item Types

Types of items that can be added to a Smart Tag:

  • DesignerActionHeaderItem

    Bold text heading each area of grouped items.

  • DesignerActionTextItem

    String text that just displays information.

  • DesignerActionPropertyItem

    A control will represent each property in the Smart Tag.

    Textbox, Dropdown, and Modal will appear as they do in the PropertyGrid, but Boolean properties will appear as a checkbox. Methods will appear as hypertext. The expandable properties don't play nice, however. Only the string value for all the child properties is shown and no plus sign to expand it. You would have to type the whole string in the exact order and format to make any changes (not very friendly). You could separate them out and handle them individually, but that takes up a lot of space on the Smart Tag. Instead, what is better is an Action Method that opens a dialog to edit all the child properties using an IDesignerHost (more on that later). After all the properties are made, they can be added to the ActionList.

  • DesignerActionMethodItem

    Blue hypertext that when clicked calls the routine assigned to it.

Adding Items to the ActionList

Public Overrides Function GetSortedActionItems() As _
  System.ComponentModel.Design.DesignerActionItemCollection

This is where you can order the ActionItems on the Smart Tag the way you want. Create a reference to a new DesignerActionItemCollection and start adding the item headers first.

Dim items As New DesignerActionItemCollection()

Examples

Header
items.Add(New DesignerActionHeaderItem("Shape Appearance"))
Text

Format:

Text,
Header Name to put item in

Sample:

Dim txt As String = "Width=" & _ShapeSelector.Width & _
 " Height=" & _ShapeSelector.Height
items.Add( _
    New DesignerActionTextItem( _
        txt, _
        "Information"))
Property

Format:

Property Name,
Label Text,
Header Name to put item in,
Description

Sample:

items.Add( _
    New DesignerActionPropertyItem( _
        "Shape", _
        "Shape", _
        "Shape Appearance", _
        "The Shape of the Control"))
Method

Format:

ActionList,
Property Name,
Label Text,
Header Name to put item in,
Description,
Show HyperText in PropertyGrid

Sample:

items.Add( _
    New DesignerActionMethodItem( _
         Me, _
        "AdjustCorners", _
        "Adjust Corners ", _
        "Rectangle Only", _
        "Adjust Corners", _
        True))

SmartTag.jpg

Verbs

At the bottom of the PropertyGrid, you can add hypertext that when clicked, calls a method from the Control Designer. To do this, override the Verbs property and add a new DesignerVerb. This new verb has a reference to an event that does what you want. If you need the text for the verb to change, it can't be changed directly, so you have to remove it from the IMenuCommandService and then add it back with the updated text.

Public Overrides ReadOnly Property Verbs() As _
  System.ComponentModel.Design.DesignerVerbCollection
    Get
        Dim myVerbs As DesignerVerbCollection = _
            New DesignerVerbCollection
        ClipRegion = New DesignerVerb(GetVerbText, _
            New EventHandler(AddressOf ClipToRegionClicked))
        myVerbs.Add(ClipRegion)
        Return myVerbs
    End Get
End Property

Private Function GetVerbText() As String
    Return "Region Clipping " & IIf(_Shape.RegionClip, "ON", "OFF")
End Function

Public Sub ClipToRegionClicked(ByVal sender As Object, ByVal e As EventArgs)
    Me.VerbRegionClip = Not Me.VerbRegionClip
End Sub

Public Property VerbRegionClip() As Boolean
    Get
        Return _Shape.RegionClip
    End Get
    Set(ByVal value As Boolean)
        Dim prop As PropertyDescriptor = _
            TypeDescriptor.GetProperties(GetType(Shape)) _
            ("RegionClip")
        Me.RaiseComponentChanging(prop)
        _Shape.RegionClip = value
        Me.RaiseComponentChanged(prop, Not (_Shape.RegionClip), _Shape.RegionClip)
        Dim menuService As IMenuCommandService = _
          CType(Me.GetService(GetType(IMenuCommandService)), IMenuCommandService)
        If Not (menuService Is Nothing) Then
            If menuService.Verbs.IndexOf(ClipRegion) >= 0 Then
                menuService.Verbs.Remove(ClipRegion)
                ClipRegion = New DesignerVerb( _
                GetVerbText, _
                New EventHandler(AddressOf ClipToRegionClicked))
                menuService.Verbs.Add(ClipRegion)
            End If
        End If
        _Shape.Refresh()
    End Set
End Property

If you do not see the hyperlink, Right Click and check Commands.

Verbs.jpg

Expandable Property

Now, back to the expandable Corners property. First, how to make one, and then, how to deal with it in the Smart Tag.

For, the Corners property, I wanted to have an expandable property like the Padding property. I could have simply made a property from the Padding type, but the names were wrong (All, Top, Bottom, Left, Right). I wanted All, UpperRight, UpperLeft, LowerRight, LowerLeft.

The Corners property value looks like this: 2, 2, 2, 2 in the property grid, which is actually a string representation of the four properties (with the fifth hidden). There needs to be a process to convert the real properties to and from this string.

First, I needed the TypeConverter properties class:

<TypeConverter(GetType(CornerConverter))> _
Public Class CornersProperty
.
.
.
    <DescriptionAttribute("Set the Radius of the Upper Left Corner"))> _
    <RefreshProperties(RefreshProperties.Repaint))> _
    <NotifyParentProperty(True))> _
    <DefaultValue(0)> _
    Public Property UpperLeft() As Short
        Get
            Return _UpperLeft
        End Get
        Set(ByVal Value As Short)
            _UpperLeft = Value

            CheckForAll(Value)
        End Set
    End Property
.
.
.
End Class

Then, the actual converter class which inherits ExpandableObjectConverter. Simply put, this just splits the string into pieces and assigns the parts to each property, or joins each property into the string to display as the Corners property value.

Friend Class CornerConverter : Inherits ExpandableObjectConverter

    Public Overloads Overrides Function CanConvertFrom( _
      ByVal context As System.ComponentModel.ITypeDescriptorContext, _
      ByVal sourceType As System.Type) As Boolean
        If (sourceType Is GetType(String)) Then
            Return True
        End If
        Return MyBase.CanConvertFrom(context, sourceType)
    End Function

    Public Overloads Overrides Function ConvertFrom( _
      ByVal context As System.ComponentModel.ITypeDescriptorContext, _
      ByVal culture As System.Globalization.CultureInfo, _
      ByVal value As Object) As Object
        If TypeOf value Is String Then
            Try
                Dim s As String = CType(value, String)
                Dim cornerParts(4) As String
                cornerParts = Split(s, ",")
                If Not IsNothing(cornerParts) Then
                    If IsNothing(cornerParts(0)) Then cornerParts(0) = 0
                    If IsNothing(cornerParts(1)) Then cornerParts(1) = 0
                    If IsNothing(cornerParts(2)) Then cornerParts(2) = 0
                    If IsNothing(cornerParts(3)) Then cornerParts(3) = 0
                    Return New CornersProperty( _
                      cornerParts(0), _
                      cornerParts(1), _
                      cornerParts(2), _
                      cornerParts(3))
                End If
            Catch ex As Exception
                Throw New ArgumentException("Can not convert '" & _
                  value & "' to type Corners")
            End Try
        Else
            Return New CornersProperty()
        End If

        Return MyBase.ConvertFrom(context, culture, value)
    End Function
    Public Overloads Overrides Function ConvertTo( _
      ByVal context As System.ComponentModel.ITypeDescriptorContext, _
      ByVal culture As System.Globalization.CultureInfo, _
      ByVal value As Object, ByVal destinationType As System.Type) As Object

        If (destinationType Is GetType(System.String) _
          AndAlso TypeOf value Is CornersProperty) Then
            Dim _Corners As CornersProperty = CType(value, CornersProperty)

            ' build the string as "UpperLeft,UpperRight,LowerLeft,LowerRight" 
            Return String.Format("{0},{1},{2},{3}", _
                   _Corners.LowerLeft, _
                   _Corners.LowerRight, _
                   _Corners.UpperLeft, _
                   _Corners.UpperRight)
        End If
        Return MyBase.ConvertTo(context, culture, value, destinationType)

    End Function

End Class 'CornerConverter Code

If you need the Control to reflect the changes made to the child properties immediately the <RefreshProperties(RefreshProperties.Repaint))> and <NotifyParentProperty(True))> in the Property are not enough to trigger a refresh. Add the GetCreateInstanceSupported and CreateInstance Functions to the ExpandableObjectConverter Class. GetCreateInstanceSupported should simply always Return True, then in the CreateInstance Function create a new Instance and Return it.

    Public Overrides Function GetCreateInstanceSupported( _
      ByVal context As System.ComponentModel.ITypeDescriptorContext) As Boolean

        Return True

    End Function

    Public Overrides Function CreateInstance( _
      ByVal context As System.ComponentModel.ITypeDescriptorContext, _
      ByVal propertyValues As System.Collections.IDictionary) As Object

        Dim crn As New CornersProperty
        Dim AL As Short = CType(propertyValues("All"), Short)
        Dim LL As Short = CType(propertyValues("LowerLeft"), Short)
        Dim LR As Short = CType(propertyValues("LowerRight"), Short)
        Dim UL As Short = CType(propertyValues("UpperLeft"), Short)
        Dim UR As Short = CType(propertyValues("UpperRight"), Short)


        Dim oAll As Short = CType(CType(context.Instance, Shape).Corners, _
            CornersProperty).All

        If oAll <> AL And AL > -1 Then
            crn.All = AL
        Else
            crn.LowerLeft = LL
            crn.LowerRight = LR
            crn.UpperLeft = UL
            crn.UpperRight = UR

        End If

        Return crn

    End Function

The CornersProperty was a bit trickier because each property could change the other properties, so I had to look back to the original values from the context.Instance to see what changed before creating the new instance.

For a more straight forward example, here is the CreateInstance for the FocalPoints:

    Public Overrides Function CreateInstance( _
      ByVal context As System.ComponentModel.ITypeDescriptorContext, _
      ByVal propertyValues As System.Collections.IDictionary) As Object
        
        Dim fPt As New cFocalPoints

        fPt.CenterPtX = CType(propertyValues("CenterPtX"), Single)
        fPt.CenterPtY = CType(propertyValues("CenterPtY"), Single)
        fPt.FocusPtX = CType(propertyValues("FocusPtX"), Single)
        fPt.FocusPtY = CType(propertyValues("FocusPtY"), Single)

        Return fPt

    End Function

To deal with this property in the Smart Tag, I made a dialog (dlgCorners) that allowed me to adjust the corners visually and open it in a method using an IDesignerHost.

' Update new Corners values if OK button was pressed
If dlg.ShowDialog() = DialogResult.OK Then
    Dim designerHost As IDesignerHost = _
    CType(Me.Component.Site.GetService( _
         GetType(IDesignerHost)), IDesignerHost)

    If designerHost IsNot Nothing Then
        Dim t As DesignerTransaction = designerHost.CreateTransaction()
        Try
            SetControlProperty("Corners", _
                New CornersProperty( _
                dlg.TheShape.Corners.LowerLeft / ratio, _
                dlg.TheShape.Corners.LowerRight / ratio, _
                dlg.TheShape.Corners.UpperLeft / ratio, _
                dlg.TheShape.Corners.UpperRight / ratio))
            t.Commit()
        Catch
            t.Cancel()
        End Try
    End If
End If
_ShapeSelector.Refresh()

dlgCorners.jpg

The ColorFillBlend property is another example of an expandable property plus it has a Modal dialog option button too.

PropertyGrid.jpg

How to Override a Base Editor

Some Property Types need a little extra step to work properly. The Color property has a built in editor system. If you create your own Color Picker control, you can substitute yours for the Microsoft Color Picker. If you have a more complex Color Picker and need it to open in a Modal Dialog, you will find a problem. A simple dropdown list will appear instead of the custom dialog. In order to make it play nice, the GetStandardValuesSupported in the TypeConverter must be turned off by creating a new ColorConverter and overriding the Function.

First make a Converter class:

Imports System.ComponentModel

Public Class AltColorConverter
    Inherits ColorConverter

    Public Overrides Function GetStandardValuesSupported( _
        ByVal context As ITypeDescriptorContext) As Boolean
        Return False
    End Function

End Class

Then add the TypeConverter attribute:

    Private _colorC As Color = Color.White
    <Category("Appearance")> _
    <DefaultValue(GetType(Color), "White")> _
    <Editor(GetType(AltColorPickerModalUI), GetType(UITypeEditor))> _
    <TypeConverter(GetType(AltColorConverter))> _
    Public Property ColorC() As Color
        Get
            Return _colorC
        End Get
        Set(ByVal Value As Color)
            _colorC = Value
            Panel4.BackColor = ColorC
            Invalidate()
        End Set
    End Property

Look at Form2 to see examples of different ways of implementing a custom Color Editor to a Color Property.

ColorExampleControl.vb

The ColorExampleControl is a simple UserControl with different Color Properties utilizing different UIEditors.

  • ColorA - uses the AltColorPicker Dropdown and AltColorPickerDropDownUI Editor. The IWindowsFormsEditorService is not transferred to the AltColorPicker Dropdown so the property will not update until the Dropdown loses focus and closes on its own.
  • ColorB - uses the AltColorPicker_ES Dropdown and AltColorPickerDropDownUI_ES Editor. Here the IWindowsFormsEditorService is passed through so the CloseDropDown can be called were ever needed.
  • ColorC - uses the AltColorModalDialog Dialog and AltColorPickerModalUI Editor.

Global Custom TypeEditor

The TypeDescriptor.AddAttributes allows you to change ALL color property whether Base or Custom to use the same Editor throughout the entire project. Remove the editor attributes from the properties and use the code below.

*** Note if you change your mind and take this out, you have to close the project and re-open to reset correctly.

TypeDescriptor.AddAttributes(GetType(Color), _
        New EditorAttribute(GetType(AltColorPickerDropDownUI_ES), _
        GetType(UITypeEditor)), _
        New TypeConverterAttribute(GetType(Color)))

History

  • Version 1.0 - September 2008
  • Version 1.1 - September 2008
    • Using this .TheShape.FocalPoints = Instance.FocalPoints doesn't always get committed. For example, if you only change the FocalPoints by themselves, it visually appears to have worked in the designer, but after the Build it reverts back. To fix it, I changed to this .TheShape.FocalPoints = New cFocalPoints(Instance.FocalPoints.CenterPoint, Instance.FocalPoints.FocusScales) and all is right again.
  • Version 1.2 - December 2008
    • Added New ControlDesigner Features GetHitTest, OnPaintAdornments, and SelectionRules. Mouse interaction with the control directly on the design surface.
  • Version 1.3 - December 2008
    • Fixed the ExpandableObjectConverter classes CornerConverter and FocalPointsConverter so they update the control immediately after a Child property is changed.
    • Added two HatchStyle Editors to show different ways to handle them.
  • Version 1.4 - January 2011
    • Added the Type Converter to the ColorFillBlend
    • Various of tweaks and fixes
  • Version 1.5 - January 2011
    • Added Form2 demo of how to use Custom Editors on the Color Property

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here