Reputation: 3
In a WPF DataGrid I need to display multiple column based on the same complex base classe (which have sub properties) and be able to custom the display of the DataGridCell (like the background color) depending on a sub-binding properties different from the DataGridCell value to display. Here is an exemple to be clear :
<Window x:Class="Wpf_DataGrid_In_out_range.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:Wpf_DataGrid_In_out_range"
mc:Ignorable="d"
Title="MainWindow" Height="200" Width="250">
<Window.Resources>
<Style x:Key="InRangeStyle" TargetType="DataGridCell">
<Setter Property="HorizontalAlignment" Value="Center"></Setter>
<Setter Property="Background" Value="Orange"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsInRange}" Value="False" >
<Setter Property="Background" Value="Red"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsInRange}" Value="True" >
<Setter Property="Background" Value="Green"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="191*"/>
<ColumnDefinition Width="326*"/>
</Grid.ColumnDefinitions>
<DataGrid ItemsSource="{Binding}" AutoGenerateColumns="False" Grid.ColumnSpan="2">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding X}" CellStyle="{StaticResource InRangeStyle}" Header="X"></DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Y}" CellStyle="{StaticResource InRangeStyle}" Header="Y"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
List<MySample> samples = new List<MySample>();
samples.Add(new MySample(5, 25));
samples.Add(new MySample(25, 15));
samples.Add(new MySample(0, 0));
samples.Add(new MySample(15, 45));
DataContext = samples;
}
}
public class MySample
{
public RangeValue X { get; set; }
public RangeValue Y { get; set; }
public MySample(int x,int y)
{
X = new RangeValue(x, 1, 10);
Y = new RangeValue(y, 20, 40);
}
}
public class RangeValue
{
public int Value { get; set; }
public int Min { get; set; }
public int Max { get; set; }
public bool IsInRange
{
get
{
if (Value <= Max && Value >= Min) return true;
else return false;
}
}
public RangeValue(int value, int min, int max)
{
Value = value;
Min = min;
Max = max;
}
public override string ToString()
{
return Value.ToString("F2");
}
}
Thanks in advance. Rgds, Pascal.
Upvotes: 0
Views: 986
Reputation: 21
for generic auto-generation of custom columns 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;
}
I 'd suggest to make a facade for the data class, instances of which you intend(ed) to bind to 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:
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 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 XAML code will than 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}">
<TextBlock Text="{Binding Prop0.Enum}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:EnumContainer1}">
<TextBlock Text="{Binding Prop1.Enum}"/>
</DataTemplate>
</DataGrid.Resources>
</DataGrid>
</Grid>
</Window>
Note that you have to bind to individual properties in the data template. The reason is that the template control's data context is the containing instance. This is the reason why we need such a workaround. 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 cost certainly less time than I've spent when searching for a solution.
Regards, Vassili
Upvotes: 0
Reputation: 3
Thanks Andy, It works and that's good step forward. But is there any way to get it more generic like :
<DataGrid ItemsSource="{Binding}" AutoGenerateColumns="False" Grid.ColumnSpan="2">
<DataGrid.Resources>
<DataTemplate x:Key="InOutDataTemplate">
<Grid Style="{StaticResource InRangeStyle}">
<TextBlock Text="{Binding Value}"/>
</Grid>
</DataTemplate>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Header="X" ClipboardContentBinding="{Binding X}" CellEditingTemplate="{StaticResource InOutDataTemplate}"/>
<DataGridTemplateColumn Header="Y" ClipboardContentBinding="{Binding Y}" CellEditingTemplate="{StaticResource InOutDataTemplate}"/>
</DataGrid.Columns>
</DataGrid>
I did use ClipboardContentBinding because I didn't find any other 'binding stuff' or DataContext for DataGridTemplateColumn.
Rgds, Pascal.
Upvotes: 0
Reputation: 12276
The complication here is that the datacontext of a row is the entire object in your collection, you specify which property to bind to and see as the text. The cell doesn't "know" you intend it to be interested in that property as an object and use it's properties for anything. If you want the style to be re-usable then it has to somehow be directed to look at the property on X or Y.
One way to do that would be if it's got a datacontext of x or y. Make the style apply to a grid.
<Window.Resources>
<Style x:Key="InRangeStyle" TargetType="Grid">
<Setter Property="Background" Value="Orange"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding IsInRange}" Value="False" >
<Setter Property="Background" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsInRange}" Value="True" >
<Setter Property="Background" Value="Green"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
Then make sure you have a grid in your cell:
<DataGridTemplateColumn Header="X">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid DataContext="{Binding X}" Style="{StaticResource InRangeStyle}">
<TextBlock Text="{Binding Value}"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Since IsInrange is always red or green then you could just have one datatrigger for true ( or false ) and give it the default for the other state rather than orange.
Upvotes: 0