WPF DataGrid validation errors not clearing
Asked Answered
B

15

45

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.

Behalf answered 24/2, 2011 at 0:27 Comment(0)
O
26

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>
Orbital answered 17/9, 2011 at 1:8 Comment(3)
If I do not use the Mode=TwoWay the text cells are not editable anymore and the DataGridTextColumn.EditingElementStyle is never applied.Cushiony
<Setter Property="ValidationErrorTemplate" Value="{x:Null}"/> doesn't help at all. (I am trying to hide the ValidationErrorTemplate in an external file (Style.xaml)Shuman
I'm only using DataGridTextColumns in my DataGrid and your first solution works perfectly for me! I also don't have any editing problems like @Cushiony mentioned. So thank you even though I still don't understand why this works :DMidlothian
Y
9

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>
Youlandayoulton answered 5/5, 2020 at 12:59 Comment(6)
Have no idea how to integrate that to my project maybe can someone explain?Supercool
what "using" must be used? using System.Reflection;? linq?Supercool
This seems to work for me. I have a DataGridComboBoxColumn with validation, that was not clearing the error when a valid value was selected. Adding this code has fixed that.Sycee
Correction - this ONLY works for DataGridComboBoxColumn when its property binding is the only property that has validation. If I include a DataGridTextColumn with a property binding bound to another property on the same object, I get multiple instances of the same error message appearing.Sycee
I created this workaround to fix the root cause of the problem described in the other answer and it actually worked for every DataGridColumn type and every binding scenario. If it does not work for you, then it can be caused by something else. If it works for one property of the bound object and not for another, you should check your validation logic, which actually creates the duplicate ValidationErrors. I've seen some online examples for IDataErrorInfo that uses a cache for the errors and then forget to reset the cache.Youlandayoulton
Another scenario, where this workaround does not apply, is when you use a different BindingGroup for the DataGridCell than for the DataGridRow this cell belongs to. But this isn't the default DataGrid binding behavior, this has to be coded explicitly.Youlandayoulton
P
4

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

Podophyllin answered 1/10, 2014 at 10:46 Comment(0)
P
3

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.

Precedent answered 24/9, 2014 at 19:14 Comment(0)
B
2

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.)

Bitolj answered 15/3, 2013 at 15:8 Comment(0)
C
1

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

Cartilage answered 16/9, 2011 at 17:44 Comment(1)
And how am I then suppose to edit through DataGrid row and cells?Hogtie
B
1

If you are seeing an increasing number of errors similar to Meleak, I would be interested to know how your error collection gets populated. In Meleaks version of the problem, he sees three errors (and more) after resolving the invalid data.

In my Data Validation code, I remove the previous instance of a particular error then re-add every time the data changes. For reference, here is a sample:

The Validation Plumbing

#Region " Validation workers "

    Private m_validationErrors As New Dictionary(Of String, String)
    Private Sub AddError(ByVal ColName As String, ByVal Msg As String)
        If Not m_validationErrors.ContainsKey(ColName) Then
            m_validationErrors.Add(ColName, Msg)

        End If
    End Sub
    Private Sub RemoveError(ByVal ColName As String)
        If m_validationErrors.ContainsKey(ColName) Then
            m_validationErrors.Remove(ColName)
        End If
    End Sub


    Public ReadOnly Property [Error]() As String Implements System.ComponentModel.IDataErrorInfo.Error
        Get
            If m_validationErrors.Count > 0 Then
                Return "Shipment data is invalid"
            Else
                Return Nothing
            End If
        End Get
    End Property

    Default Public ReadOnly Property Item(ByVal columnName As String) As String Implements System.ComponentModel.IDataErrorInfo.Item
        Get
            If m_validationErrors.ContainsKey(columnName) Then
                Return m_validationErrors(columnName).ToString
            Else
                Return Nothing
            End If
        End Get
    End Property

#End Region

A Property Being Validated

    Private Sub OnZIPChanged()
        Me.RemoveError("ZIP")
        If _ZIP Is Nothing OrElse _ZIP.Trim = "" Then
            Me.AddError("ZIP", "Please enter a ZIP Code")
        Else
            Select Case _ZIP.Length
                Case 5

                Case 10

                Case Else
                    Me.AddError("ZIP", "Please enter a ZIP Code")
            End Select
        End If
        OnPropertyChanged("CanShip")
    End Sub

So, when the property Changed handler is run, if an error exists in the ValidationErrors dictionary, it is removed, then the value is checked, and it if does not match requirements, an error is added to the dictionary. This helps ensure that only one instance of any error is present in that entities validation error dictionary.

Blucher answered 22/9, 2011 at 14:39 Comment(2)
All of the errors in the collection were exactly identical and they are populated internally by the DataGrid to the DataGridRow once the content of a DataGridCell gets a validation error. Unfortunately, you can't do anything to this collection but look at it. The error in my version just came from the viewmodels IDataErrorInfo implementation. To get to the bottom of what the problem is you would probably need access to the source code (not the source code from the toolkit since they work differently in this aspect). Reflector might work though..Orbital
Hmm, yeah, I havent implemented IDataErrorInfo on ViewModel before. I do it on my data objects so they can report their own validation status (not that it really matters in the long run). So the DataGrid populated those errors, and they are passed to it from the DataGridCell? I have not heard of this (I am by no means an expert). Is this the vanilla .NET DataGrid?Blucher
F
1

My workaround was not to use Validation.Errors, but use DataGridRow.Item property. If your DataGrid is bound to business objects which implement IDataErrorInfo interface, then you can add IsNotValid property (or IsValid), and make sure Error property returns all errors associated with the object. Then customize default style for DataGridRowHeader:

<Style x:Key="{x:Type DataGridRowHeader}" TargetType="{x:Type DataGridRowHeader}">
    ...
    <Control SnapsToDevicePixels="false"
             Visibility="{Binding RelativeSource={RelativeSource 
                          AncestorType={x:Type DataGridRow}}, 
                          Path=Item.IsNotValid, Converter={StaticResource 
                          Bool2VisibilityConverter}}"
             Template="{Binding RelativeSource={RelativeSource 
                        AncestorType={x:Type DataGridRow}}, 
                        Path=ValidationErrorTemplate}" />

    ...
</Style>

Also in DataGridRow style customize ValidationErrorTemplate, so that it shows error message from DataGridRow.Item.Error proeprty.

Fullgrown answered 18/1, 2013 at 11:28 Comment(0)
T
1

The original Question is from 2011 and Datagrids Validation-System is still so buggy, that it is unusable. I spent 2 Days trying to find a solution to make the following work:

  • My Model-Items implement INotifyDataErrorInfo and INotifyPropertyChanged
  • They are in a BindingList bound to the DataGrid
  • The DataGrid should Display Validation-Errors from Userinputs as well as from changes to the Model from different sources
  • The RowValidation-Error-mark should show if any cell has validation errors and hide otherwise, no matter if the user is currently editing, commiting or doing nothing with the row
  • Invalid cells should display a tooltip with error text
  • No bugs or glitches

The only way to achieve this behaviour was to ditch RowValidation and CellValidation and instead use the RowHeader and Styles. I copied together the following code from various sources across the net. I haven't yet been able to do extensive testing of this, but it looks promising at first glances.

In DataGrids XAML:

<DataGrid ... local:DataGridProps.ShowCellErrorBorder="False">
<DataGrid.Resources>
    <local:DataGridValidationConverter x:Key="DataGridValidationConverter" />
    <Style TargetType="TextBlock" x:Key="errTemplate">
        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="true">
                <Setter Property="ToolTip"
                      Value="{Binding Path=(Validation.Errors)[0].ErrorContent}
                        RelativeSource={x:Static RelativeSource.Self}, "/>
                <Setter Property="Background" Value="LightSalmon"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</DataGrid.Resources>

<DataGrid.RowValidationErrorTemplate>
    <ControlTemplate>
            
    </ControlTemplate>
</DataGrid.RowValidationErrorTemplate>

<DataGrid.RowHeaderTemplate>
    <DataTemplate>
        <Grid Margin="0,-2,0,-2" 
              Visibility="{Binding Path=DataContext.HasErrors,
                RelativeSource={RelativeSource Mode=FindAncestor,
                  AncestorType={x:Type DataGridRow}},
                Converter={StaticResource DataGridValidationConverter},
                FallbackValue=Hidden}">
            <Ellipse StrokeThickness="0" Fill="Red"
                    Width="{Binding Path=FontSize,
                      RelativeSource={RelativeSource Mode=FindAncestor,
                        AncestorType={x:Type DataGridRow}}}"
                    Height="{Binding Path=FontSize,
                      RelativeSource={RelativeSource Mode=FindAncestor,
                        AncestorType={x:Type DataGridRow}}}" />
            <TextBlock Text="!" FontWeight="Bold" Foreground="White" HorizontalAlignment="Center"
                    FontSize="{Binding Path=FontSize,
                      RelativeSource={RelativeSource Mode=FindAncestor,
                        AncestorType={x:Type DataGridRow}}}" />               
        </Grid>
    </DataTemplate>
</DataGrid.RowHeaderTemplate>

<DataGrid.Columns>
    <DataGridTextColumn Header="Vorname" ElementStyle="{StaticResource errTemplate}"
          Binding="{Binding Path=Vorname,
             ValidatesOnNotifyDataErrors=True,
             NotifyOnValidationError=True}" />
    ...
</DataGrid.Columns>
</DataGrid>

DataGridValidationConverter:

public class DataGridValidationConverter : IValueConverter
{
       
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if ((bool)value)
            return Visibility.Visible;
        else
            return Visibility.Hidden;
    }

       
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

DataGridProps:

public class DataGridProps
{
public static readonly DependencyProperty ShowCellErrorBorderProperty = DependencyProperty.RegisterAttached(
            "ShowCellErrorBorder", typeof(bool), typeof(DataGridProps), new PropertyMetadata(true, ShowCellErrorBorderPropertyChangedCallback));

public static bool GetShowCellErrorBorder(DependencyObject element)
{
    return (bool)element.GetValue(ShowCellErrorBorderProperty);
}

public static void SetShowCellErrorBorder(DependencyObject element, bool value)
{
    element.SetValue(ShowCellErrorBorderProperty, value);
}

private static void ShowCellErrorBorderPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
    if (GetShowCellErrorBorder(dependencyObject)) return;
    var dg = dependencyObject as DataGrid;
    if (null != dg)
    {
        dg.Loaded += (sender, args) =>
        {
            var scrollView = dg.Template.FindName("DG_ScrollViewer", dg) as ScrollViewer;
            if (null == scrollView) return;
            var scrollContent = scrollView.Template.FindName("PART_ScrollContentPresenter", scrollView) as ScrollContentPresenter;
            if (null == scrollContent) return;
            scrollContent.AdornerLayer.Visibility = Visibility.Hidden;
        };
    }
}
}

Implementation of Model:

public Model()
{
    if (Vorname == null)
        Vorname = "";
    ...

    errorsByPropertyName = new Dictionary<string, List<string>>();
    this.PropertyChanged += Model_PropertyChanged;
    ForceRevalidation(null);
}

private string _vorname;
public string Vorname { get => _vorname; set => SetField(ref _vorname, value); }
...

public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(field, value)) return false;
    field = value;
    OnPropertyChanged(propertyName);
    return true;
}

private Dictionary<string, List<string>> errorsByPropertyName;

public void ForceRevalidation(string propertyName)
{
    if (string.IsNullOrEmpty(propertyName))
    {
        foreach (PropertyInfo property in GetType().GetProperties())
        {
            ValidateProperty(property.Name);
        }
    }
    else
    {
        ValidateProperty(propertyName);
    }
}

protected virtual void OnErrorsChanged(string propertyName)
{
    ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
    OnPropertyChanged(nameof(HasErrors));
}

public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

public bool HasErrors => errorsByPropertyName.Any();

public System.Collections.IEnumerable GetErrors(string propertyName)
{
    if (propertyName == null)
        propertyName = "";
    return errorsByPropertyName.ContainsKey(propertyName) ? errorsByPropertyName[propertyName] : null;
}

private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    ValidateProperty(e.PropertyName);
}

protected virtual void ValidateProperty(string propertyName)
{
    if (propertyName == null)
        propertyName = "";

    ClearErrors(propertyName);

    switch (propertyName)
    {
        case nameof(Vorname):
            if (string.IsNullOrWhiteSpace(Vorname))
                AddError(propertyName, propertyName + " is empty");
            break;
        ...
        default:
            break;

    }
}

protected void AddError(string propertyName, string error)
{
    if (!errorsByPropertyName.ContainsKey(propertyName))
        errorsByPropertyName[propertyName] = new List<string>();

    if (!errorsByPropertyName[propertyName].Contains(error))
    {
        errorsByPropertyName[propertyName].Add(error);
        OnErrorsChanged(propertyName);
    }
}

protected void ClearErrors(string propertyName)
{
    if (errorsByPropertyName.ContainsKey(propertyName))
    {
        errorsByPropertyName.Remove(propertyName);
        OnErrorsChanged(propertyName);
    }
}

I created this minimal example from my bigger Model-Base-Class and hope I've got everything important for this aspect here.

Torse answered 8/11, 2020 at 13:57 Comment(0)
T
0

My scenario was like this:

  1. Model implements IDataErrorInfo
  2. Custom Row Validation rule based on WPF DataGrid Practical Examples -Validation with IDataErrorInfo , that combined all errors from Model using IDataErrorInfo.

    <DataGrid.RowValidationRules>
        <local:RowDataInfoValidationRule ValidationStep="UpdatedValue" />
    </DataGrid.RowValidationRules>
    
  3. ValidatesOnDataErrors=True, ValidatesOnExceptions=True, NotifyOnValidationError=True within the binding (which I started with)

This caused multiple access to my validation Engine and eventualy left my DataGrid in inconsistent state (Error notification on row header even when row Valid).

The solution was to remove switches from the binding (point 3.)

I suggest reading through Clearing a DataGrid row validation error too.

There answered 12/12, 2011 at 0:22 Comment(0)
I
0

My workaround was to simply remove the property UpdateSourceTrigger="LostFocus" from the binding declaration in each datagridcolumn.

Isabelleisac answered 15/5, 2012 at 10:54 Comment(0)
P
0

In my case, it worked all well and good when we were using the DataGrid WPF3.5 version. We upgraded to 4.0, then it stopped resetting. After searches on SO, google etc, I chanced upon my solution. Setting UpdateSourceTrigger=PropertyChanged on the Binding in the DataGridTextColumn fixed it for me.

I just realised that the red exclamation mark does not clear on setting it to a correct value.

Pentagrid answered 13/5, 2013 at 15:28 Comment(0)
O
0

In my case I had to remove from binding definition

UpdateSourceTrigger=PropertyChanged

For me it works with both of these definitions:

<DataGridTextColumn                                         
Header="Time, min" 
x:Name="uiDataGridTextColumnTime"
Width="Auto"                                            
CellStyle="{StaticResource ResourceKey=DataGridCellText}"                                            
IsReadOnly="False">
<DataGridTextColumn.Binding>
    <Binding Path="fTime" StringFormat="{}{0:0.00}">
        <Binding.ValidationRules>
            <Validation:CellDataInfoValidationRule ValidationStep="UpdatedValue"/>
        </Binding.ValidationRules>
    </Binding>
</DataGridTextColumn.Binding>

And

<DataGridTextColumn                                         
Header="Time, min" 
x:Name="uiDataGridTextColumnTime"
Width="Auto"                                            
CellStyle="{StaticResource ResourceKey=DataGridCellText}"     
Binding="{Binding fTime, StringFormat={}\{0:0.00\}, ValidatesOnDataErrors=True}" 
IsReadOnly="False">

Validation:CellDataInfoValidationRule is custom class & get it here

public class CellDataInfoValidationRule : ValidationRule
{
    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        // obtain the bound business object
        BindingExpression expression = value as BindingExpression;
        IDataErrorInfo info = expression.DataItem as IDataErrorInfo;

        // determine the binding path
        string boundProperty = expression.ParentBinding.Path.Path;

        // obtain any errors relating to this bound property
        string error = info[boundProperty];
        if (!string.IsNullOrEmpty(error))
        {
            return new ValidationResult(false, error);
        }

        return ValidationResult.ValidResult;
    }
}

And your data object must implement IDataErrorInfo

Overarch answered 6/9, 2013 at 11:30 Comment(1)
( The attachable property 'RowValidationRules' was not found in type 'DataGrid'. )Credits
E
0

I am not using IDataErrorInfo or INotifyDataErrorInfo and my solution was to change my bindings from UpdateSourceTrigger="PropertyChanged" to UpdateSourceTrigger="LostFocus" This was the only thing that

If you are using ValidationRules in your DataGrid column defintion and you need the validation rules to run when ever the property changes (in the UI or the property) look into setting ValidatesOnTargetUpdated="True" on your ValidationRule

XAML Example:

<DataGridTextColumn Header="Name"
    CellStyle="{StaticResource DGCellStyle}"
    ElementStyle="{StaticResource DGTextColValidationStyle}"
    EditingElementStyle="{StaticResource DGTextColEditValidationStyle}">
    <DataGridTextColumn.Binding>
        <Binding Path="Name" UpdateSourceTrigger="LostFocus">
            <Binding.ValidationRules>
                <ValidationResource:YourValidationRule ValidationStep="UpdatedValue" ValidatesOnTargetUpdated="True" />
            </Binding.ValidationRules>
        </Binding>
    </DataGridTextColumn.Binding>
</DataGridTextColumn>
Entrails answered 6/12, 2013 at 19:40 Comment(0)
H
0

I have used this technique which do away with RowValidationRules and instead use the property validations in a viewmodel. This requires static variables and data annotations :

//uses Prism.MVVM for BindableBase and INotifyDataErrorInfo

private static int _xxStartNo;
private static int _xxEndNo;

// in property getter/setter
private int _startNo;
[CustomValidation(typeof(YourModel), "ValidateStartNoRange")]
public int StartNo
{
    get
       {
          _xxStartNo=_startNo;
          return _startNo;
       }
    set
       {
         .......... 
         ValidateProperty("StartNo") 
       }
}
.......

public static ValidationResult ValidateStartNoRange(int number)
{
   if(number > _xxEndNo) 
   {
       return ValidationResult("Start No must be less than End No.";
   }
   return ValidationResult.Success;
}       
Herald answered 5/10, 2016 at 11:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.