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

Customized Display of Collection Data in a PropertyGrid

0.00/5 (No votes)
30 Jun 2003 12  
PropertyGrid is widely used to display an object's properties and values
In this article, I will explore an answer to two questions. How is it possible that an array provides its contained objects to the PropertyGrid as if they were properties? Furthermore, the names of the objects contained in the array are displayed with its sequence numbers ( [0],[1],...). The second question is how to customize so that more meaningful data is displayed instead of sequence numbers? We will also develop a solution.

Image 1

Figure 1: Using an Array

Image 2

Figure 2: Using a customized collection

Introduction

The PropertyGrid is widely used to display an object's properties and values. In previous articles, I have shown how to customize and localize the displayed property names and property descriptions (Globalized Property Grid and Globalized Property Grid - Revisited).

This time, I focus myself on Collections and its data. I want to show how you can customize the way a collection displays the objects it contains in a PropertyGrid.

If you assign an array to the PropertyGrid, you'll see that the PropertyGrid displays all the objects contained within the array (see above, figure 1).

The first question here, how is it possible that an array provides its contained objects to the PropertyGrid as if they were properties? Furthermore, the names of the objects contained in the array are displayed with its sequence numbers ( [0],[1],...). This raises the second question. How can we customize so that more meaningful data is displayed instead of sequence numbers?

In this article, I will explore an answer to these questions. We develop a solution that will look like figure 2 (see above).

The Basics

An object may provide custom information about itself by implementing an interface ICustomTypeDescriptor. This interface can be used to provide dynamic type information. This is the case for a Collection object to return its content appearing as properties in a PropertyGrid.

Information returned by a type descriptor contains information about a type regarding its properties, events, type conversion, design time editor, .... .

If ICustomTypeDescriptor is not used, then a static TypeDescriptor will be used at runtime, which provides type information based on the meta data obtained via reflection.

So, we already get half the way to the answer: We need to implement ICustomTypeDescriptor to get access to custom property information.

The property information will be returned by the interface method GetProperties(). This method returns an object of type PropertyDescriptorCollection. This object is a type safe collection of (you might guess it) PropertyDescriptor objects. PropertyDescriptor is an abstract base class. By providing your own class derived from PropertyDescriptor, you can provide customized property information as needed, to return the content of a collection and display something meaningful for each item (display name and description), as shown above (see figure 2 again).

It is more easy than it sounds. I will demonstrate it in a sample project provided for download.

A Sample Project

For demonstration purposes, I will use a sample project. This project is simple but it shows the overall concept. It is a Windows Forms based application. Here is the scenario for the sample.

Scenario

In a PropertyGrid, I want to display the data of a list of employees in a company. The employees should be shown with their full names. I want to add or remove employees, so the list should be editable. An employee item in the list should be expandable to show its complete data. Figure 2 (see above) shows the desired result.

Concept

An instance of Organization represents the company. It knows its employees which are Persons. The Organization object will be assigned to a PropertyGrid to display its data. The main form of the application holds the PropertyGrid. Figure 2 shows this initial structure in a class diagram.

Image 3

Figure 3: Class diagram of Sample

Design

The domain is represented by the classes Organization, Employee, Person. An instance of the class Organization represents the company. It holds a collection of Employee objects. An Employee object represents an employee of the company containing his name, age, department, etc. An employee is a person so it is derived from Person.

The Organization object needs to hold a list of Employee objects. Using an array doesn't meet our requirements, because it is not editable and uses sequence numbers instead of a required employee full name.

As mentioned in the basics section, we have to implement ICustomTypeDescriptor to return our custom property descriptors. So we choose to define a custom type safe collection class, called EmployeeCollection. This collection class will implement ICustomTypeDescriptor.

To return custom information about properties, the EmployeeCollection object will create a custom property descriptor of type PropertyDescriptor, for each of its items. So, the final class diagram for our sample will look like:

Image 4

Figure 4: Final design class diagram

Let's do the implementation.

Implementation - Part I

Implementation of Person and Employee

The implementation of the Person and Employee classes is very simple. They only act as entities holding domain data, to display in the PropertyGrid. I don't provide them here. Have a look at the sample code (Employee.cs and Person.cs) instead.

Implementation of EmployeeCollection

The implementation of EmployeeCollection is more interesting. The most relevant parts are bold.

public class EmployeeCollection : CollectionBase, ICustomTypeDescriptor
{

Inheriting from CollectionBase provides basic collection behavior. Only methods for adding, removing and querying items will be added. The EmployeeCollection class implements the interface ICustomTypeDescriptor to provide custom type information.

First, we add collection methods.

// Collection methods

implementation public void Add( Employee emp )
{
    this.List.Add( emp );
}
public void Remove( Employee emp )
{
    this.List.Remove( emp);
}
public Employee this[ int index ]
{
    get
    {
        return (Employee)this.List[index];
    }
}

Then, we implement the interface ICustomTypeDescriptor. Though the interface has a lot of methods, most of the methods are trivial to implement because we can delegate the call to a corresponding method of the static TypeDescriptor object to provide standard behavior.

// Implementation of ICustomTypeDescriptor:

public String GetClassName()
{
    return TypeDescriptor.GetClassName(this,true);
}

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

public String GetComponentName()
{
    return TypeDescriptor.GetComponentName(this, true);
}

public TypeConverter GetConverter()
{
    return TypeDescriptor.GetConverter(this, true);
}

public EventDescriptor GetDefaultEvent()
{
    return TypeDescriptor.GetDefaultEvent(this, true);
}

public PropertyDescriptor GetDefaultProperty()
{
    return TypeDescriptor.GetDefaultProperty(this, true);
}

public object GetEditor(Type editorBaseType)
{
    return TypeDescriptor.GetEditor(this, editorBaseType, true);
}

public EventDescriptorCollection GetEvents(Attribute[] attributes)
{
    return TypeDescriptor.GetEvents(this, attributes, true);
}

public EventDescriptorCollection GetEvents()
{
    return TypeDescriptor.GetEvents(this, true);
}

public object GetPropertyOwner(PropertyDescriptor pd)
{
    return this;
}

The only methods we implement in a custom way are the overloaded GetProperties() methods. These are used to return a collection of PropertyDescriptor objects used to describe the properties, in a custom way. This property descriptor objects will be used by the PropertyGrid later.

    public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        return GetProperties();
    }

    public PropertyDescriptorCollection GetProperties() 
    {
        // Create a new collection object PropertyDescriptorCollection
        pds = new PropertyDescriptorCollection(null);

        // Iterate the list of employees
        for( int i=0; i<this.List.Count; i++ )
        {
            // For each employee create a property descriptor 
            // and add it to the 
            // PropertyDescriptorCollection instance
            CollectionPropertyDescriptor pd = new 
                          CollectionPropertyDescriptor(this,i);
            pds.Add(pd);
        }
        return pds;
    }
}

The implementation of GetProperties() is quite straight forward. First, we create a PropertyDescriptor collection object. This collection will hold all the PropertyDescriptors that are returned to the client. Here, we can decide what kind of information we want to return. In our case, we create a PropertyDescriptor object of type EmployeeCollectionPropertyDescriptor for each item in the employee list and add it to the PropertyDescriptor collection.

Note: Only the employee list members will be visible to the PropertyGrid then. If you want to provide descriptions about other properties, i.e., the Count property, then get the standard PropertyDescriptor collection from the TypeDescriptor object, retrieve the PropertyDescriptor for Count and add it to the PropertyDescriptor collection as well.

EmployeeCollectionPropertyDescriptor is our custom property descriptor class derived from the abstract base class PropertyDescriptor. This class is used to format display name and description the way we want to. My implementation associates the PropertyDescriptor with the EmployeeCollection and an index to the appropriate item. Both will be provided during construction (an alternative implementation would be to let the PropertyDescriptor reference the Employee object directly).

public class EmployeeCollectionPropertyDescriptor : PropertyDescriptor
{
    private EmployeeCollection collection = null;
    private int index = -1;

    public CollectionPropertyDescriptor(EmployeeCollection coll, 
                       int idx) : base( "#"+idx.ToString(), null )
    {
        this.collection = coll;
        this.index = idx;
    } 

    public override AttributeCollection Attributes
    {
        get 
        { 
            return new AttributeCollection(null);
        }
    }

    public override bool CanResetValue(object component)
    {
        return true;
    }

    public override Type ComponentType
    {
        get 
        { 
            return this.collection.GetType();
        }
    }

    public override string DisplayName
    {
        get 
        {
            Employee emp = this.collection[index];
            return emp.FirstName + " " + emp.LastName;
        }
    }

    public override string Description
    {
        get
        {
            Employee emp = this.collection[index];
            StringBuilder sb = new StringBuilder();
            sb.Append(emp.LastName);
            sb.Append(",");
            sb.Append(emp.FirstName);
            sb.Append(",");
            sb.Append(emp.Age);
            sb.Append(" years old, working for ");
            sb.Append(emp.Department);
            sb.Append(" as ");
            sb.Append(emp.Role);

            return sb.ToString();
        }
    }

    public override object GetValue(object component)
    {
        return this.collection[index];
    }

    public override bool IsReadOnly
    {
        get { return true;  }
    }

    public override string Name
    {
        get { return "#"+index.ToString(); }
    }

    public override Type PropertyType
    {
        get { return this.collection[index].GetType(); }
    }

    public override void ResetValue(object component) {}

    public override bool ShouldSerializeValue(object component)
    {
        return true;
    }

    public override void SetValue(object component, object value)
    {
        // this.collection[index] = value;
    }
}

Have a look at the implementation of DisplayName and Description: DisplayName will be formatted to return the full name of an employee and Description returns a more descriptive string for an employee.

Implementation of Organization

The implementation of Organization is simple. It just creates some sample objects of type Employee and adds them to the EmployeeCollection.

public class Organization
{
    EmployeeCollection employees = new EmployeeCollection(); 

    public Organization()
    {
        // Instantiate test data objects and fill employee collection

        Employee emp1 = new Employee();
        emp1.FirstName = "Max";
        emp1.LastName = "Headroom";
        emp1.Age = 42;
        emp1.Department = "Sales";
        emp1.Role = "Manager";
        this.employees.Add(emp1);

        Employee emp2 = new Employee();
        emp2.FirstName = "Lara";
        emp2.LastName = "Croft";
        emp2.Age = 24;
        emp2.Department = "Accounting";
        emp2.Role = "Manager";
        this.employees.Add(emp2);

        emps[0] = emp1;
        emps[1] = emp2;
    }

    [TypeConverter(typeof(ExpandableObjectConverter ))]
    public EmployeeCollection Employees
    {
            get { return employees; }
    }
}

Organization has only the employee collection as a member, which will be returned in a property Employees. Note the use of a TypeConverterAttribute attached to Employees property. This will be needed to make the Employees property expandable, that we can see the collection content at all.

An instance of Organization will be created in the constructor of the Form1 class and assigned to the PropertyGrid:

public Form1()
{
    InitializeComponent();

    organization = new Organization();
    PropertyGrid1.SelectedObject = organization;
}

The result of the implementation so far is shown in figure 5. The items are not displayed by sequence number anymore. The employees in the list are displayed by their full names.

The collection is editable. If you select the value side of the employee node in the PropertyGrid, you will see a button to call the standard editor (customizing the editor fits into this context but will be a topic for the next article) to modify the employee collection (see figure 6):

Image 5

Figure 5: Edit collection content

Image 6

Figure 6: Standard collection editor

You can add, remove or modify collection items.

Implementation - Part II

In figure 5, you see the class name (PropertyGridSample.Employee) in the value field of the PropertyGrid. That doesn't look nice. Furthermore, we cannot view or edit the employee data inline. This is what we improve now.

Type Converter

A PropertyGrid uses a type converter attached to an object or property to customize the view of an item in the PropertyGrid. Type converter objects are of type TypeConverter and its most common use is to convert to and from a text representation. A custom type converter derives from TypeConverter.

A type converter can be attached to a property or a class by using the TypeConverterAttribute.

We already used one type converter in the Organization class to make the Employees property expandable:

[TypeConverter(typeof(ExpandableObjectConverter ))]
public EmployeeCollection Employees
{
    get { return employees; }
}

ExpandableObjectConverter is one of the type converters defined by the .NET Framework. .NET provides standard type converters for basic and most common types.

ExpandableObjectConverter may also be used for expanding our employee object to see its properties. This time, we attached that type converter with a class ( Employee):

[TypeConverter(typeof(ExpandableObjectConverter))]
public class Employee : Person
{

Now the Employee object is expandable (see figure 8).

Image 7

Figure 8: Expandable Employee

Nice, but the ExpandableObjectConverter displays the class name in the value field of an employee item. We should change this. We provide our own type converter. It is a good idea to derive our custom type converter from ExpandableObjectConverter, because we still want it to be expandable.

internal class EmployeeConverter : ExpandableObjectConverter
{

The class EmployeeConverter is defined as internal so that it is not visible to clients. For our purpose, we only need to override ConvertTo(). This method converts an Employee object into any other object. We define it as follows:

    public override object ConvertTo(ITypeDescriptorContext context, 
                             System.Globalization.CultureInfo culture, 
                             object value, Type destType )
    {
        if( destType == typeof(string) && value is Employee )
        {
            Employee emp = (Employee)value;
            return emp.Department + "," + emp.Role;
        }
        return base.ConvertTo(context,culture,value,destType);
    }
}

We make sure that the value to be converted is of type Employee and that the destination type of conversion is a string. We can return any text that we'd like to. I have chosen that the department followed by the employee department's role should be displayed.

To apply our type converter, we modify the previous use of the TypeConverterAttribute a bit.

[TypeConverter(typeof(EmployeeConverter))]
public class Employee : Person
{

Now our custom type converter EmployeeConverter will be used. That's it!

Our final result looks like figure 9 (I also added a custom type converter to EmployeeConverter that simply displays a static string "Company's employee data").

Image 8

Figure 9: Final look

We have now:

  • An employee list which displays the full name of an employee in the left column instead of simple sequence numbers.
  • Employee items in the employee list which are expandable for inline editing.
  • Meaningful data in the value field (right column) of an employee item instead of a displayed class name.
  • Selecting the Employees collection, let us call the standard editor to modify the collection by adding or removing employee objects.

Summary

Wow, seems like a lot of stuff. But it is really not as much if you have understood the concept. Type descriptors and member descriptors are central to provide dynamic information. You can customize the dynamic information by providing your own implementation.

Here are the steps to customize display and description of collection content in a PropertyGrid:

  • Provide a custom property descriptor by deriving a class form the abstract base class PropertyDescriptor.
  • Override abstract methods and properties. Provide a proper implementation for the DisplayName and description properties.
  • Let your collection class implement the ICustomTypeDescriptor interface.
  • Return a collection of custom property descriptor by the GetProperties() method.
  • Optionally use TypeConverter derived objects provided by .NET or implement your own classes to customize the textual representation of your domain classes. Assign them to the appropriate classes or properties by using the TypeConverterAttribute class.

To globalize the PropertyGrid data, property descriptors may be chained together (See also Globalized property grid).

References

History

  • 1<st>st July, 2003: Initial version

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.

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