Utilizing INotifyDataErrorInfo in WPF MvvM app

By Mirek on (tags: INotifyDataErrorInfo, mvvm, validation, WPF, categories: code)

Today I will show you how I utilize the INotifyDataErrorInfo interface to accomplish validation mechanisms in WPF application.

 

A time ago I have described how to validate model in WPF application with use of IDataErrorInfo interface. Now in WPF 4.5 we have another possibility which is the INotifyDataErrorInfo interface. The MSDN documentation is here a little scarce, but since this interface was ported from Silverlight we can read more on Silverlight verion of documentation. The documentation on MSDN suggest that all new entities should implement INotifyDataErrorInfo instead of IDataErrorInfo, for more flexibility, thus we can assume that IDataErrorInfo is or will be deprecated soon.

The INotifyDataErrorInfo interface itself is simple and quite self-explanatory

public interface INotifyDataErrorInfo
{
    bool HasErrors { get; }
 
    event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
 
    IEnumerable GetErrors(string propertyName);
}
The model we want to validate can implement  those three members to provide a validation support. HasErrors property should return true if model has any errors at the moment. Nevertheless this property is not used by the binding engine (which wondered me a little), so we can utilize it for our own use.  ErrorsChanged event should be raised from inside model whenever errors change. This basically informs a visual part to display or hide error messages, so also when errors are solved we should call this event. The constructor for argument DataErrorsChangedEventArgs gets the name of property or null if errors changed for whole model.
GetErrors method is called by binding engine to retrieve errors for either property if its name is provided or for whole model if propertyName is null;

INotifyDataErrorInfo interface gives 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.

I have put all validation logic in a base class which all my models then inherit from.

   1: public abstract class ModelValidation : INotifyDataErrorInfo
   2: {
   3:    private Dictionary<string, List<string>> errors = new Dictionary<string, List<string>>();
   4:    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
   5:    private object threadLock = new object();
   6:  
   7:    public bool IsValid
   8:    {
   9:        get { return !this.HasErrors; }
  10:  
  11:    }
  12:  
  13:    public void OnErrorsChanged(string propertyName)
  14:    {
  15:        if (ErrorsChanged != null)
  16:            ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
  17:    }
  18:  
  19:    public IEnumerable GetErrors(string propertyName)
  20:    {
  21:        if (!string.IsNullOrEmpty(propertyName))
  22:        {
  23:            if (errors.ContainsKey(propertyName) && (errors[propertyName] != null) && errors[propertyName].Count > 0)
  24:                return errors[propertyName].ToList();
  25:            else
  26:                return null;
  27:        }
  28:        else
  29:            return errors.SelectMany(err => err.Value.ToList());
  30:    }
  31:    
  32:    public bool HasErrors
  33:    {
  34:        get { return errors.Any(propErrors => propErrors.Value != null && propErrors.Value.Count > 0); }
  35:    }
  36:  
  37:    public void ValidateProperty(object value, [CallerMemberName] string propertyName = null)
  38:    {
  39:        lock (threadLock)
  40:        {
  41:            var validationContext = new ValidationContext(this, null, null);
  42:            validationContext.MemberName = propertyName;
  43:            var validationResults = new List<ValidationResult>();
  44:            Validator.TryValidateProperty(value, validationContext, validationResults);
  45:  
  46:            //clear previous errors from tested property
  47:            if (errors.ContainsKey(propertyName))
  48:                errors.Remove(propertyName);
  49:            OnErrorsChanged(propertyName);
  50:  
  51:            HandleValidationResults(validationResults);
  52:        }
  53:    }
  54:  
  55:    public void Validate()
  56:    {
  57:        lock (threadLock)
  58:        {
  59:            var validationContext = new ValidationContext(this, null, null);
  60:            var validationResults = new List<ValidationResult>();
  61:            Validator.TryValidateObject(this, validationContext, validationResults, true);
  62:  
  63:            //clear all previous errors
  64:            var propNames = errors.Keys.ToList();
  65:            errors.Clear();
  66:            propNames.ForEach(pn => OnErrorsChanged(pn));
  67:  
  68:            HandleValidationResults(validationResults);
  69:        }
  70:    }
  71:  
  72:    private void HandleValidationResults(List<ValidationResult> validationResults)
  73:    {
  74:        //Group validation results by property names
  75:        var resultsByPropNames = from res in validationResults
  76:                                 from mname in res.MemberNames
  77:                                 group res by mname into g
  78:                                 select g;
  79:  
  80:        //add errors to dictionary and inform  binding engine about errors
  81:        foreach (var prop in resultsByPropNames)
  82:        {
  83:            var messages = prop.Select(r => r.ErrorMessage).ToList();
  84:            errors.Add(prop.Key, messages);
  85:            OnErrorsChanged(prop.Key);
  86:        }
  87:    }
  88: }

As the example on MSDN suggests I keep all errors in a dictionary where the key is a property name. Then whenever a property is validated by method ValidateProperty I clear previous errors for the property, signal the change to the binding engine, validate property with use of Validator helper class and if any errors are found I add them to the dictionary and signals binding engine again with a proper property name.

The same, but for whole model, is performed in Validate method. Here I check all model properties at once.
The one thing I am missing here is functionality for handling model level errors, that are not strictly attached to any of properties. Here instead (in line 29) I return a list of all errors for all properties. For me this solution is satisfactory, however there should be no problem implementing it in above model.

 

Now lets see how the model looks like

   1: public class MainWindowModel :  ModelValidation
   2: {
   3:     private string _name;
   4:     private string _email;
   5:  
   6:     [Required]
   7:     [StringLength(20)]
   8:     public string Name
   9:     {
  10:         get { return _name; }
  11:         set
  12:         {
  13:             _name = value;
  14:             ValidateProperty(value);
  15:             NotifyChangedThis();
  16:         }
  17:     }
  18:  
  19:     [Required]
  20:     [EmailAddress]
  21:     public string Email
  22:     {
  23:         get { return _email; }
  24:         set
  25:         {
  26:             _email = value;
  27:             ValidateProperty(value);
  28:             NotifyChangedThis();
  29:         }
  30:     }
  31: }

Just for clarifying method NotifyChangedThis() handles change notification from INotifyPropertyChanged interface. Here each property has validation attributes assigned, therefore those are considered by Validator in our ModelValidation class. For cross property errors we can use CustomValidationAttribute.
For asynchronous validation we could add a ValidateAsync and ValidatePropertyAsync methods to the ModelValidation class. The implementation should be simple and straight foreword. We should then use a ConcurrentDictionary instead of regular one as our errors cache. 

 
Update: In method Validate() at lines 64-66 there was a bug. Corrected.