Daniel Crowe
Daniel Crowe

Reputation: 321

DataGridRow error indicator not working with INotifyDataErrorInfo

I'm attempting to update my ViewModel to use INotifyDataErrorInfo instead of IDataErrorInfo and am running into the following problem:

Validation for the currently editing field appears to work correctly, but the row-level error indicator doesn't appear until I end editing on the field with an error, and then start re-editing it. After that, the error indicator to go away, event after fixing the validation error.

Put another way: The first time I edit the row, the TextBox outline turns red correctly, but the row indicator doesn't appear. Re-editing the row causes the row indicator to appear. Fixing the validation error causes the field outline to go away, but leaves the exclamation point behind.

Note that IDataErrorInfo seems to work fine. It's INotifyDataErrorInfo I'm having trouble with.

Half the solution: Changing the binding to TwoWay causes the row indicator to appear correctly, but it still doesn't want to go away.

Here's the view:

<DataGrid ItemsSource="{Binding Items, ValidatesOnNotifyDataErrors=True}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Name" Binding="{Binding Name, ValidatesOnNotifyDataErrors=True,Mode=TwoWay}" />
    </DataGrid.Columns>
</DataGrid>

And here's the view model:

public class Item : INotifyDataErrorInfo, INotifyPropertyChanged
{
    Dictionary<string, IEnumerable<string>> _errors = new Dictionary<string,IEnumerable<string>>();
    string _name;

    public string Name
    {
        get { return _name; }
        set
        {
            if (_name != value)
            {
                ValidateProperty("Name", value);
                _name = value;
                RaisePropertyChanged("Name");
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void RaisePropertyChanged(string p)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(p));
    }

    private void ValidateProperty(string p, object value)
    {
        if (p == "Name")
        {
            if (string.IsNullOrWhiteSpace((string)value))
                _errors["Name"] = new[] { "Name is required." };
            else
                _errors["Name"] = new string[0];
        }

        if (ErrorsChanged != null)
            ErrorsChanged(this, new DataErrorsChangedEventArgs(null));
    }

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public System.Collections.IEnumerable GetErrors(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName))
            return _errors.Values.SelectMany(es2 => es2);

        IEnumerable<string> es;
        _errors.TryGetValue(propertyName ?? "", out es);
        return es;
    }

    public bool HasErrors
    {
        get
        {
            var e = _errors.Values.Any(es => es.Any());
            return e;
        }
    }
}

It seems like the question has already been asked on SO, but was removed by the original author: https://stackoverflow.com/questions/18113718/wpf-datagridrow-inotifydataerrorinfo-as-tooltip-buggy?answertab=active A copy of the original question, but no answer is here: http://bolding-techaswere1.blogspot.com.au/2013/08/wpf-datagridrow-inotifydataerrorinfo-as.html

Edit:

Here's my test source code: https://github.com/dcrowe/WPF-DataGrid-Validation-Issue/tree/master/DataGrid%20Validation%20Issue

Here's the report I've submitted to MS: https://connect.microsoft.com/VisualStudio/feedback/details/807728/datagridrow-error-indicator-not-working-with-inotifydataerrorinfo

Upvotes: 2

Views: 3205

Answers (1)

dev hedgehog
dev hedgehog

Reputation: 8801

I know I told you before to set Binding Mode to TwoWay and that was correct though you also have to also be careful how you define your validation rule. If you mind to find balance between those two everything shall work just fine.

Here is an example where everything works just that fine. Like I mentioned I couldnt identify myself with your example and I was missing few details to be able to reproduce your issue therefore here is an short example.

<Grid>
    <DataGrid
       CanUserAddRows="True"
       AutoGenerateColumns="False"  
       ItemsSource="{Binding Pricelist}" >
       <DataGrid.Columns>
            <DataGridTextColumn
               Header="Price" 
               Width="60" 
               Binding="{Binding Price, 
                         Mode=TwoWay, 
                         UpdateSourceTrigger=PropertyChanged,
                         ValidatesOnDataErrors=True}">
            </DataGridTextColumn>
       </DataGrid.Columns>
    </DataGrid>
</Grid>

And this is how ViewModel looks like:

public partial class MainWindow : Window, INotifyPropertyChanged
{
    private ObservableCollection<MyProduct> priceList;

    public MainWindow()
    {
        InitializeComponent();
        Pricelist = new ObservableCollection<MyProduct>();
        this.DataContext = this;
    }

    public ObservableCollection<MyProduct> Pricelist
    {
        get
        {
            return this.priceList;
        }

        set
        {
            this.priceList = value; 
            if (PropertyChanged != null) 
                PropertyChanged(this, new PropertyChangedEventArgs("PriceList"));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

public class MyProduct : INotifyPropertyChanged, IDataErrorInfo
{
    private string _price;
    public string Price
    {
        get
        {
            return _price;
        }
        set
        {
            _price = value;

            this.RaisePropertyChanged("Price");
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        var handler = this.PropertyChanged;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    protected void RaisePropertyChanged(String propertyName)
    {
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }

    public string Error
    {
        get
        {
            return string.Empty;
        }
    }

    public string this[string columnName]
    {
        get
        {
            string result = null;
            switch (columnName)
            {
                case "Price":
                    {
                        decimal temdecimal = 0.00m;

                        if (Price != null && !decimal.TryParse(Price, out temdecimal))
                        {
                            result = "Price is invalid";
                        }
                        break;
                    }
                default:
                    {
                        break;
                    }
            }
            return result;
        }
    }
}

In my case the validation may allow NULL to be the value of Price property but it doesnt allow string.Empty and any other text which contain letters.

I think if you would change the validation of your example it will work for you too.

I hope I helped you out anyhow. Feel free to mark this answer or vote it up if you find it helpful.

The example should run just fine on your side and it should do what you asked for.

EDIT2:

INotifyDataErrorInfo made easy:

<TextBox Text="{Binding LastName, Mode=TwoWay, NotifyOnValidationError=true }" />

<Button x:Name="OKButton" Content="OK" Click="OKButton_Click" Width="75" Height="23" />

This is the click handler:

private void ValidateButton_Click(object sender, RoutedEventArgs e)
{
   owner.FireValidation();
}

This is the class that implements INotifyDataErrorInfo

public class Owner : INotifyPropertyChanged, INotifyDataErrorInfo
{
   public Owner()
   {
      FailedRules = new Dictionary<string, string>();
   }

   private Dictionary<string, string> FailedRules
   {
      get;
      set;
   }

   public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

   public IEnumerable GetErrors(string propertyName)
   {
      if (FailedRules.ContainsKey(propertyName))
         return FailedRules[propertyName];
      else
         return FailedRules.Values;
   }

   internal void FireValidation()
   {
      if (lastName.Length > 20)
      {
         if (!FailedRules.ContainsKey("LastName"))
            FailedRules.Add("LastName", "Last name cannot have more than 20 characters");
      }
      else
      {
         if (FailedRules.ContainsKey("LastName"))
            FailedRules.Remove("LastName");
      }

      NotifyErrorsChanged("LastName");
   }

   public bool HasErrors
   {
      get { return FailedRules.Count > 0; }
   }

   private void NotifyErrorsChanged(string propertyName)
   {
      if (ErrorsChanged != null)
         ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
   }
}

Upvotes: 1

Related Questions