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 designer2. A class to instruct the designer as to the expected design-time experience3. 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; } } } }
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(); } } } }
// 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); } } }
using System.ComponentModel;using System.Drawing.Design;using System.Windows.Forms;using System.Windows.Forms.Design;
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); } } }
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!
Powered by: newtelligence dasBlog 2.0.7226.0
Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.
© Copyright 2010R. Aaron Zupancic
E-mail