Build a Property Editor That Can Edit Any .NET Type

Build a Property Editor That Can Edit Any .NET Type

he .NET framework offers a large set of controls for people who are creating Windows Forms applications: the TextField control for editing text, the CheckBox for Boolean values, the ComboBox for choosing a value between a list, and even a DateTimePicker control for selecting dates, among many others. But even this rich control set doesn’t cover all the needs of every application. For example, the platform does not provide a simple control for letting users choose a color?and of course there’s no control for editing user-defined types. Creating such controls can be very time consuming.

This article explains how to create a Windows Forms control that can edit and represent values of all .NET types, called the GenericValueEditor. It exploits two notions present in the .NET framework: type editors (UITypeEditor) and type converters (TypeConverter). With such a control, programmers can use either the predefined UITypeEditors provided by the framework or custom design-time editors they have developed, and use them in a final application to provide rich runtime editors.

Figures 1-4 show some examples of the types of editor controls that you’ll be able to create using the techniques discussed in this article:

Figure 1. Color Editor Control: Lets users pick a color from among three different color sets: Custom, Web, or System.
Figure 2..NET DashStyle Enumeration Editor: This editor lets users pick a dash style.
Figure 3..NET Enum DayOfWeek Editor: This editor lets users pick a day of the week.
Figure 4. Font Editor: This editor lets users select a font from among the fonts installed on the system.

All the examples shown in Figures 1-4 are instances of the GenericValueEditor. This article shows and explains important code fragments of the control, but you will need to download the full source code to use the control in your projects.

Design-time Features: UITypeEditor and TypeConverter
When you select a control in the Visual Studio.NET form designer, the Property Grid displays the properties of the selected control. Users can enter some property values only by typing text; but others, such as the BackColor property, provide a UI-based interface that appears or “drops down” when users click a button in the Property Grid row as illustrated in Figure 5.

Figure 5 BackColor Property Editor: Note that when a property has a dropdown editor, users can either enter the text value in the property field or pick from the dropdown list, and that the dropdown list can be a complex dialog in its own right.

For such properties the framework must convert the value of the property to and from a string to allow in-place text editing. The .NET framework’s TypeConverter feature performs those conversions. You can use the TypeConverter class to convert from any type to any other allowable type, but in this control you’ll use a TypeConverter to convert the edited value to and from a string.

At design time, the UITypeEditor feature provides a user interface to edit some types. For example, the design-time editor shown in Figure 5 lets users select a Color object).

The sample control’s implementation is based on the UITypeEditor and TypeConverter classes described in the following sections.

A UITypeEditor can display in one of three different styles defined by the UITypeEditorEditStyle enumeration:

  • DropDown: Users edit the value via a drop-down dialog box.
  • Modal: Users edit the value through a modal dialog box.
  • None: The editor does not provide a special user interface for editing.

The GenericValueEditor control uses this information to determine whether to display a button to drop the editor. When the UITypeEditor style is DropDown, the control shows a drop-down arrow. When the style is Modal, the control shows an ellipsis (?) button.

The UITypeEditor.EditValue method handles the user interface, user input processing, and value assignment. The GenericValueEditor calls this method to launch the editor when the user clicks the button.

UITypeEditor provides additional methods that offer support for painting a value representation:

  • The GetPaintValueSupported method indicates whether the editor supports displaying the value representation.
  • The PaintValue method implements the display of the value representation.

The GenericValueEditor control uses these two methods to paint the edited value on the left edge of its text box when appropriate (such as for color values).

You use the TypeConverter class to allow in-place editing of the value in the text box. When a user selects a value from some visual representation, the TypeConverter converts the edited value to a string. Similarly, when a user changes the string value in the text box the TypeConverter converts that string back to a value of the edited type.

In addition to converting values, the TypeConverter may also provide a list of standard values appropriate to the type for which it is designed. For example, the TypeConverter for the Color type returns the list of all named and system colors defined in the .NET framework. Similarly, a type converter for an enum type provides the standard values and named constants defined by the enumeration. When the TypeConverter provides standard values for a type and no UITypeEditor is available, the GenericValueEditor lets users select one of the values from a drop-down list as illustrated in Figure 6:

Figure 6. GenericValueEditor: To edit most common enum types, you obtain the list of standard value names from the enum and place those in the list.

Implementing the GenericValueEditor
The first step in creating the GenericValueEditor control is to derive a new control, called GenericValueEditor, from the System.Windows.Forms.Control class.

The GenericValueEditor control embeds three subcontrols:

  • A TextBox for editing the value as text
  • An optional Button for dropping the UITypeEditor
  • Another optional control that paints the edited value if the UITypeEditor supports this feature.

You also need to define the type that the control will edit. The default constructor creates a GenericValueEditor for the String type.

Here is the constructor code for the class:

   public class GenericValueEditor : Control {            private TextBox textbox;       private EditorButton editorButton;       private PreviewControl previewControl;          private boolean autoSize;       private EditorService editorService;       …           public GenericValueEditor() :           this(typeof(string)) {       }          public GenericValueEditor(Type editedType) {           SetStyle(ControlStyles.Selectable, true);           SetStyle(ControlStyles.FixedHeight, true);           autoSize = true;              SuspendLayout();                        // Text box Control           textBox = new TextBox();           InitTextBox();              // Editor button           editorButton = new EditorButton();           editorButton.Click += new               EventHandler(ButtonClicked);              // Paint value box           previewControl = new PreviewControl(this);           previewControl.Click += new               EventHandler(PreviewControlClicked);              // Adds the subcontrols           Controls.AddRange(              new Control[] {previewControl, textBox,                  editorButton});              // Creates the editor service           editorService  = new EditorService(this);              // Stores the edited type            EditedType = editedType;           ResumeLayout();       }   ...   }

This constructor creates the three subcontrols and adds them to the GenericValueEditor (see Figure 7). The first control is the TextBox for editing the string representation. The second control is an instance of the class EditorButton (a subclass of Button) and the last control is a small control that displays the current value (an instance of PreviewControl).

Figure 7. GenericValueEditor Subcontrols: The GenericValueEditor coordinates three controls to provide a preview (for such properties as colors), a TextBox where users can enter the text representation of a value, and an EditorButton that drops a dialog appropriate for the type being edited.

The button is a subclass of the .NET Framework Button class called EditorButton. This button class draws itself as a drop-down arrow when the current UITypeEditor has the DropDown style, or as three dots when the UITypeEditor has the Modal style. The PreviewControl control uses the PaintValue method of the UITypeEditor class.

The GenericValueEditor’s constructor also creates an EditorService instance (described later in this article), which handles the process of dropping down the list for the UITypeEditor. Finally, the editor stores the edited type in the EditedType property described below.

The three controls are laid out and made visible or hidden depending on the UITypeEditor or the TypeConverter associated with the edited type.

Defining the Edited Type, UITypeEditor, and TypeConverter
The EditedType property defines the type of objects that the GenericValueEditor will edit. Setting this property also sets the TypeConverter and the UITypeEditor associated with the type. To get the TypeConverter and UITypeEditor associated with a type, use the .NET framework’s TypeDescriptor class, which provides information about a type.

To store the information define three new fields named editedType, converter, and editor. Here’s the EditedType property definition:

   private Type editedType;   private UITypeEditor editor;   private TypeConverter converter;      public Type EditedType {      get {         return editedType;      }               set {         if (value == null)            throw new ArgumentNullException(               "type should not be null");         if (editedType != value) {            editedType = value;            converter =                TypeDescriptor.GetConverter(editedType);            editor =                 (UITypeEditor)TypeDescriptor.GetEditor(              editedType, typeof(UITypeEditor));                OnConverterOrEditorChanged();         }      }   }

You must also define two additional properties called Converter and Editor that let you change the TypeConverter and UITypeEditor:

   public TypeConverter Converter {      get {         return converter;      }      set {         if (converter != value) {            converter = value;            OnConverterOrEditorChanged();         }      }   }      public UITypeEditor Editor {      get {         return editor;      }      set {               if (editor != value) {            editor = value;            OnConverterOrEditorChanged();         }      }   }

The OnConverterOrEditorChanged method fires whenever the editor or converter changes. It stores information related to the current converter and editor.

   private bool paintValueSupported;   private bool hasStandardValue;   private bool hasButton;      private void OnConverterOrEditorChanged() {      paintValueSupported = editor != null &&          editor.GetPaintValueSupported();         hasStandardValues = converter != null &&          converter.GetStandardValuesSupported() &&          converter.GetStandardValues().Count != 0;         hasButton = (editor != null &&          editor.GetEditStyle() !=          UITypeEditorEditStyle.None)          || hasStandardValues;         editorButton.IsDialog =          editor != null &&          editor.GetEditStyle() ==          UITypeEditorEditStyle.Modal;         LayoutSubControls();      UpdateTextBoxWithValue();   }

The preceding code fragment defines three new fields for the GenericValueEditor:

  • The paintValueSupported field indicates whether the editor can paint the edited value.
  • The hasStandardValues field indicates whether the type supports standard values.
  • The hasButton field indicates whether the GenericValueEditor should display a button.

You use this information to show or hide the subcontrols in the LayoutSubControls method. The button becomes visible when the UITypeEditor has a Modal or DropDown style (in which case the button launches the UITypeEditor) or when you can select from a set of standard values for the type (in which case the button drops a list box that lets you select one of the standard values). The PreviewControl becomes visible only when the paintValueSupported field is true.

Changing the Edited Value
You still have to write code to drop the UITypeEditor when a user clicks the button. But before that, you must first define a property to hold the edited value. The following Value property is of type object because it must be able to hold any kind of value.

   private object currentValue;      public object Value {      get {         return currentValue;      }      set {         if (value != null &&             !EditedType.IsAssignableFrom(            value.GetType()))            throw new InvalidCastException(               "Bad value type.");            currentValue = value;         UpdateTextBoxWithValue();         if (paintValueSupported)             Invalidate(true);            OnValueChanged(EventArgs.Empty);          }   }

The property first verifies that the entered or selected value can be assigned to the type stored in the EditedType property?if not, the control throws an exception. Whenever the value changes, the property code updates the text in the text box using the UpdateTextBoxWithValue method. Then, if the current value supports being painted, it repaints the control by calling the Invalidate method. Finally, it raises a ValueChanged event to notify control users that the value has changed.

To set the correct text in the text box, the UpdateTextBoxWithValue method uses the TypeConverter (stored in the converter field) to convert the value to a string:

   private void UpdateTextBoxWithValue() {      textBox.Text = GetValueAsText(currentValue);     }      string GetValueAsText(object value) {      if (value == null) return string.Empty;      if (value is string) return (string)value;      try {         if (converter != null &&                converter.CanConvertTo(typeof(string)))            return converter.ConvertToString(value);      }       catch (Exception) {}      return value.ToString();   }

The type converter’s ConvertToString method returns a text value for that type. If the TypeConverter cannot convert the value to a string, the code uses the ToString method that is defined for every .NET object as a fallback solution.

The TypeConverter converts the stored current value to a string for display purposes, but when the user enters or changes the text in the property text box you also have to convert that text back into a valid object. You can do that with the TypeConverter as well.

Validate the text entered by the user in the text box by overriding the OnValidating method of the control. Validating there ensures that the Value property will be up-to-date when the control loses the focus, and lets you cancel the validation when the text is not valid. Here’s the OnValidating method code:

   protected override void OnValidating(      CancelEventArgs e) {      ...      base.OnValidating(e);      if (!ValidateText())         e.Cancel = true;   }

The ValidateText method used above is defined as:

   bool ValidateText() {      if (!ValidateText(textBox.Text)) {         UpdateTextBoxWithValue();         return false;      }       else         return true;   }      private bool ValidateText(string text) {      object value = null;      try  {         if (converter != null &&             converter.CanConvertFrom(typeof(string)))            value = converter.ConvertFromString(null,               CultureInfo.CurrentCulture, text);      }      catch (Exception) {}      return value != null ? ValidateValue(value) :           false;   }      private bool ValidateValue(object value) {      try {          ...          Value = value;      }       catch (Exception) {          return false;      }      return true;   }

The call to ValidateText() with no parameters calls the second version of the ValidateText method?the one that takes a string parameter?passing the text from the TextBox as the parameter value. This method again uses the type converter to try to convert the text into an object using the TypeConverter.ConvertFromString method. If the result is valid it stores the value back into the Value property.

Dropping the UITypeEditor
Now that you know how to edit the value in the text box using the type converter, you can implement a way to edit the value using a UITypeEditor. The UITypeEditor class defines the following EditValue method that you should use to launch an editor for editing the value.

   public object EditValue(IServiceProvider provider,       object value);

The value parameter is the value to edit, and the result returned by this method is the new edited value, but what’s that first IServiceProvider parameter? The .NET framework documentation says it’s “An IServiceProvider that this editor can use to obtain services.” How helpful. This does not explain what you need to do. In fact, if you explore the UITypeEditor documentation, you’ll find that it requires a service interface called IWindowsFormsEditorService to show the drop-down control. However, the .NET framework contains no public implementation of such a service that you can use. You have to create a class to provide such a service. You’ll see how to do that later. For the moment, suppose that you have such a service stored in a class field called editorService. Dropping the editor is then very easy. The DropEditor method does the work:

   void DropEditor() {      UITypeEditor editor = Editor;          if (editor != null) {         try {            Value = editor.EditValue(               editorService, Value);            } catch (Exception) {}        }   }

The code simply calls the EditValue method of the UITypeEditor and sets the Value property of the control to the result.

Unfortunately, many .NET types do not define a UITypeEditor. For example, none of the Enumeration types (subclasses of the .NET Enum class) define a UITypeEditor?but they do have a TypeConverter that can convert the named constants defined by the enum to and from a string representation. The TypeConverter provided for enum types can also return the list of available constants through the GetStandardValues method. For example the little program below uses the GetStandardValues method to display the list of available constants in the Console window.

   TypeConverter converter =       TypeDescriptor.GetConverter(typeof(DayOfWeek));   foreach (object obj in converter.GetStandardValues()) {       Console.WriteLine(obj);   }

When you run that code, you’ll see all the named constants defined by the DayOfWeek enum: Sunday, Monday, etc. You can use this TypeConverter feature to provide a UI for editing enum types; you just have to create an internal UITypeEditor yourself for those types. The created UITypeEditor can drop down a ListBox containing all the standard values defined for the type. This new UITypeEditor is called a StandardValuesUIEditor. For more details on the UITypeEditor, refer to the source. The DropEditor method code is too complex to show for enum types; see the sample code for more information:

   void DropEditor() {      UITypeEditor editor = Editor;                  if ((editor == null ||          editor.GetEditStyle() ==          UITypeEditorEditStyle.None) &&          hasStandardValues) {         if (standardValuesUIEditor == null)            standardValuesUIEditor = new                StandardValuesUIEditor(this);           editor = standardValuesUIEditor;      }                    if (editor != null) {         try {            Value = editor.EditValue(editorService,                currentValue);            } catch (Exception) {}                       }   }

You call the DropEditor method when the user clicks the arrow button on the GenericValueEditor. An event handler on the button that calls DropEditor makes the call, but you’re still missing the mysterious IWindowsFormsEditorService.

Implementing the IWindowsFormsEditorService Interface
A class that implements the IWindowsFormsEditorService interface launches the UITypeEditor instances. Although the interface contains only three methods, the service is quite complex to implement.

   interface IWindowsFormsEditorService {      void DropDownControl(Control control);      void CloseDropDown();      DialogResult ShowDialog(Form dialog);   }

The DropDownControl method must drop the control passed as a parameter; UITypeEditors with the DropDown style use this method. The CloseDropDown method must hide the control dropped by DropDownControl, and the ShowDialog method must display the dialog box passed as a parameter and return the result. UITypeEditors with the Modal style use this method.

To understand how to implement this service you must first understand how the UITypeEditor uses it

Opening a Modal UITypeEditor produces the following sequence of method calls:

  • The application calls the UITypeEditor’s EditValue method, passing a service provider and the value to edit. For example, editor.EditValue(serviceProvider provider, object value)
  • The EditValue method creates a dialog box for editing the value.
  • The EditValue method asks the service provider for a service of type IWindowsFormsEditorService and asks this service to show the dialog box it has just created by calling ShowDialog.
  • The IWindowsFormsEditorService.ShowDialog method opens the dialog box and waits until the user chooses a new value from the dialog box or cancels the dialog.
  • The EditValue method returns the new value from the dialog box.

Opening a DropDown UITypeEditor produces a slightly different series of method calls:

  • The application calls the UITypeEditor’s EditValue method, passing a service provider and the value to edit. For example, editor.EditValue(serviceProvider provider, object value)
  • The EditValue method creates a UI control for editing the value.
  • The EditValue method asks the service provider for a service of type IWindowsFormsEditorService and asks this service to show the UI control it has just created by calling DropDownControl.
  • The DropDownControl method displays the control and then waits until the user has chosen a new value.
  • After the user chooses the new value, the UI control calls CloseDropDown to close itself.
  • The EditValue method returns the new value from the UI control.

So, you need a service provider that can return an object that implements the IWindowsFormsEditorService interface. The following EditorService class implements both the IServiceProvider and the IWindowsFormsEditorService interfaces:

   class EditorService : IServiceProvider,       IWindowsFormsEditorService {         private GenericValueEditor editor;         public EditorService(GenericValueEditor editor) {         this.editor = editor;      }         public object GetService(Type serviceType) {         if (serviceType ==             typeof(IWindowsFormsEditorService))             return this;         return null;      }      ...   }

Implementing the ShowDialog method of the interface is easy; just call the ShowDialog method of the dialog itself:

   public DialogResult ShowDialog(Form dialog) {       dialog.ShowDialog(editor);       return dialog.DialogResult;     }

Implementing the two remaining methods is more difficult. To display the editor control below the GenericValueEditor, you have to embed it inside a form. Therefore you must define a new class?a subclass of the Form class with no caption and a single-line border. In the DropDownControl method you add the control to this form, place it below the UITypeEditor, and then show the form. The DropDownControl method must block until the user finishes editing; therefore, you need the method to wait until the form is made invisible by a call to CloseDropDown. Here’s the code for the DropDownControl method.

   public void DropDownControl(Control ctl) {         if (dropDownForm == null)         dropDownForm = new DropDownForm(this);         dropDownForm.Visible = false;      dropDownForm.Component = ctl;         Rectangle editorBounds = editor.Bounds;                     Size size = dropDownForm.Size;         // Location of the form      Point location           = new Point(editorBounds.Right - size.Width,           editorBounds.Bottom+1);         // Location in screen coordinate      location = editor.Parent.PointToScreen(location);                             // Check that the form is in the screen       // working area      Rectangle screenWorkingArea =          Screen.FromControl(editor).WorkingArea;                  location.X = Math.Min(         screenWorkingArea.Right - size.Width,          Math.Max(screenWorkingArea.X, location.X));                        if (size.Height + location.Y +          editor.textBox.Height >          screenWorkingArea.Bottom)         location.Y = location.Y - size.Height --             editorBounds.Height -1;                  dropDownForm.SetBounds(location.X, location.Y,          size.Width, size.Height);      dropDownForm.Visible = true;         ctl.Focus();         editor.SelectEdit();      // Wait for the end of the editing      while (dropDownForm.Visible) {         Application.DoEvents();         MsgWaitForMultipleObjects(0, 0, true, 250, 255);      }                        // Editing is done or aborted            }

The tricky part here is the message loop that does a call to MsgWaitForMultipleObjects. This Win32 API method waits?without using 100% of the CPU cycles?until an event appears in the message queue.

Using the techniques shown in this article, you can take advantage of design time classes in the .NET framework (such as TypeConverter and UITypeEditor) to create rich runtime editors such as those used in the .NET property grid. Moreover, the techniques work for building editors that can edit any .NET type.

Author’s Note: This article grew out of work done during the creation of a new ILOG product called “ILOG Gantt for .NET.” Overall, the effort has been very useful, and has allowed us to provide rich editor support inside our product. We hope you will find this control very useful too.


Share the Post: