Reputation: 4923
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.
Upvotes: 6
Views: 17946
Reputation: 222
It's surprising how complicated it can be to set up something as simple as a background color for error indications.
I'm working with INotifyDataErrorInfo and ended up with a solution similar to the one proposed here.
Attached property:
internal static class DataGridColumnProperties
{
public static readonly DependencyProperty BindedPropertyNameProperty = DependencyProperty.RegisterAttached("BindedPropertyName", typeof(string), typeof(DataGridColumnProperties), new PropertyMetadata(string.Empty));
public static string GetBindedPropertyName(DependencyObject dependencyObject)
{
return (string)dependencyObject.GetValue(BindedPropertyNameProperty);
}
public static void SetBindedPropertyName(DependencyObject dependencyObject, string value)
{
dependencyObject.SetValue(BindedPropertyNameProperty, value);
}
}
Now, I can set it for columns like this:
<DataGridTemplateColumn myNamespace:DataGridColumnProperties.BindedPropertyName="MyPropertyName">
DataGridCell Styles:
<Style x:Key="DataGridCellStyle" TargetType="{x:Type DataGridCell}">
<EventSetter Event="Loaded" Handler="DataGridCell_Loaded"/>
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style x:Key="DataGridCellErrorStyle" TargetType="{x:Type DataGridCell}" BasedOn="{StaticResource DataGridCellStyle}">
<Setter Property="Background" Value="Red"/>
</Style>
DataGridCell Loaded Handler:
private void DataGridCell_Loaded(object sender, RoutedEventArgs e)
{
if (sender is not DataGridCell dataGridCell)
{
return;
}
if (dataGridCell.DataContext is not INotifyDataErrorInfo notifyDataErrorInfo)
{
return;
}
var propertyName = DataGridColumnProperties.GetBindedPropertyName(dataGridCell.Column);
if (string.IsNullOrEmpty(propertyName))
{
return;
}
notifyDataErrorInfo.ErrorsChanged += errorChangedHandler;
void errorChangedHandler(object? _, DataErrorsChangedEventArgs eventArgs)
{
if (eventArgs.PropertyName == propertyName)
{
var errors = notifyDataErrorInfo.GetErrors(propertyName).Cast<object>();
dataGridCell.Style = errors.Any()
? (Style)Application.Current.Resources["DataGridCellErrorStyle"]
: (Style)Application.Current.Resources["DataGridCellStyle"];
}
}
}
This approach allows you to apply an entire style rather than just a single property (like the background). This can be useful if you want to adjust other elements, such as the template, borders, and so on.
Upvotes: 0
Reputation: 69
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.
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.
Upvotes: 1
Reputation: 101
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>
Upvotes: 10
Reputation: 8343
There's a nice tutorial from Diederik Krols that does exactly what you're asking for the WPF Toolkit DataGrid.
Upvotes: 2