Implementing INotifyDataErrorInfo for complex models in WPF

September 15, 2015

This is part one of a two part series, in which I'll detail an effective approach for architecting a validation framework for complex objects in WPF. In this part, I'll talk about what is necessary to fully implement INotifyDataErrorInfo. There are a number of other tutorials about this interface, but I don't feel any go into enough depth about what to do when facing a complex hierarchies of models. For this tutorial, I'm going to assume you've seen a few of these tutorials or have tried implementing INotifyDataErrorInfo, so I may skip over some of the more mundane aspects.

How INotifyDataErrorInfo Works

publicclassViewModel1//Assume some implementation of IPropertyNotifyChanged{privateint_maxChildren;publicintMaxChildren{get{return_maxChildren;}set{_maxChildren=value;NotifyPropertyChanged();}}privatestring_someProperty;publicstringSomeProperty{get{return_someProperty;}set{_someProperty=value;OnPropertyChanged();}}publicList<ChildViewModel1>ChildViewModels{get;set;}}publicclassChildViewModel1{privatestring_anotherProperty;publicstringAnotherProperty{get{return_anotherProperty;}set{_anotherProperty=value;OnPropertyChanged();}}publicChildViewModel2{get;set;}}publicclassChildViewModel2{privatestring_yetAnotherProperty;publicstringYetAnotherProperty{get{return_yetAnotherProperty;}set{_yetAnotherProperty=value;OnPropertyChanged();}}}

To implement this correctly, your viewmodel needs to handle the appropriate items:

Whenever a property that you want validated is modified, it has to notify the ErrorsChanged event. For example, if we wanted ChildViewModel1 to correctly notify that its AnotherProperty field is too short, it might look like this:

Important to note that my example is a hierarchy, and I'm going to assume the views are as well. This can open up a lot of complexities if validation of one model requires checking other models in the hierarchy. I'll cover this later.

HasErrors must return whether any errors exist on the view model. This is usually straightforward, but in the case of ViewModel1 above, what if you wanted it to return true if any there are any errors in its children or its children's child?

GetErrors is the easiest to implement in most cases, as it just needs to return the validation errors for the given view model.

So above I mentioned what can be some major problems implementing INotifyDataErrorInfo in this example, but here is a full list I've come up with:

How to best deal with HasErrors if it's necessary to account for child errors?

If an error occurs in a child view model, how will the parent view get notified to rebind?

If a parent/child viewmodel property is modified, how would the child/parent be notified to rerun validation, assuming there are dependencies in executing validation?

When should OnErrorsChanged(String.Empty) ever be called?

Sample Implementation

Here's a naive implementation of ViewModel1 and ChildViewModel1 to detail how ugly this can all get (no guarantees this would compile):

publicclassViewModel1:IPropertyNotifyChanged,INotifyDataErrorInfo//Assume there are simple implementations{privateList<ValidationResult>validationResult;publicViewModel1(){ChildViewModels=newObservableCollection<ChildViewModel1>();ChildViewModels.CollectionChanged+=childViewModels_changed;validationResult=newList<ValidationResult>();}publicvoidchildViewModels_changed(objectsender,NotifyCollectionChangedEventArgsargs){//Have to watch for changes in the children :(foreach(varnewIteminargs.NewItems.OfType<IPropertyNotifyChanged>()){newItem.PropertyChanged+=childViewModels_itemchanged;}foreach(varoldIteminargs.OldItems.OfType<IPropertyNotifyChanged>()){oldItem.PropertyChanged-=childViewModels_itemchanged;}Validate();//To have less code, just revalidate}privatestring_someProperty;publicstringSomeProperty{get{return_someProperty;}set{_someProperty=value;OnPropertyChanged();Validate();}}publicIEnumerableGetErrors(stringpropertyName){returnvalidationResult.Where(p=>p.MemberNames.Any(q=>q==propertyName));}publicboolHasErrors{get{returnvalidationResult.Any()||ChildViewModels.Any(p=>p.HasError);}}publicvoidValidate(){//Have to keep old one around so we can notify the view that errors have disappeared as well :(varexisting=validationResult;validationResult=newList<ValidationResult>();if(SomeProperty.Length>50){validationResult.Add(newValidationResult("SomeProperty must be 50 characters or less",newstring[]{"SomeProperty"}));}if(MaxChildren<ChildViewModels.Count()){validationResult.Add(newValidationResult("There can be no more than "+MaxChildren+" ChildViewModels",newstring[]{"ChildViewModels"}));}foreach(varresultinexisting){OnErrorsChanged(result.MemberNames.First());}foreach(varresultinvalidationResult){OnErrorsChanged(result.MemberNames.First());}OnErrorsChanged(String.Empty);}publicObservableCollection<ChildViewModel1>ChildViewModels{get;set;}}publicclassChildViewModel1:INotifyDataErrorInfo,IPropertyNotifyChanged//Assume missing functions are implementedpublicViewModel1Parent{get;set;}publicChildViewModel1(ViewModel1parent){parent.PropertyChanged+=parent_propertyChanged;}privatestring_anotherProperty;publicstringAnotherProperty{get{return_anotherProperty;}set{_anotherProperty=value;OnPropertyChanged();Validate();}}publicChildViewModel2{get;set;}publicvoidValidate(){varexisting=validationResult;validationResult=newList<ValidationResult>();if(Parent.SomeProperty==AnotherProperty){validationResult.Add(newValidationResult("SomeProperty cannot equal AnotherProperty",newstring[]{"AnotherProperty"}));}foreach(varresultinexisting){OnErrorsChanged(result.MemberNames.First());}foreach(varresultinvalidationResult){OnErrorsChanged(result.MemberNames.First());}OnErrorsChanged(String.Empty);}publicIEnumerableGetErrors(stringpropertyName){returnvalidationResult.Where(p=>p.MemberNames.Any(q=>q==propertyName));}publicboolHasErrors{get{returnvalidationResult.Any();}}}

So I'm sure you can imagine, in a full-blown production application, this would get really ugly, really fast. Also, it can over-notify due to it not checking if each property has changed error state. At least it would work and behave relatively predictably with any bugs worked out. Hopefully this gave you a good idea of what is needed to correctly INotifyDataErrorInfo. In the next part, I'll cover my solution using FluentValidation and ReactiveUI.