Sunday, May 15, 2005
« Release Valve | Main | Anakin and Vader »

Following our last Utah .NET User Group meeting I was asked a question about component / control development, specifically geared towards design-time enhancements.  That is, the individual wanted to know how to enhance the design-time experience for users of the control.

Visual Studio .NET provides some pretty cool design-time features.  When an object (e.g. a control, component, etc) is selected in the designer, the properties pane displays its set of browsable properties.  Some properties simply allow string or numeric inputs whereas others (e.g. enumerations, booleans, et al) display a dropdown list of acceptable values.  Others take this concept a bit further and provide a drop-down GUI, such as Anchor, the various Color-related properties, TextAlign, and many more properties.  Some properties that are more geared towards collections or that need a more sophisticated UI can even present a dialog box with a slew of options.

If you're a component developer (as I have been for years), you'd probably like to be able to enhance the design-time usage of your components in a like manner.  Well, in fact, you can - and it's not difficult at all.  As with most other things, it's just a matter of knowing what's available and how to use it.

In order to accomplish this, you need three things:

1.  A control with a read/write property for which you'd like to have a designer
2.  A class to instruct the designer as to the expected design-time experience
3.  A control/form to display in order to edit the property.

Your UI (from now on referred to as the EditorUI) comes in two forms: a dropdown in the properties pane, or a dialog.  In the event you want your EditorUI to be a dropdown, you must create a UserControl-derived class.  Dialog-based UIs are Form-derived classes.

The class that you use to designate your EditorUI is a class that derives from System.Drawing.Design.UITypeEditor.  An example might best illustrate this.  Suppose you have a control called Calculator with a property called Formula thus:

using System;
using System.Windows.Forms;


namespace Devstone.Junk {

   public sealed class Calculator : UserControl {
      private string _formula = string.Empty;
    
      [Browsable(true)]
      [Description("Designates the formula to calculate.")]
      [DefaultValue("")]
      public string Formula {
         get { return _formula; }
         set { _formula = value; }
      }
   
   }
  
}

This simple control, when rendered within the Properties dialog will simply have a textbox entry for the Formula property.  However, in our case we've created a nice dialog box that will appear, giving the user the ability to edit a formula.  We must establish a property on the dialog so that we can programmatically set the formula to show when the dialog appears and get the value back when it's accepted. (Note, in the following example, I've included both a Dialog and a Control version, but only or the other is required).


namespace Devstone.Junk {

   // the FormulaEditorDialog is used in the event we want to display a modal, dialog-based UI
   public sealed class FormulaEditorDialog : Form {
      // ... all the code to make this dialog work
     
      public FormulaEditorDialog() {
         txtFormula.KeyPress += new KeyPressEventHandler(keyPress);
         btnOk.Click += new EventHandler(okClick);
      }
     
     
      public string FormulaString {
         get { return txtFormula.Text; }
         set { txtFormula.Text = value; }
      }
     
     
      private void keyPress(object sender, KeyPressEventArgs e) {
         if ( e.KeyChar == (char)Keys.Enter && null != _editorService ) {
            e.Handled = true;
            accept();
         }
      }
     
     
      private void okClick(object sender, EventArgs e) {
         accept();
      }
     
     
      private void accept() {
         DialogResult = DialogResult.Ok;
         this.Close();
      }
     
   }
  
  
  
   // the FormulaEditorControl is used in the event we want to display our editor as a dropdown
   // note that it's constructor takes an IWindowsFormEditorService instance.
   // this allows the control to programmatically close itself later

   public sealed class FormulaEditorControl : UserControl {
      // ... all the code to make this control work
     
      private IWindowsFormEditorService   _editorService;
     
      public FormulaEditorControl(IWindowsFormEditorService editorService) {
         _editorService = editorService;
        
         txtFormula.KeyPress += new KeyPressEventHandler(keyPress);
      }
     
     
      public string FormulaString {
         get { return txtFormula.Text; }
         set { txtFormula.Text = value; }
      }
     
     
      private void keyPress(object sender, KeyPressEventArgs e) {
         if ( e.KeyChar == (char)Keys.Enter && null != _editorService ) {
            e.Handled = true;
            _editorService.CloseDropDown();
         }
      }
     
   }
  
}

At this point, it is incumbent on us to tie the two together so that the designer knows to display our dialog in order to edit the property.  This is done with our UITypeEditor-derived class.  Within this class, we should override at least the EditValue() and GetEditStyle() methods.

The GetEditStyle() method will allow the designer to know what type of UI we're going to display: a modal dialog, a dropdown, or None.  If we don't override the method, it defaults to None, so our UI would never display.

The EditValue() method is a bit more complicated.  When this method is overridden we are responsible for creating the control/form instance to display.

using System.ComponentModel;
using System.Drawing.Design;
using System.Windows.Forms;
using System.Windows.Forms.Design;

namespace Devstone.Junk {

   public sealed class FormulaEditor : UITypeEditor {
  
      public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) {
         if ( null != context && null != provider ) {
            IWindowsFormsEditorService svc = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
            if ( null != svc ) {
               // create the editor instance
               using ( FormulaEditorDialog dlg = new FormulaEditorDialog() ) {
                  // initialize it to the proper state
                  dlg.FormulaString = (string)value;
                  // display the dialog, returning either the updated or the original value
                  return ( DialogResult.OK == svc.ShowDialog(dlg) ) ? dlg.FormulaString : value;
               }
            }
         }
        
         return base.EditValue(context, provider, value);
      }
     
     
      public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) {
         return ( null != context ) ? UITypeEditorEditStyle.Modal : base.GetEditStyle(context);
      }
     
   }
  
}


If you are going to display a dropdown rather than a modal dialog, your code might resemble the following:

namespace Devstone.Junk {

   public sealed class FormulaEditor : UITypeEditor {
  
      public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) {
         if ( null != context && null != provider ) {
            IWindowsFormsEditorService svc = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
            if ( null != svc ) {
               // create the editor instance
               FormulaEditorControl ctl = new FormulaEditorControl(svc);
               // initialize it to the proper state
               ctl.FormulaString = (string)value;
               // display the control
               svc.DropDownControl(ctl);
               // return the updated value
               return ctl.FormulaString;
            }
         }
        
         return base.EditValue(context, provider, value);
      }
     
     
      public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) {
         return ( null != context ) ? UITypeEditorEditStyle.DropDown : base.GetEditStyle(context);
      }
     
   }
  
}


The last step is to tie our Formula property in the Calculator control to the UI, and this is done via the custom EditorAttribute:

   [EditorAttribute("Devstone.Junk.FormulaEditor, CalculatorControl", typeof(System.Drawing.Design.UITypeEditor)]
   public string Formula {
      get { return _formula; }
      set { _formula = value; }
   }

Have fun with this!  It can really enhance the user's design-time experience and makes your components feel MUCH more professional!

Sunday, May 15, 2005 2:16:00 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
Comments are closed.