November 26, 2007

ICustomTypeDescriptor

Recently I had what is probably a very strange need, but I doubt I'm the first one to have it. I essentially had a two dimensional array, more specifically an object of type List<Dictionary<string,string>>, that I needed to bind to a GridView. I couldn't use a custom collection because the schema of the data structure could be just about anything (the GridView columns were being built at runtime). I won't get into the details of why I was using a List of Dictionary objects as opposed to simply using a DataTable, but for the sake of simplicity let's just say it was the only option I had. Now what I needed to do with this collection was to bind it to the GridView, using the keys in the Dictionary as the DataField for the columns.

My first thought..."maybe it'll just work". I knew I was being overly optimistic, but I had to try anyway, and I received an error message of "'KeyName' is not a member of Dictionary", or something to that effect. Now I knew that the ability to bind to a dynamic schema based on column names existed in the DataTable, so I opened up Reflector and took a peek under the hood. After a few minutes of rummaging around I came across the golden treasure that is ICustomTypeDescriptor.

If you were to think of a custom class as a spy for some reason, then ICustomTypeDescriptor would be like a disguise that allows your object to masquarade behind enemy lines pretending to be something it's not (I've been playing a lot of Team Fortress 2 lately). On a more technical level, any time your object is accessed via reflection, in databinding for example, ICustomTypeDescriptor allows you to define the facade that your object exposes at runtime. So let's say I have a column in a GridView that is bound to the "ID" property of my custom object. If my object doesn't have an "ID" property then we get an error, but if my object implements ICustomTypeDescriptor then I can trick the calling code into thinking it has a property called "ID", and whenever the value of that property is requested my object can run whatever custom logic I write in order to return a valid value.

Let's get to the code. We need to start by abstracting that generic dictionary into a custom class that we can slap ICustomTypeDescriptor on. Here's what the end result looks like:

public class MyCustomClass : Dictionary<string, string>, ICustomTypeDescriptor
{
private PropertyDescriptorCollection m_PropertyDescriptorCollectionCache;

AttributeCollection ICustomTypeDescriptor.GetAttributes()
{
return new AttributeCollection(null);
}

string ICustomTypeDescriptor.GetClassName()
{
return null;
}

string ICustomTypeDescriptor.GetComponentName()
{
return null;
}

TypeConverter ICustomTypeDescriptor.GetConverter()
{
return null;
}

EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
{
return null;
}

PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
{
return null;
}

object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
{
return null;
}

EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
{
return new EventDescriptorCollection(null);
}

EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
{
return new EventDescriptorCollection(null);
}

PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
{
if (m_PropertyDescriptorCollectionCache == null)
{
PropertyDescriptor[] properties = new PropertyDescriptor[this.Count];
int i = 0;
foreach (string key in this.Keys)
{
properties[i] = new MyCustomClassPropertyDescriptor(key);
i++;
}
m_PropertyDescriptorCollectionCache = new PropertyDescriptorCollection(properties);
}
return m_PropertyDescriptorCollectionCache;
}

PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
{
return ((ICustomTypeDescriptor)this).GetProperties(null);
}

object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
{
return this;
}
}

Mostly it's a lot of methods that I didn't need for the purpose of databinding, so I just return null. The heart of the whole thing is the GetProperties method. Note that it is overloaded in the interface to allow for filtering by certain attribute types. I didn't have a need for this so I didn't write the code to do it, but you're welcomed to if you like. Anyway, the purpose of this method is to return a PropertyDescriptorCollection that describes all of the properties of the object. In my case I needed to expose every key in the Dictionary as a property that I could bind to, so that's what it does.

Now if you're paying attention you'll notice that it's slightly more complicated than that. We also have to create another class that contains the logic to retrieve a value from the dictionary when a property is requested:

public class MyCustomClassPropertyDescriptor : PropertyDescriptor
{
public ResponsePropertyDescriptor(string key) : base(key, null)
{

}

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

public override Type ComponentType
{
get { return typeof(Dictionary<string, string>); }
}

public override object GetValue(object component)
{
return ((Dictionary<string, string>)component)[base.Name];
}

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

public override Type PropertyType
{
get { return typeof(string); String.IsNullOrEmpty(string value); }
}

public override void ResetValue(object component)
{
((Dictionary<string, string>)component)[base.Name] = string.Empty;
}

public override void SetValue(object component, object value)
{
((Dictionary<string, string>)component)[base.Name] = value.ToString();
}

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

This is a pretty simple class that inherits from PropertyDescriptor. When we instantiate it we pass the value of the Dictionary key, which becomes the name of the fake property we are exposing.

So our problem is solved. As the GridView is being bound, it makes a request for the property of MyCustomClass definded in the DataField. Even though that property doesn't actually exist in MyCustomClass, because we have implemented ICustomTypeDescriptor everything is ok. The request gets passed on to the GetValue method of MyCustomClassPropertyDescriptor, which uses the requested property name to retrieve the value from the Dictionary object by key. Very simple and very cool.