I have a question about data binding DataGrid in WPF. I am using the VS 2010 Beta 2 which has its own DataGrid, not the Toolkit one, although I think it is pretty much the same.
I want to bind to a dataset which has 52 columns, one for every week of the year. For this reason I want to bind the data dynamically rather than specifying each field. The value for each field is true or false depending on some condition. Based on this value I want to show an image in the cell template if the condition is true and hide it if the condition is not true.
My problem is that all the examples of using templates that I have found refer to the case of fixed, predefined fields, where you can have a binding like Text ="{Binding UserName}". This is no good to me because I don't know what the field names will be at design time.
I have made up a simplified example which illustrates the problem. In this example a data table is generated which contains true and false values. The Image in my template is never visible. How would I make it invisible depending on the true or false value in the data?
<Window.Resources>
<!--This is the bit that doesn't work...-->
<Style TargetType="{x:Type Image}" x:Key="HideWhenFalse">
<Setter Property="Visibility" Value="Hidden" />
<Style.Triggers>
<DataTrigger
Binding="{Binding Path=???}"
Value="True"> <!--What to put for the path? -->
<Setter Property="Visibility">
<Setter.Value>
Visible
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<!--Up to here-->
<Style x:Key="{x:Type DataGridCell}" TargetType="{x:Type DataGridCell}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<Image Source="Images/tick.bmp" Style="{StaticResource HideWhenFalse}">
</Image>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<DataGrid
x:Name="myDataGrid"
AutoGenerateColumns="True" >
</DataGrid>
</Grid>
Code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataTable dtTable = new DataTable();
dtTable.Columns.Add("A", typeof(Boolean));
dtTable.Columns.Add("B", typeof(Boolean));
dtTable.Columns.Add("C", typeof(Boolean));
dtTable.Columns.Add("D", typeof(Boolean));
dtTable.Columns.Add("E", typeof(Boolean));
dtTable.Columns.Add("F", typeof(Boolean));
for (int i = 0; i < 5; i++)
{
object[] oValues = new Object[dtTable.Columns.Count];
for (int j = 0; j < dtTable.Columns.Count; j++)
{
oValues[j] = (j % 2 == 1) ? true : false;
}
dtTable.Rows.Add(oValues);
}
myDataGrid.ItemsSource = dtTable.DefaultView;
myDataGrid.Items.Refresh();
}
}
NB This is probably obvious and I am approaching the problem in completely the wrong way. Here is a confession: I have been trying to get my head around WPF for a couple of months now and I still seem to find myself approaching EVERY problem the wrong way. I hope the penny drops soon.
You can use a MultiBinding, with a first binding taking the actual data context from the cell (that would be the row), and the second one taking the column. From there, you can retrieve the cell value.
Converter code:
public class RowColumnToCellConverter : IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
DataRowView row = values[0] as DataRowView;
DataGridColumn column = values[1] as DataGridColumn;
return row != null && column != null
? row[column.SortMemberPath]
: DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
throw new NotSupportedException();
}
}
XAML:
<Style x:Key="{x:Type DataGridCell}" TargetType="{x:Type DataGridCell}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<TextBlock x:Name="TextOK" Text="OK" Visibility="Collapsed" />
</StackPanel>
<ControlTemplate.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource RowColumnToCellConverter}">
<Binding />
<Binding RelativeSource="{x:Static RelativeSource.Self}" Path="Column" />
</MultiBinding>
</DataTrigger.Binding>
<Setter TargetName="TextOK" Property="Visibility" Value="Visible" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I used a TextBlock instead an image for testing, but the code will be the same. Just avoid defining a style for the image if that can be done directly in the DataGridCell's style.
Related
I'm using this DataTrigger:
<Window x:Class="_11_5_Style_demo4_DataTrigger.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:_11_5_Style_demo4_DataTrigger"
mc:Ignorable="d"
Title="MainWindow" Height="130" Width="300">
<Window.Resources>
<local:L2BConverter x:Key="cvtr"/>
<!--TextBox DataTrigger-->
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding
RelativeSource={x:Static RelativeSource.Self},
Path=Text.Length,
Converter={StaticResource cvtr},
UpdateSourceTrigger=PropertyChanged}"
Value="false">
<DataTrigger.Setters>
<Setter Property="BorderBrush" Value="Red"/>
<Setter Property="BorderThickness" Value="1"/>
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel>
<TextBox Margin="5"/>
<TextBox Margin="5,0"/>
<TextBox Margin="5"/>
</StackPanel>
</Window>
I expected that the TextBox will have the red border when typed more than 6 characters, and UpdateSourceTrigger=PropertyChanged just not work when there are more than 6 characters in the TextBox. It updated only when lost focus.
Here is the Converter:
public class L2BConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
int textLength = (int)value;
return textLength < 6 ? true : false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I've googled, but didn't find relative problem. Can anybody explains why this not work, am I not using it right?
Your trigger is working correctly, but your expectations of the result of its work are not correct. The trigger correctly sets the border brush, but the problem is that the color of the border while the TextBox has focus is not taken from the TextBox.BorderBrush brush, but from the TextBox Template constant. And you can't change it with a trigger. You need to change the template of the TextBox itself or apply another way to solve your problem.
You can make sure that the trigger works correctly, for example, by changing the frame thickness:
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding
RelativeSource={x:Static RelativeSource.Self},
Path=Text.Length,
Converter={StaticResource cvtr}}"
Value="false">
<DataTrigger.Setters>
<Setter Property="BorderBrush" Value="Red"/>
<Setter Property="BorderThickness" Value="10"/>
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
</Style>
Another way to implement such validation is to use a ValidationRule. But its use is only possible in a binding that you don't have. You can use a little "voodoo magic" for this:
public class LengthValidate : ValidationRule
{
public LengthValidate() :base(ValidationStep.UpdatedValue, true) { }
public int LengthLimit { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
int limit = LengthLimit;
if(value is BindingExpression expression)
{
value = expression.GetSourceValue();
}
if (value is not string text)
{
text= value?.ToString()?? string.Empty;
}
return text.Length <= limit
? ValidationResult.ValidResult
: new ValidationResult(false, $"String length exceeds limit={limit}.");
}
}
The GetSourceValue method from the BindingExpressionHelper class is used.
Style with this rule:
<Style TargetType="TextBox">
<Setter Property="Tag">
<Setter.Value>
<Binding Path="Text" RelativeSource="{RelativeSource Self}">
<Binding.ValidationRules>
<local:LengthValidate LengthLimit="6"/>
</Binding.ValidationRules>
</Binding>
</Setter.Value>
</Setter>
</Style>
I have a ListView, with its items represented by an ItemTemplate like so:
<ListView dependencyObjects:InterestingItem.Interesting="{Binding InterestingItem}"
ItemsSource="{Binding Quotations}" >
<ListView.ItemTemplate>
<DataTemplate>
<Border>
<Grid>
<StackPanel x:Name="NotImportant">
</StackPanel>
<Grid x:Name="HiddenGrid"
Background="Red"
Visibility="Hidden" >
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<Grid.Triggers>
<DataTrigger Binding="{Binding Path=DataContext.InterestingItem,
RelativeSource={RelativeSource AncestorType={x:Type ListView }}}"
Value="*this instance here*!">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Grid.Triggers>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>
</Grid>
</Border>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The ListView has an attached property InterestingItem that is one of the items in the ListView.
What I can't hook up is when the InterestingItem is the same as one of the items, the second Grid should become visible.
I would prefer not to change and bind to the actual objects in the list - but rather have the ListView control which item is to be altered.
What is the Value in the DataTrigger that I need?
There are multiple issues in your XAML and conceptually that prevent it from working.
To bind attached properties, you have to use the correct syntax with parentheses.
Path="{Binding (local:InterestingItem.Interesting), RelativeSource={RelativeSource AncestorType={x:Type ListView}}}"
See the Binding path syntax documenation for reference.
The Triggers property does only support EventTriggers, see FrameworkElement.Triggers.
Note that the collection of triggers established on an element only supports EventTrigger, not property triggers (Trigger). If you require property triggers, you must place these within a style or template and then assign that style or template to the element either directly through the Style property, or indirectly through an implicit style reference.
You cannot bind the Value property of DataTrigger, since it is not a dependency property.
You could of course change the bound type to expose a property that indicates if it is a special object or not and bind that in XAML using a DataTrigger, similar to this (where IsSpecial is the new bool property).
<Grid x:Name="HiddenGrid"
Background="Red">
<TextBlock Text="Hidden Grid"/>
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Setter Property="Visibility" Value="Hidden"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsSpecial}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>
If you want to stick to your current approach, you could create a custom IMultiValueConverter that enables binding multiple properties. It would check if all of the bound values are equal and return Visibility.Visible or Visibility.Hidden otherwise. This example uses Linq to check this and supports an arbitrary number of values bound, but there are many other options.
public class EqualityToVisibilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values is null || values.Length < 2)
return Binding.DoNothing;
return values.Distinct().Count() == 1 ? Visibility.Visible : Visibility.Hidden;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
Next, instantiate the converter in the resources of the ListView or any other resource dictionary in scope and bind the Visibility property of the Grid to both the current item (just <Binding/>) and the attached property local:InterestingItem.Interesting with a MultiBinding that uses the converter to convert them to a Visibility.
<ListView local:InterestingItem.Interesting="{Binding InterestingItem}"
ItemsSource="{Binding Quotations}">
<ListView.Resources>
<local:EqualityToVisibilityConverter x:Key="EqualityToVisibilityConverter"/>
</ListView.Resources>
<ListView.ItemTemplate>
<DataTemplate>
<Border>
<Grid>
<StackPanel x:Name="NotImportant">
<TextBlock Text="Not Important"/>
</StackPanel>
<Grid x:Name="HiddenGrid"
Background="Red">
<Grid.Visibility>
<MultiBinding Converter="{StaticResource EqualityToVisibilityConverter}">
<Binding/>
<Binding Path="(local:InterestingItem.Interesting)"
RelativeSource="{RelativeSource AncestorType={x:Type ListView}}"/>
</MultiBinding>
</Grid.Visibility>
<TextBlock Text="Hidden Grid"/>
</Grid>
</Grid>
</Border>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
There are two other things to note here. I have added two dummy TextBlocks, otherwise the result will not be visible, as the panels are empty. Replace them with your content. Furthermore, both the StackPanel and the Grid are overlapping in the parent Grid, I do not know if this is intentional or not, but you can change it by adding rows or columns and moving the elements there.
What is the Value in the DataTrigger that I need?
I am afraid XAML has no support for something like the this keyword in C#.
You may use a MultiBinding with an IMultiValueConverter implementation that determines whether the items are equal:
<Grid x:Name="HiddenGrid" Background="Red">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Hidden" />
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding>
<MultiBinding.Converter>
<local:MultiConverter />
</MultiBinding.Converter>
<Binding Path="{Binding Path=DataContext.InterestingItem,
RelativeSource={RelativeSource AncestorType={x:Type ListView }}}" />
<Binding Path="{Binding}" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>
Converter:
public class MultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) =>
values != null && values.Length == 2 && values[0] == values[1];
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
Note that you cannot set the Visibility property of the Grid to a local value if you want to be able to override the value using a Style setter.
<Grid x:Name="HiddenGrid" Background="Red" Visibility="Hidden">
(before anyone asks, the title is right: I have code working only at design time in Blend, while the most common by far would be the opposite :o )
While designing a DataTemplate in Expression Blend, I can see my DataTriggers working fine, my sample ViewModel generates a random value for the level of a battery, and both border width and background color display accordingly, either in the BatteryLevelTemplate itself, and another panel containing a lot of devices with their respective (random) battery level, with a design-time DataContext.
Here is a screenshot from Expression Blend:
And here a screenshot from the running application. Notice that, while both use exactely the same class as DataContext (but as design time in Blend), at runtime only the default RedBattery color setter is applied, even if the value itself (which also affects width) varies:
And here are the relevant code parts:
<Border.Width>
<MultiBinding Converter="{StaticResource NormalValueConverter}" FallbackValue="10">
<Binding Path="NívelBateria"/>
<Binding Path="ActualWidth" ElementName="BatteryChargeContainer"/>
</MultiBinding>
</Border.Width>
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{StaticResource BatteryRed}"/>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding NívelBateria, Converter={StaticResource ValorMaiorQue}, ConverterParameter=0.25}" Value="True">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{StaticResource BatteryOrange}"/>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding NívelBateria, Converter={StaticResource ValorMaiorQue}, ConverterParameter=0.5}" Value="True">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{StaticResource BatteryYellow}"/>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding NívelBateria, Converter={StaticResource ValorMaiorQue}, ConverterParameter=0.75}" Value="True">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{StaticResource BatteryGreen}"/>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
============
<DockPanel x:Name="PainelSetupsSensores" Background="#FFB8E6E8"/>
<DockPanel x:Name="PainelSensoresDisponiveis" Background="#FFF0F0F0"
Grid.RowSpan="2" Grid.Column="1"
DataContext="{Binding ReceiverAtivo}"
d:DataContext="{d:DesignInstance Type=local:ReceiverSimulado, IsDesignTimeCreatable=True}">
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Sensores}" Margin="10">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</DockPanel>
====================
class ValorMaiorQue : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double valor = (double)value;
double limite = double.Parse((string)parameter);
return valor > limite;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
UPDATE (using the valuable Snoop tip by Contango):
I "snooped" the BatteryCharge (Border) element, and found out an interesting thing:
The Width property, which is affected by a Multi Value Element Binding, is working fine and displays in a "Local", green-shaded row;
On the other hand, the Background property, which is not working, displays unsurprisingly as Style with the default red value. This one is not being "DataTriggered".
My doubt now is how I am supposed to use Snoop (or anything else) to find out why the DataTrigger is not being applied.
I discovered the problem "accidentally", and here goes the explanation:
I installed Snoop, TriggerTracing and also WPF Inspector to check properties applied by my DataTriggers, and found out the comparison provided by the DataConverter was always False;
Then I put a breakpoing inside the DataConverter, to discover that, for example, the string "0.75" provided by ConverterParameter was being Double.Parsed as 75.0;
Then I realized that my current language is pt-BR, and the decimal separator is comma instead of dot. Then I changed the converter, adding an InvariantCulture parameter to Double.Parse. And now it works!
double limite = double.Parse((string)parameter, CultureInfo.InvariantCulture);
Your run time DataContext is not being set correctly, so your code can't bind to the correct properties at runtime.
Note that the run time DataContext is completely separate to the design time DataContext, and uses different XAML to setting the runtime DataContext.
I would recommend using Snoop to fix the problem, you can use it to flag up binding errors due to a bad runtime DataContext, see my answer here:
ReSharper WPF error: "Cannot resolve symbol "MyVariable" due to unknown DataContext"
In the previous question of mine, I have asked how to hide an empty TextBlock, so that it doesn't take space in the panel. I have a new challenge now. How am I supposed to hide an empty Hyperlink:
<TextBlock>
<Hyperlink
NavigateUri="{Binding Path=Email}"
RequestNavigate="Hyperlink_RequestNavigate">
<TextBlock Text="{Binding Path=Email}" />
</Hyperlink>
</TextBlock>
This is what made the hiding possible in the previous question:
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="Text" Value="">
<Setter Property="Visibility" Value="Collapsed" />
</Trigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
Providing this style on first TextBlock won't work because the Text property is not set. Providing style on Hyperlink doesn't hide the parent TextBlock and same happens if I try to hide the TextBlock inside the Hyperlink.
I am certain that my style needs to be applied on the Hyperlink, but the trigger inside should target the Visibility property of the 'Hyperlink's parentTextBlock`. What is the style supposed to look like?
Just use DataTrigger on the top level TextBlock to check whether the bound property is an empty string:
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Email}" Value="">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
EDIT:
Also you can try binding to the child hyperlink's NavigationUri property:
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Inlines
[0].NavigateUri}" Value="">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
The solution provided by Foovanadil that solves the similar issue by implementing visibility converter works the best in my opinion. It is the easiest to implement and can be reused whenever needed.
The converter should be implemented like this:
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
public class VisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
if (string.IsNullOrEmpty(value as string))
{
return Visibility.Collapsed;
}
else
{
return Visibility.Visible;
}
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
And used like this:
<Window.Resources>
<!-- Visibility converter -->
<converters:VisibilityConverter x:Key="visibleConv" />
</Window.Resources>
...
<TextBlock Visibility="{Binding Something, Converter={StaticResource visibleConv}}">
<Hyperlink NavigateUri="{Binding Something}">
<TextBlock Text="{Binding Something}" />
</Hyperlink>
</TextBlock>
All credits go to the original solution provider: Foovanadil
I am trying to change the default style of a DataGridCell (within a WPF Toolkit DataGrid) when there is a validation error. The default is a red border. How can I put my own template?
Thanks.
Try this:
<!-- Cell Style -->
<Style x:Key="CellErrorStyle" TargetType="{x:Type TextBlock}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
<Setter Property="Background" Value="Yellow"/>
</Trigger>
</Style.Triggers>
</Style>
And use it:
<DataGrid.Columns>
<DataGridTextColumn
ElementStyle="{StaticResource CellErrorStyle}">
</DataGridTextColumn>
</DataGrid.Columns>
There's a nice tutorial from Diederik Krols that does exactly what you're asking for the WPF Toolkit DataGrid.
One kind of solution is below, but first, let me share my findings.
It seems like the validation errors never reach inside the column's ElementStyle or CellStyle. The reason I suspect this is because it reaches and can be used in the column's EditingElementStyle and the datagrid's RowStyle.
For example, you can set the style based on Validation.HasError:
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
or you can set the Validation.ErrorTemplate as well:
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<Border BorderBrush="Red" BorderThickness="3">
<AdornedElementPlaceholder />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.RowStyle>
and both work just fine. Same on the EditingElementStyle. Neither of these really solves the problem: changing the row style obviously doesn't show which cell the error is in, and the editing style is not visible once the text box is defocused.
Unfortunately, for some reason, the same method doesn't work on the ElementStyle or the CellStyle. I'm inclined to believe this is a bug because in this tutorial it says after showing an example of setting a Validation.HasError triggered style on the EditingElementStyle:
You can implement more extensive customization by replacing the
CellStyle used by the column.
The solution
One workaround is not to use a trigger but rather bind the background (or whatever style property you want) of the cell to a new property of the data object. I'll show what I mean.
In this example, there are products, they have a category, which will be displayed in a text column in the datagrid. Here's the XAML:
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Products}">
<DataGrid.Columns>
<!-- other columns -->
<DataGridTextColumn Header="Category">
<DataGridTextColumn.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Background"
Value="{Binding Mode=OneWay, Path=CategoryErrorBackgroundColor}" />
</Style>
</DataGridTextColumn.CellStyle>
<DataGridTextColumn.Binding>
<Binding Path="Category" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
<!-- other columns -->
</DataGrid.Columns>
</DataGrid>
And here's the Product class:
public class Product : INotifyPropertyChanged
{
// ...other fields and properties
private string category;
private SolidColorBrush categoryErrorBackgroundColor;
public string Category
{
get
{
return category;
}
set
{
// validation checks
if (value.Lenght < 5)
{
CategoryErrorBackgroundColor = Brushes.Red;
// Notice that throwing is not even necessary for this solution to work
throw new ArgumentException("Category cannot be shorter than 5 characters.");
}
else
{
CategoryErrorBackgroundColor = Brushes.Transparent;
}
category = value;
}
}
// This is the property I'm binding to the cell's background
// It has to have the appropriate type
public SolidColorBrush CategoryErrorBackgroundColor
{
get
{
return categoryErrorBackgroundColor;
}
set
{
categoryErrorBackgroundColor = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Obviously, the huge downside of this solution is that it requires one (or more if you want more complex styles) style property for every property in the data object, and it needs a lot of manual settings of these properties. But it is kind of a solution nevertheless.