The PropertyGrid control, events and custom editors
In the previous article in this series I did show how to use the basic features of the PropertyGrid control to edit the properties of a particular object or collection of objects. In this article I will show you the most important events of this control as well as the implementation of custom editors that allow editing of properties of types for which there is no default editor, such as the classes developed by yourself in your program.
If you prefer to start the series at the beginning, this is the link to the article on the basics of the PropertyGrid control.
In this other link you can download the source code of the project with the samples of custom property editors, written in CSharp with Visual Studio 2013.
The Shape class
To begin with, I will add to the basic project a class representing different geometric shapes. There is the abstract base class Shape, which has a Size property with the width and height of the figure, another property of type Color that indicates the color with which will be drawn the figure, and the abstract method DrawShape to draw it:
public abstract class Shape
{
protected string _name;
public Shape()
{
ShapeSize = new Size(16, 16);
ShapeColor = Color.White;
}
public Shape(Size sz, Color c)
{
ShapeSize = sz;
ShapeColor = c;
}
[DisplayNameGlobal("NAME_SHAPESIZE")]
[DescriptionGlobal("DESC_SHAPESIZE")]
public Size ShapeSize { get; set; }
[DisplayNameGlobal("NAME_SHAPECOLOR")]
[DescriptionGlobal("DESC_SHAPECOLOR")]
public Color ShapeColor { get; set; }
public abstract void DrawShape(Graphics gr, int x, int y);
public override string ToString()
{
return _name;
}
}
In this class I had also decorated the properties with the globalization attributes to translate and provide help to the user, as they also can appear in the PropertyGrid to be edited directly.
I overriden the ToString method in order to provide a custom identifier for the custom property editor. If you don't do this, the type name of the class is used by default.
Now we derive three classes for different geometric shapes, a circle, a rectangle and a triangle. For example, for the circle:
public class Circle : Shape
{
public Circle()
: base()
{
_name = Resources.SHAPE_CIRCLE;
}
public Circle(Size sz, Color c)
: base(sz, c)
{
_name = Resources.SHAPE_CIRCLE;
}
public override void DrawShape(Graphics gr, int x, int y)
{
Pen p = new Pen(ShapeColor);
gr.DrawEllipse(p,
new Rectangle(x, y, ShapeSize.Width, ShapeSize.Height));
p.Dispose();
}
}
As in the basic design we used a resource file to provide various languages for the application, you also can take advantage to provide a name for the class translated into the current language.
User controls to edit the properties
Then we will write two specialized controls to editing objects of type Shape. The first will be a ListBox to show the different types of figures available. We started creating a general purpose ListBox appropriate for use with a property editor:
public class ListBoxEditor : ListBox
{
protected object m_oSelection = null;
protected IWindowsFormsEditorService m_iwsService = null;
public ListBoxEditor(object selection, IWindowsFormsEditorService edsvc)
{
m_iwsService = edsvc;
SelectionMode = SelectionMode.One;
BorderStyle = BorderStyle.None;
m_oSelection = selection;
}
public object Selection
{
get
{
return m_oSelection;
}
}
protected override void OnSelectedIndexChanged(EventArgs e)
{
base.OnSelectedIndexChanged(e);
if (SelectedItem != null)
{
m_oSelection = SelectedItem;
}
}
protected override void OnClick(EventArgs e)
{
base.OnClick(e);
if (m_iwsService != null)
{
m_iwsService.CloseDropDown();
}
}
}
The m_oSelection object is needed to save the selection once the list control has been destroyed, and IWindowsFormsEditorService is provided by the property editor, and is used to indicate when the user's selection is made, pressing in any of the elements, and the control can return to the caller.
From this generic list, you can derive another one that is specialized in objects of type Shape:
public class ShapeListBoxEditor : ListBoxEditor
{
public ShapeListBoxEditor(object selection,
IWindowsFormsEditorService edsvc)
: base(selection, edsvc)
{
DrawMode = DrawMode.OwnerDrawFixed;
}
protected override void OnDrawItem(DrawItemEventArgs e)
{
base.OnDrawItem(e);
if (e.Index >= 0)
{
Shape shape = Items[e.Index] as Shape;
if (shape != null)
{
Rectangle rect = e.Bounds;
rect.Inflate(-2, -2);
shape.ShapeSize = new Size(rect.Height, rect.Height);
shape.DrawShape(e.Graphics, rect.X, rect.Y);
Font f = new Font("Arial", 9);
e.Graphics.DrawString(shape.ToString(), f,
SystemBrushes.WindowText, rect.Height + 4,
rect.Top +
((rect.Height -
e.Graphics.MeasureString(shape.ToString(),
f).Height) / 2));
f.Dispose();
}
}
}
}
All that this class adds is the custom drawing of Shape objects by calling his DrawShape method.
The other control used to edit objects of type Shape is an ordinary dialog box used to edit its size and color, implemented in the form ShapeDialogBox class.
Custom property editors
With these elements we can now implement the custom property editors. For this, you have to derive a class from UITypeEditor. The first editor, in the ShapeTypeEditor class, is used to select a type of Shape, from the drop-down list with all available options.
First, you have to override the GetEditStyle method to indicate that the editor will show a user control as a dropdown:
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext
context)
{
return UITypeEditorEditStyle.DropDown;
}
Also you can indicate that the control handles the drawing of the selected value, overridding the GetPaintValueSupported method and returning true:
public override bool GetPaintValueSupported(ITypeDescriptorContext context)
{
return true;
}
Then, to draw the figure, you must overload the PaintValue method:
public override void PaintValue(PaintValueEventArgs e)
{
Shape shape = e.Value as Shape;
if (shape != null)
{
Rectangle rect = e.Bounds;
rect.Inflate(-2, -2);
shape.ShapeSize = new Size(rect.Height, rect.Height);
shape.DrawShape(e.Graphics, rect.X + 3, rect.Y);
}
}
And to display the list with the options and select the value of the property, we must also overload the EditValue method:
public override object EditValue(ITypeDescriptorContext context,
IServiceProvider provider, object value)
{
IWindowsFormsEditorService edSvc =
(IWindowsFormsEditorService)provider.GetService
(typeof(IWindowsFormsEditorService));
Shape shape = value as Shape;
if (edSvc != null)
{
ShapeListBoxEditor dropdown = new ShapeListBoxEditor(shape, edSvc);
dropdown.Items.Add(_shapes[0]);
dropdown.Items.Add(_shapes[1]);
dropdown.Items.Add(_shapes[2]);
edSvc.DropDownControl(dropdown);
return dropdown.Selection;
}
return value;
}
The context parameter provides information about the object and property that is edited. The current object can be taken from the Instance property, and the property descriptor from PropertyDescriptor. The current value of the property is passed in the value parameter. This method should return the new value of the property selected by the user. We obtain an instance of IWindowsFormsEditorService that allows expand the list of Shape objects in the appropriate control by calling the DropDownControl method. This call waits until the control terminates the editing.
Now we can add to the DemoObject class a property of Shape type:
[CategoryGlobal("CAT_CUSTOMEDITORS")]
[DisplayNameGlobal("NAME_SHAPETYPEPROPERTY")]
[DescriptionGlobal("DESC_SHAPETYPEPROPERTY")]
[RefreshProperties(RefreshProperties.All)]
[Editor(typeof(ShapeTypeEditor), typeof(UITypeEditor))]
public Shape ShapeTypeProperty { get; set; }
To indicate that this property has a custom editor, decorate it with the Editor attribute, indicating the type of the editor and his base class type. This is the aspect of our editor control:
Now let's build another custom editor to edit an object of type Shape using a dialog box. As in the previous case, we derive the ShapeEditor class of UITypeEditor and we override the GetEditStyle method to indicate that, in this case, the editor shows a modal dialog box to the user:
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext
context)
{
return UITypeEditorEditStyle.Modal;
}
The dialog box is displayed it in the EditValue overloaded method:
public override object EditValue(ITypeDescriptorContext context,
IServiceProvider provider, object value)
{
Shape shape = value as Shape;
if (shape != null)
{
ShapeDialogBox dlg = new ShapeDialogBox();
dlg.SelectedObject = shape;
dlg.ShowDialog();
return shape;
}
return value;
}
And we can then add another Shape property to the DemoObject class, indicating the custom editor with the Editor attribute, as before:
[CategoryGlobal("CAT_CUSTOMEDITORS")]
[DisplayNameGlobal("NAME_SHAPEPROPERTY")]
[DescriptionGlobal("DESC_SHAPEPROPERTY")]
[Editor(typeof(ShapeEditor), typeof(UITypeEditor))]
public Shape ShapeProperty { get; set; }
And this is the dialog box displayed when you edit the property by clicking the edit button of the property in the PropertyGrid:
PropertyGrid events
Finally, we will capture the PropertyValueChanged event of the PropertyGrid control that is fired when the user changes the value of some of the properties:
private void pgControl_PropertyValueChanged(object s,
PropertyValueChangedEventArgs e)
{
if (e.ChangedItem.PropertyDescriptor.Name == "ShapeTypeProperty")
{
Shape shape = e.ChangedItem.Value as Shape;
if (shape != null)
{
_object.ShapeProperty =
(Shape)shape.GetType().GetConstructor
(Type.EmptyTypes).Invoke(null);
}
}
}
The data from modified property are in the event arguments, specifically in the ChangedItem property. The name of the property is in the Name member. When the user modifies the property ShapeTypeProperty, we build a Shape object of the selected type and then assign it to the ShapeProperty property. As the ShapeTypeProperty is decorated with the RefreshProperties attribute, the value is automatically refreshed in control. We can get the same effect by calling the Refresh method of the PropertyGrid.
In the next article, I will show how to use type converters with the PropertyGrid control.