INotifyDataErrorInfo and binding exceptions

2019-05-28 08:35发布

I'm using the INotifyDataErrorInfo interface to implement a general MVVM validation mechanism. I'm implementing the interface by calling OnValidate instead of OnPropertyChanged:

public void OnValidate(dynamic value, [CallerMemberName] string propertyName = null)
{
        if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        Validate(propertyName, value);
}

In the Validate Method I'm generating the validation errors, add them to a Dictionary and raise the ErrorsChanged event if a validation error was found or cleared:

if (entry.Validate(strValue, out errorNumber, out errorString) == false)
{
      _validationErrors[propertyName] = new List<string> {errorString};
      RaiseErrorsChanged(propertyName);
}
else if (_validationErrors.ContainsKey(propertyName))
{
      _validationErrors.Remove(propertyName);
      RaiseErrorsChanged(propertyName);
}

The HasErrors property is implemented by looking at the errors dictionary:

    public bool HasErrors
    {
         get { return _validationErrors.Any(kv => kv.Value != null 
                   && kv.Value.Count > 0); }
    }

To prevent the save button from being enabled when there is a validation error - The save command canExecuteMethod looks at the HasErrors property:

private bool IsSaveEnabled()
{
    return HasErrors == false;
}

Everything works fine except the case where I'm having binding errors - if the binded value is (for example) an integer a non integer is entered - the textbox's ErrorContent is updated with an error string: "Value 'something' could not be converted". But the INotifyDataErrorInfo mechanism is not updated about this. The HasErrors remains false and Save is enabled although there is an error in the view. I would like to find a way to propagate the binding exception to the INotifyDataErrorInfo mechanism so I would be able to:

  1. Disable the Save button (must).
  2. Change the validation error message to a more meaningful error string (nice to have).

I would like to find a general MVVM solution without adding code behind in the view.

Thank you for the help

3条回答
The star\"
2楼-- · 2019-05-28 08:42

the string int case doesn't work with MVVM because your viewmodel doesn't get any information because of the binding exception.

I see two ways to get the validation you want:

  1. Just use string properties in your viewmodel and when you have to go to your model just convert the string to your model type.
  2. Create behaviors or "special" controls so the the input in your view is always "convertible" to your viewmodel type.

Btw I use the second approach because I have to :) but the first will always work and seems easier to me.

查看更多
再贱就再见
3楼-- · 2019-05-28 08:44

Here is the solution that I have found. It makes the INotifyDataErrorInfo behave correctly in the ViewModel (When there is any validation error – the HasError is true), and it allows adding validation errors from the viewModel. Other than this, it does not require changes in the view, changes in the binding or converters.

This solution involves:

  • Adding a custom validation rule.
  • Adding a base user control (which all view must derive from).
  • Adding some code in the ViewModel base.

Adding a custom validation rule – Validation Entity which does the actual validation and raises an event when the validation changes:

class ValidationEntity : ValidationRule
{
   public string Key { get; set; }

   public string BaseName = "Base";

   public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
   {
       var fullPropertyName = BaseName + "." + Key;
       ValidationEntry entry;

       var validationResult = new ValidationResult(true, null);

       if ((entry = ValidationManager.Instance.FindValidation(fullPropertyName)) != null)
       {
           int errorNumber;
           string errorString;

           var strValue = (value != null) ? value.ToString() : string.Empty;

           if (entry.Validate(strValue, out errorNumber, out errorString) == false)
           {
               validationResult = new ValidationResult(false, errorString);
           }
       }

       if (OnValidationChanged != null)
       {
           OnValidationChanged(Key, validationResult);
       }
       return validationResult;
   }

   public event Action<string, ValidationResult> OnValidationChanged;
}

Adding a base user control which keeps a list of the active textboxs, and adds the validation rule to each textbox binding: This is the code at the user control base:

private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
    _textBoxes = FindAllTextBoxs(this);

    var vm = DataContext as ViewModelBase;
    if (vm != null) vm.UpdateAllValidationsEvent += OnUpdateAllValidationsEvent;

    foreach (var textbox in _textBoxes)
    {
        var binding = BindingOperations.GetBinding(textbox, TextBox.TextProperty);

        if (binding != null)
        {
            var property = binding.Path.Path;
            var validationEntity = new ValidationEntity {Key = property};
            binding.ValidationRules.Add(validationEntity);
            validationEntity.ValidationChanged += OnValidationChanged;
        }
    }
}
private List<TextBox> FindAllTextBoxs(DependencyObject fe)
{
    return FindChildren<TextBox>(fe);
}

private List<T> FindChildren<T>(DependencyObject dependencyObject)
            where T : DependencyObject
{
    var items = new List<T>();

    if (dependencyObject is T)
    {
        items.Add(dependencyObject as T);
        return items;
    }

    var count = VisualTreeHelper.GetChildrenCount(dependencyObject);
    for (var i = 0; i < count; i++)
    {
        var child = VisualTreeHelper.GetChild(dependencyObject, i);
        var children = FindChildren<T>(child);
        items.AddRange(children);
    }
    return items;
}

When the ValidationChange event happens – the view is called to be notified about the validation error:

private void OnValidationChanged(string propertyName, ValidationResult validationResult)
{
    var vm = DataContext as ViewModelBase;

    if (vm != null)
    {
        if (validationResult.IsValid)
        {
            vm.ClearValidationErrorFromView(propertyName);
        }
        else
        {
            vm.AddValidationErrorFromView(propertyName, validationResult.ErrorContent as string);
        }
    }
}

The ViewModel base keeps two lists:

  • _notifyvalidationErrors which is used by the INotifyDataErrorInfo interface to display the validation errors.
  • _privateValidationErrors which is used to display the errors generated from the Validation rule to the user.

When adding a validation error from the view – the _notifyvalidationErrors is updated with an empty value (just to denote there is a validation error) the error string is not added to the _notifyvalidationErrors. If we add it to there we would get the validation error string twice in the textbox ErrorContent. The validation error string is also added to _privateValidationErrors (Because we want to be able to keep it at the viewmodel) This is the code at the ViewModel base:

private readonly Dictionary<string, List<string>> _notifyvalidationErrors =
        new Dictionary<string, List<string>>();
private readonly Dictionary<string, List<string>> _privateValidationErrors =
            new Dictionary<string, List<string>>();

public void AddValidationErrorFromView(string propertyName, string errorString)
{
   _notifyvalidationErrors[propertyName] = new List<string>();
   // Add the error to the private dictionary
   _privateValidationErrors[propertyName] = new List<string> {errorString};
   RaiseErrorsChanged(propertyName);
}

 public void ClearValidationErrorFromView(string propertyName)
 {
     if (_notifyvalidationErrors.ContainsKey(propertyName))
     {
         _notifyvalidationErrors.Remove(propertyName);
     }
     if (_privateValidationErrors.ContainsKey(propertyName))
     {
         _privateValidationErrors.Remove(propertyName);
     }
     RaiseErrorsChanged(propertyName);

 }

The INotifyDataErrorInfo implementation in the view:

public bool HasErrors
{
    get { return _notifyvalidationErrors.Any(kv => kv.Value != null); }
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

public void RaiseErrorsChanged(string propertyName)
{
       var handler = ErrorsChanged;
       if (handler != null)
           handler(this, new DataErrorsChangedEventArgs(propertyName));
}
public IEnumerable GetErrors(string propertyName)
{
    List<string> errorsForProperty;
    _notifyvalidationErrors.TryGetValue(propertyName, out errorsForProperty);

    return errorsForProperty;
}

The user has an option to add validation errors from the view by calling the ViewModelBase AddValidationError and ClearValidationError methods.

public void AddValidationError(string errorString, [CallerMemberName] string propertyName = null)
{
    _notifyvalidationErrors[propertyName] = new List<string>{ errorString };
    RaiseErrorsChanged(propertyName);
}

public void ClearValidationError([CallerMemberName] string propertyName = null)
{
    if (_notifyvalidationErrors.ContainsKey(propertyName))
    {
        _notifyvalidationErrors.Remove(propertyName);
        RaiseErrorsChanged(propertyName);
    }
}

The view can get a list of all current validation errors from the ViewModel base by calling the GetValidationErrors and GetValidationErrorsString methods.

public List<string> GetValidationErrors()
{
    var errors = new List<string>();
    foreach (var key in _notifyvalidationErrors.Keys)
    {
        errors.AddRange(_notifyvalidationErrors[key]);

        if (_privateValidationErrors.ContainsKey(key))
        {
            errors.AddRange(_privateValidationErrors[key]);
        }
    }
    return errors;
}

public string GetValidationErrorsString()
{
    var errors = GetValidationErrors();
    var sb = new StringBuilder();
    foreach (var error in errors)
    {
        sb.Append("● ");
        sb.AppendLine(error);
    }
    return sb.ToString();
}
查看更多
贼婆χ
4楼-- · 2019-05-28 08:52

Set

ValidatesOnExceptions="True"

In your Binding expression.

查看更多
登录 后发表回答