Binding property | Mechanism | Description |
---|---|---|
ValidatesOnExceptions | Exceptions |
|
ValidatesOnDataErrors | IDataErrorInfo |
|
ValidatesOnNotifyDataErrorInfo | INotifyOnDataErrorInfo |
|
Data Annotations | Attributes |
|
The INotifyOnDataErrorInfo approach provides the most flexibility. The fact that it is enabled by default saves you a lot of time in case you have a lot of views currently not using any validation rules. But the implementation of the interface seems like a lot of work. So I started thinking about how to make things easy.
How things should work
My idea was to write validation code like this: public String Description
{
get
{
return Get(() => Description);
}
set
{
Set(() => Description, value);
}
}
public IEnumerable<ValidationError> Validate_Description()
{
if (Description.Length < 20)
yield return new ValidationError("Description is too short.");
}
The Get() / Set() methods are described in our view model base class. I wanted to use tha same aproach for validation as we use for executing commands through methods prefixed with Execute_.
How to get things to work
The wohle magic happens in the view model base class. This is the one to implement the INotifyOnDateErrorInfo interface:
public abstract class ViewModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
{
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public virtual bool HasErrors
{
get { return Get(() => HasErrors); }
set { Set(() => HasErrors, value); }
}
public IEnumerable GetErrors(string propertyName)
{
}
protected virtual void OnValidationChanged(String PropertyName)
{
if (ErrorsChanged != null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(PropertyName));
}
}
In the constructor of the class we scan for methods that start with the prefix Validate_ to create validation rules for them. To do so, we define an interface IValidationRule and a class RelayValidator - very similar to the ICommand / RelayCommand implementation:
public interface IValidationRule
{
IEnumerable<ValidationError> GetValidationErrors(Object Value);
}
public class RelayValdiator : IValidationRule
{
private Func<object, IEnumerable<ValidationError>> ValidateFunction;
public RelayValdiator(Func<Object, IEnumerable<ValidationError>> Validator)
{
this.ValidateFunction = Validator;
}
public IEnumerable<ValidationError> GetValidationErrors(Object Value)
{
return ValidateFunction(Value);
}
}
The object parameter passed to the GetValidationErrors method is the property value to be validated. Now we can build up a Dictionary<String, List<IValidationRule>> containing the property name as key and a list of validation rules for the property (as I wanted to be able to have multiple validation rules later).
var ValidateMethodNames =
this.GetType().GetMethods()
.Where(m => m.Name.StartsWith(VALIDATE_PREFIX))
.Select(m => m.Name.StripLeft(VALIDATE_PREFIX.Length));
var result = ValidateMethodNames
.ToDictionary(
name => name,
name => new List<IValidationRule>()
{
new RelayValdiator(x => GetValidationErrors(name, x))
}
);
The function GetValidationErrors is defined in the base class as well:
private IEnumerable<ValidationError> GetValidationErrors(
String PropertyName, Object PropertyValue)
{
var validateMethodInfo = ViewModel.GetType().GetMethod(VALIDATE_PREFIX + PropertyName);
if (validateMethodInfo == null)
return null;
return (IEnumerable<ValidationError>)
validateMethodInfo.Invoke(ViewModel,
validateMethodInfo.GetParameters().Length == 1 ? new[] { PropertyValue } : null);
}
The method takes the passed in method name, looks up the method in the class via reflection and invokes it. One little trick is done here anyway: the Validate_Description method as shown above does not take a parameter, as we do not need it in the view model where we can access the value directly. So we see wether we have a parameter and if not, we just do not pass it.
With this in place, we can easily implement the GetErrors method, where m_ValidationRules is the above constructed dictionary.
public IEnumerable GetErrors(string propertyName)
{
if (propertyName.IsNullOrEmpty())
return null;
if (m_ValidateionRules == null)
return null;
if (!m_ValidateionRules.ContainsKey(propertyName))
return null;
var rules = m_ValidateionRules[propertyName];
var result = rules.SelectMany(r => r.GetValidationErrors(Get<Object>(propertyName)))
.Select(e => new ValidationError(propertyName, e.ErrorMessage));
// Update HasErrors property
return result;
}
As mentioned before, the Get<Object>(propertyName) method is part of the view model and delivers the value of a property given its name. It is based on a dictionary as well.
The one thing we missed so far is the HasErrors property. Of course we want it to be set automatically according to the validation errors. So we introduce a list of property names with properties being in an errenous state. Each time we encounter an error we add the name to the list, otherwise we remove it. So if the list contains any element HasErrors must be true, if it is empty it must be false. This is done by the following code, to be inserted in the above listing at the comment:
var propertyHasErrors = result != null && result.Count() > 0;
if (propertyHasErrors)
if (!m_ErrorProperties.Contains(propertyName))
m_ErrorProperties.Add(propertyName);
else
m_ErrorProperties.Remove(propertyName));
HasErrors = m_ErrorProperties.Count != 0;
Further steps
In the first place there is quite some code to write to get things working. But once you have it, you can do some other neat things. For example you can define attributes for validation:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class IsNotNullValidation : Attribute, IValidationRule
{
public IEnumerable<ValidationError> GetValidationErrors(object Value)
{
var StringValue = Value as String;
if (StringValue != null && StringValue.IsNullOrEmpty())
{
yield return new ValidationError("Der Wert darf nicht leer sein.");
}
if (Value == null)
yield return new ValidationError("Der Wert darf nicht leer sein.");
}
}
You can stack them on top of your properties. The only thing to do is to not only scan for perfixed methods but for attributes as well and add them to your dictionary.
Happy validating!