ExpandableObjectConverters help break down a complex multivalue property into a nested list of its atomic values. Although this technique simplifies editing of a complicated property, it may not be suitable for other properties that exhibit the following behavior:
-
Hard to construct, interpret, or validate, such as a regular expression
-
One of a list of values so large it would be difficult to remember all of them
-
A visual property, such as a ForeColor, that is not easily represented as a string
Actually, the ForeColor property satisfies all three points. First, it would be hard to find the color you wanted by typing comma-separated integers like 33, 86, 24 or guessing a named color, like PapayaWhip. Second, there are a lot of colors to choose from. Finally, colors are just plain visual.
In addition to supporting in-place editing in the Property Browser, properties such as ForeColor help the developer by providing an alternative UI-based property-editing mechanism. You access this tool, shown in Figure 9.27, from a drop-down arrow in the Property Browser.
Figure 9.27. Color Property Drop-Down UI Editor
The result is a prettier, more intuitive way to select a property value. This style of visual editing is supported by the UI type editor, a design-time feature that you can leverage to similar effect. There are two types of "editor" you can choose from: modal or drop-down. Drop-down editors support single-click property selection from a drop-down UI attached to the Property Browser. This UI might be a nice way to enhance the clock control's Face property, allowing developers to visualize the clock face style as they make their selection, shown in Figure 9.28.
Figure 9.28. Custom View Drop-Down UI Editor
You begin implementing a custom UI editor by deriving from the UITypeEditor class (from the System.Drawing.Design namespace):
public class FaceEditor : UITypeEditor { ... }
The next step requires you to override the GetEditStyle and EditValue methods from the UITypeEditor base class:
public class FaceEditor : UITypeEditor { public override UITypeEditorEditStyle GetEditStyle( ITypeDescriptorContext context) {...} public override object EditValue( ITypeDescriptorContext context, IServiceProvider provider, object value) {...} }
As with type converters, the appropriate UI type editor, provided by the GetEditor method of the TypeDescription class, is stored with each property. When the Property Browser updates itself to reflect a control selection in the Designer, it queries GetEditStyle to determine whether it should show a drop-down button, an open dialog button, or nothing in the property value box when the property is selected. This behavior is determined by a value from the UITypeEditorEditStyle enumeration:
enum UITypeEditorEditStyle { DropDown, // Display drop-down UI Modal, // Display modal dialog UI None, // Don't display a UI }
Not overriding GetEditStyle is the same as returning UITypeEditorEditStyle.None, which is the default edit style. To show the drop-down UI editor, the clock control returns UITypeEditorEditStyle.DropDown:
public class FaceEditor : UITypeEditor { public override UITypeEditorEditStyle GetEditStyle( ITypeDescriptorContext context) { if( context != null ) return UITypeEditorEditStyle.DropDown; return base.GetEditStyle(context); } ... }
ITypeDescriptorContext is passed to GetEditStyle to provide contextual information regarding the execution of this method, including the following:
-
The container and, subsequently, the designer host and its components
-
The component design-time instance being shown in the Property Browser
-
A PropertyDescriptor type describing the property, including the TypeConverter and UITypeEditor assigned to the component
-
A PropertyDescriptorGridEntry type, which is a composite of the PropertyDescriptor and the property's associated grid entry in the Property Browser
Whereas GetEditStyle is used to initialize the way the property behaves, EditValue actually implements the defined behavior. Whether the UI editor is drop-down or modal, you follow the same basic steps to edit the value:
-
Access the Property Browser's UI display service, IWindowsFormsEditorService.
-
Create an instance of the editor UI implementation, which is a control that the Property Browser will display.
-
Pass the current property value to the UI editor control.
-
Ask the Property Browser to display the UI editor control.
-
Choose the value and close the UI editor control.
-
Return the new property value from the editor.
Drop-Down UI Type Editors
Here's how the clock control implements these steps to show a drop-down editor for the Face property:
public class FaceEditor : UITypeEditor { ... public override object EditValue( ITypeDescriptorContext context, IServiceProvider provider, object value) { if( (context != null) && (provider != null) ) { // Access the Property Browser's UI display service IWindowsFormsEditorService editorService = (IWindowsFormsEditorService) provider.GetService(typeof(IWindowsFormsEditorService)); if( editorService!= null ) { // Create an instance of the UI editor control FaceEditorControl dropDownEditor = new FaceEditorControl(editorService); // Pass the UI editor control the current property value dropDownEditor.Face = (ClockFace)value; // Display the UI editor control editorService.DropDownControl(dropDownEditor); // Return the new property value from the UI editor control return dropDownEditor.Face; } } return base.EditValue(context, provider, value); } }
When it comes to displaying the UI editor control, you must play nicely in the design-time environment, particularly regarding UI positioning in relation to the Property Browser. Specifically, drop-down UI editors must appear flush against the bottom of the property entry and must be sized to the width of the property entry.
To facilitate this, the Property Browser exposes a service, an implementation of the IWindowsFormsEditorService interface, to manage the loading and unloading of UI editor controls as well as their positioning inside the development environment. The FaceEditor type references this service and calls its DropDownControl method to display the FaceEditorControl, relative to Property's Browser edit box. When displayed, FaceEditorControl has the responsibility of capturing the user selection and returning control to EditValue with the new value. This requires a call to IWindowsFormsEditorService.CloseDropDown from FaceEditorControl, something you do by passing to FaceEditorControl a reference to the IWindowsFormsEditorService interface:
public class FaceEditorControl : UserControl { ClockFace face = ClockFace.Both; IWindowsFormsEditorService editorService = null; ... public FaceEditorControl(IWindowsFormsEditorService editorService) { ... this.editorService = editorService; } public ClockFace Face { get { ... } set { ... } } void picBoth_Click(object sender, System.EventArgs e) { face = ClockFace.Both; // Close the UI editor control upon value selection editorService.CloseDropDown(); } void picAnalog_Click(object sender, System.EventArgs e) { face = ClockFace.Analog; // Close the UI editor control upon value selection editorService.CloseDropDown(); } void picDigital_Click(object sender, System.EventArgs e) { face = ClockFace.Digital; // Close the UI editor control upon value selection editorService.CloseDropDown(); } ... }
The final step is to associate FaceEditor with the Face property by adorning the property with EditorAttribute:
[ CategoryAttribute("Appearance"), DescriptionAttribute("Which style of clock face to display"), DefaultValueAttribute(ClockFace.Both), EditorAttribute(typeof(FaceEditor), typeof(UITypeEditor)) ] public ClockFace Face { ... }
Now FaceEditor is in place for the Face property. When a developer edits that property in Propery Browser, it will show a drop-down arrow and the FaceEditorControl as the UI for the developer to use to choose a value of the ClockFace enumeration.
Modal UI Type Editors
Although drop-down editors are suitable for single-click selection, there are times when unrestricted editing is required. In such situations, you would use a modal UITypeEditor implemented as a modal form. For example, the clock control has a digital time format sufficiently complex to edit with a separate dialog outside the Property Browser:
public class ClockControl : Control { ... string digitalTimeFormat = "dd/MM/yyyy hh:mm:ss tt"; ... [ CategoryAttribute("Appearance"), DescriptionAttribute("The digital time format, ..."), DefaultValueAttribute("dd/MM/yyyy hh:mm:ss tt"), ] public string DigitalTimeFormat { get { return digitalTimeFormat; } set { digitalTimeFormat = value; this.Invalidate(); } } }
Date and Time format strings are composed of a complex array of format specifiers that are not easy to remember and certainly aren't intuitive in a property browser, as shown in Figure 9.29.
Figure 9.29. The DigitalTimeFormat Property
Modal UITypeEditors are an ideal way to provide a more intuitive way to construct hard-to-format property values. By providing a custom form, you give developers whatever editing experience is the most conducive for that property type. Figure 9.30 illustrates how the Digital Time Format Editor dialog makes it easier to edit the clock control's DigitTimeFormat property.
Figure 9.30. Custom DigitalTimeFormat Modal UI Editor
A modal UITypeEditor actually requires slightly different code from that of its drop-down counterpart. You follow the same logical steps as with a drop-down editor, with three minor implementation differences:
-
Returning UITypeEditorEditStyle.Modal from UITypeEditor.GetEditStyle
-
Calling IWindowsFormsEditorService.ShowDialog from EditValue to open the UI editor dialog
-
Not requiring an editor service reference to be passed to the dialog, because a Windows Form can close itself
The clock control's modal UI type editor is shown here:
public class DigitalTimeFormatEditor : UITypeEditor { public override UITypeEditorEditStyle GetEditStyle( ITypeDescriptorContext context) { if( context != null ) { return UITypeEditorEditStyle.Modal; } return base.GetEditStyle(context); } public override object EditValue( ITypeDescriptorContext context, IServiceProvider provider, object value) { if( (context != null) && (provider != null) ) { // Access the Property Browser's UI display service IWindowsFormsEditorService editorService = (IWindowsFormsEditorService) provider.GetService(typeof(IWindowsFormsEditorService)); if( editorService != null ) { // Create an instance of the UI editor form DigitalTimeFormatEditorForm modalEditor = new DigitalTimeFormatEditorForm(); // Pass the UI editor dialog the current property value modalEditor.DigitalTimeFormat = (string)value; // Display the UI editor dialog if( editorService.ShowDialog(modalEditor) == DialogResult.OK ) { // Return the new property value from the UI editor form return modalEditor.DigitalTimeFormat; } } } return base.EditValue(context, provider, value); } }
At this point, normal dialog activities (as covered in Chapter 3: Dialogs) apply for the UI editor's modal form:
public class DigitalTimeFormatEditorForm : Form { ... string digitalTimeFormat = "dd/MM/yyyy hh:mm:ss tt"; public string DigitalTimeFormat { get { return digitalTimeFormat; } set { digitalTimeFormat = value; } } ... void btnOK_Click(object sender, System.EventArgs e) { DialogResult = DialogResult.OK; digitalTimeFormat = txtFormat.Text; } ... }
Again, to associate the new UI type editor with the property requires applying the EditorAttribute:
[ CategoryAttribute("Appearance"), DescriptionAttribute("The digital time format, ..."), DefaultValueAttribute("dd/MM/yyyy hh:mm:ss tt"), EditorAttribute(typeof(DigitalTimeFormatEditor), typeof(UITypeEditor)) ] public string DigitalTimeFormat { ... }
After EditorAttribute is applied, the modal UITypeEditor is accessed via an ellipsis-style button displayed in the Property Browser, as shown in Figure 9.31.
Figure 9.31. Accessing a Modal UITypeEditor
UI type editors allow you to provide a customized editing environment for the developer on a per-property basis, whether it's a drop-down UI to select from a list of possible values or a modal dialog to provide an entire editing environment outside the Property Browser.