DSL Tools #6 - Custom Property Editors
Well, just a quick return to this DSL Tools how to's to reference here a feature that is commonly required and that's not so easy to implement.
The scenario is the following. You have a given domain property - let's say "Dependency Entity" - that should have a dynamic list of possible values - this means that the values depend on the current state of the model (the other domain classes already in place).
To illustrate this concept, I've prepared the following meta-model:
This meta-model allows the user to add multiple entities to a graph. Each entity has 2 properties: Name and DependencyEntity.
For this last property I want the user to be able to select from a combo any of the other entities placed in the designer. Something like this:
This can be done by implementing what I would call a "custom property editor".
1. Create the custom UITypeEditor
Create a new class named DependencyEntityTypeEditor and have it inherit from System.Drawing.Design.UITypeEditor.
You will need to override 2 methods:
- GetEditStyle - to inform the framework that this editor will use the DropDown style.
- EditValue - to perform the actual feature, by creating and filling a list box with the possible values that you want the property to have.
This code is pretty straightforward. Notice that I iterate through all the entities present in the graph to fill the list box.
using System;
using System.ComponentModel;
using System.Drawing.Design;
using System.Windows.Forms;
using System.Globalization;
using System.Windows.Forms.Design;
namespace MyModel.EntitiesModel
{
/// <summary>
/// Implements the DependencyEntity domain attribute property
/// editor.
/// </summary>
public class DependencyEntityTypeEditor : UITypeEditor
{
#region Members
private IWindowsFormsEditorService formsEditorService;
#endregion
#region Public Methods
/// <summary>
/// Gets the editor style used by the EditValue method.
/// </summary>
/// <param name="context">Additional context information.
/// </param>
/// <returns>
/// A value that indicates the style of editor used by the
/// EditValue method.
/// If the UITypeEditor does not support this method, then
/// GetEditStyle will return None.
/// </returns>
public override UITypeEditorEditStyle GetEditStyle
(ITypeDescriptorContext context)
{
if (context != null)
{ return UITypeEditorEditStyle.DropDown;
}
return base.GetEditStyle(context);
}
/// <summary>
/// Edits the specified object's value using the editor style
/// indicated by the GetEditStyle method.
/// </summary>
/// <param name="context">Additional context information.</param>
/// <param name="provider">A provider that this editor can use to
/// obtain services.</param>
/// <param name="value">The object to edit.</param>
/// <returns>
/// The new value of the object. If the value of the object has not
/// changed, this should return the same object it was passed.
/// </returns>
public override object EditValue(ITypeDescriptorContext context,
IServiceProvider provider, object value)
{
// Default behavior
if ((context == null) ||
(provider == null) || (context.Instance == null))
{
return base.EditValue(context, provider, value);
} // Get current entity
EntityShape entityShape = (context.Instance as EntityShape);
if (entityShape != null)
{
Entity entity = (entityShape.ModelElement as Entity);
if (entity != null)
{
// Current entity name
string entityName = entity.Name;
// Current graph
EntitiesGraph graph = entity.Graph;
if (graph != null)
{
// Build list box
ListBox listBox = new ListBox();
listBox.Sorted = true;
listBox.Click += new EventHandler(listBox_Click);
listBox.BorderStyle = BorderStyle.None;
foreach (Entity childEntity in graph.Entities)
{
if (string.Compare(childEntity.Name,
entityName, true,
CultureInfo.CurrentUICulture) != 0)
{
listBox.Items.Add(childEntity.Name);
}
}
listBox.SelectedItem = value;
// Handle the service
this.formsEditorService =
(IWindowsFormsEditorService)provider.
GetService(typeof(IWindowsFormsEditorService));
this.formsEditorService.DropDownControl(listBox);
// Result
return listBox.SelectedItem;
}
}
}
// Default behavior
return base.EditValue(context, provider, value);
}
#endregion
#region Private Methods
/// <summary>
/// Handles the Click event of the listBox control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance
/// containing the event data.</param>
private void listBox_Click(object sender, EventArgs e)
{
if (this.formsEditorService != null)
{
this.formsEditorService.CloseDropDown();
}
}
#endregion
}
}
2. Create a new custom attribute for DependencyEntity domain property
Back in the DslDefinition designer, select the DependencyEntity property and, under its properties, click the ellipsis next to Custom Attributes.
You'll need to add the following values:
- Name: System.ComponentModel.Editor
- Parameter: typeof(MyModel.EntitiesModel.DependencyEntityTypeEditor), typeof(System.Drawing.Design.UITypeEditor)
Notice that MyModel.EntitiesModel.DependencyEntityTypeEditor matches the full name of the class created in the previous step.
3. Transform all templates
Before this extension starts working you will need to transform all text templates in your DSL solution. After this step the customization is setup and you'll have the behavior described.
This is simpler than you could imagine from the requirements. :)