
Introduction
The PropertyGridEx control shows how to add a new tab to the standardSystem.Winows.Forms.PropertyGrid. In this sample, a custom page shows all instance fields of the selected object. Additionally it shows how to implement and use the IPropertyValueUIService to show additional icons in the grid rows behind the property name.
I first saw this when I started using the .NET 3.0 Workflow classes and saw this little blue icon for theDependencyProperties. In this sample, the icons will show an icon if the member is serializable and a second icon if the member implements ISerializable. A double click on the icon will open a (very raw - and probably erroneous) assumption of the resulting serialization graph.
For the sake of brevity and readability, I omitted most of the source code from this article. I tried to focus on the approach and not on the implementation details. Those interested should read the source code.
Background
I am currently developing a pretty large application that uses some serialization. When I started to optimize the serialization of my objects, I found it hard to follow the serialization graph and to see what is actually serialized. Since I use the BinaryFormatter I thought it would be nice to utilize the PropertyGrid (which I already use in my project to show the properties of my objects) to show me the members and their serialization attribute.
Adding a new PropertyTab
Implementing the PropertyTab
First I created my own RawMemberTab by deriving it from the abstractSystem.Windows.Forms.Design.PropertyTab class. A valid PropertyTab must return a valid Bitmap and a valid Name. And since I wanted my tab to work with any object, I implemented the CanExtend to always return true.
The tricky part was implementing the GetProperties method. It returns a PropertyDescriptorCollectioncontaining a PropertyDescriptor for each property in the grid. In my example, I chose to return not the properties but the fields of the selected object. To get a list of all instance (not static) fields I used reflection on the object's type:
Collapse | Copy Code
FieldInfo[] fieldInfos = type.GetFields( BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic);
Next thing to do was to wrap the returned array of FieldInfo into a collection of PropertyDescript objects.
The System.ComponentModel.PropertyDescriptor is an abstract class and cannot be used directly. All derived classes that Microsoft uses in the PropertyGrid are internal. So I had to write my own.
Implementing a custom PropertyDescriptor (FieldMemberDescriptor)
A PropertyDescriptor is a wrapper class to allow generalized access to (virtual) properties. It does not only describe the property by providing a name and the associated attributes, it also provides access to the value and the child properties.
I created a PropertyDescriptor called FieldMemberDescriptor to wrap the FieldInfo return via reflection. TheFieldInfo is passed to the FieldMemberDescriptor's constructor. (Additionally the owning object's type is passed to construct a name for the PropertyDescriptor)
Most members of the FieldMemberDescriptor are straight forward (see code for details). Worth mentioning is theAttributes property. The Attributes property returns a list of Attributes that are attached to the underlying type. The nice thing about the PropertyDescriptor is that you are allowed to return whatever attributes you like.
There are some attributes that have a strong relation to the PropertyGrid:
| Attribute |
PropertyGrid usage |
System.ComponentModel.CategoryAttribute |
Used to group properties by category |
System.ComponentModel.DescriptionAttribute |
Text displayed in the help pane |
System.ComponentModel.TypeConverterAttribute |
Used to determine the TypeConverter. TheTypeConverter is also used to determine if a property is expandable. |
Knowing this enables the FieldMemberDescriptor not only to provide a meaningful category and description, but also to ensure that the object will always be expandable in the grid (if there is no TypeConverterAttribute attribute provided or the provided TypeConverter does not derive from ExpandableObjectConverter, simply override it with an ExpandableObjectConverter).
Implementing two more custom TypeDescriptors
After having the FieldMemberDescriptor implemented and tested I was still missing one feature in my grid. Even though I had all types tweaked to be expandable I had still no convenient way to inspect the items of collection (especially of Hashtables having no member containing an array of the items nor for the keys).
I needed two more TypeDescriptors to cope with the elements of lists and collections: TheListItemMemberDescriptor deals with classes implementing IList and theDictionaryItemMemberDescriptor with those implementing the IDicionary interface.
Enabling the new PropertyTab
The PropertyGrid holds a collection of PropertyTabs that has the public method AddTabType to add new tabs.
The first parameter is the Type of the PropertyTab, the second is the scope. I chose to make the RawMemberTabstatic i.e. it will be always available. It is added in the constructor of the PropertyGridEx.
If the tab should be displayed only for certain object types, simply override the OnSelectedObjectsChanged method and add the tab with a different scope.
Adding the Icons
IServiceProvider Background
The designer infrastructure of .NET uses IServiceProvider pattern in many places.
An IServiceProvider is a great way to offer lots of different services to components in very versatile way. Any component that has access to an IServiceProvider can query it for a certain type (interface) of service and use it without knowing anything about the actual implementation.
Some common services are:
| Service |
Used for |
System.ComponentModel.Design.ISelectionService |
Access to the current selection and notification about selection changes |
System.ComponentModel.Design.IComponentChangeService |
Notifications on component changes (i.e. rename, remove) |
System.Windows.Forms.Design.IUIService |
Provide access to GUI functions (like show a dialog) |
System.ComponentModel.Design.IDesignerEventService |
Tracking of the active IDesignerHost |
System.ComponentModel.Design.IDesignerHost |
Access to the currently designed component and its designer, this one is a service provider by itself |
System.ComponentModel.Design.IMenuCommandService |
Provides global menu command handling |
System.Drawing.Design.IToolboxService |
Toolbox management |
System.Drawing.Design.IToolboxUser |
Client service for toolbox users |
System.ComponentModel.Design.IPropertyValueUIService |
PropertyGrid ValueUIHandlers |
A component can access an IServiceProvider through its Site property.
One thing to always keep in mind is, that no IServiceProvider guarantees to implement a certain service. So, before using any service you have to check if the IServiceProvider actually provides it.
For example a ListView control sets the globally selected component to the Tag of the current selected item:
Collapse | Copy Code
private void listView1_SelectedIndexChanged(object sender, EventArgs e)
{
if (this.Site != null)
{
System.ComponentModel.Design.ISelectionService selectionService =
this.Site.GetService
(typeof(System.ComponentModel.Design.ISelectionService))
as System.ComponentModel.Design.ISelectionService;
if (selectionService != null)
{
if (this.listView1.SelectedIndices.Count == 1)
{
selectionService.SetSelectedComponents(new object[]
{ this.listView1.Items[this.listView1.SelectedIndices[0]].Tag });
}
else
{
selectionService.SetSelectedComponents(new object[] { null });
}
}
}
}
The IPropertyValueUIService
The PropertyGrid uses the IPropertyValueUIService to allow service consumers to add type or value specific extensions to the PropertyGrid. The extensions are displayed as 9x9 images with a tooltip that can react to a double click.
The IPropertyValueUIService has two aspects:
- For the
PropertyGrid it returns an array of PropertyValueUIItem that should be added to the value.
- For the client that wants to add
PropertyValueUIItem to a PropertyGrid it offers a methods to (un-)register itself.
The .NET framework does not come along with a ready to use implementation of the IPropertyValueUIService. So I had to implement one. The interesting thing implementing this service was the necessity to implement a delegate that is assigned through a method (AddPropertyValueUIHandler and RemovePropertyValueUIHandler) and not simply by having a public event.
My first approach was a little crude by having a list of all delegates that were invoked via an iterator. After a little research I came across the Delegate.Combine method.
Make the service available
The implementation of the service alone does not yet allow the PropertyGrid to use it.
My straight forward approach (having a ServiceContainer as a private member in my PropertyGridEx, adding my IPropertyValueUIService implementation to it and overriding the GetService method did - surprisingly - NOT work. But why? It looked so simple. The PropertyGrid has a public and virtual method named "GetService". Why was it not called with a request for an IPropertyValueUIService?
And yet another moment to bow down before Lutz Roeder and his brilliant Reflector tool!
After digging through the classes used by the PropertyGrids I finally found the location where theIPropertyValueUIService is queried. In the PainLabel method in theSystem.Windows.Forms.PropertyGridInternal.PropertyDescriptorGridEntry class call to thePropertyValueUIService property. Walking up the call tree that this property issues I ended at theSystem.Windows.Forms.PropertyGridInternal.SingleSelectRootGridEntry class and its GetServiceimplementation. This method first checks if it has an active IDesignerHost (which I did not provide) and then queries its "baseProvider" for the service in question. This "baseProvider" was passed to the constructor of theSingleSelectRootGridEntry. After locating the call to the constructor, I found out this mysterious base provider is the PropertyGrid's Site!
So I created a DummySite that is only used to publish the private ServiceContainer. This DummySite is only used if no other valid site is set.
Utilize the IPropertyValueUIService
After having made the IPropertyValueUIService available, the usage of the service is quite simple. As soon as a new ServiceProvider is applied to the PropertyGridEx (via constructor, the Site property or theSetServiceProvider method) any handlers on the old IPropertyValueUIService (if any) are deregistered (RemovePropUIHandler) and if the IServiceProvider provides an IPropertyValueUIService a new handler is added (AddPropUIHandler).
The handler itself is a PropertyValueUIHandler delegate. It is implemented in the PropertyValueUIHandlermethod in the PropertyGridEx control. The handler has two branches: One for FieldMemberDescriptor and one for other descriptors. If the field in a FieldMemberDescriptor is marked as serializable (not having theNotSerialized attribute) a blue disk icon is added. If the value type of the field implements ISerialzable a second icon (three blue discs - squeezed from 16x16 to 9x9 pixels). A double click on the icon opens an experimental serialization graph viewer (not in the scope of this article, so please no comments on this. It is only in to have some meaningful action behind the icon)
Using the control
The sample control is used just like any other control.
If you already have an IServiceProvider that you use in your project you might want to use this for the control too. There are two ways to use an existing IServiceProvider:
Use the PropertyGridEx constructor that takes an IServiceProvider as parameter
Collapse | Copy Code
ServiceContainer globalServiceContainer = new ServiceContainer();
PropertyGridEx propGrid = new PropertyGridEx(globalServiceContainer);
this.Controls.Add(propGrid);
Or assign the IServiceProvider anytime you like:
Collapse | Copy Code
private void Form1_Load(object sender, EventArgs e)
{
this.propertyGridEx1.SetServiceProvider(this.GlobalServiceProvider);
}
History
- 15/01/2007: Initial release.