I am trying to display a 3-dimensional data in a WPF DataGrid
via DataTable
binding. Since DataGrid
only has rows and columns (2D), my idea is to template cells so that they host a ListView
(or some other control), and display the third dimension values stacked vertically.
The desired result is this:
Here is a heavily simplified but reproducible code with mock data, my attempt at for creating such a DataTable
:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ViewModel vm = new();
MyDataGrid.DataContext = vm.Table.DefaultView;
MyDataGrid.IsReadOnly = true;
}
public class ViewModel
{
public DataTable Table { get; init; }
public ViewModel()
{
Table = new DataTable();
Table.Columns.Add(" ");
Table.Columns.Add("c1");
Table.Columns.Add("c2");
Table.Columns.Add("c3");
for (int i = 1; i <= 3; i++ )
{
DataRow row = Table.NewRow();
row[0] = new List<object>() { "r" + i } ;
row[1] = new List<object>() { 10 * i, 20 * i, 25 * i } ;
row[2] = new List<object>() { 12 * i, 26 * i, 30 * i } ;
row[3] = new List<object>() { 16 * i, 28 * i, 36 * i };
Table.Rows.Add(row);
}
}
}
}
As you can see, I am assigning List to each cell, idea being that this creates the third dimension, storing multiple values in a single cell.
Problem is, I don't know how to template the DataGrid in the WPF markup so that this third dimension is correctly unpacked and displayed like in that image I've shown above. I tried this:
<DataGrid x:Name="MyDataGrid" ItemsSource="{Binding}">
<DataGrid.Resources>
<DataTemplate x:Key="DataGridTemplate">
<ListView ItemsSource="{Binding}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DataTemplate>
</DataGrid.Resources>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="ContentTemplate" Value="{StaticResource DataGridTemplate}"/>
</Style>
</DataGrid.CellStyle>
</DataGrid>
But when I compile and run it, DataGrid
just shows empty cells, even though there are no binding errors. If I don't use any template, cells show System.Data.DataRowView System.Collections.Generic.List
1[System.Object]`.
I would really appreciate it if anyone could take a look (the code I provided above is fully reproducible example), and let me know how to create the correct template? Or, if I'm going about this the wrong way, I'm open to other suggestions as well.
EDIT: clarification: in my full application, the number of columns and rows, and their header names is not known in advance, and might change during runtime, so the DataGrid
must be bound in a way that allows these changes to be reflected when the underlying DataTable
is modified or recreated.
There are a few issues with the code as you have it.
Type
for the complex DataTable
columnsIssue 1 is that your columns are not typed, and this seems to be resulting in DataRow["c1"]
etc showing as a string
value of the type System.Collections.Generic.List1[System.Int32]
rather than the collection itself. See the debugger screenshot below:
You will need to update those columns and add the types as shown below (note that I also named the first column so that we can reference it).
// row headers
Table.Columns.Add("c0", typeof(string));
// three-dimensional data
Table.Columns.Add("c1", typeof(IList));
Table.Columns.Add("c2", typeof(IList));
Table.Columns.Add("c3", typeof(IList));
DataGridTemplateColumn
to build the complex DataGrid
columnsThe second challenge is that I don't think that you can accomplish what you want with auto-generated columns. If I change your CellTemplate
to just bind to a TextBlock
, you can see that the data context for those cells is actually the whole DataRowView
object and not just the bound column:
<DataGrid.Resources>
<DataTemplate x:Key="DataGridTemplate">
<TextBlock Text="{Binding}" />
</DataTemplate>
</DataGrid.Resources>
Updating your DataGrid
as shown below, I was able to generate what I believe you are looking for.
<DataGrid x:Name="MyDataGrid"
ItemsSource="{Binding}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding c0}" />
<DataGridTemplateColumn Header="c1">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding c1}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="c2">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding c2}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="c3">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding c3}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
If your use case requires you to auto-generate columns, as mentioned in the comment, you can override the column generation for those specific columns. This approach is a little less clean and relies on building those columns in the code-behind. The example below references these two links:
How to: Customize Auto-Generated Columns in the DataGrid Control https://stackoverflow.com/a/1755556/15534202
XAML: Add an auto-generation event handler
<DataGrid x:Name="MyDataGrid"
ItemsSource="{Binding}"
AutoGenerateColumns="True"
AutoGeneratingColumn="MyDataGrid_AutoGeneratingColumn">
</DataGrid>
Code Behind: Override generation of List
columns
private void MyDataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
// Optional: Remove the column header for the row headers
if (e.Column.Header == "c0") e.Column.Header = "";
// Let the non-list data auto-generate normally
if (!e.PropertyType.IsAssignableFrom(typeof(IList))) return;
// create a new column
var newColumn = new DataGridTemplateColumn();
newColumn.Header = e.Column.Header;
// create a new binding
var binding = new Binding(e.PropertyName);
binding.Mode = BindingMode.TwoWay;
// setup the cell template
var elementFactory = new FrameworkElementFactory(typeof(ListBox));
elementFactory.SetValue(ListBox.ItemsSourceProperty, binding);
var newCellTemplate = new DataTemplate();
newCellTemplate.VisualTree = elementFactory;
newColumn.CellTemplate = newCellTemplate;
// assign the new column
e.Column = newColumn;
}