Auto-Generating DataGrid Columns From DynamicObjects

Tags: WPF DataGrid DynamicObject ITypedList DataBinding

I came across a problem recently about how to populate a WPF DataGrid with dynamic data, and automatically generate the columns. By dynamic I mean data which is essentially built up by key/value pairs defined at runtime, where one dataset may or may not be different from the other. One way to do it would obviously be to create a list of IDictionary<string, T> and generate columns that bind to the dictionary keys. However this requires you to get involved in creating the columns. Another approach is to create a runtime type with properties matching the dynamically defined fields. However there are some serious performance implications connected with creating runtime types.

The approach I am looking at in this post is to use a DynamicObject to wrap a dictionary. This makes it easy for the DataGrid to auto-generate the columns based on the dynamic properties. The problem is how to get the different keys exposed as properties? The DynamicObject class has a GetDynamicMemberNames method, where you can return the dictionary keys. The code below shows a sample implementation.

C#

public class DynamicItem : DynamicObject
{
	private readonly Dictionary<string, object> dynamicProperties;

	public DynamicItem(IEnumerable<string> propertyNames)
	{
		dynamicProperties = propertyNames.ToDictionary(s => s, s => (object)null);
	}

	public override bool TrySetMember(SetMemberBinder binder, object value)
	{
		if (dynamicProperties.ContainsKey(binder.Name))
		{
			dynamicProperties[binder.Name] = value;
			return true;
		}

		return base.TrySetMember(binder, value);
	}

	public override bool TryGetMember(GetMemberBinder binder, out object result)
	{
		if (dynamicProperties.ContainsKey(binder.Name))
		{
			result = dynamicProperties[binder.Name];
			return true;
		}

		return base.TryGetMember(binder, out result);
	}

	public override IEnumerable<string> GetDynamicMemberNames()
	{
		return dynamicProperties.Keys.ToArray();
	}
}

This should take care of auto-generating the columns, except it doesn't. The problem is that the DataGrid relies on reflection to get the property list, so it won't call the GetDynamicMemberNames method. Instead you have to use a PropertyDescriptor to describe the existing properties (or in the existing case, the fake properties that you are presenting). The following shows an implementation of a PropertyDescriptor:

C#

public class DynamicPropertyDescriptor : PropertyDescriptor
{
	public DynamicPropertyDescriptor(string name)
		: base(name, null)
	{
	}

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

	public override object GetValue(object component)
	{
		return GetDynamicMember(component, Name);
	}

	public override void ResetValue(object component)
	{
	}

	public override void SetValue(object component, object value)
	{
		SetDynamicMember(component, Name, value);
	}

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

	public override Type ComponentType
	{
		get { return typeof(object); }
	}

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

	public override Type PropertyType
	{
		get { return typeof(object); }
	}

	private static void SetDynamicMember(object obj, string memberName, object value)
	{
		var binder = Binder.SetMember(
			CSharpBinderFlags.None,
			memberName,
			obj.GetType(),
			new[]
				{
					CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
				});
		var callsite = CallSite<Action<CallSite, object, object>>.Create(binder);
		callsite.Target(callsite, obj, value);
	}

	private static object GetDynamicMember(object obj, string memberName)
	{
		var binder = Binder.GetMember(
			CSharpBinderFlags.None,
			memberName,
			obj.GetType(),
			new[]
				{
					CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
				});
		var callsite = CallSite<Func<CallSite, object, object>>.Create(binder);
		return callsite.Target(callsite, obj);
	}

Having a PropertyDescriptor is still not enough. WPF has to be told to use a PropertyDescriptor for a particular object. When setting the ItemsSource on a DataGrid, the source collection has to signal to the DataGrid that it should use a PropertyDescriptor to understand the shape of the contained object, i.e. use this metadata in order to auto-generate the columns.

In order to inform the DataGrid that items contained in a given collection have explicit metadata, the collection must implement the IList interface (most collections do) as well as the ITypedList interface, which few people have heard of. The ITypedList interface exposes two methods: GetListName, which can be ignored, and GetItemProperties. The GetItemProperties method returns a PropertyDescriptorCollection, which is the collection for all the PropertyDescriptors for an item in the collection, hence the name. In the example below it uses the first item in the collection and gets the dynamic member names from that, from the assumption that the items in the collection are the same (or at least should be displayed in the same way). However there is nothing to prevent you from using another way to resolve the PropertyDescriptors to return. The example code below, shows an implementation of the ITypedList built on to an ObservableCollection.

C#

public class DynamicItemCollection<T> : ObservableCollection<T>, IList, ITypedList
	where T : DynamicObject
{
	public string GetListName(PropertyDescriptor[] listAccessors)
	{
		return null;
	}

	public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
	{
		var dynamicDescriptors = new PropertyDescriptor[0];
		if (this.Any())
		{
			var firstItem = this[0];

			dynamicDescriptors =
				firstItem.GetDynamicMemberNames()
				.Select(p => new DynamicPropertyDescriptor(p))
				.Cast<PropertyDescriptor>()
				.ToArray();
		}

		return new PropertyDescriptorCollection(dynamicDescriptors);
	}
}

By using the DynamicObjectCollection as a datasource, the DataGrid will auto-generate the columns from the provided descriptors. Note however that there is a certain overhead when using DynamicObjects, so if possible, you are better off defining the shape of the data that you wish to expose.

Latest Tweets