Click here to Skip to main content
15,878,945 members
Articles / Programming Languages / C#
Article

Extending the PropertyGrid with a new PropertyTab

Rate me:
Please Sign up or sign in to vote.
4.89/5 (39 votes)
15 Jan 2007CPOL8 min read 121K   4.1K   128   12
Add a PropertyTab showing the fields of an object and overlay icons to the PropertyGrid

Sample Image - PropertyGridEx.gif

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:

C#
// 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 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:

AttributePropertyGrid usage
System.ComponentModel.CategoryAttributeUsed to group properties by category
System.ComponentModel.DescriptionAttributeText displayed in the help pane
System.ComponentModel.TypeConverterAttributeUsed 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 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 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:

ServiceUsed for
System.ComponentModel.Design.ISelectionServiceAccess to the current selection and notification about selection changes
System.ComponentModel.Design.IComponentChangeServiceNotifications on component changes (i.e. rename, remove)
System.Windows.Forms.Design.IUIServiceProvide access to GUI functions (like show a dialog)
System.ComponentModel.Design.IDesignerEventServiceTracking of the active IDesignerHost
System.ComponentModel.Design.IDesignerHostAccess to the currently designed component and its designer, this one is a service provider by itself
System.ComponentModel.Design.IMenuCommandServiceProvides global menu command handling
System.Drawing.Design.IToolboxServiceToolbox management
System.Drawing.Design.IToolboxUserClient service for toolbox users
System.ComponentModel.Design.IPropertyValueUIServicePropertyGrid 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:

C#
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 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

C#
ServiceContainer globalServiceContainer = new ServiceContainer();
// ... add some services

// Instantiate a new PropertyGridEx
PropertyGridEx propGrid = new PropertyGridEx(globalServiceContainer);
this.Controls.Add(propGrid);

Or assign the IServiceProvider anytime you like:

C#
private void Form1_Load(object sender, EventArgs e)
{
     // assign the global ServiceProvider
     this.propertyGridEx1.SetServiceProvider(this.GlobalServiceProvider);
}

History

  • 15/01/2007: Initial release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Germany Germany
Carsten started programming Basic and Assembler back in the 80’s when he got his first C64. After switching to a x86 based system he started programming in Pascal and C. He started Windows programming with the arrival of Windows 3.0. After working for various internet companies developing a linguistic text analysis and classification software for 25hours communications he is now working as a contractor.

Carsten lives in Hamburg, Germany with his wife and five children.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Mazen el Senih24-Jul-13 7:37
professionalMazen el Senih24-Jul-13 7:37 
GeneralGreat Pin
Member 40496979-Apr-09 16:37
Member 40496979-Apr-09 16:37 
GeneralHint for the bitmap Pin
Simon Mourier13-Mar-09 4:23
Simon Mourier13-Mar-09 4:23 
QuestionWhat license is the source under? Pin
Jeremy Thomas12-Jan-09 21:12
Jeremy Thomas12-Jan-09 21:12 
AnswerRe: What license is the source under? Pin
Carsten Zeumer13-Jan-09 1:19
Carsten Zeumer13-Jan-09 1:19 
QuestionHow to remove one? Pin
Sergey Alexandrovich Kryukov31-Jan-08 11:08
mvaSergey Alexandrovich Kryukov31-Jan-08 11:08 
Please pardon my naive question: how to remove a button from the toolbar, in particular the one that has a tool tip of "Property Pages" on it, usually disabled? (Thank to you, we know how to enable it, but wouldn't it be better to remove it completely when not in use instead of confusing the users?)

Thank you very much.

Sergey A Kryukov

Generalenabling the "property pages" button Pin
gabegabe11-Apr-07 2:10
gabegabe11-Apr-07 2:10 
GeneralRe: enabling the "property pages" button Pin
GraceChao19-Apr-07 14:01
GraceChao19-Apr-07 14:01 
GeneralAwesome article Pin
smc75024-Jan-07 17:32
smc75024-Jan-07 17:32 
GeneralI agree...Very Cool... Pin
antecedents24-Jan-07 5:29
antecedents24-Jan-07 5:29 
GeneralVery, very cool... Pin
0xfded17-Jan-07 4:09
0xfded17-Jan-07 4:09 
AnswerRe: Very, very cool... Pin
Carsten Zeumer17-Jan-07 8:56
Carsten Zeumer17-Jan-07 8:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.