Utilizing INotifyDataErrorInfo in WPF MvvM app
By Mirek on (tags: INotifyDataErrorInfo, mvvm, validation, WPF, categories: code)Today I will show you how I utilize the INotifyDataErrorInfo interface to accomplish validation mechanisms in WPF application.
A time ago I have described how to validate model in WPF application with use of IDataErrorInfo interface. Now in WPF 4.5 we have another possibility which is the INotifyDataErrorInfo interface. The MSDN documentation is here a little scarce, but since this interface was ported from Silverlight we can read more on Silverlight verion of documentation. The documentation on MSDN suggest that all new entities should implement INotifyDataErrorInfo instead of IDataErrorInfo, for more flexibility, thus we can assume that IDataErrorInfo is or will be deprecated soon.
The INotifyDataErrorInfo interface itself is simple and quite self-explanatory
public interface INotifyDataErrorInfo
{
bool HasErrors { get; }
event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
IEnumerable GetErrors(string propertyName);
}
GetErrors method is called by binding engine to retrieve errors for either property if its name is provided or for whole model if propertyName is null;
INotifyDataErrorInfo interface gives us more flexibility on model validation. We can decide when we want to validate properties, for example in property setters. We can signal errors on single properties as well as cross-property errors and model level errors.
I have put all validation logic in a base class which all my models then inherit from.
1: public abstract class ModelValidation : INotifyDataErrorInfo
2: {
3: private Dictionary<string, List<string>> errors = new Dictionary<string, List<string>>();
4: public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
5: private object threadLock = new object();
6:
7: public bool IsValid
8: {
9: get { return !this.HasErrors; }
10:
11: }
12:
13: public void OnErrorsChanged(string propertyName)
14: {
15: if (ErrorsChanged != null)
16: ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
17: }
18:
19: public IEnumerable GetErrors(string propertyName)
20: {
21: if (!string.IsNullOrEmpty(propertyName))
22: {
23: if (errors.ContainsKey(propertyName) && (errors[propertyName] != null) && errors[propertyName].Count > 0)
24: return errors[propertyName].ToList();
25: else
26: return null;
27: }
28: else
29: return errors.SelectMany(err => err.Value.ToList());
30: }
31:
32: public bool HasErrors
33: {
34: get { return errors.Any(propErrors => propErrors.Value != null && propErrors.Value.Count > 0); }
35: }
36:
37: public void ValidateProperty(object value, [CallerMemberName] string propertyName = null)
38: {
39: lock (threadLock)
40: {
41: var validationContext = new ValidationContext(this, null, null);
42: validationContext.MemberName = propertyName;
43: var validationResults = new List<ValidationResult>();
44: Validator.TryValidateProperty(value, validationContext, validationResults);
45:
46: //clear previous errors from tested property
47: if (errors.ContainsKey(propertyName))
48: errors.Remove(propertyName);
49: OnErrorsChanged(propertyName);
50:
51: HandleValidationResults(validationResults);
52: }
53: }
54:
55: public void Validate()
56: {
57: lock (threadLock)
58: {
59: var validationContext = new ValidationContext(this, null, null);
60: var validationResults = new List<ValidationResult>();
61: Validator.TryValidateObject(this, validationContext, validationResults, true);
62:
63: //clear all previous errors
64: var propNames = errors.Keys.ToList();
65: errors.Clear();
66: propNames.ForEach(pn => OnErrorsChanged(pn));
67:
68: HandleValidationResults(validationResults);
69: }
70: }
71:
72: private void HandleValidationResults(List<ValidationResult> validationResults)
73: {
74: //Group validation results by property names
75: var resultsByPropNames = from res in validationResults
76: from mname in res.MemberNames
77: group res by mname into g
78: select g;
79:
80: //add errors to dictionary and inform binding engine about errors
81: foreach (var prop in resultsByPropNames)
82: {
83: var messages = prop.Select(r => r.ErrorMessage).ToList();
84: errors.Add(prop.Key, messages);
85: OnErrorsChanged(prop.Key);
86: }
87: }
88: }
As the example on MSDN suggests I keep all errors in a dictionary where the key is a property name. Then whenever a property is validated by method ValidateProperty I clear previous errors for the property, signal the change to the binding engine, validate property with use of Validator helper class and if any errors are found I add them to the dictionary and signals binding engine again with a proper property name.
The same, but for whole model, is performed in Validate method. Here I check all model properties at once.
The one thing I am missing here is functionality for handling model level errors, that are not strictly attached to any of properties. Here instead (in line 29) I return a list of all errors for all properties. For me this solution is satisfactory, however there should be no problem implementing it in above model.
Now lets see how the model looks like
1: public class MainWindowModel : ModelValidation
2: {
3: private string _name;
4: private string _email;
5:
6: [Required]
7: [StringLength(20)]
8: public string Name
9: {
10: get { return _name; }
11: set
12: {
13: _name = value;
14: ValidateProperty(value);
15: NotifyChangedThis();
16: }
17: }
18:
19: [Required]
20: [EmailAddress]
21: public string Email
22: {
23: get { return _email; }
24: set
25: {
26: _email = value;
27: ValidateProperty(value);
28: NotifyChangedThis();
29: }
30: }
31: }
Just for clarifying method NotifyChangedThis() handles change notification from INotifyPropertyChanged interface. Here each property has validation attributes assigned, therefore those are considered by Validator in our ModelValidation class. For cross property errors we can use CustomValidationAttribute.
For asynchronous validation we could add a ValidateAsync and ValidatePropertyAsync methods to the ModelValidation class. The implementation should be simple and straight foreword. We should then use a ConcurrentDictionary instead of regular one as our errors cache.
Update: In method Validate() at lines 64-66 there was a bug. Corrected.
luis
2/5/2015 7:46 AM
Where is the NotifyChangedThis() event declared? i cant see it on ModelValidation class5/7/2015 9:05 PM
Why do you call "OnErrorsChanged(propertyName);" and "HandleValidationResults(validationResults);" at end of both "Validate" method ? It sounds to me that it will notify twice instead of one.itsChris
7/20/2020 12:53 PM
Thanks for your illustrations. What i don't understand is: You did a lot of effort but the sample or explanation is too incomplete. Where's the XAML Part? Where's the NotifyChangedThis() Method? Sure, one can read but why publishing such incomplete post?