Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
662 views
in Technique[技术] by (71.8m points)

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

I have a data collection of type ObservableCollection (say instance as myClassTypes). After some user operation, this myClassTypes populated with values in ViewModel. In view, there is a TextBox where user can enter text. I need to validate textbox data against myClassTypes values. So if myClassTypes contains the text inserted by User in textbox, Validation is passed otherwise it will fail. My code snippet is: ViewModel:

public ObservableCollection < MyClassType > ViewModelClassTypes {
    get {

        return _myClassTypes;
    }
    set {
        _myClassTypes = value;
        NotifyOfPropertyChange(() = >MyClassTypes);
    }
}

public class TestValidationRule: ValidationRule {
    public ObservableCollection < MyClassType > MyClassTypes {
        get = >(ObservableCollection < MyClassType > ) GetValue(MyClassTypesProperty);
        set = >SetValue(MyClassTypesProperty, value);
    }
}

FYI : MyClassTypesProperty is a dependency property

My View.xaml is :

<TextBox>
    <TextBox.Text>
        <Binding UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <validationRules:TestValidationRule MyClassTypes="{Binding ViewModelClassTypes}"/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

I am not able to get ViewModelClassTypes populated value in MyClassTypes. Can anyone please suggest what's wrong I am doing ?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.


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.

This validation feedback visualization 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

  • a ValidationRule (class to encapsulate the actual data validation implementation)
  • Lambdas (or delegates)
  • validation attributes (used to decorate the validated property).

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.


ViewModel.cs

The view model is responsible for validating its own properties to ensure the data integrity of the model.
Since .NET 4.5, the recommended way is to let the view model implement the INotifyDataErrorInfo interface.
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
      ValidateProperty(value);

      this.userInput = value; 
      OnPropertyChanged();
    }
  }

  // Constructor
  public ViewModel()
  {
    this.Errors = new Dictionary<string, List<string>>();
    this.ValidationRules = new Dictionary<string, List<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 ValidateProperty<TValue>(TValue propertyValue, [CallerMemberName] string propertyName = null)  
  {  
    // Clear previous errors of the current property to be validated 
    this.Errors.Remove(propertyName); 
    OnErrorsChanged(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
      propertyValidationRules
        .Select(validationRule => validationRule.Validate(propertyValue, CultureInfo.CurrentCulture))
        .Where(result => !result.IsValid)
        .ToList()
        .ForEach(invalidResult => AddError(propertyName, invalidResult.ErrorContent as string));

      return !PropertyHasErrors(propertyName);
    }

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

  // 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. 
  // A property can have multiple errors.
  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);
    } 
  }

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

  #region INotifyDataErrorInfo implementation

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

  // This implementatio 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 List<string> errors) 
        ? errors 
        : new List<string>();

  // 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, List<String>> Errors { get; }

  // Maps a property name to a list of ValidationRules that belong to this property
  private Dictionary<String, List<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;
  }
}

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.<b


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...