
Introduction
The PropertyGridEx
control shows how to add a new tab to the standard System.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 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 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 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 instance (not static) fields I used reflection on the object's type:
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. 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 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: 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 PropertyTab
s 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 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.
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:
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 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, 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 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 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
ServiceContainer globalServiceContainer = new ServiceContainer();
PropertyGridEx propGrid = new PropertyGridEx(globalServiceContainer);
this.Controls.Add(propGrid);
Or assign the IServiceProvider
anytime you like:
private void Form1_Load(object sender, EventArgs e)
{
this.propertyGridEx1.SetServiceProvider(this.GlobalServiceProvider);
}
History
- 15/01/2007: Initial release.