Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF DataGrid validation errors not clearing

So I have a WPF DataGrid, which is bound to an ObservableCollection. The collection has validation on its members, through IDataErrorInfo. If I edit a cell in a way so as to be invalid, and then tab away from it before hitting enter, then come back and make it valid, the cell will stop showing invalid, however, the "!" at the head of the row will still be there, and the ToolTip will reference the previous, invalid value.

like image 491
s73v3r Avatar asked Feb 24 '11 00:02

s73v3r


6 Answers

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>
like image 172
Fredrik Hedblad Avatar answered Nov 08 '22 01:11

Fredrik Hedblad


I found the root cause of this problem. It has to do with the way how BindingExpressionBases 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 by DataGridCell.BuildVisualTree(), all the old BindingExpressions of the BindingGroup that belongs to this cell are removed, before its Content property is changed to the new value
  • explicitly, when the Content property for the DataGridCell is changed (by DataGridCell.BuildVisualTree() or other way) , the BindingExpressionBase.Detach() method is called for all the bindings on the old property value, which also removes the reference to the BindingGroup before any ValidationError has a chance to be removed
  • implicitly, because mostly all references to and from BindingExpressionBase are actually WeakReferences, even when all the above scenarios would not cause the remove of the reference, but when something looks up the TargetElement of BindingExpressionBase, there is a chance that the underlying WeakReference returns null and the property accessor calls again the broken Detach() 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>
like image 26
Anateus Avatar answered Nov 08 '22 03:11

Anateus


I found best answer that worked for me. Just clear your DataGrid's RowValidationErrorTemplate.

  1. In Code

    YourGrid.RowValidationErrorTemplate = new ControlTemplate();
    
  2. In Xaml

    <DataGrid.RowValidationErrorTemplate>
        <ControlTemplate>
        </ControlTemplate>
    </DataGrid.RowValidationErrorTemplate>`
    
  3. Then make your own Row Validation Error Template.

    If your data item is INotifyPropertyChanged

    ((INotifyPropertyChanged)i).PropertyChanged += this.i_PropertyChanged;`
    

    then

    private void i_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        this.Dispatcher.BeginInvoke(new Action(() =>
        {
            var row = this.ItemContainerGenerator.ContainerFromItem(sender) as DataGridRow;
            if (row == null)
                return;
    
            var Errs = IsValid(row);
    
            if (Errs.Count == 0) row.Header = null;
            else
            {
                // Creatr error template
                var gg = new Grid { ToolTip = "Error Tooltip" };
    
                var els = new Ellipse { Fill = new SolidColorBrush(Colors.Red), Width = row.FontSize, Height = row.FontSize };
    
                var tb = new TextBlock
                {
                    Text = "!",
                    Foreground = new SolidColorBrush(Colors.White),
                    HorizontalAlignment = HorizontalAlignment.Center,
                    FontWeight = FontWeights.Bold
                };
    
                gg.Children.Add(els);
                gg.Children.Add(tb);
    
                row.Header = gg;
            }
        }),
         System.Windows.Threading.DispatcherPriority.ApplicationIdle);
    }
    
  4. Write your own IsValid method, the way you like

like image 4
MSL Avatar answered Nov 08 '22 01:11

MSL


I have the same problem with the RowHeader error template not going away. I am using INotifyDataErrorInfo. Following up on the research by Fredrik Hedblad I have made a workaround; I have modified the DataGridRowHeader template to use a MultiBinding for the ValidationErrorTemplate visibility:

  <Style x:Key="DataGridRowHeaderStyle" TargetType="{x:Type DataGridRowHeader}">
<!--<Setter Property="Background" Value="{DynamicResource {ComponentResourceKey TypeInTargetAssembly=Brushes:BrushesLibrary1,
             ResourceId=HeaderBrush}}"/>-->
<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type DataGridRowHeader}">
      <Grid>
        <Microsoft_Windows_Themes:DataGridHeaderBorder BorderBrush="{TemplateBinding BorderBrush}"
                          BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}"
                          IsPressed="{TemplateBinding IsPressed}" IsHovered="{TemplateBinding IsMouseOver}"
                            IsSelected="{TemplateBinding IsRowSelected}" Orientation="Horizontal"
                            Padding="{TemplateBinding Padding}" SeparatorBrush="{TemplateBinding SeparatorBrush}"
                            SeparatorVisibility="{TemplateBinding SeparatorVisibility}">
          <StackPanel Orientation="Horizontal">
            <ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"
                                                                Width="15"/>
            <Control SnapsToDevicePixels="false"
                                       Template="{Binding ValidationErrorTemplate, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}}}">
              <Control.Visibility>
              <MultiBinding Converter="{StaticResource ValidationConverter}">
                <Binding Path="(Validation.HasError)" RelativeSource="{RelativeSource AncestorType={x:Type DataGridRow}}"/>
                <Binding Path="DataContext.HasErrors" RelativeSource="{RelativeSource AncestorType={x:Type DataGridRow}}"/>
              </MultiBinding>
              </Control.Visibility>
              <!-- Original binding below -->
              <!--Visibility="{Binding (Validation.HasError), Converter={StaticResource bool2VisibilityConverter},
                     RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}}}">-->
            </Control>
          </StackPanel>
        </Microsoft_Windows_Themes:DataGridHeaderBorder>
        <Thumb x:Name="PART_TopHeaderGripper" Style="{StaticResource RowHeaderGripperStyle}" VerticalAlignment="Top"/>
        <Thumb x:Name="PART_BottomHeaderGripper" Style="{StaticResource RowHeaderGripperStyle}" VerticalAlignment="Bottom"/>
      </Grid>
    </ControlTemplate>
  </Setter.Value>
</Setter>

This relies on the bound objects having a "HasErrors" property with change notification. In my project I have ensured that the HasErrors property is updated by raising the PropertyChanged for HasErrors in the item EndEdit event.

like image 3
ChangedDaily Avatar answered Nov 08 '22 02:11

ChangedDaily


My solution was to implement custom row validation feedback, similar to this page under the To customize row validation feedback section. The row error then disappears appropriately.

(I also added RowHeaderWidth="20" to the DataGrid definition, to avoid the table shift to the right the first time the exclamation point appears.)

like image 2
Conrad Avatar answered Nov 08 '22 01:11

Conrad


try removing the Mode=TwoWay for each of the DataGridTextColumns from each of the Binding elements.

like image 1
Priyank Avatar answered Nov 08 '22 03:11

Priyank