The validation infrastructure contained in the ViewModelBase
and the DataErrorNotifier
classes caters for both asynchronous and synchronous validation models. When using this system, validation code should reside in one of two overridable methods: BeginValidation
or GetPropertyErrors
.
BeginValidation
uses an asynchronous event driven validation model, and GetPropertyErrors
provides for a simpler synchronous model for when asynchronous processing is not required.
When using either the synchronous or asynchronous validation models, validation errors are represented using a custom class named DataValidationError
. DataValidationError
has an ErrorMessage
property of type string, and an Id
property of type int. The purpose of the Id
property is that it allows you to use the same error message for multiple fields.
The ViewModelBase.GetPropertyErrors
method allows your viewmodel to perform input validation using a simple synchronous approach. The GetPropertyErrors
method is overridden to validate specific properties in your viewmodel. The method returns all errors for the specified property as an IEnumerable<DataValidationError>
.
In the following example, the GetPropertyErrors
method has been overridden in a viewmodel containing two properties, neither of which is allowed to be null or to consist of entirely whitespace:
protected override IEnumerable<DataValidationError> GetPropertyErrors(
string propertyName, object value)
{
if (!string.IsNullOrWhiteSpace((string)value))
{
yield break;
}
switch (propertyName)
{
case "ValidatedString1":
yield return new DataValidationError(1,
"ValidatedString1 is required.");
break;
case "ValidatedString2":
yield return new DataValidationError(2,
"ValidatedString2 is required.");
break;
}
yield break;
}
If an exception is raised during the execution of this method, it is handled by the ViewModelBase
class. Exception handling is discussed further in the next section.
The BeginValidation
method of the ViewModelBase
class is the main extensibility point for providing asynchronous validation. This method should be overridden in subclasses to perform composite validation or validation of property values using, for example, WCF services.
The ViewModelBase
class implementation of the BeginValidation
method calls the virtual GetPropertyErrors
method, which as you saw in the preceding section, retrieves the list of errors for a specified property. See the following excerpt:
public virtual void BeginValidation(string memberName, object value)
{
IEnumerable<DataValidationError> errors;
try
{
errors = GetPropertyErrors(memberName, value);
}
catch (Exception ex)
{
OnValidationComplete(
new ValidationCompleteEventArgs(memberName, ex));
return;
}
OnValidationComplete(
new ValidationCompleteEventArgs(memberName, errors));
}
The base implementation of the BeginValidation
method executes synchronously, retrieving the list of errors and raising the ValidationComplete
event.
Note
When the BeginValidation
method has finished validating, it is imperative that the ValidationComplete
event is raised; otherwise, the DataErrorNotifier
will not raise the INotifyDataErrorInfo.ErrorsChanged
event, which prevents the UI from signaling that a validation error exists.
The following excerpt provides an example implementation of the BeginValidation
method:
public override void BeginValidation(string memberName, object value)
{
try
{
if (string.IsNullOrEmpty((string)value))
{
List<DataValidationError> errors
= new List<DataValidationError>();
DataValidationError error = null;
switch (memberName)
{
case "ValidatedString1":
error = new DataValidationError(1,
"ValidatedString1 is required.");
break;
case "ValidatedString2":
error = new DataValidationError(2,
"ValidatedString2 is required.");
break;
}
if (error != null)
{
errors.Add(error);
}
OnValidationComplete(
new ValidationCompleteEventArgs(memberName, errors));
}
else
{
OnValidationComplete(
new ValidationCompleteEventArgs(memberName));
}
}
catch (Exception ex)
{
OnValidationComplete(
new ValidationCompleteEventArgs(memberName, ex));
}
}
The ViewModelBase
class contains a string keyed Dictionary
of errors for each property, which is of type Dictionary<string, List<DataValidationError>>
.
The DataErrorNotifier
instance relies on an IValidateData
object to perform the actual semantic validation of viewmodel property values. Thus, the strategy for validating the viewmodel can be decoupled from the viewmodel itself. In most cases, however, the viewmodel implements IValidateData
.
The DataErrorNotifier
class subscribes to the viewmodel’s PropertyChanged
event so that when a property is changed, it is also validated. See the following excerpt:
public DataErrorNotifier(
INotifyPropertyChanged owner, IValidateData validator)
{
this.validator
= ArgumentValidator.AssertNotNull(validator, "validator");
ArgumentValidator.AssertNotNull(owner, "owner");
validator.ValidationComplete += validator_ValidationComplete;
owner.PropertyChanged += HandleOwnerPropertyChanged;
}
void HandleOwnerPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e == null || e.PropertyName == null)
{
return;
}
BeginGetPropertyErrorsFromValidator(e.PropertyName);
}
When a viewmodel property changes, a potentially asynchronous validation operation is commenced from the BeginGetPropertyErrorsFromValidator
method, shown in the following excerpt:
void BeginGetPropertyErrorsFromValidator(string propertyName)
{
Func<object> propertyFunc;
lock (propertyDictionaryLock)
{
if (!propertyDictionary.TryGetValue(propertyName, out propertyFunc))
{
/* No property registered with that name. */
return;
}
}
validator.BeginValidation(propertyName, propertyFunc());
}
The BeginGetPropertyErrorsFromValidator
method performs the following tasks:
The property Func
is retrieved from the property Dictionary
.
The property value is retrieved by invoking the property Func
.
The BeginValidation
method of the viewmodel (implementing IValidateData
) is called using the property name and the property value.
When the IValidateData
finishes validating a property value, it raises the IValidateData.ValidationComplete
event. This event is handled by the HandleValidationComplete
method.
The first thing that the HandleValidationComplete
method does is update the list of validation errors for the specified property. It does this by calling the SetPropertyErrors
method, which attempts to retrieve the list of errors for that property. If the specified list of errors is different from the existing list of known errors for that property, the list of known errors is updated (or removed if empty) and the ErrorsChanged
event is raised:
public void SetPropertyErrors(
string propertyName, IEnumerable<DataValidationError> dataErrors)
{
ArgumentValidator.AssertNotNullOrEmpty(propertyName, "propertyName");
List<DataValidationError> list;
bool raiseEvent = false;
lock (errorsLock)
{
bool created = false;
int paramErrorCount = dataErrors == null ? 0 : dataErrors.Count();
if ((errorsField == null || errorsField.Count < 1)
&& paramErrorCount < 1)
{
return;
}
if (errorsField == null)
{
errorsField
= new Dictionary<string, List<DataValidationError>>();
created = true;
}
bool listFound = false;
if (created ||
!(listFound = errorsField.TryGetValue(propertyName, out list)))
{
list = new List<DataValidationError>();
}
if (paramErrorCount < 1)
{
if (listFound)
{
errorsField.Remove(propertyName);
raiseEvent = true;
}
}
else
{
var tempList = new List<DataValidationError>();
foreach (var dataError in dataErrors)
{
if (created || list.SingleOrDefault(
e => e.Id == dataError.Id) == null)
{
tempList.Add(dataError);
raiseEvent = true;
}
}
list.AddRange(tempList);
errorsField[propertyName] = list;
}
}
if (raiseEvent)
{
OnErrorsChanged(propertyName);
}
}
The OnErrorsChanged
method raises the ErrorsChanged
event on the UI thread using the app’s Dispatcher
. An extension method is used to ensure that the method is invoked only if the thread is not the UI thread; otherwise, the delegate is called from the current thread.
protected virtual void OnErrorsChanged(string property)
{
Dispatcher dispatcher = Deployment.Current.Dispatcher;
dispatcher.InvokeIfRequired(
delegate
{
ErrorsChanged.Raise(this,
new DataErrorsChangedEventArgs(property));
});
}
18.118.163.158