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).
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…