Search code examples
c#wpfvalidationdata-bindinginotifydataerrorinfo

Using INotifyDataErrorInfo with attributes


I am working setting up INotifyDataErrorInfo on a view model, to handle validation with attributes.

I have it working fine in the UI, the text box gets a nice red boarder and the mouse over event says what is wrong.

But I can not work out how in the ViewModel to work out the view model is valid. I am guessing I have to set up the HasErrors. In the examples I have seen they have a variable

 private Dictionary<string, List<string>> _PropertyErrors = new Dictionary<string, List<string>>(); 

But then do nothing to set it.

I would like to check in the Save() method if the view model is valid.

 public class CustomerViewModel : EntityBase, INotifyPropertyChanged, INotifyDataErrorInfo
 {

  public CustomerViewModel ()
  {
    //SET UP
  }

  private string _HomePhone;

    [Required]     
    public string HomePhone
    {
        get { return _HomePhone; }
        set
        {
            if (_HomePhone != value)
            {                  
                _HomePhone = value;
                PropertyChanged(this, new PropertyChangedEventArgs("HomePhone"));
            }
        }
    }

    private void Save()
    {
        //Break point here
    }

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;  

    public bool HasErrors
    {
        get { return true; }
    }

    public IEnumerable GetErrors(string propertyName)
    {            
        return null;
    }

Solution

  • You can check the HasErrors property.

    This is an example implementation of INotifyDataErrorInfo with ValidationAttribute support and providing an example TrySave(), which checks if the view model has any validation errors:

    public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
    {    
      // Usage example property which validates its value 
      // before applying it using a Lambda expression.
      // Example uses System.ValueTuple.
      private string userInput;
      public string UserInput
      { 
        get => this.userInput; 
        set 
        { 
          // Use Lambda
          if (ValidateProperty(value, newValue => newValue.StartsWith("@") ? (true, string.Empty) : (false, "Value must start with '@'.")))
          {
            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 (ValidateProperty(value, AlternativeValidation))
          {
            this.userInputAlternativeValidation = value; 
            OnPropertyChanged();
          }
        }
      }
    
      private (bool IsValid, string ErrorMessage) AlternativeValidation(string value)
      {
        return value.StartsWith("@") 
          ? (true, string.Empty) 
          : (false, "Value must start with '@'.");
      }
    
      // Alternative usage example property which validates its value 
      // before applying it using a ValidationAttribute.
      private string userInputAttributeValidation;
     
      [Required(ErrorMessage = "Value is required.")]
      public string UserInputAttributeValidation
      { 
        get => this.userInputAttributeValidation; 
        set 
        { 
          // Use only the attribute (can be combined with a Lambda or Method group)
          if (ValidateProperty(value))
          {
            this.userInputAttributeValidation = value; 
            OnPropertyChanged();
          }
        }
      }
    
      private bool TrySave()
      {
        if (this.HasErrors)
        {
          return false;
        }
    
        // View model has no errors. Save data.
    
        return true;
      }
    
      // Constructor
      public ViewModel()
      {
        this.Errors = new Dictionary<string, List<string>>();
      }
    
      // Example uses System.ValueTuple
      public bool ValidateProperty(object value, Func<object, (bool IsValid, string ErrorMessage)> validationDelegate = null, [CallerMemberName] string propertyName = null)  
      {  
        // Clear previous errors of the current property to be validated 
        this.Errors.Remove(propertyName); 
        OnErrorsChanged(propertyName); 
    
        // First validate using the delegate
        (bool IsValid, string ErrorMessage) validationResult = validationDelegate?.Invoke(value) ?? (true, string.Empty);
    
        if (!validationResult.IsValid)
        {
          AddError(propertyName, validationResult.ErrorMessage);
        } 
    
        // 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 attributes if present
        if (validationAttributes.Any())
        {
          var validationResults = new List<ValidationResult>();
          if (!Validator.TryValidateProperty(value, new ValidationContext(this, null, null) { MemberName = propertyName }, validationResults))
          {           
            foreach (ValidationResult attributeValidationResult in validationResults)
            {
              AddError(propertyName, attributeValidationResult.ErrorMessage);
            }
    
            validationResult = (false, string.Empty);
          }
        }
    
        return validationResult.IsValid;
      }   
    
      // Adds the specified error 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. 
      public void AddError(string propertyName, string errorMessage, bool isWarning = false)
      {
        if (!this.Errors.TryGetValue(propertyName, out List<string> propertyErrors))
        {
          propertyErrors = new List<string>();
          this.Errors[propertyName] = propertyErrors;
        }
    
        if (!propertyErrors.Contains(errorMessage))
        {
          if (isWarning) 
          {
            // Move warnings to the end
            propertyErrors.Add(errorMessage);
          }
          else 
          {
            propertyErrors.Insert(0, errorMessage);
          }
          OnErrorsChanged(propertyName);
        } 
      }
    
      public bool PropertyHasErrors(string propertyName) => this.Errors.TryGetValue(propertyName, out List<string> 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 IEnumerable<string> errors) 
            ? errors 
            : new List<string>();
    
      // 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)
      {
        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, List<String>> Errors { get; }    
    }
    

    This link contains an explanation and links to more examples: How to add validation to view model properties or how to implement INotifyDataErrorInfo