16

I have implemented INotifyDataErrorInfo exactly as described in the following link:

http://blog.micic.ch/net/easy-mvvm-example-with-inotifypropertychanged-and-inotifydataerrorinfo

I have a TextBox which is bound to a string property in my model.

XAML

<TextBox Text="{Binding FullName,
                        ValidatesOnNotifyDataErrors=True,
                        NotifyOnValidationError=True,
                        UpdateSourceTrigger=PropertyChanged}" />

Model

private string _fullName;
public string FullName
{
    get { return _fullName; }
    set
    {
        // Set raises OnPropertyChanged
        Set(ref _fullName, value);

        if (string.IsNullOrWhiteSpace(_fullName))
            AddError(nameof(FullName), "Name required");
        else
            RemoveError(nameof(FullName));                
    }
}

INotifyDataError Code

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

public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

// get errors by property
public IEnumerable GetErrors(string propertyName)
{
    if (_errors.ContainsKey(propertyName))
        return _errors[propertyName];
    return null;
}

public bool HasErrors => _errors.Count > 0;

// object is valid
public bool IsValid => !HasErrors;

public void AddError(string propertyName, string error)
{
    // Add error to list
    _errors[propertyName] = new List<string>() { error };
    NotifyErrorsChanged(propertyName);
}

public void RemoveError(string propertyName)
{
    // remove error
    if (_errors.ContainsKey(propertyName))
        _errors.Remove(propertyName);
    NotifyErrorsChanged(propertyName);
}

public void NotifyErrorsChanged(string propertyName)
{
    // Notify
    if (ErrorsChanged != null)
       ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}

Now all this works fine, but it only validates as soon as I type something in my TextBox. I would like some way to validate on demand, without even touching the textbox, say on a button click.

I have tried raising PropertyChanged for all my properties as described in this question, but it does not detect the errors. I somehow need my property setter to be called so the errors can be detected. I'm looking for a MVVM solution.

Community
  • 1
  • 1
kkyr
  • 3,785
  • 3
  • 29
  • 59
  • Why don't you simply call NotifyErrorsChanged method? This will raise ErrorsChanged event and all bound controls should react on it if they have ValidatesOnNotifyDataErrors=True. – Stipo Jan 07 '16 at 21:55
  • I've tried it, it does nothing and I'm assuming that's because the _errors dictionary is empty at that time. – kkyr Jan 07 '16 at 22:13
  • What is the purpose of on-demand validation? Your model will validate itself immediately when any of its properties are changed. A manual validation will just yield the same result because the model has already been validated by itself. – TreeTree Jan 07 '16 at 22:29
  • In order to disable a button. Without on-demand validation at the start of the program the button is enabled when it shouldn't be, since my FullName property is null. – kkyr Jan 07 '16 at 22:56
  • One way around it I have found is to set the property again, i.e.: FullName = FullName, but this doesn't seem to be an elegant solution, especially when there are multiple properties. – kkyr Jan 07 '16 at 23:15
  • 1
    Then set property to null in the constructor of your class, since initial state of your class is invalid. – Stipo Jan 08 '16 at 00:33
  • 2
    The problem is that you perform the validation in the setter (check whether the value is null or white space). I suggest you extract the check to a separate method (say, `ValidateFullName()`), and then you'll be able to re-validate the value by simple call to this method - it will re-evaluate whether current value of `FullName` is valid, set appropriate validation info and raise `ErrorsChanged` if necessary. – Grx70 Jan 08 '16 at 07:14
  • How about raising property changed event for the `Error` property? – Mike Eason Jan 08 '16 at 08:50
  • @Stipo & Grx70 Both of these work, thank you for your comments. I will use the latter but I was hoping for a more elegant solution. – kkyr Jan 08 '16 at 14:42
  • What about manually calling UpdateSource/UpdateTarget on the corresponding BindingExpression? – Simon Mourier Jan 10 '16 at 12:52
  • That requires knowledge of the control which doesn't follow mvvm – kkyr Jan 10 '16 at 15:31
  • I'm running into the same issue. @TreeTree the biggest application of this is on a form that starts out blank, but blank is an error. At least in my app that doesn't show an error until someone actually changes that field, but hitting Apply should show the error. When I try to manually validate this form, ErrorsChanged is null. HasErrors is true and my error information is populated just fine, so the solution below does not apply. – jpwkeeper Jan 28 '21 at 14:50

2 Answers2

23

The INotifyDataErrorInfo implementation you use is somewhat flawed IMHO. It relies on errors kept in a state (a list) attached to the object. Problem with stored state is, sometimes, in a moving world, you don't have the chance to update it when you want. Here is another MVVM implementation that doesn't rely on a stored state, but computes error state on the fly.

Things are handled a bit differently as you need to put validation code in a central GetErrors method (you could create per-property validation methods called from this central method), not in the property setters.

public class ModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
{
    public event PropertyChangedEventHandler PropertyChanged;
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public bool HasErrors
    {
        get
        {
            return GetErrors(null).OfType<object>().Any();
        }
    }

    public virtual void ForceValidation()
    {
        OnPropertyChanged(null);
    }

    public virtual IEnumerable GetErrors([CallerMemberName] string propertyName = null)
    {
        return Enumerable.Empty<object>();
    }

    protected void OnErrorsChanged([CallerMemberName] string propertyName = null)
    {
        OnErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }

    protected virtual void OnErrorsChanged(object sender, DataErrorsChangedEventArgs e)
    {
        var handler = ErrorsChanged;
        if (handler != null)
        {
            handler(sender, e);
        }
    }

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        OnPropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    protected virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(sender, e);
        }
    }
}

And here are two sample classes that demonstrate how to use it:

public class Customer : ModelBase
{
    private string _name;

    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            if (_name != value)
            {
                _name = value;
                OnPropertyChanged();
            }
        }
    }

    public override IEnumerable GetErrors([CallerMemberName] string propertyName = null)
    {
        if (string.IsNullOrEmpty(propertyName) || propertyName == nameof(Name))
        {
            if (string.IsNullOrWhiteSpace(_name))
                yield return "Name cannot be empty.";
        }
    }
}

public class CustomerWithAge : Customer
{
    private int _age;
    public int Age
    {
        get
        {
            return _age;
        }
        set
        {
            if (_age != value)
            {
                _age = value;
                OnPropertyChanged();
            }
        }
    }

    public override IEnumerable GetErrors([CallerMemberName] string propertyName = null)
    {
        foreach (var obj in base.GetErrors(propertyName))
        {
            yield return obj;
        }

        if (string.IsNullOrEmpty(propertyName) || propertyName == nameof(Age))
        {
            if (_age <= 0)
                yield return "Age is invalid.";
        }
    }
}

It works like a charm with a simple XAML like this:

<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}" />

(UpdateSourceTrigger is optional, if you don't use it it will only work when focus is lost).

With this MVVM base class, you shouldn't have to force any validation. But should you need it, I have added a ForceValidation sample method in ModelBase that should work (I have tested it with for example a member value like _name that would have been changed without passing through the public setter).

Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • Doing this puts red error boxes around all my containers, like my stackpanels in [this example](http://i.imgur.com/uM4iCOg.png). I'm only using validation on a textbox which isn't even in this stackpanel. Additionally, this method forces validation as soon as the model is created. I would like validation **only** when a property is changed by the user **or** on demand. – kkyr Jan 11 '16 at 19:16
  • What you're asking is unclear and/or incomplete. There's no mention of stackpanel in your question. You can change validation style the way you want, for example: http://www.nbdtech.com/Blog/archive/2010/07/05/wpf-adorners-part-3-ndash-adorners-and-validation.aspx – Simon Mourier Jan 11 '16 at 19:35
  • I'm not trying to change the validation style, all I'm saying is after using your code all my containers are now highlighted by a red line. The stackpanels were just one example. – kkyr Jan 11 '16 at 19:45
  • The fact containers are highlighted by a red line is by design in WPF: if your MVVM/databound object is invalid, then its style uses WPF's default invalid validation style, that's why I pointed you a way to play with this. It in fact demonstrates my sample code works as WPF expects. – Simon Mourier Jan 17 '16 at 18:03
  • The specific control which is data bound to an object should in fact be highlighted by a red line, not containers which have no relation whatsoever to the control in question. – kkyr Jan 17 '16 at 18:10
  • 1
    I just switched over from the "error-dictionary" approach to the "GetErrors-on-demand" approach described here. I had none of the problems described above. It worked perfectly, resulting code much cleaner, and eliminates the timing headaches of keeping the error dictionary fresh. Makes much more sense for validation logic to run exactly when the binding engine needs it. Good stuff – nmarler Nov 11 '16 at 22:18
  • Does this approach still work with dependent properties? Like, e.g., property Age validation depends on property Country, and if country changed I need to re-evaluate Age? – Andreas Duering Mar 01 '18 at 08:16
  • @AndreasDuering - yes but you have to think differently. You will send any detected error(s) only when asked for it, not when properties change. So you will evaluate all what's required only at that moment (again, not when properties change) – Simon Mourier Mar 01 '18 at 09:11
  • 1
    @SimonMourier, in above example you don't rise `ErrorsChanged` event even once. I was sure this event is essential to tell bindings in the view "please call GetErrors() now, because something has changed". Could you edit your answer explaining that moment please? – Sinatr Feb 04 '20 at 08:58
  • @sinatr - You need nothing more, you don't have to raise anything, as WPF will check errors and call HasErrors automatically when a property changes (UpdateSourceTrigger=PropertyChanged) or when focus is lost. – Simon Mourier Feb 04 '20 at 09:34
1

Your best bet is to use a relay command interface. Take a look at this:

public class RelayCommand : ICommand
{
    Action _TargetExecuteMethod;
    Func<bool> _TargetCanExecuteMethod;

    public RelayCommand(Action executeMethod)
    {
        _TargetExecuteMethod = executeMethod;
    }

    public RelayCommand(Action executeMethod, Func<bool> canExecuteMethod)
    {
        _TargetExecuteMethod = executeMethod;
        _TargetCanExecuteMethod = canExecuteMethod;
    }

    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged(this, EventArgs.Empty);
    }
    #region ICommand Members

    bool ICommand.CanExecute(object parameter)
    {
        if (_TargetCanExecuteMethod != null)
        {
            return _TargetCanExecuteMethod();
        }
        if (_TargetExecuteMethod != null)
        {
            return true;
        }
        return false;
    }

    public event EventHandler CanExecuteChanged = delegate { };

    void ICommand.Execute(object parameter)
    {
        if (_TargetExecuteMethod != null)
        {
            _TargetExecuteMethod();
        }
    }
    #endregion
}

You would declare this relay command in your view model like:

public RelayCommand SaveCommand { get; private set; }

Now, in addition to registering your SaveCommand with OnSave and a CanSave methods, since you extend from INotifyDataErrorInfo, you can sign up to ErrorsChanged in your constructor as well:

public YourViewModel()
{
    SaveCommand = new RelayCommand(OnSave, CanSave);
    ErrorsChanged += RaiseCanExecuteChanged;
}

And you'll need the methods:

private void RaiseCanExecuteChanged(object sender, EventArgs e)
{
        SaveCommand.RaiseCanExecuteChanged();
}

public bool CanSave()
{
    return !this.HasErrors;
}

private void OnSave()
{
    //Your save logic here.
}

Also, each time after you call PropertyChanged, you can call this validation method:

    private void ValidateProperty<T>(string propertyName, T value)
    {
        var results = new List<ValidationResult>();
        ValidationContext context = new ValidationContext(this);
        context.MemberName = propertyName;
        Validator.TryValidateProperty(value, context, results);

        if (results.Any())
        {
            _errors[propertyName] = results.Select(c => c.ErrorMessage).ToList();
        }
        else
        {
            _errors.Remove(propertyName);
        }

        ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }

With this setup, and if your viewmodel both extends from INotifyPropertyChanged and INotifyDataErrorInfo (or from a base class that extends from these two), when you bind a button to the SaveCommand above, WPF framework will automatically disable it if there are validation errors.

Hope this helps.

A. Burak Erbora
  • 1,054
  • 2
  • 12
  • 26
  • I'm familiar with a `RelayCommand` and I'm already using something similar. My validation is being done on the model so my viewmodel is not implementing `INotifyDataErrorInfo`. Also, the problem I have is that my validation is being done in the property setter which is not being called when I want it to, so I fail to see how your answer solves this. I will go with the way @Grx70 mentioned in his comment above. – kkyr Jan 08 '16 at 14:39