WPF DataGrid validation errors not clearing
Not using Mode=TwoWay
for DataGridTextColumns
solves one version of the problem, however it seems that this problem can appear out of nowhere for other reasons as well.
(Anyone who has a good explanation as of why not using Mode=TwoWay
solves this in the first place is probably close to a solution to this problem)
The same thing just happened to me with a DataGridComboBoxColumn
so I tried to dig a little deeper.
The problem isn't the Binding
in the Control
that displays the ErrorTemplate
inside DataGridHeaderBorder
. It is binding its Visibility
to Validation.HasError
for the ancestor DataGridRow
(exactly as it should be doing) and that part is working.
Visibility="{Binding (Validation.HasError),
Converter={StaticResource bool2VisibilityConverter},
RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}}}"/>
The problem is that the validation error isn't cleared from the DataGridRow
once it is resolved. In my version of the problem, the DataGridRow
started out with 0 errors. When I entered an invalid value it got 1 error so, so far so good. But when I resolved the error it jumped up to 3 errors, all of which were the same.
Here I tried to resolve it with a DataTrigger
that set the ValidationErrorTemplate
to {x:Null}
if Validation.Errors.Count
wasn't 1. It worked great for the first iteration but once I cleared the error for the second time it was back. It didn't have 3 errors anymore, it had 7! After a couple of more iterations it was above 10.
I also tried to clear the errors manually by doing UpdateSource
and UpdateTarget
on the BindingExpressions
but no dice. Validation.ClearInvalid
didn't have any effect either. And looking through the source code in the Toolkit didn't get me anywhere :)
So I don't have any good solutions to this but I thought I should post my findings anyway..
My only "workaround" so far is to just hide the ErrorTemplate
in the DataGridRowHeader
<DataGrid ...>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="ValidationErrorTemplate" Value="{x:Null}"/>
</Style>
</DataGrid.RowStyle>
<!-- ... -->
</DataGrid>
I found the root cause of this problem. It has to do with the way how BindingExpressionBase
s lose their reference to the BindingGroup
, because only the BindingExpression
is responsible to remove its ValidationErrors
.
In this case of DataGrid validation, it has multiple sources where it can lose the reference:
- explicitly, when the visual tree is rebuild for a
DataGridCell
byDataGridCell.BuildVisualTree()
, all the oldBindingExpressions
of theBindingGroup
that belongs to this cell are removed, before itsContent
property is changed to the new value - explicitly, when the
Content
property for theDataGridCell
is changed (byDataGridCell.BuildVisualTree()
or other way) , theBindingExpressionBase.Detach()
method is called for all the bindings on the old property value, which also removes the reference to theBindingGroup
before anyValidationError
has a chance to be removed - implicitly, because mostly all references to and from
BindingExpressionBase
are actuallyWeakReference
s, even when all the above scenarios would not cause the remove of the reference, but when something looks up theTargetElement
ofBindingExpressionBase
, there is a chance that the underlyingWeakReference
returnsnull
and the property accessor calls again the brokenDetach()
method
With the above findings it is now also clear why not using Mode=TwoWay
for DataGridTextColumn
can sometimes be a solution to the problem. The DataGridTextColumn
would become read-only and the Content
property of the DataGridCell
is therefore never changed.
I've written a workaround by using an attached DependencyProperty
for this.
using System;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Controls.Primitives;
namespace Utilities
{
public static class DataGridExtension
{
/// <summary>
/// Identifies the FixBindingGroupValidationErrorsFor attached property.
/// </summary>
public static readonly DependencyProperty FixBindingGroupValidationErrorsForProperty =
DependencyProperty.RegisterAttached("FixBindingGroupValidationErrorsFor", typeof(DependencyObject), typeof(DataGridExtension),
new PropertyMetadata(null, new PropertyChangedCallback(OnFixBindingGroupValidationErrorsForChanged)));
/// <summary>
/// Gets the value of the FixBindingGroupValidationErrorsFor property
/// </summary>
public static DependencyObject GetFixBindingGroupValidationErrorsFor(DependencyObject obj)
{
return (DependencyObject)obj.GetValue(FixBindingGroupValidationErrorsForProperty);
}
/// <summary>
/// Sets the value of the FixBindingGroupValidationErrorsFor property
/// </summary>
public static void SetFixBindingGroupValidationErrorsFor(DependencyObject obj, DependencyObject value)
{
obj.SetValue(FixBindingGroupValidationErrorsForProperty, value);
}
/// <summary>
/// Handles property changed event for the FixBindingGroupValidationErrorsFor property.
/// </summary>
private static void OnFixBindingGroupValidationErrorsForChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DependencyObject oldobj = (DependencyObject)e.OldValue;
if (oldobj != null)
{
BindingGroup group = FindBindingGroup(d); //if d!=DataGridCell, use (DependencyObject)e.NewValue
var leftOverErrors = group.ValidationErrors != null ?
Validation.GetErrors(group.Owner).Except(group.ValidationErrors).ToArray() : Validation.GetErrors(group.Owner).ToArray();
foreach (var error in leftOverErrors)
{
//HINT: BindingExpressionBase.Detach() removes the reference to BindingGroup, before ValidationErrors are removed.
if (error.BindingInError is BindingExpressionBase binding && (binding.Target == null ||
TreeHelper.IsDescendantOf(binding.Target, oldobj)) && binding.BindingGroup == null &&
(binding.ValidationErrors == null || binding.ValidationErrors.Count == 0 || !binding.ValidationErrors.Contains(error)))
{
typeof(Validation).GetMethod("RemoveValidationError", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] {error, group.Owner, group.NotifyOnValidationError});
}
}
}
}
private static BindingGroup FindBindingGroup(DependencyObject obj)
{
do
{
if (obj is FrameworkElement fe)
{
return fe.BindingGroup;
}
if (obj is FrameworkContentElement fce)
{
return fce.BindingGroup;
}
obj = LogicalTreeHelper.GetParent(obj);
} while (obj != null);
return null;
}
private static class TreeHelper
{
private static DependencyObject GetParent(DependencyObject element, bool recurseIntoPopup)
{
if (recurseIntoPopup)
{
// Case 126732 : To correctly detect parent of a popup we must do that exception case
Popup popup = element as Popup;
if ((popup != null) && (popup.PlacementTarget != null))
return popup.PlacementTarget;
}
Visual visual = element as Visual;
DependencyObject parent = (visual == null) ? null : VisualTreeHelper.GetParent(visual);
if (parent == null)
{
// No Visual parent. Check in the logical tree.
parent = LogicalTreeHelper.GetParent(element);
if (parent == null)
{
FrameworkElement fe = element as FrameworkElement;
if (fe != null)
{
parent = fe.TemplatedParent;
}
else
{
FrameworkContentElement fce = element as FrameworkContentElement;
if (fce != null)
{
parent = fce.TemplatedParent;
}
}
}
}
return parent;
}
public static bool IsDescendantOf(DependencyObject element, DependencyObject parent)
{
return TreeHelper.IsDescendantOf(element, parent, true);
}
public static bool IsDescendantOf(DependencyObject element, DependencyObject parent, bool recurseIntoPopup)
{
while (element != null)
{
if (element == parent)
return true;
element = TreeHelper.GetParent(element, recurseIntoPopup);
}
return false;
}
}
}
}
Then attach this property with a binding to the Content
property of DataGridCell
.
<Window ...
xmlns:utils="clr-namespace:Utilities">
...
<DataGrid ...>
<DataGrid.CellStyle>
<Style BasedOn="{StaticResource {x:Type DataGridCell}}" TargetType="{x:Type DataGridCell}">
<Setter Property="utils:DataGridExtension.FixBindingGroupValidationErrorsFor" Value="{Binding Content, RelativeSource={RelativeSource Self}}" />
</Style>
</DataGrid.CellStyle>
</DataGrid>
...
</Window>