Auto-generating multiple custom columns in a WPF DataGrid in case where the data class has multiple properties of the same type doesn’t work like one would have expected because the custom control’s data context is not the property one intends to bind to, but the containing instance. So, the custom control or its view model doesn't know to which of the same-type properties to bind.
Introduction
There is an issue when auto-generating multiple custom columns in a WPF DataGrid
in case where the data class has multiple properties of the same type (like Item
here below).
public class Item
{
public Enum Prop0 { get; set; } = Enum.CustomEnum1;
public Enum Prop1 { get; set; } = Enum.CustomEnum2;
}
The reason why it doesn’t work like one would have expected is that the custom control’s data context is not the property one intends to bind to (Prop0
or Prop1
) but their containing instance (an instance of Item
in our case). So, if you auto-generate custom columns, you cannot tell the custom control or its view model which of the two properties to bind to.
Workaround
A workaround I'd suggest, consists in making a facade for the data class, instances of which you intend(ed) to bind to in the data grid. The facade class would basically replicate the initial data class with the exception that the properties of the same type, which require custom date cells, would have individual types in the containing facade:
public class ItemFacade
{
private readonly Item item;
public ItemFacade(Item item) => this.item = item;
public EnumContainer0 Prop0
{
get => new EnumContainer0 { Enum = this.item.Prop0 };
set => this.item.Prop0 = value.Enum;
}
public EnumContainer1 Prop1
{
get => new EnumContainer1 { Enum = this.item.Prop1 };
set => this.item.Prop1 = value.Enum;
}
}
These individual specific types can either be sub-classes of the original property's type in question, if it is possible, or be sub-classes of a facade type thereof:
public class EnumContainer
{
public Enum Enum { get; set; }
}
public class EnumContainer0 : EnumContainer { }
public class EnumContainer1 : EnumContainer { }
You can even consider implicit cast in that case :).
The related XAML code will then look as below:
Window x:Class="TestWpfDataGrid.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestWpfDataGrid"
xmlns:i="clr-namespace:System.Windows.Interactivity;
assembly=System.Windows.Interactivity"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<DataGrid ItemsSource="{Binding Items}">
<i:Interaction.Behaviors>
<local:ColumnHeaderBehaviour/>
</i:Interaction.Behaviors>
<DataGrid.Resources>
<DataTemplate DataType="{x:Type local:EnumContainer0}">
<StackPanel>
<TextBlock Text="Custom "/>
<TextBlock Text="{Binding Prop0.Enum}"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type local:EnumContainer1}">
<StackPanel>
<TextBlock Text="Custom "/>
<TextBlock Text="{Binding Prop1.Enum}"/>
</StackPanel>
</DataTemplate>
</DataGrid.Resources>
</DataGrid>
</Grid>
</Window>
Note that you must bind to individual properties in the data templates, which is now possible as the individual properties have their own types and thus can have individual data templates.
The attached behavior in local:ColumnHeaderBehaviour
can be as follows:
public class ColumnHeaderBehaviour : Behavior<DataGrid>
{
protected override void OnAttached()
{
AssociatedObject.AutoGeneratingColumn += OnGeneratingColumn;
}
protected override void OnDetaching()
{
AssociatedObject.AutoGeneratingColumn -= OnGeneratingColumn;
}
private static void OnGeneratingColumn(object sender,
DataGridAutoGeneratingColumnEventArgs eventArgs)
{
if (eventArgs.PropertyDescriptor is PropertyDescriptor descriptor)
{
var control = (DataGrid)sender;
var resourceDictionary = control.Resources;
var dataTemplate = resourceDictionary.Values
.OfType<DataTemplate>()
.Where(el => (Type)el.DataType == descriptor.PropertyType)
.FirstOrDefault();
if (dataTemplate != null)
{
var column = new DataGridTemplateColumn()
{
CellTemplate = dataTemplate,
};
eventArgs.Column = column;
}
eventArgs.Column.Header = descriptor.DisplayName ?? descriptor.Name;
}
else
{
eventArgs.Cancel = true;
}
}
}
Note that the above behavior code works if the data templates are specified within the related data grid.
This facade class is fully testable. Typing it down and the tests thereof would certainly cost me less time than I've spent when searching for a solution.
Conclusion
A simple facade for the data object, just to have its properties typed differently, won't be sufficient. It's all about custom data cells, isn't it? So, there will be a control in the custom data cell, that will display and probably change the related property. So, it should also be aware if the property changes. In this way, we come to an idea that:
- the data object facade should be a view model, and
- the properties' facades should be view models too.
Moreover, it would be more secure to always use a view model as a data grid context.
History
- 1st April, 2020: Initial version