WPF Dynamically Generated DataGrid
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).