Custom Editor Menus...
Custom editors modify any type of property, and can be linked to any property through the Editor metadata attribute. Editors use the IWindowsFormsEditorService interface to display a Windows form as a drop-down box or a dialog box, within the Visual Studio .NET environment.
The Visual Studio .NET designer offers a lot of design-time functionality for your custom controls. You can customize the HTML rendered for a web control, as well as add design-time support, such as designer verbs, template editing, etc. for your controls. This article will focus on another aspect of design-time support: custom editors. You probably have seen an existing example of this already; the DropDownList control has an Items property that provides a collection editor for this property. An ellipsis button appears upon selecting the property in the property grid, showing a built-in dialog box.
Custom editors using the following technique are for editing properties in the property grid. Additional features can also be added (such as invoking an editor through a designer verb), but those features will not be discussed in this article. Editors are typically for complex properties, or properties with a definable set of values (such as colors) that an editor will aid in choosing the correct value. An editor class and a Windows form are required in displaying a drop-down or dialog box; the editor is responsible for displaying the Windows Form appropriately. The Windows Form, shown as a dialog box in this particular example, will contain the UI and returns an appropriate DialogResult value denoting the action taken when the user clicks one of the buttons in the dialog box (such as OK or Cancel).
The basic hierarchy for this scenario is this: a property is linked to an editor class through the Editor attribute. The parameters accepted in the Editor constructor are System.Type references (for early bounding) to the editor class, or a string value representing the class (the name of the class with the namespaces it belongs to information for the class defined in the GAC). In the Visual Studio .NET designer, when this property is selected, the GetEditStyle method fires, returning a value defined in the UITypeEditorEditStyle enumeration.
Upon clicking the button rendered in the property grid, the EditValue method is invoked. The IServiceProvider interface is responsible for getting the IWindowsFormsEditorService service, which will invoke an instance of our Windows form and display it as a dialog box. Even though Windows forms have these capabilities already, the Windows form will not be shown in the Visual Studio .NET designer if you invoke the forms ShowDialog method directly; this editing service is required.
Before we continue with the code, let's look at some of the methods define with these classes. The following is a list of the methods (only some will be utilized) that the editor class supports, derived from System.Drawing.Design.UITypeEditor.
EditValue - This method invokes the windows form through an IWindowsFormsEditorService service. This method displays the drop-down list or modal dialog box, and returns the appropriate value for the property. For the example used in this article, the Details property in the example sets and returns an object of type CarDetails. The EditValue method's responsibility is to pass in the current object reference and return an updated version to the Details property. The Windows form editor service does not return an instance of the new object; the form is responsible for everything but displaying the form. Typically, more complex editors utilize the ITypeDescriptorContext parameter; however, this example doesn’t utilize this object.
GetEditStyle - This method returns the display mode for the editor. The enumerated values allowed are DropDown, Modal, or None. DropDown acts as a dropdown form, which a down arrow button appears for the property (like the DataMember or DataSource fields specifying a typed DataSet to bind to). The Modal (or dialog box) option displays an editor on the screen, similar to the Collection dialog box described above. The difference between the two is the DropDown mode must appear above or below the property, whereas the dialog box can be dragged around the entire surface of the Visual Studio .NET designer.
GetPaintValueSupported – Returns a Boolean stating whether painting the value on the form is supported. This method will not be illustrated in this article.
PaintValue - This method uses GDI+ to render the value to the screen. This method will not be illustrated in this article.
IServiceProvider uses the GetService method to retrieve an instance of IWindowsFormsEditorService. This editor service object handles the rendering of a Windows form within the property grid. IWindowsFormsEditorService has several other methods that are important to understand before the article goes further:
CloseDropDown – When rendered as a drop-down control, this method closes the form.
DropDownControl – This method displays the Windows form as a drop-down in the property grid. Depending on size, this control will be resized to fit within the size restriction of the grid. Be aware that this resizing may expand/shrink your form, causing changes in appearance.
ShowDialog – This method shows a form as a dialog box. The form returns an appropriate DialogResult for the appropriate button clicked by the user.
For an example of this approach, the following control contains an editor for the Details property of type CarDetails. Note the Editor attribute. This overloaded constructor defines the System.Type of custom editor, as well as the base class editor. Other overloaded constructors accept a string stating the name of the editor class. I use the early binding approach for more accurate results; however, this may not always be an option.
|
public class CarView : WebControl { private CarDetails _details = new CarDetails(); [Editor(typeof(CarDetailsEditor), typeof(System.Drawing.Design.UITypeEditor))] public CarDetails Details { get { return _details; } set { _details = value; } } protected override void RenderContents(HtmlTextWriter writer) { base.RenderContents (writer); writer.Write("CarView Control - For Design-time only"); } } |
The CarDetails complex type defines two properties: EngineSpecs and Manual, which define attributes about a car. Although these properties are somewhat random, they are meant to illustrate the editing of a complex type. These properties can be set through the public properties or in the constructor.
|
public class CarDetails { public string EngineSpecs; public bool Manual; public CarDetails() { } public CarDetails(string engine, bool manual) { this.EngineSpecs = engine; this.Manual = manual; } public override string ToString() { return "(Car Details)"; } } |
The editor class (CarDetailsEditor) inherits from System.Drawing.Design.UITypeEditor and the methods mentioned previously are overridden. EditValue retrieves an instance of the IWindowsFormsEditorService provider, displays the form modally, checks the returned DialogResult, and creates a new instance of the CarDetails class (returned from the GetFinalDetails() method of the Windows form). Acceptable values from the DialogResult enumeration are Yes and OK. GetEditValue is simplistic; although most examples verify that the context value isn't null and then call the base class’s method if it is, this example just returns Modal in every case.
|
public class CarDetailsEditor : System.Drawing.Design.UITypeEditor { public CarDetailsEditor() { } public override object EditValue(System.ComponentModel.ITypeDescriptorContext context, IServiceProvider provider, object value) { IWindowsFormsEditorService ies; CarDetailsEditorForm form; try { ies = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; if (ies == null) throw new Exception("Editor Service not available"); form = new CarDetailsEditorForm(); form.SetContextDetails(context, provider); if (value != null) { form._details = (CarDetails)value; } DialogResult diag = ies.ShowDialog(form); if (diag == DialogResult.Yes || diag == DialogResult.OK) { return form.GetFinalDetails(); } } catch(System.Exception ex) { MessageBox.Show(ex.ToString(), "Error"); } finally{ form = null; } return value; } public override System.Drawing.Design.UITypeEditorEditStyle GetEditStyle(System.ComponentModel.ITypeDescriptorContext context) { return UITypeEditorEditStyle.Modal; } } |
The ServiceProvider object passed into the EditValue method calls the GetService method, returning an editing service instance. This interface renders the Windows form correctly, as the form object is created independently of the editing service. In this way, any value (or multiple values) can be passed to the windows form for whatever reason that is needed. The editing service is primarily used to show the form as a dialog or a drop-down. Otherwise the form functions on its own.
The form used to edit the property is a standard Windows form. There isn't anything special about the form; it is only setup to return DialogResult values. In Windows applications, each button supports a DialogResult value to return when clicked, when a form is rendered modally. The interface contains a TabControl that allows the user to tab through each category and select a value. The design of the editor is simplistic, but is meant to illustrate the use of an editor. I won’t get into the class definition; instead, I’ll focus on the internal pieces that matter in the areas of the editor form. First and foremost, the ways to pass the values back and forth in the form are shown below:
|
internal void SetDetails(CarDetails details) { if (details.EngineSpecs != null && details.EngineSpecs.Length > 0) { string[] engine = details.EngineSpecs.Split(",".ToCharArray()); for(int i = 0; i < lvwEngine.Items.Count; i++) { ListViewItem item = lvwEngine.Items[i]; if ((item.Text == engine[0]) && (item.SubItems[1].Text == engine[1]) && (item.SubItems[2].Text == engine[2])) { lvwEngine.Items[i].Selected = true; } } lvwEngine.Update(); } radYes.Checked = (details.Manual == true); } internal CarDetails GetFinalDetails() { ListViewItem item = lvwEngine.SelectedItems[0]; _details.EngineSpecs = item.Text + "," + item.SubItems[1].Text + "," + item.SubItems[2].Text; _details.Manual = (radYes.Checked); //TODO:Messagebox message here; remove if unwanted MessageBox.Show("Details: " + _details.EngineSpecs + _details.Manual.ToString()); return _details; } |
The SetDetails method receives the current CarDetails object. If its properties aren't null, the values get passed to the existing controls. Note that the methods used are different. The radio button acts as a group, so if the Yes radio button is clicked, the No button won’t (which no is the default in the windows form, and gets overridden here). The engine type property, however, must be checked against the ListView to ensure that the correct property is chosen. One thing to note in this example is that my Windows Forms programming is somewhat lacking and I was unsuccessful in getting the ListView to select the current item. However, testing shows that it was selecting the correct value, due to a series of MessageBox parameters.
SetDetails gets called in the Load event. The reference to the CarDetails object exists in the public _details property. This property holds the reference for the object and is set within the editor. In addition, the GetFinalDetails method returns the finalized CarDetails object back to the editor form. This reference then gets passed back to the control itself. The selected engine type gets pieced together as a comma-separated list, so the same syntax is always used.
The close button within the form is always set to return a dialog result of Cancel; however, the OK button varies, based on the results. Whenever the appropriate values are selected, the OK button returns a result of OK. Otherwise, the Abort dialog result is returned letting the user know that not all of the values have been provided.
|
private void btnOK_Click(object sender, System.EventArgs e) { if (lvwEngine.SelectedItems.Count <= 0) { MessageBox.Show("Please select an engine type.", "Select Engine"); this.DialogResult = DialogResult.Abort; } else { this.DialogResult = DialogResult.OK; } } |
The ex04.aspx page illustrates this example. The control is setup within the page. Click the ellipsis button for the Details property, and try selecting different values. Again, I’m not the strongest Windows Forms programmer yet, so I apologize for any issues with the editor. But at least this example shows the concepts. One other "tidbit" to note. The Info button presents the type names of the objects that are used within this example. It is good to know which objects you are dealing with, to understand them a little further.
I left the MessageBox statements where the assignments and returns took place, to illustrate that the example does work. However, if you would like them removed, I placed TODO: comments where they exist in the code. Using the Visual Studio .NET tasks option, make sure you right-click the Task List and select Show Tasks > All, which will show the TODO: tasks. Typically it is set to Build Errors, which only shows errors at compile time (or design time with VB.NET).
You may download the code here.