WPF Validation with Attributes and IDataErrorInfo interface in MVVM
WPF provides validation infrastructure for binding scenarios through IDataErrorInfo interface. Basically you have to implement the Item[columnName] property putting the validation logic for each property in your Model (or ModelView) requiring validation. From XAML you need to set ValidatesOnDataErrors to true and decide when you want the binding invoke the validation logic (through UpdateSourceTrigger).
The idea of this post is try to generalize the validation logic in IDataErrorInfo.Item[] using the validation attributes in System.ComponentModel.DataAnnotations assembly.
First, we decorate each property needing validation with one or more validation attributes (An explanation of each validation attribute is out of the scope of this post) depending on the validation rules. For example if we have a property named Range we can add RangeAttribute to validate that the property value is between the boundaries:
[Required(ErrorMessage = "Field 'Range' is required.")]
[Range(1, 10, ErrorMessage = "Field 'Range' is out of range.")]
public int Range
{
get
{
return this.range;
}
set
{
if (this.range != value)
{
this.range = value;
this.OnPropertyChanged("Range");
}
}
}
Now, we need a few code to get the validation attributes and the value getters for each property requiring validation, just a couple of simple LINQ queries:
private static readonly Dictionary<string, Func<ValidationModelView, object>>
propertyGetters = typeof(ValidationModelView).GetProperties()
.Where(p => GetValidations(p).Length != 0)
.ToDictionary(p => p.Name, p => GetValueGetter(p));
private static readonly Dictionary<string, ValidationAttribute[]> validators =
typeof(ValidationModelView).GetProperties()
.Where(p => GetValidations(p).Length != 0)
.ToDictionary(p => p.Name, p => GetValidations(p));
private static ValidationAttribute[] GetValidations(PropertyInfo property)
{
return (ValidationAttribute[])property
.GetCustomAttributes(typeof(ValidationAttribute), true);
}
private static Func<ValidationModelView, object> GetValueGetter(PropertyInfo property)
{
var instance = Expression.Parameter(typeof(ValidationModelView), "i");
var cast = Expression.TypeAs(
Expression.Property(instance, property),
typeof(object));
return (Func<ValidationModelView, object>)Expression
.Lambda(cast, instance).Compile();
}
It is important emphasize that in order to improve the performance in the validation we use static dictionaries and Lamda expressions to generate the value getters. The validation attributes are cached in other dictionary in order to improve the performance too.
Finally we need implement the Item[columnName] property which is the part of code that really do the validation. It is very simple, WPF send the name of the property in the columnName argument, then all you have to do is get the value of the property using the propertyGetters dictionary and test this value against the validation attributes in the validators dictionary.
public string this[string columnName]
{
get
{
if (propertyGetters.ContainsKey(columnName))
{
var value = propertyGetters[columnName](this);
var errors = validators[columnName].Where(v => !v.IsValid(value))
.Select(v => v.ErrorMessage).ToArray();
return string.Join(Environment.NewLine, errors);
}
return string.Empty;
}
}
All of this returns the list of message errors that will be shown in the UI.
You can do download a complete sample here.