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.
Figure 1: Using an Array
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 Person
s. 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.
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:
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.
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.
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()
{
pds = new PropertyDescriptorCollection(null);
for( int i=0; i<this.List.Count; i++ )
{
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 PropertyDescriptor
s 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)
{
}
}
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()
{
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):
Figure 5: Edit collection content
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).
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
").
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.