Share via


.NET Matters

ICustomTypeDescriptor, Part 1

Stephen Toub

Code download available at:NETMatters0504.exe(163 KB)

Q I write a lot of one-off utilities for personal use, and since they don't require any sophisticated user interfaces, I often use a System.Windows.Forms.PropertyGrid bound to a settings class in order to allow the user to configure a tool's operations. Unfortunately, sometimes I don't write these settings classes, and often they've been constructed in a way that's incompatible with the PropertyGrid. For example, I often bind to client proxy classes created by wsdl.exe in order to make Web service requests from my tool. The problem is that these proxy classes expose fields rather than properties. Thus, none of these configuration settings shows up in the PropertyGrid. Short of manually going in and changing each field to a property each time these proxy classes are autogenerated, is there any way to get the behavior I need?

Q I write a lot of one-off utilities for personal use, and since they don't require any sophisticated user interfaces, I often use a System.Windows.Forms.PropertyGrid bound to a settings class in order to allow the user to configure a tool's operations. Unfortunately, sometimes I don't write these settings classes, and often they've been constructed in a way that's incompatible with the PropertyGrid. For example, I often bind to client proxy classes created by wsdl.exe in order to make Web service requests from my tool. The problem is that these proxy classes expose fields rather than properties. Thus, none of these configuration settings shows up in the PropertyGrid. Short of manually going in and changing each field to a property each time these proxy classes are autogenerated, is there any way to get the behavior I need?

A It sounds like you're using the Microsoft® .NET Framework 1.x, given your description of wsdl.exe and the proxy classes it generates. In the .NET Framework 2.0, the default operation of wsdl.exe is to generate public properties that wrap private fields, rather than the only mode of operation in version 1.x, which is to generate public fields. So, using proxy classes as you describe will just start working for you when you start using the .NET Framework 2.0. But obviously that doesn't solve the larger problem, nor does it work for you in the .NET Framework 1.x.

A It sounds like you're using the Microsoft® .NET Framework 1.x, given your description of wsdl.exe and the proxy classes it generates. In the .NET Framework 2.0, the default operation of wsdl.exe is to generate public properties that wrap private fields, rather than the only mode of operation in version 1.x, which is to generate public fields. So, using proxy classes as you describe will just start working for you when you start using the .NET Framework 2.0. But obviously that doesn't solve the larger problem, nor does it work for you in the .NET Framework 1.x.

If the PropertyGrid bound to your types used only reflection to examine a type's metadata, you'd be out of luck, and there'd be no way to accomplish what you're trying to do (short of some hackish and non-user-friendly workarounds, one of which might be dynamic code generation). Luckily, it doesn't. PropertyGrid relies on special types from the System.ComponentModel namespace to provide it with information about the objects to which it is bound.

One such type, System.ComponentModel.TypeDescriptor, is used to retrieve information about the properties and events a particular component exposes. In the most general case, TypeDescriptor simply uses reflection to return a System.ComponentModel.PropertyDescriptor for each public instance property found on the type. However, other factors influence exactly which PropertyDescriptors are returned. For example, if the object being described is a component and is associated with a Container that also contains extender provider components (such as System.Windows.Forms.ToolTip) to provide additional properties to this object, TypeDescriptor.GetProperties will return a System.ComponentModel.PropertyDescriptorCollection that contains descriptors for these extender properties in addition to the ones found through reflection and the type's metadata (for more information on provider controls, see the Cutting Edge column in the November 2003 issue of MSDN®Magazine).

Before going straight to the metadata to get property information, TypeDescriptor first checks to see if the type being examined implements the System.ComponentModel.ICustomTypeDescriptor interface. If it does, and if some other conditions are met (to be discussed shortly), rather than using the metadata to get property information, TypeDescriptor will simply ask the object (through its ICustomTypeDescriptor.GetProperties method) which properties it supports. This gives the object itself a chance to hand back a PropertyDescriptorCollection containing exactly the properties it wants displayed in the PropertyGrid. That's good news for you, because it means your type can fool the PropertyGrid into thinking your type has properties it doesn't really have. More specifically, you can convince the PropertyGrid that each of your fields is actually a property. To do so, you need to create a custom PropertyDescriptor for each field you want displayed—in this case, all public fields.

PropertyDescriptor is an abstract class that derives from another abstract class, MemberDescriptor. You can think of MemberDescriptor as providing the basic information for each property, specifically its name and any System.Attribute instances associated with that property. It also provides some useful helper functions that pull out information from those attributes (for example, MemberDescriptor.Category looks to see if one of the attributes is a CategoryAttribute, and if it finds one, returns that attribute's Category string value). PropertyDescriptor adds functionality related to changing a property's value and determining when that value has changed. So, for example, it exposes GetValue and SetValue abstract methods as well as an OnValueChanged method, which will notify any listeners that this property's value has changed. Tricky naming, huh?

To create a PropertyDescriptor for each field, you first need to create a custom class that derives from PropertyDescriptor. An example of one viable class for your purposes is shown in Figure 1. This FieldPropertyDescriptor class wraps a System.Reflection.FieldInfo class describing a field on a type. The constructor initializes the base MemberDescriptor with the name of the field as the property name and the field's attributes as the property's attributes. (The PropertyGrid uses the attributes from the PropertyDescriptor to determine important display settings, such as name, category, description, and even which UITypeEditor to use.)

Figure 1 Custom PropertyDescriptor for Fields

public class FieldPropertyDescriptor : PropertyDescriptor { private FieldInfo _field; public FieldPropertyDescriptor(FieldInfo field) : base(field.Name, (Attribute[])field.GetCustomAttributes(typeof(Attribute), true)) { _field = field; } public FieldInfo Field { get { return _field; } } public override bool Equals(object obj) { FieldPropertyDescriptor other = obj as FieldPropertyDescriptor; return other != null && other._field.Equals(_field); } public override int GetHashCode() { return _field.GetHashCode(); } public override bool IsReadOnly { get { return false; } } public override void ResetValue(object component) {} public override bool CanResetValue(object component) { return false;} public override bool ShouldSerializeValue(object component) { return true; } public override Type ComponentType { get { return _field.DeclaringType; } } public override Type PropertyType { get { return _field.FieldType; }} public override object GetValue(object component) { return _field.GetValue(component); } public override void SetValue(object component, object value) { _field.SetValue(component, value); OnValueChanged(component, EventArgs.Empty); } }

The rest of the class is fairly self-explanatory. GetValue and SetValue use reflection and the stored FieldInfo to access and modify the underlying field's value. PropertyType returns the field's type. And the Equals method compares the stored FieldInfo objects rather than the FieldPropertyDescriptors. Note that any type that overrides Object.Equals should also override Object.GetHashCode, as I've done here.

With FieldPropertyDescriptor in hand, you now need to provide instances of it back to the PropertyGrid. As noted earlier, this can be accomplished by having your type implement the ICustomTypeDescriptor interface, paying particular attention to the GetProperties method. Unfortunately, yes, this requires you to modify your type. However, I'll show you how to cut down on the number of changes you need to make to each type by putting all of this functionality into a common base class. Then, all you need to do to use this functionality with your field-exposing type is to derive from this new base class; no changes to your type's members will be necessary.

Figure 2 shows just such a base class. The most important method is GetProperties, which returns the PropertyDescriptionCollection that'll be used by callers to determine which properties this instance supports. The core of it, ignoring for a moment all of the extra boilerplate, is implemented as follows:

PropertyDescriptorCollection props = new PropertyDescriptorCollection(null); foreach(PropertyDescriptor prop in TypeDescriptor.GetProperties(this, attributes, true)) props.Add(prop); foreach (FieldInfo field in GetType().GetFields()) props.Add(new FieldPropertyDescriptor(field)); return props;

Figure 2 Implementing ICustomTypeDescriptor

public abstract class FieldsToPropertiesTypeDescriptor : ICustomTypeDescriptor { object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd) { return this; } AttributeCollection ICustomTypeDescriptor.GetAttributes() { return TypeDescriptor.GetAttributes(this, true); } string ICustomTypeDescriptor.GetClassName() { return TypeDescriptor.GetClassName(this, true); } ... private PropertyDescriptorCollection _propCache; private FilterCache _filterCache; PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties() { return ((ICustomTypeDescriptor)this).GetProperties(null); } PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties( Attribute[] attributes) { bool filtering = (attributes != null && attributes.Length > 0); PropertyDescriptorCollection props = _propCache; FilterCache cache = _filterCache; // Use a cached version if possible if (filtering && cache != null && cache.IsValid(attributes)) return cache.FilteredProperties; else if (!filtering && props != null) return props; // Create the property collection and filter props = new PropertyDescriptorCollection(null); foreach(PropertyDescriptor prop in TypeDescriptor.GetProperties( this, attributes, true)) { props.Add(prop); } foreach (FieldInfo field in this.GetType().GetFields()) { FieldPropertyDescriptor fieldDesc = new FieldPropertyDescriptor(field); if (!filtering || fieldDesc.Attributes.Contains(attributes)) props.Add(fieldDesc); } // Store the computed properties if (filtering) { cache = new FilterCache(); cache.Attributes = attributes; cache.FilteredProperties = props; _filterCache = cache; } else _propCache = props; return props; } private class FilterCache { public Attribute[] Attributes; public PropertyDescriptorCollection FilteredProperties; public bool IsValid(Attribute[] other) { if (other == null || Attributes == null) return false; if (Attributes.Length != other.Length) return false; for (int i = 0; i < other.Length; i++) { if (!Attributes[i].Match(other[i])) return false; } return true; } } }

The code first gets the collection of descriptors for the actual properties defined on the type with a call to TypeDescriptor.GetProperties. The collection returned will also include any properties inserted by extender provider components. (You'll notice, too, that the third argument to this call, noCustomTypeDesc, is true, which tells TypeDescriptor not to use the instance's ICustomTypeDescriptor implementation. Passing in a value of false here would be fatal, as it would result in infinite recursion and eventually a StackOverflowException as TypeDescriptor proceeded to call into your GetProperties implementation again and again and again.) All of these base descriptors are copied into a new collection. The code then loops through each of the public instance fields on the type and creates a FieldPropertyDescriptor for each, which are promptly added to that new collection. When all of the fields have been wrapped, the collection is returned.

Of course, you'll notice that there's more to my implementation of GetProperties than what I've just described. What I've outlined will be correct functionally, but it won't perform as it otherwise could. Specifically, the resulting PropertyDescriptorCollection could be cached such that future calls to this method could simply return the cached collection rather than recomputing it. However, you have to be careful with how you cache, since TypeDescriptor.GetProperties filters what properties are returned based on the attributes that are passed to the method (only properties that are attributed with one of the specified attributes, either explicitly or by default, are returned). Since I'm augmenting the results of calling to TypeDescriptor.GetProperties in my implementation of ICustomTypeDescriptor.GetProperties, and since TypeDescriptor.GetProperties will filter results, the set to be returned from my implementation must be recomputed based on a different set of attribute filters than were used originally.

To compensate for this, I maintain multiple cached property sets. First, I cache the properties to be used when no attributes are specified as a filter (in other words, the full list of properties). Second, I cache the last property set computed based on the last attribute filter specified, which is also cached. This way, I only have to compute the full set of properties once, and if the same attribute filter is used multiple times in a row, I likewise only have to compute it once.

As an aside, note that in the .NET Framework 1.x filtering in this situation is strictly optional, as TypeDescriptor.GetProperties will filter the property collection before returning it to the caller. If the creation of a custom property descriptor is expensive, you'll want to filter as much as possible before creating the descriptor. In the .NET Framework 2.0 Beta 1, only in certain scenarios (including, for backward compatibility, the scenarios available in the .NET Framework 1.x) will TypeDescriptor perform this filtering for you. More on this next month.

The rest of the FieldsToPropertiesTypeDescriptor class is boilerplate. GetPropertyOwner returns a reference to "this", since the type to be described is deriving from this class (FieldsToPropertiesTypeDescriptor), and it "owns" all of the properties being returned. Almost every other method then delegates to the appropriate method on TypeDescriptor, letting that method do all the heavy lifting (again, with the noCustomTypeDesc parameter set to true).

You can now give this solution a try. Assuming you have a type that looks something like the following

public class Person { public string Name; public int Age; public string Hobbies; }

you can simply modify it to use this new base class as follows:

public class Person : FieldsToPropertiesTypeDescriptor { public string Name; public int Age; public string Hobbies; }

And, poof, PropertyGrid will show all of your fields in the grid. This is made possible by the process I've described up to this point, outlined in Figure 3.

Figure 3 Using ICustomTypeDescriptor in the .NET Framework 1.x

Figure 3** Using ICustomTypeDescriptor in the .NET Framework 1.x **

Of course, this isn't quite what you asked for. While this requires minimal modifications to your type, you didn't want to have to change your type at all. Unfortunately, in the .NET Framework 1.x, in order for TypeDescriptor to give back custom properties for a type, that type must implement ICustomTypeDescriptor.

There is a slight variation to this solution that might work for you, however. You can modify the FieldsToPropertiesTypeDescriptor class to be a proxy class rather than an abstract base class for the type in question. This would result in a class that looks similar to the code shown in Figure 4. Here I've renamed FieldsToPropertiesTypeDescriptor to be FieldsToPropertiesProxyTypeDescriptor, removed the abstract modifier, and added a constructor and a private object member variable. The constructor accepts as a parameter the target object that you would have otherwise set as the SelectObject for the PropertyGrid. This object is stored internally to the _target member, which is then used everywhere that the "this" reference was used in Figure 2. Any requests for metadata information about this proxy custom type descriptor will then actually return information about the target object. This allows you to keep your original type unmodified, writing code like the following to show the object in the property grid:

propertyGrid.SelectedObject = new FieldsToPropertiesProxyTypeDescriptor(person);

The solution discussed in this column is useful for more than just working with the PropertyGrid. In fact, another application of descriptors is used much more frequently: ASP.NET data binding. The System.Web.UI.DataBinder class is used heavily in applications that implement data binding. According to the documentation, the DataBinder.Eval method "uses reflection to parse and evaluate a data binding expression against an object at run time." More specifically, however, it uses property descriptors rather than directly using the System.Reflection namespace. DataBinder.Eval calls DataBinder.GetPropertyValue, which is implemented approximately as follows:

public static object GetPropertyValue(object container, string propName) { ... PropertyDescriptor propDesc = TypeDescriptor.GetProperties( container).Find(propName, true); if (propDesc == null) throw new HttpException(...); return propDesc.GetValue(container); }

Thus, you can use the FieldsToPropertiesTypeDescriptor or FieldsToPropertiesProxyTypeDescriptor classes described in this column to allow ASP.NET data binding to work with fields in addition to with properties.

Figure 4 A Proxy ICustomTypeDescriptor

public class FieldsToPropertiesProxyTypeDescriptor : ICustomTypeDescriptor { private object _target; // object to be described public FieldsToPropertiesProxyTypeDescriptor(object target) { if (target == null) throw new ArgumentNullException("target"); _target = target; } object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd) { return _target; // properties belong to the target object } AttributeCollection ICustomTypeDescriptor.GetAttributes() { // Gets the attributes of the target object return TypeDescriptor.GetAttributes(_target, true); } string ICustomTypeDescriptor.GetClassName() { // Gets the class name of the target object return TypeDescriptor.GetClassName(_target, true); } ... // the rest of the class is the same as // FieldsToPropertiesTypeDescriptor except using // "_target" instead of "this" }

Next time in this column, I'll continue this discussion of property descriptors, focusing on the .NET Framework 2.0, specifically on how the new TypeDescriptionProvider infrastructure dramatically improves the situation.

Send your questions and comments to  netqa@microsoft.com.

Stephen Toub is the Technical Editor for MSDN Magazine.