
Introduction
The PropertyGridEx control shows how to add a new tab to the standard System.Winows.Forms.PropertyGrid. In this sample a custom page that 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 saw first this when I started using the .NET 3.0 Workflow classes
and saw this little blue icon for the DependencyProperties. 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 not on the implementation details. The 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 abstract System.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 PropertyDescriptorCollection containing 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 instances (not
static) fields I used reflection on the object's type:
// get all instance FieldInfos
FieldInfo[] fieldInfos = type.GetFields( BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic);
Next thing to do was to wrap the returned array of FieldInfo in to 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 aPropertyDescriptor called FieldMemberDescriptor to wrap the FieldInfo return via reflection. The FieldInfo 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 the Attributes 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. The TypeConverter 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 is provided or the provided TypeConverter does not derive from ExpandableObjectConverter simply override it with an ExpandableObjectConverter).
Implementing a 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: The ListItemMemberDescriptor deals with classes implementing IList and the DictionaryItemMemberDescriptor 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 the make the RawMemberTab static 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.
Step2: 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 nofiication 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 | provide 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 the 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 set the globally selected component to the Tag of the current selected item:
private void listView1_SelectedIndexChanged(object sender, EventArgs e)
{
// has a site?
if (this.Site != null)
{
// site provides ISelectionService?
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)
{
// set the current selection the current items tag
selectionService.SetSelectedComponents(new object[]
{this.listView1.Items[this.listView1.SelectedIndices[0]].Tag});
}
else
{
// multi selection is no supported
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 to 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 private member in my PropertyGridEx, adding my IPropertyValueUIService implementation to it and overiding 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?
An yet another moment to bow down before Lutz Roeder and his
brillant
Reflector tool!
After digging through the classes used by the PropertyGrids I finally found the location where the IPropertyValueUIService is queried.
In the PainLabel method in the System.Windows.Forms.PropertyGridInternal.PropertyDescriptorGridEntry class call to the PropertyValueUIService property.
Walking up the call tree that this property issues I ended at the System.Windows.Forms.PropertyGridInternal.SingleSelectRootGridEntry class and its GetService implementation. 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 the SingleSelectRootGridEntry. After locating the call to the constructor if 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 the SetServiceProvider 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 PropertyValueUIHandler method 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 the NotSerialized attribute) a blue disk icon is added. If the value type of the field implements ISerialzable a second icon (three blue disc - 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 exsisting IServiceProvider:
Use the PropertyGridEx constructor that takes an IServiceProvider as parameter
ServiceContainer globalServiceContainer = new ServiceContainer();
// ... add some services
// Instatiate a new PropertyGridEx
PropertyGridEx propGrid = new PropertyGridEx(globalServiceContainer);
this.Controls.Add(propGrid);
Or assign the IServiceProvider
anytime you like:
private void Form1_Load(object sender, EventArgs e)
{
// assign the global ServiceProvider
this.propertyGridEx1.SetServiceProvider(this.GlobalServiceProvider);
}
History
-
15/01/2007: initial release