How to add validation to view model properties or how to implement INotifyDataErrorInfo

The preferred way since .Net 4.5 to implement data validation is to let your view model implement INotifyDataErrorInfo (example from Technet, example from MSDN (Silverlight)).

Note: INotifyDataErrorInfo replaces the obsolete IDataErrorInfo.
The new framework infrastructure related to the INotifyDataErrorInfo interface provides many advantages like

  • support of multiple errors per property
  • custom error objects and customization of visual error feedback (e.g. to adapt visual cues to the custom error object)
  • asynchronous validation using async/await

How INotifyDataErrorInfo works

When the ValidatesOnNotifyDataErrors property of Binding is set to true, the binding engine will search for an INotifyDataErrorInfo implementation on the binding source and subscribe to the INotifyDataErrorInfo.ErrorsChanged event.

If the ErrorsChanged event of the binding source is raised and INotifyDataErrorInfo.HasErrors evaluates to true, the binding engine will invoke the INotifyDataErrorInfo.GetErrors(propertyName) method for the actual source property to retrieve the corresponding error message and then apply the customizable validation error template to the target control to visualize the validation error.
By default a red border is drawn around the element that has failed to validate.

In case of an error, which is when INotifyDataErrorInfo.HasErrors returns true, the binding engine will also set the attached Validation properties on the binding target, for example Validation.HasError and Validation.ErrorTemplate.
To customize the visual error feedback, we can override the default template provided by the binding engine, by overriding the value of the attached Validation.ErrorTemplate property (see example below).

The described validation procedure only executes when Binding.ValidatesOnNotifyDataErrors is set to true on the particular data binding and the Binding.Mode is set to either BindingMode.TwoWay or BindingMode.OneWayToSource.

How to implement INotifyDataErrorInfo

The following examples show three variations of property validation using

  1. a ValidationRule (class to encapsulate the actual data validation implementation)
  2. lambda expressions (or delegates)
  3. validation attributes (used to decorate the validated property).

Of course, you can combine all three variations to provide maximum flexibility.

The code is not tested. The snippets should all work, but may not compile due to typing errors. This code is intended to provide a simple example on how the INotifyDataErrorInfo interface could be implemented.


Preparing the view

MainWindow.xaml

To enable the visual data validation feedback, the Binding.ValidatesOnNotifyDataErrors property must be set to true on each relevant Binding i.e. where the source of the Binding is a validated property. The WPF framework will then show the control’s default error feedback.

Note: to make this work, the Binding.Mode must be either OneWayToSource or TwoWay (which is the default for the TextBox.Text property):

<Window>
    <Window.DataContext>
        <ViewModel />       
    </Window.DataContext>
    
    <!-- Important: set ValidatesOnNotifyDataErrors to true to enable visual feedback -->
    <TextBox Text="{Binding UserInput, ValidatesOnNotifyDataErrors=True}" 
             Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}" />  
</Window>

The following is an example of a custom validation error template.
The default visual error feedback is a simple red border around the validated element. In case you like to customize the visual feedback e.g., to allow showing error messages to the user, you can define a custom ControlTemplate and assign it to the validated element (in this case the TextBox) via the attached property Validation.ErrorTemplate (see above).
The following ControlTemplate enables showing a list of error messages that are associated with the validated property:
enter image description here

<ControlTemplate x:Key="ValidationErrorTemplate">
  <StackPanel>
    <Border BorderBrush="Red" 
            BorderThickness="1">
      
      <!-- Placeholder for the DataGridTextColumn itself -->
      <AdornedElementPlaceholder x:Name="AdornedElement"  />
    </Border>

    <Border Background="White" 
            BorderBrush="Red" 
            Padding="4"
            BorderThickness="1,0,1,1" 
            HorizontalAlignment="Left">
      <ItemsControl ItemsSource="{Binding}" HorizontalAlignment="Left">
        <ItemsControl.ItemTemplate>
          <DataTemplate>
            <TextBlock Text="{Binding ErrorContent}" 
                       Foreground="Red"/>
          </DataTemplate>
        </ItemsControl.ItemTemplate>
      </ItemsControl>
    </Border>
  </StackPanel>
</ControlTemplate>

The view model is responsible for validating its own properties to ensure the data integrity of the model.
I recommend moving the implementation of INotifyDataErrorInfo into a base class (e.g. an abstract ViewModel class) together with the INotifyPropertyChanged implementation and let all your view models inherit it. This makes the validation logic reusable and keeps your view model classes clean.

You can change the example’s implementation details of INotifyDataErrorInfo to meet requirements.

1 Data validation using ValidationRule

ViewModel.cs
When using ValidationRule, the key is to have separate ValidationRule implementations for each property or rule.

Extending ValidationRule is optional. I chose to extend ValidationRule because it already provides a complete validation API and because the implementations can be reused with binding validation if necessary.
Basically, the result of the property validation should be a bool to indicate fail or success of the validation and a message that can be displayed to the user to help him to fix his input.

All we have to do in case of a validation error is to generate an error message, add it to a private string collection to allow our INotifyDataErrorInfo.GetErrors(propertyName) implementation to return the proper error messages from this collection and raise the INotifyDataErrorInfo.ErrorChanged event to notify the WPF binding engine about the error:

public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
  // Example property, which validates its value before applying it
  private string userInput;
  public string UserInput
  { 
    get => this.userInput; 
    set 
    { 
      // Validate the value
      bool isValueValid = IsPropertyValid(value);

      // Optionally reject value if validation has failed
      if (isValueValid)
      {
        this.userInput = value; 
        OnPropertyChanged();
      }
    }
  }

  // Constructor
  public ViewModel()
  {
    this.Errors = new Dictionary<string, IList<object>>();
    this.ValidationRules = new Dictionary<string, IList<ValidationRule>>();

    // Create a Dictionary of validation rules for fast lookup. 
    // Each property name of a validated property maps to one or more ValidationRule.
    this.ValidationRules.Add(nameof(this.UserInput), new List<ValidationRule>() { new UserInputValidationRule() });
  }

  // Validation method. 
  // Is called from each property which needs to validate its value.
  // Because the parameter 'propertyName' is decorated with the 'CallerMemberName' attribute.
  // this parameter is automatically generated by the compiler. 
  // The caller only needs to pass in the 'propertyValue', if the caller is the target property's set method.
  public bool IsPropertyValid<TValue>(TValue propertyValue, [CallerMemberName] string propertyName = null)  
  {  
    // Clear previous errors of the current property to be validated 
    _ = ClearErrors(propertyName); 

    if (this.ValidationRules.TryGetValue(propertyName, out List<ValidationRule> propertyValidationRules))
    {
      // Apply all the rules that are associated with the current property 
      // and validate the property's value            
      IEnumerable<object> errorMessages = propertyValidationRules
        .Select(validationRule => validationRule.Validate(propertyValue, CultureInfo.CurrentCulture))
        .Where(result => !result.IsValid)
        .Select(invalidResult => invalidResult.ErrorContent);
      AddErrorRange(propertyName, errorMessages);

      return !errorMessages.Any();
    }

    // No rules found for the current property
    return true;
  }   

  // Adds the specified errors to the errors collection if it is not 
  // already present, inserting it in the first position if 'isWarning' is 
  // false. Raises the ErrorsChanged event if the Errors collection changes. 
  // A property can have multiple errors.
  private void AddErrorRange(string propertyName, IEnumerable<object> newErrors, bool isWarning = false)
  {
    if (!newErrors.Any())
    {
      return;
    }

    if (!this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors))
    {
      propertyErrors = new List<object>();
      this.Errors.Add(propertyName, propertyErrors);
    }

    if (isWarning)
    {
      foreach (object error in newErrors)
      {
        propertyErrors.Add(error);
      }
    }
    else
    {
      foreach (object error in newErrors)
      {
        propertyErrors.Insert(0, error);
      }
    }

    OnErrorsChanged(propertyName);
  }

  // Removes all errors of the specified property. 
  // Raises the ErrorsChanged event if the Errors collection changes. 
  public bool ClearErrors(string propertyName)
  {
    this.ValidatedAttributedProperties.Remove(propertyName);
    if (this.Errors.Remove(propertyName))
    { 
      OnErrorsChanged(propertyName); 
      return true;
    }
    return false;
  }

  // Optional method to check if a certain property has validation errors
  public bool PropertyHasErrors(string propertyName) => this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors) && propertyErrors.Any();

  #region INotifyDataErrorInfo implementation

  // The WPF binding engine will listen to this event
  public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

  // This implementation of GetErrors returns all errors of the specified property. 
  // If the argument is 'null' instead of the property's name, 
  // then the method will return all errors of all properties.
  // This method is called by the WPF binding engine when ErrorsChanged event was raised and HasErrors return true
  public System.Collections.IEnumerable GetErrors(string propertyName) 
    => string.IsNullOrWhiteSpace(propertyName) 
      ? this.Errors.SelectMany(entry => entry.Value) 
      : this.Errors.TryGetValue(propertyName, out IList<object> errors) 
        ? (IEnumerable<object>)errors 
        : new List<object>();

  // Returns 'true' if the view model has any invalid property
  public bool HasErrors => this.Errors.Any(); 

  #endregion

  #region INotifyPropertyChanged implementation

  public event PropertyChangedEventHandler PropertyChanged;

  #endregion

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

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

  // Maps a property name to a list of errors that belong to this property
  private Dictionary<string, IList<object>> Errors { get; }

  // Maps a property name to a list of ValidationRules that belong to this property
  private Dictionary<string, IList<ValidationRule>> ValidationRules { get; }
}

UserInputValidationRule.cs

This example validation rule extends ValidationRule and checks if the input starts with the ‘@’ character. If not, it returns an invalid ValidationResult with an error message that can be displayed to the user to help him to fix his input.

public class UserInputValidationRule : ValidationRule
{        
  public override ValidationResult Validate(object value, CultureInfo cultureInfo)
  {
    if (!(value is string userInput))
    {
      return new ValidationResult(false, "Value must be of type string.");    
    }

    if (!userInput.StartsWith("@"))
    {
      return new ValidationResult(false, "Input must start with '@'.");    
    }

    return ValidationResult.ValidResult;
  }
}

2 Data validation using lambda expressions and delegates

As an alternative approach, the ValidationRule can be replaced (or combined) with delegates to enable the use of Lambda expressions or Method Groups.
The validation expressions in this example return a tuple containing a boolean to indicate the validation state and a collection of string error objects for the actual messages. Since all error object related properties are of type object, the expressions can return any custom data type, in case you need more advanced error feedback and string is not a sufficient error object. In this case, we must also adjust the validation error template, to enable it to handle the data type.

ViewModel.cs

public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{  
  private string userInput;
  public string UserInput
  { 
    get => this.userInput; 
    set 
    { 
      // Validate the new property value.
      bool isValueValid = IsPropertyValid(value, 
        newValue => newValue.StartsWith("@") 
          ? (true, Enumerable.Empty<object>()) 
          : (false, new[] { "Value must start with '@'." }));

      // Optionally reject value if validation has failed 
      if (isValueValid)
      {
        // Accept the valid value
        this.userInput = value; 
        OnPropertyChanged();
      }
    }
  }

  // Alternative usage example property which validates its value 
  // before applying it using a Method Group.
  // Example uses System.ValueTuple.
  private string userInputAlternativeValidation;
  public string UserInputAlternativeValidation
  { 
    get => this.userInputAlternativeValidation; 
    set 
    { 
      // Use Method group
      if (IsPropertyValid(value, AlternativeValidation))
      {
        this.userInputAlternativeValidation = value; 
        OnPropertyChanged();
      }
    }
  }

  // Constructor
  public ViewModel()
  {
    this.Errors = new Dictionary<string, IList<object>>();
  }

  // The validation handler
  private (bool IsValid, IEnumerable<object> ErrorMessages) AlternativeValidation(string value)
  {
    return value.StartsWith("@") 
      ? (true, Enumerable.Empty<object>()) 
      : (false, new[] { "Value must start with '@'." });
  }

  // Example uses System.ValueTuple
  public bool IsPropertyValid<TValue>(
    TValue value, 
    Func<TValue, (bool IsValid, IEnumerable<object> ErrorMessages)> validationDelegate, 
    [CallerMemberName] string propertyName = null)  
  {  
    // Clear previous errors of the current property to be validated 
    _ = ClearErrors(propertyName); 

    // Validate using the delegate
    (bool IsValid, IEnumerable<object> ErrorMessages) validationResult = validationDelegate?.Invoke(value) ?? (true, Enumerable.Empty<object>());

    if (!validationResult.IsValid)
    {
      AddErrorRange(propertyName, validationResult.ErrorMessages);
    } 

    return validationResult.IsValid;
  }  

  // Adds the specified errors to the errors collection if it is not 
  // already present, inserting it in the first position if 'isWarning' is 
  // false. Raises the ErrorsChanged event if the Errors collection changes. 
  // A property can have multiple errors.
  private void AddErrorRange(string propertyName, IEnumerable<object> newErrors, bool isWarning = false)
  {
    if (!newErrors.Any())
    {
      return;
    }

    if (!this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors))
    {
      propertyErrors = new List<object>();
      this.Errors.Add(propertyName, propertyErrors);
    }

    if (isWarning)
    {
      foreach (object error in newErrors)
      {
        propertyErrors.Add(error);
      }
    }
    else
    {
      foreach (object error in newErrors)
      {
        propertyErrors.Insert(0, error);
      }
    }

    OnErrorsChanged(propertyName);
  }

  // Removes all errors of the specified property. 
  // Raises the ErrorsChanged event if the Errors collection changes. 
  public bool ClearErrors(string propertyName)
  {
    this.ValidatedAttributedProperties.Remove(propertyName);
    if (this.Errors.Remove(propertyName))
    { 
      OnErrorsChanged(propertyName); 
      return true;
    }
    return false;
  }

  // Optional method to check if a certain property has validation errors
  public bool PropertyHasErrors(string propertyName) => this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors) && propertyErrors.Any();

  #region INotifyDataErrorInfo implementation

  // The WPF binding engine will listen to this event
  public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

  // This implementation of GetErrors returns all errors of the specified property. 
  // If the argument is 'null' instead of the property's name, 
  // then the method will return all errors of all properties.
  // This method is called by the WPF binding engine when ErrorsChanged event was raised and HasErrors retirn true
  public System.Collections.IEnumerable GetErrors(string propertyName) 
    => string.IsNullOrWhiteSpace(propertyName) 
      ? this.Errors.SelectMany(entry => entry.Value) 
      : this.Errors.TryGetValue(propertyName, out IList<object> errors) 
        ? (IEnumerable<object>)errors 
        : new List<object>();

  // Returns 'true' if the view model has any invalid property
  public bool HasErrors => this.Errors.Any(); 

  #endregion

  #region INotifyPropertyChanged implementation

  public event PropertyChangedEventHandler PropertyChanged;

  #endregion

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

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

  // Maps a property name to a list of errors that belong to this property
  private Dictionary<string, IList<object>> Errors { get; }
}

3 Data validation using ValidationAttribute

This is an example implementation of INotifyDataErrorInfo with ValidationAttribute support e.g., MaxLengthAttribute. This solution combines the previous Lambda version to additionally support validation using a Lambda expression/delegate simultaneously.
While validation using a lambda expression or a delegate must be explicitly invoked by calling the TryValidateProperty method in the properties setter, the attribute validation is executed implicitly from the OnPropertyChanged event invocator (as soon the property was decorated with validation attributes):

ViewModel.cs

public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{    
  private string userInputAttributeValidation;
 
  // Validate property using validation attributes
  [MaxLength(Length = 5, ErrorMessage = "Only five characters allowed.")]
  public string UserInputAttributeValidation
  { 
    get => this.userInputAttributeValidation; 
    set 
    {           
      // Optional call to 'IsPropertyValid' to combine attribute validation
      // with a delegate
      bool isValueValid = IsPropertyValid(value, newValue => newValue.StartsWith("@") 
        ? (true, Enumerable.Empty<object>()) 
        : (false, new[] { "Value must start with '@'." }));

      // Optionally reject value if validation h as failed
      if (isValueValid)
      {
        this.userInputAttributeValidation = value; 
      }

      // Triggers checking for validation attributes and their validation, 
      // if any validation attributes were found (like 'MaxLength' in this example)
      OnPropertyChanged();
    }
  }

  // Constructor
  public ViewModel()
  {
    this.Errors = new Dictionary<string, IList<object>>();
    this.ValidatedAttributedProperties = new HashSet<string>();
  }      

  // Validate property using decorating attributes. 
  // Is invoked by 'OnPropertyChanged' (see below).
  private bool IsAttributedPropertyValid<TValue>(TValue value, string propertyName)  
  {  
    this.ValidatedAttributedProperties.Add(propertyName);

    // The result flag
    bool isValueValid = true;

    // Check if property is decorated with validation attributes
    // using reflection
    IEnumerable<Attribute> validationAttributes = GetType()
      .GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
      ?.GetCustomAttributes(typeof(ValidationAttribute)) ?? new List<Attribute>();

    // Validate using attributes if present
    if (validationAttributes.Any())
    {
      var validationContext = new ValidationContext(this, null, null) { MemberName = propertyName };
      var validationResults = new List<ValidationResult>();
      if (!Validator.TryValidateProperty(value, validationContext, validationResults))
      {           
        isValueValid = false;
        AddErrorRange(validationResults.Select(attributeValidationResult => attributeValidationResult.ErrorMessage));
      }
    }

    return isValueValid;
  }       

  // Example uses System.ValueTuple
  public bool IsPropertyValid<TValue>(
    TValue value, 
    Func<TValue, (bool IsValid, IEnumerable<object> ErrorMessages)> validationDelegate = null, 
    [CallerMemberName] string propertyName = null)  
  {  
    // Clear previous errors of the current property to be validated 
    ClearErrors(propertyName); 

    // Validate using the delegate
    (bool IsValid, IEnumerable<object> ErrorMessages) validationResult = validationDelegate?.Invoke(value) ?? (true, Enumerable.Empty<object>());

    if (!validationResult.IsValid)
    {
      // Store the error messages of the failed validation
      AddErrorRange(validationResult.ErrorMessages);
    } 

    bool isAttributedPropertyValid = IsAttributedPropertyValid(value, propertyName);

    return isAttributedPropertyValid && validationResult.IsValid;
  }      

  // Adds the specified errors to the errors collection if it is not 
  // already present, inserting it in the first position if 'isWarning' is 
  // false. Raises the ErrorsChanged event if the Errors collection changes. 
  // A property can have multiple errors.
  private void AddErrorRange(string propertyName, IEnumerable<object> newErrors, bool isWarning = false)
  {
    if (!newErrors.Any())
    {
      return;
    }

    if (!this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors))
    {
      propertyErrors = new List<object>();
      this.Errors.Add(propertyName, propertyErrors);
    }

    if (isWarning)
    {
      foreach (object error in newErrors)
      {
        propertyErrors.Add(error);
      }
    }
    else
    {
      foreach (object error in newErrors)
      {
        propertyErrors.Insert(0, error);
      }
    }

    OnErrorsChanged(propertyName);
  }

  // Removes all errors of the specified property. 
  // Raises the ErrorsChanged event if the Errors collection changes. 
  public bool ClearErrors(string propertyName)
  {
    this.ValidatedAttributedProperties.Remove(propertyName);
    if (this.Errors.Remove(propertyName))
    { 
      OnErrorsChanged(propertyName); 
      return true;
    }
    return false;
  }

  public bool PropertyHasErrors(string propertyName) => this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors) && propertyErrors.Any();

  #region INotifyDataErrorInfo implementation

  public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

  // Returns all errors of a property. If the argument is 'null' instead of the property's name, 
  // then the method will return all errors of all properties.
  public System.Collections.IEnumerable GetErrors(string propertyName) 
    => string.IsNullOrWhiteSpace(propertyName) 
      ? this.Errors.SelectMany(entry => entry.Value) 
      : this.Errors.TryGetValue(propertyName, out IList<object> errors) 
        ? (IEnuemrable<object>)errors 
        : new List<object>();

  // Returns if the view model has any invalid property
  public bool HasErrors => this.Errors.Any(); 

  #endregion

  #region INotifyPropertyChanged implementation

  public event PropertyChangedEventHandler PropertyChanged;

  #endregion

  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    // Check if IsAttributedPropertyValid Property was already called by 'IsValueValid'.
    if (!this.ValidatedAttributedProperties.Contains(propertyName))
    {
      _ = IsAttributedPropertyValid(value, propertyName);
    }

    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }

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

  // Maps a property name to a list of errors that belong to this property
  private Dictionary<string, IList<object>> Errors { get; }    


  // Track attribute validation calls
  private HashSet<string> ValidatedAttributedProperties { get; } 
}

Leave a Comment