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

ee if you can tell what’s wrong with the following sequence: ND, MN, WI, SD, NE, KS, MO, IL, IN. Unless you have a PhD in North American geography, you may have trouble even realizing that there is a potential problem much less finding it. If you take a look at Figure 1, however, the problem is obvious. There’s a gaping hole in this group of Midwestern states.

?
Figure 1. Selecting States: The StateSelector control makes problems in state selections obvious.

A few years ago, I worked on a fuel tax application for the state of Minnesota. Each year, a trucking company reports the states where it purchased fuel and the states where it actually drove. Then after a long series of Byzantine calculations, the company sends Minnesota a check for $4.17.

In this application, I built a dialog similar to the one shown in Figure 1 to let the users select and view states graphically. While the selection shown in Figure 1 is conceivable, it’s pretty unlikely. Unless the company’s owner once got a bad haircut or had some other traumatic experience in Iowa, it’s pretty unlikely that the company would operate in all the surrounding states, but skip Iowa. Visuals can help immensely in some circumstances. For example, while it’s difficult to determine a missing state from a string of state abbreviations such as “ND, MN, WI, SD, NE, KS, MO, IL, IN,” the omission becomes obvious in the graphical representation.

This article isn’t really about the StateSelector control shown in Figure 1 (although the control is contained in the downloadable sample project that accompanies this article). Instead, this article is about making controls easier to use. It explores some tools that you can provide to make life easier for developers who use your controls. These tools let developers view and modify control properties at design-time by using smart tags, command verbs, property pages, custom editors, and special property representations in the Properties window.

A Few Warnings
Before you hear about this control’s special design-time features, you should be warned: building these features into your controls is rather tricky. Not because the code is particularly complicated?it isn’t?it’s somewhat involved but surprisingly short. These techniques are tricky for three main reasons.

First, these techniques are not very well documented. The online help describes each of the tools you need separately, but you need to find an article such as this one to put all the pieces together.

Second, tools that help developers at design time run in a strange in-between mode, neither in the control’s run time nor in the control’s design time. While you are manipulating the control in the IDE at design time, UI editors and classes that display the control’s properties in the Properties window execute in their own special runtime. Because they don’t run in the control’s runtime, you don’t have complete control over their code. For example, you cannot set breakpoints in the code, and it’s sometimes hard to get the code to even respond directly to the user with message boxes, so debugging can be quite difficult. When something goes wrong, the IDE may display no message and no editor at all, giving only a relatively unhelpful error message such as “Object reference not set to an instance of an object,” or even a downright confusing message similar to my personal favorite “Could not convert value StatesMapControls.StatesMap to the type StatesMapControls.StatesMap.”

Finally, sometimes the editors seem to get “stuck” or somehow cached in the IDE. Even after you make a change to one of the control’s editors, the IDE still tries to use an older version and gets thoroughly confused. (This causes the “Could not convert” message in the previous paragraph.) The easiest way to fix this is to remove all instances of the control from the forms, close the IDE, reopen the IDE, rebuild the project, and then add a new instance of the control to the form. After a while, you get pretty good at unclogging the IDE in this way?I can do it in about 10 seconds. If you make a change to the code but don’t see any difference in the result, give it a try before you start throwing things.

Because of all these difficulties, you may want to skip the whole issue. If you plan to use the control yourself only once or twice, or if you only plan for one or two other developers to use the control, then you might want to ignore these design-time tools and do something more productive like watching Futurama reruns.

However, if you plan to use the control a lot, give the control to a lot of other developers, or sell the control to other programmers, then it may be worth your while to provide some or all of these special design time tools. In that case, grab your favorite caffeinated beverage and read on.

Understanding Control Concepts
Before discussing design time tools (we’ll get there eventually), you should know what the StateSelector control does. This control displays a StatesMap object. That object contains an array of MapState objects that represent regions on the map. Each MapState has a Name property and contains an array of Point objects that define its border.

The control loads a file containing an XML serialization of a StatesMap object and draws the MapStates that it contains. At run time, users can click on the states to select and deselect them as shown in Figure 1.

Author’s Note: You can use the MapMaker program, included in this article’s download, to build map serializations. It lets you load an image file containing a map and then trace over the image to define the states. This program is useful for building map files but doesn’t demonstrate any design time features so it’s not described here.)

Here’s a brief overview of the StateSelector control’s properties and methods. The BorderColor property determines the color used to draw the states’ borders. The SelectedColor and DeselectedColor properties determine the color used to fill selected and non-selected states.

The LineStyle property holds one of the LineStyles enumeration values: Solid, Dashed, or Dotted. It determines the line styles used to draw the states’ borders.

The control’s Selected property gets or sets a comma-separated list of the selected states’ names. For example, for the states selected in Figure 1, the property returns the string “ND, MN, WI, SD, NE, KS, MO, IL, IN.” If you set this property, the control automatically selects the appropriate states.

The control’s LoadMap method loads a map’s XML serialization. The SelectAll, DeselectAll, and ToggleAll methods give you a programmatic way to select, deselect, or toggle the selection status of all the map’s states.

Exceptional Design-Time Support
The Properties window does a great job of allowing developers to view and modify common control properties. It displays numeric and text values and lets the user enter new ones, validating numeric values. It provides dropdown lists for Boolean values and enumerated types. It automatically displays selection dialogs for colors, fonts, and images.

However, there are times when the Properties window’s support doesn’t quite do the trick. For example, the Selected property lets you get and set the selected states textually, but it would be really handy to view and set these values graphically as shown in Figure 1?and that’s possible. The Properties window can display custom editors such as this one?but not without some additional help. Other advanced features such as smart tags, command verbs, and property pages also require extra work.

The following list summarizes the design time features that make using the StateSelector control easier:

  • In the Properties window shown in Figure 2, the LineStyle property displays a sample (a dashed line in this example). The Map property displays (map) if a map is loaded (as is the case here) or (none) if no map is loaded.
  • ?
    Figure 2. StateSelector Properties: The StateSelector control provides custom displays for the LineStyle and Map properties in the Properties window.
    ?
    Figure 3. Selecting a LineStyle: The LineStyle property lets you select a value from a graphical dropdown list.
  • If you click the dropdown arrow to the right of the LineStyle property, the dropdown list shown in Figure 3 appears, letting you select a new style.
  • If you select the Map property, an ellipsis appears to the right. If you click the ellipsis, a file selection dialog appears to let you select a map file. You can remove the map by right-clicking the Map property and selecting Reset, or by selecting the text in the Map property and pressing the Delete key.
  • If you click the Selected property, an ellipsis appears to the right. If you click the ellipsis, the dialog shown in Figure 1 appears so you can select and deselect states. When you’re done, click OK or Cancel to accept or cancel the changes to the Selected property.
  • The smart tag popup shown in Figure 4 lets you set the control’s key properties in a popup.
  • Command verbs appear both at the bottom of the Properties window shown in Figure 2 and Figure 3, and at the bottom of the smart tag popup shown in Figure 4. You can click these verbs to perform more complex actions on the control.
  • ?
    Figure 4. Smart Tag: A smart tag lets you set key properties and invoke verbs in a popup.
    ?
    Figure 5. Dialog-Based Property Settings: Property pages let you set properties in a custom dialog.
  • The property pages shown in Figure 5 let you set the control’s key properties in a dialog. This figure shows two tabs (on the left) for setting the Basic and Map properties. Because the dialog can be quite large and can display many pages, it can show more properties than a smart tag popup can.

Part one of this two-part article explains the first few of these tools. It uses the StateSelector example (you can download the code in both VB.NET and C#) to explain how to display LineStyle samples, letting users pick LineStyles from a dropdown; how to display custom values for properties, such as (none) and (map) values displayed for the Map property; and how to display a file selection dialog when a user clicks in the Properties dialog, such as the one the StateSelector displays when a user clicks the ellipsis beside the Map property. Part two explains how to provide a dialog editor for the Selected property, build smart tags and command verbs, and create property pages.

Performing Map Visualization
Normally the Properties window examines a property and displays an appropriate representation of the property’s value based on the property’s type. For text and numeric properties it displays a textual value, for colors it displays a color sample, and for enumerated types it displays the name of the selected value.

For properties of type image, such as a form’s BackgroundImage property, the Properties window displays a tiny thumbnail of the image and the name of the image resource stored in the property, for example, WindowsApplication1.My.Resources.Resources.us_map.

The Properties window can also display a representation of object types by showing the object’s property values separated by commas. For example, it might display a Font property as Microsoft Sans Serif, 8.25pt and a Size property as 565, 430.

Finally, the Properties window can use custom type converters and editors to display values and allow you to edit them in the Properties window. For example, the Anchor and Dock properties for Windows Forms display special popup editors that let you set their values graphically.

The StateSelector control displays a special value for its Map property. The Properties window displays “(map)” if a map is loaded (See Figure 2) or “(none)” if no map is loaded.

The StateSelector control’s Map property has the datatype StatesMap. The Properties window doesn’t really understand the StatesMap class, so it has no clue about how to display the Map property’s value.

The key to providing a custom display for a value in the Properties window is to make a type converter to translate a StatesMap value into a string. The first step is to add a TypeConverter attribute to the property’s declaration. The following code shows the Map property’s definition:

   Private m_Map As StatesMap = Nothing    _    _    _    _    _    _   Public Property Map() As StatesMap      Get         Return m_Map      End Get      Set(ByVal value As StatesMap)         m_Map = value         DoAutoSize()         Me.Invalidate()      End Set   End Property

The control stores the property’s value in the private variable m_Map. The Category and Description attributes tell the Properties window which category should hold the property and what description to display. The Browsable attribute indicates that developers should see the property in the Properties window.

The TypeConverter attribute tells the Properties window what class it can use to convert the property to other datatypes such as strings. The Editor and DefaultValue attributes are useful for editing the property’s value and are covered in the next section of this article.

The Map property’s code is straightforward. The Get procedure simply returns the value in m_Map. The Set procedure saves a new value in m_Map, and also calls the DoAutoSize method (which sizes the control to fit its map if the AutoSize property is True). Finally, it invalidates the control to force it to redraw.

With the property definition in place, you need to provide the type converter class, in this case StatesMapConverter shown in the following code. The code is actually fairly simple, but it looks imposing, largely because the two overridden methods have huge parameter lists:

   ' Converts RegionMap objects for Properties window display.   Public Class StatesMapConverter      Inherits TypeConverter         ' We can convert StatesMap objects into a string.      Public Overrides Function CanConvertTo( _         ByVal context As _         System.ComponentModel.ITypeDescriptorContext, _         ByVal destinationType As System.Type) As Boolean            If destinationType Is GetType(String) Then Return True         Return MyBase.CanConvertTo(context, destinationType)      End Function         ' Convert a StatesMap object into a string.      Public 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(String) Then            If (value Is Nothing) Then               Return "(none)"            Else               Return "(map)"            End If         End If         Return MyBase.ConvertTo(context, culture, _            value, destinationType)      End Function   End Class

The StatesMapConverter class inherits from the TypeConverter base class and overrides two methods: CanConvertTo and ConvertTo. The CanConvertTo function returns a Boolean to indicate whether the converter can convert a value from the property’s type to another type. The overridden version returns True only if the destination type is String, letting the Properties window know that the converter can convert a StatesMap into a String.

Function ConvertTo does the actual conversion, converting a StatesMap object into a String. It first checks the destination data type, taking action only if it’s converting the object into a String. The function returns either (none) or (map) depending on whether the StatesMap value is Nothing or has a value.

That’s all there is to it. If the Map property’s value is Nothing, the type converter tells the Properties window to display (none); otherwise the type converter tells the Properties window to display the value (map).

Author’s Note: Using a similar technique, you can use a type converter to display objects as compound values. For example, suppose the Contact class includes FirstName, LastName, and Phone properties. You could build a type converter to convert the set of individual property values into a string containing the properties separated by commas. By making the type converter inherit from the ExpandableObjectConverter class, you can let the user expand the property into sub-properties, much as you can expand a Font or Size property. While this article doesn’t explain how to display object values in this way, my book Expert One-on-One Visual Basic 2005 Design and Development does.

Building the Map Editor
If you select the Map property in the Properties window, an ellipsis appears on the right. When you click the ellipsis you’ll see a file selection dialog that lets you pick a map file.

The first step in providing this feature is adding the Editor attribute to the Map property definition, as shown in the previous section. The following code fragment shows just the Editor attribute:

    _

The Editor attribute takes as parameters the type of the class that the Properties window should use to edit the property and the base class from which that class inherits. In this example, the StatesMapEditor class edits the Map property, and StatesMapEditor inherits from the UITypeEditor base class.

Listing 1 shows the code for the StatesMapEditor class. The StatesMapEditor class actually inherits from the FileNameEditor class, which inherits from UITypeEditor. FileNameEditor is a pre-built class that can display a file selection dialog.

The StatesMapEditor class does all its interesting work by overriding two methods: InitializeDialog and EditValue. Again the code looks worse than it actually is because of the long parameter lists.

The Properties window calls the InitializeDialog method to prepare the file selection dialog before displaying the dialog. The code here calls the base class’s InitializeDialog method and then sets the dialog’s Title and Filter properties to values that are appropriate for the Map property.

The EditValue method actually edits the property value. It calls the base class’s EditValue method to display the file selection dialog. That call should return the property’s new value as determined by the base class’s method. If the user selected a file and clicked OK, the result is a string giving the file’s name. If the string makes sense, the editor uses the StatesMap class’s FromFile method to load the map file and build a StatesMap object. (Download the code to see how the FromFile method works.)

If the user clicked the dialog’s Cancel button, then the value returned by the base class’s EditValue function is the original value. If the value returned by the call to EditValue is not a string, then it is the original StatesMap value assigned to the Map property. In that case, the code simply returns the original value, so the property in the Properties window is unchanged.

The FileNameEditor base class handles the rest of the details. It tells the Properties window that it can display a file selection dialog to edit its value.

There is a problem though, when the user wants to clear the current file. The file selection dialog does not give users an opportunity to click OK without selecting any file (clearing the file); it lets users pick a file or cancel the operation, but doesn’t let them indicate that the Map property should be set to Nothing. To solve the problem, the Map property definition shown in the previous section includes a DefaultValue attribute:

    _

This attribute tells the Properties window to set the Map property to Nothing if the user right-clicks on it and selects Reset. The Properties window also resets the Map property if the user selects the textual representation (map) and presses the Delete key.

Building the LineStyle Editor
The LineStyle property’s editor is less standard than the Map property’s editor. Lots of properties might need to display a file selection dialog for an editor so the .NET Framework provides a FileNameEditor class to use as a starting point. But fewer properties require displaying a graphical dropdown list (giving users a choice of line style samples in this case), so you need to build the LineStyle property’s editor from scratch.

Again, the first step is to add the appropriate attributes to the StateSelector control’s LineStyle property. The following code shows how the control defines this property. The code is fairly straightforward. The only interesting point for right now is that the Editor attribute sets the property’s editor to the LineStyleEditor class:

   ' Custom line styles for state borders.   Public Enum LineStyles      Solid = 0      Dashed = 1      Dotted = 2   End Enum      Private m_LineStyle As LineStyles = LineStyles.Solid    _    _    _    _   Public Property LineStyle() As LineStyles      Get         Return m_LineStyle      End Get      Set(ByVal value As LineStyles)         m_LineStyle = value         Me.Invalidate()      End Set   End Property

The LineStyleEditor class inherits from UITypeEditor, and does its work by overriding that class’s methods. Listing 2 shows the code.

In Listing 2, the GetEditStyle method simply returns the value DropDown to indicate that this editor displays a dropdown.

The EditValue function does the most interesting work. It first gets an IWindowsFormsEditorService object to help control the dropdown. It converts the function’s value parameter into one of the LineStyles enumeration values. It then makes a LineStyleListBox control (described shortly) to use as the dropdown. The code calls the editor service’s DropDownControl method to display the LineStyleListBox as a dropdown. Finally, the function returns the value selected in the LineStyleListBox suitably converted into a LineStyles value.

The GetEditStyle and EditValue functions let the Properties window edit the LineStyle property. The next two methods let it display a graphical representation of the LineStyle property’s value:

  • Function GetPaintValueSupported simply returns True to indicate that the editor can provide a graphical representation.
  • Subroutine PaintValue is responsible for providing the representation. This code clears the sample rectangle and then calls subroutine DrawSamplePen to draw the sample.

The LineStyleListBox control also needs to draw line style samples, so to save code I put the DrawSamplePen subroutine in a separate module. The following code shows how the sample drawing code works:

   ' Return a properly styled pen.   Public Function StyledPen(ByVal pen_color As Color, _      ByVal line_style As LineStyles) As Pen      Dim line_pen As New Pen(pen_color)      Select Case line_style         Case LineStyles.Solid            line_pen.DashStyle = Drawing2D.DashStyle.Solid         Case LineStyles.Dashed            line_pen.DashStyle = Drawing2D.DashStyle.Custom            line_pen.DashPattern = New Single() {5, 5}         Case LineStyles.Dotted            line_pen.DashStyle = Drawing2D.DashStyle.Custom            line_pen.DashPattern = New Single() {1, 3}      End Select         Return line_pen   End Function      ' Draw a sample line.   Public Sub DrawSamplePen(ByVal gr As Graphics, _      ByVal sample_bounds As Rectangle, _      ByVal line_color As Color, _      ByVal line_style As LineStyles)         Dim y As Integer = sample_bounds.Y + _         sample_bounds.Height  2      Using line_pen As Pen = StyledPen( _         Color.Black, line_style)         gr.DrawLine(line_pen, _            sample_bounds.Left + 1, y, _            sample_bounds.Right - 1, y)      End Using   End Sub

The StyledPen method returns a Pen object with a DashStyle appropriate for a LineStyles value. The DrawSamplePen method calls StyledPen to create an appropriate Pen and then draws a line with it inside the sample bounds.

The final piece to the LineStyle puzzle is the LineStyleListBox control. This control inherits from ListBox and displays LineStyles samples as shown in Figure 3. The code in Listing 3 shows how the LineStyleListBox control works. Note that the control includes the ToolboxItem attribute with the value False, which prevents the control from appearing in the Toolbox window.

The control’s constructor takes two parameters: a LineStyles value and an editor service. The code saves a reference to the editor service for later use. It then adds three items to the control’s inherited Items collection. It converts the ListStyles value into an integer and sets the SelectedIndex property to select the corresponding ListStyles value. The constructor sets its DrawMode property to OwnerDrawFixed to indicate that the control’s code will draw its own fixed-height list items. Finally the code indicates that each item’s height will be 16 pixels.

When the user clicks an item in the list box, the LineStyleListBox_Click event handler executes. If the editor service object is not Nothing, the control calls its CloseDropDown method to close the editing dropdown. At that point, the LineStyleEditor’s EditValue method’s call to the editor service’s DropDownControl method returns, and the EditValue method returns the value selected in the control.

The last interesting bit in the LineStyleListBox control is the DrawItem event handler. When the control needs to draw an item, it raises the DrawItem event. The event handler erases the item’s background and calls the DrawSamplePen method to draw a sample of the item’s LineStyles value.

Points to Remember
As you can see, you can add powerful visualization and editing features to the Properties window but it takes a bit of work. The basic points to take away are these:

  • A type converter class can allow the Properties window to display a customized representation of a property. In this example, the StatesMapConverter class helps the window display (none) if the Map property is Nothing or (map) if it is not. More advanced type converters that inherit from ExpandableObjectConverter let you display an object as a string of comma-separated values and even expand the object into sub-properties.
  • A custom editor class can let the Properties window display a dropdown editor. The LineStyleEditor class described in this article displays a LineStyleListBox control in a dropdown so the user can select a line style graphically. It also draws a sample of the current LineStyle in the Properties window.

The second part of this article explains how to make an editor that displays a custom dialog where users can graphically select states to set the control’s Selected property. It also explains how to build smart tags, command verbs, and property pages. Together with the techniques described in this article, these design-time additions provide a powerful toolkit for supporting properties in the controls you build.

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

Related Posts