Filling a Datagrid with dynamic Columns
I'm currently using another approach. I am not sure if it is right to do it like this, but it works. I made a small sample.
Keep in mind that for this to work, every entry in the Datagrid needs to have the same dynamic columns, making it a bit less flexible. But if you have entries with different amounts of columns in each entry, then a Datagrid is probably the wrong Approach anyways.
These are my classes:
public class Person
{
public ObservableCollection<Activity> Hobbys { get; set; }
public string Name { get; set; }
}
public class Activity
{
public string Name { get; set; }
}
And this is the Code Behind:
public MainWindow()
{
InitializeComponent();
DataContext = this;
ObservableCollection<Activity> hobbys = new ObservableCollection<Activity>();
hobbys.Add(new Activity() { Name = "Soccer" });
hobbys.Add(new Activity() { Name = "Basketball" });
Community = new ObservableCollection<Person>();
Community.Add(new Person() { Name = "James", Hobbys = hobbys });
Community.Add(new Person() { Name = "Carl", Hobbys = hobbys });
Community.Add(new Person() { Name = "Homer", Hobbys = hobbys });
MyGrid.Columns.Add(new DataGridTextColumn() { Header = "Name", Binding = new Binding("Name") }); //Static Column
int x = 0;
foreach (Activity act in Community[0].Hobbys) //Create the dynamic columns
{
MyGrid.Columns.Add(new DataGridTextColumn() { Header = "Activity", Binding = new Binding("Hobbys["+x+"].Name") });
x++;
}
}
And in the .XAML is simply:
<DataGrid Name="MyGrid" ItemsSource="{Binding Community}" AutoGenerateColumns="False"/>
There are at least three ways of doing this:
- Modify the DataGrid's columns manually from code-behind
- Use a DataTable as the ItemsSource *
Use a CustomTypeDescriptor
*recommended for simplicity
1st approach: use code-behind to generate the DataGrid's columns at runtime. This is simple to implement, but maybe feels a bit hackish, especially if you're using MVVM. So you'd have your DataGrid with fixed columns:
<DataGrid x:Name="grid">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding id}" Header="id" />
<DataGridTextColumn Binding="{Binding image}" Header="image" />
</DataGrid.Columns>
</DataGrid>
When you have your "Names" ready, then modify the grid by adding/removing columns, eg:
// add new columns to the data grid
void AddColumns(string[] newColumnNames)
{
foreach (string name in newColumnNames)
{
grid.Columns.Add(new DataGridTextColumn {
// bind to a dictionary property
Binding = new Binding("Custom[" + name + "]"),
Header = name
});
}
}
You'll want to create a wrapper class, which should contain the original class, plus a dictionary to contain the custom properties. Let's say your main row class is "User", then you'd want a wrapper class something like this:
public class CustomUser : User
{
public Dictionary<string, object> Custom { get; set; }
public CustomUser() : base()
{
Custom = new Dictionary<string, object>();
}
}
Populate the ItemsSource
with a collection of this new "CustomUser" class:
void PopulateRows(User[] users, Dictionary<string, object>[] customProps)
{
var customUsers = users.Select((user, index) => new CustomUser {
Custom = customProps[index];
});
grid.ItemsSource = customUsers;
}
So tying it together, for example:
var newColumnNames = new string[] { "Name1", "Name2" };
var users = new User[] { new User { id="First User" } };
var newProps = new Dictionary<string, object>[] {
new Dictionary<string, object> {
"Name1", "First Name of First User",
"Name2", "Second Name of First User",
},
};
AddColumns(newColumnNames);
PopulateRows(users, newProps);
2nd approach: use a DataTable. This makes use of the custom-type infrastructure under the hood, but is easier to use. Just bind the DataGrid's ItemsSource
to a DataTable.DefaultView
property:
<DataGrid ItemsSource="{Binding Data.DefaultView}" AutoGenerateColumns="True" />
Then you can define the columns however you like, eg:
Data = new DataTable();
// create "fixed" columns
Data.Columns.Add("id");
Data.Columns.Add("image");
// create custom columns
Data.Columns.Add("Name1");
Data.Columns.Add("Name2");
// add one row as an object array
Data.Rows.Add(new object[] { 123, "image.png", "Foo", "Bar" });
3rd approach: make use of the extensibility of .Net's type system. Specifically, use a CustomTypeDescriptor
. This allows you to create a custom type at runtime; which in turn enables you to tell the DataGrid that your type has the properties "Name1", "Name2", ... "NameN", or whatever others you want. See here for a simple example of this approach.
2nd approach: use a DataTable. This makes use of the custom-type infrastructure under the hood, but is easier to use. Just bind the DataGrid's ItemsSource to a DataTable.DefaultView property:
This almost worked but instead of binding to the DataTable.DefaultView property property I created a property of type DataView and bound to that.
<DataGrid ItemsSource="{Binding DataView, Mode=TwoWay}" AutoGenerateColumns="True" />
This allows the binding to be two way, binding to the DataTable.DefaultView cannot be a TwoWay binding. In the View Model
public DataView DataView
{
get { return _dataView; }
set
{
_dataView = value;
OnPropertyChanged("DataView");
}
}
With this setup I could not only define the columns dynamically when the View Model is initialized, but could update and change the data table dynamically at any time. In using the approach as defined by McGarnagle above, the view schema was not refreshing when the DataTable was updated with a new data source.