WPF Dynamically Generated DataGrid

This is an old post and doesn't necessarily reflect my current thinking on a topic, and some links or images may not work. The text is preserved here for posterity.

When building an application, I might not have the luxury of creating model classes to represent the objects I'll be rendering on screen, because they are dynamic. If I don't know the shape of my data, I can use a meta model (a model of the final model) to represent it.

Some examples of where this exist are:

  • SharePoint - users can define custom lists with custom columns, so a particular ListItem has many "properties"
  • Microsoft CRM - users can define custom entities, again with custom attributes

For example, I might have the concept of a Property:

public class Property : INotifyPropertyChanged
{
    public Property(string name, object value)
    {
        Name = name;
        Value = value;
    }

    public string Name { get; private set; }
    public object Value { get; set; }
}

And the concept of a Record, which is really just a bag of properties:

public class Record
{
    private readonly ObservableCollection<Property> properties = new ObservableCollection<Property>();

    public Record(params Property[] properties)
    {
        foreach (var property in properties)
            Properties.Add(property);
    }

    public ObservableCollection<Property> Properties
    {
        get { return properties; }
    }
}

Here's how I might fill the data:

var records = new ObservableCollection<Record>();
records.Add(new Record(new Property("FirstName", "Paul"), new Property("LastName", "Stovell")));
records.Add(new Record(new Property("FirstName", "Tony"), new Property("LastName", "Black")));

Rendering basic columns in a DataGrid

Building a DataGrid to render this model is pretty easy. The XAML would be:

<DataGrid 
   Name="dataGrid" 
   AutoGenerateColumns="false" 
   ItemsSource="{Binding Path=Records}" 
   />

Since I don't know the names of the columns at design time, I'll have to dynamically generate them. This part is easy:

var columns = records.First()
    .Properties
    .Select((x, i) => new {Name = x.Name, Index = i})
    .ToArray();

foreach (var column in columns)
{
    var binding = new Binding(string.Format("Properties[{0}].Value", column.Index));

    dataGrid.Columns.Add(new DataGridTextColumn() {Header = column.Name, Binding = binding });
} 

As you can see, I dynamically create a Binding, and use the index of the column in my model as the binding path.

Rendering templated columns

This part gets harder. If I wanted to use a custom CellTemplate to render my properties, I might have done this in XAML:

<DataGridTemplateColumn>
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <Border Padding="3" Background="Purple">
                <TextBox Text="{Binding Path=FirstName}" />
            </Border>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

Notice I hardcoded Path=FirstName in the binding above - that's not going to work when we have a dynamic model.

It took a lot of experimentation to make this happen, but the result isn't much more complicated. I'd start by making a DataTemplate for the cell as a resource:

<Window.Resources>

    <DataTemplate x:Key="CustomTemplate">
        <Border Padding="3" Background="Purple">
            <TextBox Text="{Binding Path=Value}" />
        </Border>
    </DataTemplate>

</Window.Resources>

I'd then dynamically generate the columns like this:

foreach (var column in columns)
{
    var binding = new Binding(string.Format("Properties[{0}]", column.Index));
    dataGrid.Columns.Add(new CustomBoundColumn() 
    { 
        Header = column.Name, 
        Binding = binding, 
        TemplateName = "CustomTemplate" 
    });
} 

The part that makes it work is the CustomBoundColumn - I had to implement this myself. Here it is:

public class CustomBoundColumn : DataGridBoundColumn
{
    public string TemplateName { get; set; }

    protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
    {
        var binding = new Binding(((Binding)Binding).Path.Path);
        binding.Source = dataItem;

        var content = new ContentControl();
        content.ContentTemplate = (DataTemplate)cell.FindResource(TemplateName);
        content.SetBinding(ContentControl.ContentProperty, binding);
        return content;
    }

    protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
    {
        return GenerateElement(cell, dataItem);
    }
}

Note that the name of the DataTemplate is passed as a property to the CustomBoundColumn, so you could dynamically choose a DataTemplate to use based on the type of property (e.g., use a different template if the value is numeric).