INotifyDataErrorInfo in WPF

Introduction
 
From WPF 4.5, Microsoft added support of INotifyDataErrorInfo validation from WPF. The model we want to validate now can implement this interface and can do multiple validations for the property. It also supports aync validation. The way you want to implement varies by requirements. There are so many options with this. In this article, I will provide a simpler version. I am mostly a web developer, so I always try maintain a common standard for developing both WPF/Silverlight/MVC. You can choose your own.
 
 INotifyDataErrorInfo Interface
 
 It has only three members, all are self-explanatory:

  1. public interface INotifyDataErrorInfo  
  2. {  
  3.     bool HasErrors { get; }  
  4.     event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;  
  5.     IEnumerable GetErrors(string propertyName);  

The model class can implement these three members to provide validation support. But generally, I create a Base Validation class so that every model doesn't need to implement this every time. The HasErrors property should return true if the model has any errors at the moment. Nevertheless this property is not used by the binding engine (I wondered about that a little), so we can utilize it for our own use, like enabling/disabling a Save/Cancel button.
 
The INotifyDataErrorInfo interface rovides us more flexibility on model validation. We can decide when we want to validate properties, for example in property setters. We can signal errors on single properties as well as cross-property errors and model level errors. In this example I will use an annotation type validation trigger.
 
 Validation Base
 
 Please go through the following implementation: 
  1. public class ValidationBase : INotifyDataErrorInfo  
  2. {  
  3.      private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();  
  4.      public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;  
  5.      private object _lock = new object();  
  6.      public bool HasErrors { get { return _errors.Any(propErrors => propErrors.Value != null && propErrors.Value.Count > 0); } }  
  7.      public bool IsValid { get { return this.HasErrors; } }  
  8.    
  9.      public IEnumerable GetErrors(string propertyName)  
  10.      {  
  11.          if (!string.IsNullOrEmpty(propertyName))  
  12.          {  
  13.              if (_errors.ContainsKey(propertyName) && (_errors[propertyName] != null) && _errors[propertyName].Count > 0)  
  14.                  return _errors[propertyName].ToList();  
  15.          else  
  16.                  return null;  
  17.          }  
  18.          else  
  19.              return _errors.SelectMany(err => err.Value.ToList());  
  20.      }  
  21.    
  22.      public void OnErrorsChanged(string propertyName)  
  23.      {  
  24.          if (ErrorsChanged != null)  
  25.              ErrorsChanged(thisnew DataErrorsChangedEventArgs(propertyName));  
  26.      }  
  27.    
  28.      public void ValidateProperty(object value, [CallerMemberName] string propertyName = null)  
  29.      {  
  30.          lock (_lock)  
  31.          {  
  32.              var validationContext = new ValidationContext(thisnullnull);  
  33.              validationContext.MemberName = propertyName;  
  34.              var validationResults = new List<ValidationResult>();  
  35.              Validator.TryValidateProperty(value, validationContext, validationResults);  
  36.    
  37.              //clear previous _errors from tested property  
  38.              if (_errors.ContainsKey(propertyName))  
  39.                  _errors.Remove(propertyName);  
  40.                  OnErrorsChanged(propertyName);  
  41.                  HandleValidationResults(validationResults);  
  42.          }  
  43.      }  
  44.    
  45.      public void Validate()  
  46.      {  
  47.          lock (_lock)  
  48.          {  
  49.              var validationContext = new ValidationContext(thisnullnull);  
  50.              var validationResults = new List<ValidationResult>();  
  51.              Validator.TryValidateObject(this, validationContext, validationResults, true);  
  52.    
  53.              //clear all previous _errors  
  54.              var propNames = _errors.Keys.ToList();  
  55.              _errors.Clear();  
  56.              propNames.ForEach(pn => OnErrorsChanged(pn));  
  57.              HandleValidationResults(validationResults);  
  58.          }  
  59.      }  
  60.    
  61.      private void HandleValidationResults(List<ValidationResult> validationResults)  
  62.      {  
  63.          //Group validation results by property names  
  64.          var resultsByPropNames = from res in validationResults  
  65.          from mname in res.MemberNames  
  66.          group res by mname into g  
  67.          select g;  
  68.          //add _errors to dictionary and inform binding engine about _errors  
  69.          foreach (var prop in resultsByPropNames)  
  70.          {  
  71.              var messages = prop.Select(r => r.ErrorMessage).ToList();  
  72.              _errors.Add(prop.Key, messages);  
  73.              OnErrorsChanged(prop.Key);  
  74.          }  
  75.      }  

I kept all errors in a dictionary where the key is a property name. Then whenever a property is validated by the ValidateProperty method I clear previous errors for the property, signal the change to the binding engine, validate the property using the Validator helper class and if any errors are found then I add them to the dictionary and signal the binding engine again with a proper property name.
 
The same, except for the entire model, is done in the Validate method. Here I check all model properties at once. There can be alternative approach, but I am satisfied with this.
 
 The Model Class
 
 I used the Data Annotation technique to trigger the validation logic. Also, the model derives from ModelBase class that implements the INotifyPropertyChange interface for binding support. 
  1. public class ModelBase : ValidationBase, INotifyPropertyChanged  
  2. {  
  3.      public event PropertyChangedEventHandler PropertyChanged;  
  4.      protected void NotifyPropertyChanged(string propertyName)  
  5.      {  
  6.          if (this.PropertyChanged != null)  
  7.          {  
  8.              this.PropertyChanged(thisnew PropertyChangedEventArgs(propertyName));  
  9.          }  
  10.      }  
  11. }  
  12.    
  13. public class Customer : ModelBase  
  14. {  
  15.      private string _firstName;  
  16.      [Display(Name="First Name")]  
  17.      [Required]  
  18.      [StringLength(20)]  
  19.      public string FirstName  
  20.      {  
  21.          get { return _firstName; }  
  22.          set  
  23.          {  
  24.              _firstName = value;  
  25.              ValidateProperty(value);  
  26.              base.NotifyPropertyChanged("FirstName");  
  27.          }  
  28.      }  

So as you see, the property has two validation rules for First Name, it is required and it should be less than 20. By this, you can apply a different validation rule in one go for the same property.
 
View
 
 The view is quite simple. Bind the data context to your view model and then place the binding. But you need to include three attributes in the Binding:
  1. NotifyOnValidationError=True,ValidatesOnNotifyDataErrors=True,UpdateSourceTrigger=PropertyChanged 
Also, I didn't see the error message as Silverlight when I did this binding. I searched in the internet and found a control template. So I used it.
  1. <TextBox Text="{Binding Customer.FirstName,Mode=TwoWay, NotifyOnValidationError=True,ValidatesOnNotifyDataErrors=True,UpdateSourceTrigger=PropertyChanged}">  
  2.      <Validation.ErrorTemplate>  
  3.          <ControlTemplate>  
  4.              <StackPanel>  
  5.                  <!-- Placeholder for the TextBox itself -->  
  6.                  <AdornedElementPlaceholder x:Name="textBox"/>  
  7.                  <ItemsControl ItemsSource="{Binding}">  
  8.                      <ItemsControl.ItemTemplate>  
  9.                          <DataTemplate>  
  10.                              <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>  
  11.                          </DataTemplate>  
  12.                      </ItemsControl.ItemTemplate>  
  13.                  </ItemsControl>  
  14.              </StackPanel>  
  15.          </ControlTemplate>  
  16.      </Validation.ErrorTemplate>  
  17. </TextBox> 
I hope this helps. Please check other tutorials and see how in various ways you can implement this Interface.

Up Next
    Ebook Download
    View all
    Learn
    View all