Gain Design-Time Power with Control Editors, Part 2 of 2

he first part of this article provided a detailed description of a StateSelector control that lets users select a state from a map. The map itself is a control property named Map. In the Visual Studio Properties window, the article showed how to use a type converter to display a special representation of the Map property in the Properties window. Specifically, that type converter displays the Map property as the string (none) when the property is Nothing or (map) when it contained a value. Part 1 also showed how to use a file name editor to let users select a file, and how to use a dropdown editor to let users pick a line style from a list.

While those features are uncommon, you can go far beyond that. For example, you can display a custom dialog editor that lets users set complicated properties that require more interaction than a dropdown. You can also add smart tags and property pages to your controls, allowing users to set multiple properties in even more complex ways.

Selected Property Dialog
The StateSelector control’s Selected property determines the states that are currently selected on the map. This property represents the selected states using a comma-separated list of their abbreviations. For example, you can set the Selected property to “CA,NV,UT,CO,AZ,NM” to select the states highlighted in Figure 1.

Figure 1. Selected States: The StateSelector control’s Selected property determines which states are selected.

Setting the Selected property textually works, but it is somewhat cumbersome. Imagine instead letting users set the property graphically?by clicking on the states in a dialog such as Figure 1. Not only is selecting states in this manner more intuitive, but it’s also less error prone, and lets users easily see mistakes in the pattern of selected states.

The first part of this article showed how to use an editor class to display a dropdown list for the StateSelector control’s LineStyle property. You can use a similar technique to display an editing dialog. As before, you begin the process by adding attributes to the property declaration. The following code shows how the StateSelector control defines its Selected property:

    _    _    _    _   Public Property Selected() As String      Get         If m_Map Is Nothing Then Return ""            Dim txt As String = ""         For Each map_state As MapState In m_Map.MapStates            If map_state.Selected Then txt &= "," & map_state.Name         Next map_state            ' Remove leading comma.         If txt.Length > 0 Then txt = txt.Substring(1)            Return txt      End Get      Set(ByVal value As String)         If m_Map Is Nothing Then Exit Property            value = "," & value.ToUpper & ","         For Each map_state As MapState In m_Map.MapStates            map_state.Selected = _               value.Contains("," & map_state.Name.ToUpper & ",")         Next map_state            Me.Invalidate()      End Set   End Property

The property’s Editor attribute indicates that the Properties window can use the SelectedEditor class to edit Selected values.

The control delegates the property to the MapState objects displayed by the map. The Get portion of the property iterates through the selected MapState objects and builds a comma-delimited list of states abbreviations. The Set portion loops through the MapState objects and selects those whose abbreviations appear in the value string parameter.

Listing 1 shows how the SelectedEditor class works. When a user clicks on the ellipsis to the right of the Selected property in the Properties window, Visual Studio creates an instance of the SelectedEditor class and uses that to edit the Selected property value.

SelectedEditor inherits from UITypeEditor and then overrides key methods to do its important work. The GetEditStyle function returns Modal to indicate that the editor is a modal dialog.

The EditValue function in Listing 1 does the actual editing. It starts by getting a reference to an editor service that it can use to control the editing dialog, and then obtains a reference to the StateSelector control being edited. If the StateSelector does not have a map loaded, the function displays an error message and exits.

Next, EditValue creates a new dlgEditSelected form containing OK and Cancel buttons and a StateSelector control, which shows the selected states and lets the user modify the selection, as shown in Figure 1. (I like the symmetry of using a StateSelector control to edit a StateSelector control.)

The code sets the dialog’s StateSelector control’s Map property to a cloned copy of the Map property used by the control it is editing. This is an important point. If the code were to set the dialog control’s Map property equal to the editing control’s Map property, then the two would be references to the same StatesMap object. In that case, any changes you made to the editing dialog would immediately affect the control you were editing so you could not use the Cancel button to discard them. But because the code uses a clone, the dialog has its own copy, and users can save or discard their changes depending on which button they click.

The code calls the dialog’s SetSize method, which resizes the dialog to fit the StateSelector control.

Finally the EditValue function displays the dialog modally and examines the returned result. If the user clicked the OK button, the EditValue function returns the new cloned copy of the Map property with the user’s changes. If the user clicked Cancel, the function returns the original Map property value. In either case, the Properties window sets the edited control’s Map property to the value returned by EditValue.

Building Smart Tags and Command Verbs
If you select one or more controls in the form designer in Visual Studio 2005, a little box with an arrow appears near the control’s upper-right corner (see Figure 2). If you click this arrow, a smart tag popup appears. This popup contains the properties that you are most likely to want to modify for the selected control(s). If you select one control, or a group of controls of the same type, the popup can also include information and links that invoke command verbs to perform some action on the control. Clicking the little box again or clicking anywhere outside of the popup closes the smart tag popup.

Figure 2 shows the smart tag popup for the StateSelector control, which lets you set the control’s LineStyle and Map properties. The smart tag also displays an information section indicating that this map includes 51 states. At the bottom of the smart tag, command verbs let you reset the map, and select, deselect, or toggle every state.

Figure 2. Custom Smart Tag: Creating smart tags lets developers set a control’s common properties.

To build a smart tag for a control, start by adding the Designer attribute to the control’s class. Here’s the Designer attribute for the StateSelector class indicating that the StateSelectorDesigner class is the smart tag designer for the StateSelector control:

    _   Public Class StateSelector   ...   End Class

The following code shows the StateSelectorDesigner class:

   Public Class StateSelectorDesigner      Inherits ControlDesigner         Private m_Lists As DesignerActionListCollection         Public Overrides ReadOnly Property ActionLists() _         As System.ComponentModel. _         Design.DesignerActionListCollection         Get            If m_Lists Is Nothing Then               m_Lists = New DesignerActionListCollection               m_Lists.Add( _                  New StateSelectorActionList(Me.Component))            End If            Return m_Lists         End Get      End Property   End Class

The StateSelectorDesigner class inherits from the ControlDesigner class and then overrides its ActionLists method to perform its only real task: returning a list of the “actions” that should be listed on the smart tag popup. The ActionLists code creates a DesignerActionListCollection object and adds a new StateSelectorActionList to it.

The StateSelectorActionList class defines the smart tag’s contents. To save space, only some of the code is shown here. Download the example project to see the complete code.

The following code declares private variables to hold references to the StateSelector control that it is editing and to a designer service that it can use to refresh the smart tag popup. The class’s constructor sets these references:

   ' The StateSelector we are designing.   Private m_StateSelector As StateSelector      Private m_DesignerService As DesignerActionUIService = Nothing   Public Sub New(ByVal component As IComponent)      MyBase.New(component)         ' Save a reference to the control we are designing.      m_StateSelector = DirectCast(component, StateSelector)         ' Save a reference to the DesignerActionUIService      ' so we can refresh it later.      m_DesignerService = _         CType(GetService(GetType(DesignerActionUIService)), _         DesignerActionUIService)   End Sub

Next, for each property that the smart tag popup should display, the class includes a property. The code includes a method for each command verb that it should display. Attributes tell the smart tag how the item should be used.

The following code shows the Map property. Notice that the code uses the same TypeConverter and Editor attributes used by the control’s Map property and described in the first part of this article. This allows the smart tag to reuse the type converter and editor so it can manage the Map property the same way the Properties window does, displaying the value as (none) or (map) and providing a file selection dialog to edit the Map value:

    _    _    _    _   Public Property Map() As StatesMap      Get         Return m_StateSelector.Map      End Get      Set(ByVal value As StatesMap)         SetControlProperty("Map", value)            ' Refresh the designer service to show         ' the new number of states.         m_DesignerService.Refresh(m_StateSelector)      End Set   End Property

The Property Set procedure is worth mentioning here, too. The code calls the helper method SetControlProperty to actually set the Map property’s value. The code could simply set the control’s Map property to the new value, but then the smart tag wouldn’t know that the property has changed?and that leads to three problems:

  1. The smart tag doesn’t always update the control on the form, so you don’t see the change.
  2. The smart tag doesn’t tell the IDE that the property has changed, which means the IDE’s undo and redo commands don’t handle the change properly.
  3. The IDE doesn’t flag the form as modified, so if you don’t make any other changes, it will allow you to close the form without saving the change.

The following code shows how the SetControlProperty method updates a property while keeping the smart tag in the picture. The code uses the TypeDescriptor class to get information about the property and set its value. The SetControlProperty method keeps both the smart tag and the IDE up to date:

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

The smart tag action list’s DoResetMap method simply sets the StateSelector control’s Map property to Nothing. But it uses the SetControlProperty method to set the new property value to avoid the problems described earlier:

   ' Remove the map data.   Public Sub DoResetMap()      SetControlProperty("Map", Nothing)   End Sub

The DoSelectAll method calls the control’s SelectAll method to select all the states. It then sets the Map property to its current value by calling the SetControlProperty method as shown below:

   ' Select all map states.   Public Sub DoSelectAll()      m_StateSelector.SelectAll()      SetControlProperty("Map", m_StateSelector.Map)   End Sub

The last interesting part of the StateSelectorActionList class is its GetSortedActionItems method. This function both returns a list describing the items that appear on the smart tag popup, and connects the items to the class’s properties and methods:

   ' Return the smart tag action items.   Public Overrides Function GetSortedActionItems() _    As System.ComponentModel.Design.DesignerActionItemCollection      Dim items As New DesignerActionItemCollection()         ' Section headers.      items.Add(New DesignerActionHeaderItem("Appearance"))         If m_StateSelector.Map IsNot Nothing Then         items.Add(New DesignerActionHeaderItem("Information"))            Dim txt As String = "# States: " & _            m_StateSelector.Map.MapStates.Length         items.Add( _            New DesignerActionTextItem( _               txt, "Information"))      End If         ' Property entries.      items.Add( _         New DesignerActionPropertyItem( _            "LineStyle", "Line Style", _            "Appearance", _            "The line style used to draw state borders"))      items.Add( _         New DesignerActionPropertyItem( _            "Map", "Map", "Appearance", _            "The map data"))      ' Method entries.      items.Add( _         New DesignerActionMethodItem( _            Me, "DoResetMap", "Reset Map", _            "Data", "Remove the map data", True))      items.Add( _         New DesignerActionMethodItem( _            Me, "DoSelectAll", "Select All", _            "Data", "Select all states", True))      items.Add( _         New DesignerActionMethodItem( _            Me, "DoDeselectAll", "Deselect All", _            "Data", "Deselect all states", True))      items.Add( _         New DesignerActionMethodItem( _            Me, "DoToggleAll", "Toggle All", _            "Data", "Toggle all states", True))
Figure 3. Free Property Window Command Verbs: Command verbs you define for smart tags (Reset Map, Select All, etc.) appear automatically at the bottom of the Properties window.
Return items End Function

The function begins by creating the DesignerActionItemCollection that it will return. It adds a DesignerActionHeaderItem to display the “Appearance” header. If the StateSelector has a map loaded, it also adds an “Information” header and a text item that displays the number of states on the map.

Next the code adds DesignerActionPropertyItems to describe the properties that the smart tag popup should display. These include the LineStyle and Map properties.

The function finishes by adding DesignerActionMethodItems to represent the command verbs at the bottom of the popup.

That’s it for the smart tag, except for one bonus feature?which you get for free. When you create the smart tag code, notice that it also adds the list of command verbs at the bottom of the Properties window (see Figure 3).

Creating Property Pages
When you first create a StateSelector control, you are most likely to want to set its Map and LineStyle properties. You are much less likely to want to use the Properties window to set its Anchor, Dock, Location, Locked, Margin, MaximumSize, MinimumSize, Modifiers, Padding, RightToLeft, Size, and other properties that come with any control. A smart tag popup lets you put all the key properties at the developer’s fingertips without all of the miscellaneous debris in the Properties window.

Smart tag real estate is limited, however; there’s only so much you can squeeze onto a simple popup. When you need more property-setting functionality than the combination of the Property window and smart tags provides, consider using Property Pages.

Property pages provide an alternative to smart tags. They can be quite large?so you can put considerably more information on them than you can cram onto a smart tag popup. They can be so large, in fact, that they can host large, complex controls. Figure 4 shows the StateSelector control’s property pages. The Map tab shown in the figure contains a StateSelector control that lets you determine the states that are selected on the map. (This feature provides another nice use of a StateSelector control to edit a StateSelector control!)

Figure 4. A Property Page in Action: Property pages work well when you need to use large, complex controls or group property items into tabbed pages to set properties.

Property pages can include any number of tabs, meaning you can have page after page of information when appropriate. Separate tab pages let you group related properties and information so it’s easier for users to find the things they want to modify.

To provide a smart tag, you add the Designer attribute to the control’s Class declaration. To provide property pages, you add the Editor attribute as shown in the following code fragment:

    _   Public Class StateSelector   ...   End Class

The editor for the StateSelector control is the StateSelectorEditor class. The StateSelectorEditor class shown in the following code is quite simple. It inherits from the WindowsFormsComponentEditor class and overrides two methods. The GetComponentEditorPages method returns an array of types describing the classes used in the property pages. The GetInitialComponentEditorPageIndex method returns the index of the first page the editor should display:

   Public Class StateSelectorEditor      Inherits WindowsFormsComponentEditor         ' Return an array of pages.      Protected Overrides Function GetComponentEditorPages() _      As System.Type()         Return New Type() { _            GetType(ppBasics), _            GetType(ppMap) _         }      End Function         ' Initially display page number 0.      Protected Overrides Function _      GetInitialComponentEditorPageIndex() As Integer         Return 0      End Function   End Class

The StateSelectorEditor class’s GetComponentEditorPages function returns information about the two property page classes ppBasics and ppMap. The key pieces of these classes perform roughly the same functions so only ppMap is shown here. Listing 2 shows the most interesting portions of the ppMap class.

The ppMap class includes a ToolboxItem attribute with its value set to False so the property page doesn’t appear in the Toolbox. The class inherits from the ComponentEditorPage class.

The next part of the class mimics the structure of a form, UserControl, or other visible container?including declarations for controls and components that appear on the page. If the code needs to handle an object’s events, its declaration includes the WithEvents keyword.

The class’s constructor creates the controls and components and initializes their properties much as a form does when it loads. In fact, you can take advantage of the similarity to make building the property page easier. First, build a temporary form that contains all the controls and components that you need on the property page. Arrange them nicely. Then open the Designer.vb file that defines that form. (In the Solution Explorer window, click the Show All Files button, expand the form, and open the Designer.vb file.) Then copy the code that declares and initializes the form’s controls from the Designer.vb file to the property page class. Delete the temporary form when you’re done.

The end of the class’s constructor sets code for the property page itself. In particular, it sets the page’s tab text and icon.

The property page class must override two methods so it can edit the control. First, it must override the LoadComponent method to copy properties from the control onto the property page. In this example, the LoadComponent method initializes the lblMapLoaded control to display either “(none)” or “(map)” depending on whether the control has a map loaded. If a map is loaded, it sets its StateSelector control’s Map property to a clone of the editing control’s Map value. This version of LoadComponent also copies the BorderColor, SelectedColor, DeselectedColor, Selected, and LineStyle properties from the control being edited to the property page’s StateSelector control.

The second key method that a property page class must override is SaveComponent. This method should copy the modified values stored in the property page back to the control being edited. The ppMap property page lets the user change only two properties: Map and Selected. Its SaveComponent method needs to save only those values.

A property page may also include other code to handle the controls it contains. In this example, if the user clicks on the lblMapLoaded control, the Click event handler displays a file selection dialog to let a user pick a new map file. If the user picks a file and clicks OK, the event handler loads the new map and changes the label’s text to (map). It also calls the property page’s inherited SetDirty method to notify the property page editor that the values on this page have been changed, which causes the editor to enable the OK and Apply buttons shown in Figure 4. If the user clicks either of those buttons, the editor calls the page’s SaveComponent method to save the changes.

Finally, if the user selects or deselects a state in the property page’s StateSelector control, the sselMap_SelectedChanged event handler executes. This event handler simply calls the page’s SetDirty method to indicate that the data has changed.

The rest of the property pages’ operation is automatic. When the user right-clicks the control and selects Properties, the IDE displays the property pages, initially selecting the page indicated by the GetInitialComponentEditorPageIndex method.

Easier, Intuitive Custom Controls
This two-part article discussed how you can create powerful tools that make your custom controls much easier and more intuitive to use at design time.While convenient for users, bear in mind that these tools are fairly complex so implementing all of them for infrequently used controls may not be worth the effort; it may be easier to set the control’s properties in code. However, if you plan to use the control repeatedly?or more importantly, give it to other developers or sell it to customers?you’ll probably find it worthwhile to implement at least some of these tools. If you’re trying to decide how long your control might be in use, remember the lesson of Y2K; code that was initially expected to last only a few years was still in use decades later. Providing functional type converters and editors may be more work now, but may save you time and effort down the road.

You can find more information about building design-time tools in my book Expert One-on-One Visual Basic 2005 Design and Development.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin


Recent Articles: