Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Bending the .NET PropertyGrid to Your Will

0.00/5 (No votes)
19 Dec 2002 2  
A set of classes to simplify using custom properties in the .NET PropertyGrid control.

Sample Image - bending_property.png

Introduction

With the initial release of the .NET Framework, I was impressed that Microsoft finally provided one of their coveted custom controls for public use�in this case, the Visual Basic-like property grid, System.Windows.Forms.PropertyGrid.  Upon using it, however, I realized that while the control is very flexible and powerful, it takes quite a bit of work to customize.  Below, I present a set of classes that increase the usability of the property grid.

Background

The property grid provided with the .NET Framework operates via reflection.  By using the SelectedObject or SelectedObjects property, you can select one or more objects into the control.  It then queries the objects for all their properties, organizes them into groups based on their CategoryAttribute, and uses various UITypeEditor and TypeConverter classes to allow the user to edit the properties as strings.

The problem with this is that it's not very flexible out of the box.  To use the grid, you must write a "surrogate class" that exposes properties that you want to show in the grid, instantiate an object of this class, and then select it into the grid.

Many times, however, it may not be desirable to create such a class.  Each surrogate has to be specific to your data, because the runtime merely uses reflection to ask the class what its properties are.  You wouldn't be able to easily write a surrogate class that you could reuse across multiple pieces of data unless they all happened to share the same properties.

Depending on the design of your system and the organization of your data, it may not be convenient to write a surrogate class.  Imagine you have a database of a hundred properties that are stored as key-value pairs.  To create a class to wrap this database, you would have to hand-code the get and set calls for each of the hundred properties.  Even if you wrote an automated tool to generate the code for you, there is still the issue of code bloat in your executable.

Lastly, creating a surrogate class doesn't lend itself well to "beautifying" the contents of the grid.  The standard behavior is to display the actual name of the property from the code.  This makes sense for a component like a form designer, where the developer needs to be able to quickly and easily make the connection between a property in the grid and the same property in code.  If the grid is being used as part of an interface for end-users, however, they should be presented with something a little less obtuse than "UniqueCustID."

Custom Type Descriptors

If you take a look at the project settings dialog in Visual Studio.NET, you'll notice that the grid contains properties with nicer-looking names like "Use of MFC," which opens a drop-down list that contains values such as "Use MFC in a Shared DLL."  Clearly, there is more going on here than a simple property with an enumerated type.

The answer lies in the System.ComponentModel.ICustomTypeDescriptor interface.  This interface allows a class to provide customized type information about itself.  In short, this allows you to modify the properties and events that the property grid sees when it queries the object for this information.

ICustomTypeDescriptor provides a number of functions that look like functions in the standard TypeDescriptor class.  In fact, when you implement an ICustomTypeDescriptor, you want to be able to leverage much of the standard behavior already provided by the framework.  For most functions, you can simply pass control to the standard TypeDescriptor, like so:

AttributeCollection ICustomTypeDescriptor.GetAttributes()
{
     return TypeDescriptor.GetAttributes(this, true);
}

The first parameter for most of these functions is the object on which the information is being requested.  The second parameter tells the TypeDescriptor not to call the custom type descriptor to get the requested information.  Since we are the custom type descriptor, omitting this parameter would result in infinite recursion, as the custom type descriptor would be invoked again and again.

However, note that I originally said that ICustomTypeDescriptor allows a class to provide information about itself.  This would seem to imply that a class must exist about which we can provide type information, and this is true.  Some class must exist that can implement the ICustomTypeDescriptor interface to provide a list of properties to the grid.  But the object's interface does not need to look anything like the properties that it will expose, and that is where the flexibility lies.

The Solution

To this end, I have created the PropertyBag class.  It implements ICustomTypeDescriptor, but in the most generic fashion possible.  Rather than relying on hard-coded properties in the class itself, PropertyBag manages a collection of objects that describe the properties that should be displayed in the grid.  It does this by overriding ICustomTypeDescriptor.GetProperties(), generating its own custom property descriptors, and returning this collection.  Since control is never passed to the standard type descriptor, the grid doesn't even know about the "actual" properties that belong to the PropertyBag class itself.

The benefits of this are that properties can be added and removed at will, whereas a typical surrogate class would be fixed at compile-time.

Type descriptors only provide information about the existence and type of properties, however, because they implement methods that can be applied to any object of that type.  This means there needs to be some way to retrieve and store the values of the properties.  I have implemented the two methods below:

Method 1: Raising Events

The method implemented by the base PropertyBag class raises events whenever property values are queried or changed.  The GetValue event occurs when the property grid needs to know the value of a certain property, and SetValue occurs when the user has interactively changed the value of the property in the grid.

This method is useful in situations dealing with properties that are stored in files and databases.  When the event occurs, you can use the property name to index into the data source, using the same simple lookup code for every property.

Method 2: Storing the Values in a Table

For a simpler approach that might be more appropriate in some cases, I've derived a class from PropertyBag called PropertyTable.  This class provides all the generic functionality of PropertyBag but also contains a hashtable, indexed by property name, to store property values.  When a value is requested, the property is looked up in the table, and when the user updates it, the value in the hashtable is updated accordingly.

Rolling Your Own

Since PropertyBag provides virtual functions OnGetValue and OnSetValue that correspond to the events discussed above, you can also derive a class and provide your own method by overriding these two functions.

What's Included

PropertyBag Class

The PropertyBag class is very basic, exposing only two properties, two events, and two methods.

public string DefaultProperty { get; set; }

The DefaultProperty property specifies the name of the default property in the property bag.  This is the property that is selected by default when the bag is selected into a PropertyGrid.

public PropertySpecCollection Properties { get; }

The Properties collection manages the various properties in the bag.  Like many other .NET Framework collections, it implements IList, so functions like Add, AddRange, and Remove can be used to manipulate the properties in the bag.

public event PropertySpecEventHandler GetValue;
public event PropertySpecEventHandler SetValue;
protected virtual void OnGetValue(PropertySpecEventArgs e);
protected virtual void OnSetValue(PropertySpecEventArgs e);

The GetValue event is raised whenever the property grid needs to request the value of a property.  This can happen for several reasons, such as displaying the property, comparing it to the default value to determine if it can be reset, among others.

The SetValue event is raised whenever the user modifies the value of the property through the grid.

Derived classes can override OnGetValue and OnSetValue as an alternative to adding an event handler.

PropertyTable Class

The PropertyTable class provides all the operations of PropertyBag, as well as an indexer property:

public object this[string key] { get; set; }

This indexer is used to get and set property values that are stored in the table's internal hashtable.  Properties are indexed by name.

PropertySpec Class

PropertySpec provides 16 constructor overloads�too many to describe in detail here.  The overloads are various combinations of the following parameters, listed with the property to which it corresponds:

  • name (Name): The name of the property.
  • type (TypeName): The type of the property.  In the constructor, this can be either a Type object (such as one returned by the typeof() operator), or it can be a string representing the fully qualified type name (i.e., "System.Boolean").
  • category (Category): A string indicating the category to which the property belongs.  If this is null, the default category is used (usually "Misc").  This parameter has the same effect as attaching a CategoryAttribute to a property.
  • description (Description): A help string that is displayed in the description area at the bottom of the property grid.  If this is null, no description is displayed.  This parameter has the same effect as attaching a DescriptionAttribute to a property.
  • defaultValue (DefaultValue): The default value of the property.  If the current value of the property is not equal to the default value, the property is displayed in bold to indicate that it has been changed.  This property has the same effect as attaching a DefaultValueAttribute to a property.
  • editor (EditorTypeName): Indicates the type (derived from UITypeEditor) used to edit the property.  This can be used to provide a custom editor for a type that does not have one explicitly associated with it, or to override the editor associated with the type.  In the constructor, this parameter can be specified either as a Type object or a fully qualified type name.  It has the same effect as attaching an EditorAttribute to a property.
  • typeConverter (ConverterTypeName): Indicates the type converter (derived from TypeConverter) used to convert the property value to and from a string.  In the constructor, it can be specified either as a Type object or a fully qualified type name.  This parameter has the same effect as attaching a TypeConverterAttribute to the property.

Additionally, the following property is provided:

public Attribute[] Attributes { get; set; }

With the Attributes property, you can include any additional attributes not supported directly by PropertySpecReadOnlyAttribute, for example.

Using the Code

Using a PropertyBag is a simple process:

  1. Instantiate an object of the PropertyBag class.
  2. Add a PropertySpec object to the PropertyBag.Properties collection for each property that you want to display in the grid.
  3. Add event handlers for the GetValue and SetValue events of the property bag, and set them up to access whichever data source you are using to store the property values.
  4. Select the property bag into a PropertyGrid control.

If you are using a PropertyTable, you would not add event handlers to the bag in step 3, but instead specify appropriate initial values to the properties, using the table's indexer.

A basic example:

PropertyBag bag = new PropertyBag();
bag.GetValue += new PropertySpecEventHandler(this.bag_GetValue);
bag.SetValue += new PropertySpecEventHandler(this.bag_SetValue);
bag.Properties.Add(new PropertySpec("Some Number", typeof(int)));
// ... add other properties ...

propertyGrid.SelectedObject = bag;

The project included with the article shows a more robust demonstration of the classes.

Points of Interest

One interesting benefit that requires no extra work at all is shown in the demo application, if you select multiple objects from the list.  Multiple selection is handled entirely by the property grid, so even if the objects are property bags with completely custom properties, the grid still works as expected�only the properties and property values that are shared among all the objects are displayed.

Future Plans

This class will eventually be absorbed into a larger user-interface utility library that I'm writing (hence the awkward namespace name in the source code).

One enhancement I plan to add in the future is to make it easier to specify a list of string values for a property, instead of requiring an enumerated type for such a list.  For example, the project properties dialog mentioned before has a "Use of MFC" property with the values "Use Standard Windows Libraries," "Use MFC in a Static Library," and "Use MFC in a Shared DLL."  Currently, the way you could do this is to write a TypeConverter for the property and override the three GetStandardValues* methods to provide a list of allowable values.  Optimally, I would like to have this built-in to the property bag so the user can simply provide a list of strings for a property, without requiring any external type converters to be explicitly written.  One particularly interesting way to accomplish this may be to use the System.Reflection.Emit functionality to generate an appropriate type converter in memory at runtime.

Secondly, I plan to make the available properties dynamic at runtime as well, in the sense that the property bag would fire an event when it builds the property list for the grid.  Any objects listening to the event could provide additional properties at that time.  This would make sense for dynamic data sources where it might be inconvenient to constantly add and remove properties from the bag or create a new bag when the selected object changes.

Of course, I'm open to suggestions for other possible enhancements and extensions to these classes.

Version History

  • 12/20/2002(v1.0): Initial release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here