Introduction
From WPF 4.5, Microsoft added support of INotifyDataErrorInfo validation from WPF. The model we want to validate now can implement this interface and can do multiple validations for the property. It also supports aync validation. The way you want to implement varies by requirements. There are so many options with this. In this article, I will provide a simpler version. I am mostly a web developer, so I always try maintain a common standard for developing both WPF/Silverlight/MVC. You can choose your own.
INotifyDataErrorInfo Interface
It has only three members, all are self-explanatory:
- public interface INotifyDataErrorInfo
- {
- bool HasErrors { get; }
- event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
- IEnumerable GetErrors(string propertyName);
- }
The model class can implement these three members to provide validation support. But generally, I create a Base Validation class so that every model doesn't need to implement this every time. The HasErrors property should return true if the model has any errors at the moment. Nevertheless this property is not used by the binding engine (I wondered about that a little), so we can utilize it for our own use, like enabling/disabling a Save/Cancel button.
The INotifyDataErrorInfo interface rovides 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. In this example I will use an annotation type validation trigger.
Validation Base Please go through the following implementation:
- public class ValidationBase : INotifyDataErrorInfo
- {
- private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
- public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
- private object _lock = new object();
- public bool HasErrors { get { return _errors.Any(propErrors => propErrors.Value != null && propErrors.Value.Count > 0); } }
- public bool IsValid { get { return this.HasErrors; } }
-
- public IEnumerable GetErrors(string propertyName)
- {
- if (!string.IsNullOrEmpty(propertyName))
- {
- if (_errors.ContainsKey(propertyName) && (_errors[propertyName] != null) && _errors[propertyName].Count > 0)
- return _errors[propertyName].ToList();
- else
- return null;
- }
- else
- return _errors.SelectMany(err => err.Value.ToList());
- }
-
- public void OnErrorsChanged(string propertyName)
- {
- if (ErrorsChanged != null)
- ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
- }
-
- public void ValidateProperty(object value, [CallerMemberName] string propertyName = null)
- {
- lock (_lock)
- {
- var validationContext = new ValidationContext(this, null, null);
- validationContext.MemberName = propertyName;
- var validationResults = new List<ValidationResult>();
- Validator.TryValidateProperty(value, validationContext, validationResults);
-
-
- if (_errors.ContainsKey(propertyName))
- _errors.Remove(propertyName);
- OnErrorsChanged(propertyName);
- HandleValidationResults(validationResults);
- }
- }
-
- public void Validate()
- {
- lock (_lock)
- {
- var validationContext = new ValidationContext(this, null, null);
- var validationResults = new List<ValidationResult>();
- Validator.TryValidateObject(this, validationContext, validationResults, true);
-
-
- var propNames = _errors.Keys.ToList();
- _errors.Clear();
- propNames.ForEach(pn => OnErrorsChanged(pn));
- HandleValidationResults(validationResults);
- }
- }
-
- private void HandleValidationResults(List<ValidationResult> validationResults)
- {
-
- var resultsByPropNames = from res in validationResults
- from mname in res.MemberNames
- group res by mname into g
- select g;
-
- foreach (var prop in resultsByPropNames)
- {
- var messages = prop.Select(r => r.ErrorMessage).ToList();
- _errors.Add(prop.Key, messages);
- OnErrorsChanged(prop.Key);
- }
- }
- }
I kept all errors in a dictionary where the key is a property name. Then whenever a property is validated by the ValidateProperty method I clear previous errors for the property, signal the change to the binding engine, validate the property using the Validator helper class and if any errors are found then I add them to the dictionary and signal the binding engine again with a proper property name.
The same, except for the entire model, is done in the Validate method. Here I check all model properties at once. There can be alternative approach, but I am satisfied with this.
The Model Class I used the Data Annotation technique to trigger the validation logic. Also, the model derives from ModelBase class that implements the INotifyPropertyChange interface for binding support.
- public class ModelBase : ValidationBase, INotifyPropertyChanged
- {
- public event PropertyChangedEventHandler PropertyChanged;
- protected void NotifyPropertyChanged(string propertyName)
- {
- if (this.PropertyChanged != null)
- {
- this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
- }
- }
- }
-
- public class Customer : ModelBase
- {
- private string _firstName;
- [Display(Name="First Name")]
- [Required]
- [StringLength(20)]
- public string FirstName
- {
- get { return _firstName; }
- set
- {
- _firstName = value;
- ValidateProperty(value);
- base.NotifyPropertyChanged("FirstName");
- }
- }
- }
So as you see, the property has two validation rules for First Name, it is required and it should be less than 20. By this, you can apply a different validation rule in one go for the same property.
View The view is quite simple. Bind the data context to your view model and then place the binding. But you need to include three attributes in the Binding:
- NotifyOnValidationError=True,ValidatesOnNotifyDataErrors=True,UpdateSourceTrigger=PropertyChanged
Also, I didn't see the error message as Silverlight when I did this binding. I searched in the internet and found a control template. So I used it.
- <TextBox Text="{Binding Customer.FirstName,Mode=TwoWay, NotifyOnValidationError=True,ValidatesOnNotifyDataErrors=True,UpdateSourceTrigger=PropertyChanged}">
- <Validation.ErrorTemplate>
- <ControlTemplate>
- <StackPanel>
-
- <AdornedElementPlaceholder x:Name="textBox"/>
- <ItemsControl ItemsSource="{Binding}">
- <ItemsControl.ItemTemplate>
- <DataTemplate>
- <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
- </DataTemplate>
- </ItemsControl.ItemTemplate>
- </ItemsControl>
- </StackPanel>
- </ControlTemplate>
- </Validation.ErrorTemplate>
- </TextBox>
I hope this helps. Please check other tutorials and see how in various ways you can implement this Interface.