Samstag, 22. Dezember 2012

Windows Phone 8 Easy and intuitive way for validation using INotifyDataErrorInfo

Code should be transparent and intuitive for other developers to read. Therefore I searched for my current project – a Windows Phone 8 app to manage time sheets - a good solution to implement validation rules for my entities at a central point in my models. I found a nice implementation of a BindingModel from outcoldman at Codeproject. He looks at several validation technologies we have with Silverlight and describes them in the mentioned article in detail. Most of my classes based on this article which I have expanded by some new behaviors and properties.

My resulting validation code in the constructor of my models looks like this:

 

AddValidationFor(() => CustomerName)
        .When(x => string.IsNullOrEmpty(x._customerName))
        .Show(AppResources.InvalidNotNullOrEmpty);
 
AddValidationFor(() => OrderName)
    .When(x => string.IsNullOrEmpty(x._orderName))
    .Show(AppResources.InvalidNotNullOrEmpty);
 
AddValidationFor(() => Name)
    .When(x => string.IsNullOrEmpty(x._name))
    .Show(AppResources.InvalidNotNullOrEmpty);
 
AddValidationFor(() => ProjectHoursMax)
    .When(x => x._projectHoursMax <= 0 && x._billType == BillType.Billable)
    .Show(AppResources.InvalidZeroNotAllowed);
 
AddValidationFor(() => Start)
    .When(x => x._end <= x._start)
    .Show(AppResources.InvalidStartIsGreaterThanEnd);

 


The basics

The INotifyDataErrorInfo interface came with Silverlight 4 which can be used for  synchronous and asynchronous validation. Microsoft suggestion is, that new entity classes should implement INotifyDataErrorInfo for the added flexibility instead of implementing IDataErrorInfo. The IDataErrorInfo support enables you to use many existing entity classes that are written for the full .NET Framework.

 

Let’s start

The first step of the validation rule implementation is to expand our properties for storing validation rules and manage error messages.This is done by the PropertyValidation class:

/// <summary>
/// Add validation support to properties 
/// </summary>
/// <typeparam name="TBindingModel">Model to bind</typeparam>
public class PropertyValidation<TBindingModel>
    where TBindingModel : BindingModelBase<TBindingModel>
{
    private Func<TBindingModel, bool> _validationCriteria;
    private string _errorMessage;
    private readonly string _propertyName;
 
    public PropertyValidation(string propertyName)
    {
        _propertyName = propertyName;
    }
 
    public PropertyValidation<TBindingModel> When(Func<TBindingModel, bool> validationCriteria)
    {
        if (_validationCriteria != null)
            throw new InvalidOperationException("You can only set the validation criteria once.");
 
        _validationCriteria = validationCriteria;
        return this;
    }
 
    public PropertyValidation<TBindingModel> Show(string errorMessage)
    {
        if (_errorMessage != null)
            throw new InvalidOperationException("You can only set the message once.");
 
        _errorMessage = errorMessage;
        return this;
    }
 
    public bool IsInvalid(TBindingModel presentationModel)
    {
        if (_validationCriteria == null)
            throw new InvalidOperationException(
                "No criteria have been provided for this validation. (Use the 'When(..)' method.)");
 
        return _validationCriteria(presentationModel);
    }
 
    public string GetErrorMessage()
    {
        if (_errorMessage == null)
            throw new InvalidOperationException(
                "No error message has been set for this validation. (Use the 'Show(..)' method.)");
 
        return _errorMessage;
    }
 
    public string PropertyName
    {
        get { return _propertyName; }
    }
}

The next step is to implement a generic BindingModelBase class which will implement the INotifyPropertyChanged and INotifyDataErrorInfo interfaces. This class contains also two fields for storing the added validation rules and validation errors:

/// <summary>
/// BindingModel to support validation and handling of error messages
/// (Implements INotifyDataError)
/// </summary>
/// <typeparam name="TBindingModel">Model to bind</typeparam>
public abstract class BindingModelBase<TBindingModel> : INotifyPropertyChanged, INotifyDataErrorInfo
    where TBindingModel : BindingModelBase<TBindingModel>
{
    private readonly List<PropertyValidation<TBindingModel>> _validations = new List<PropertyValidation<TBindingModel>>();
    private Dictionary<string, List<string>> _errorMessages = new Dictionary<string, List<string>>();
 
    protected BindingModelBase()
    {
        PropertyChanged += (s, e) => { if (e.PropertyName != "HasErrors" && e.PropertyName!="ErrorMessages") ValidateProperty(e.PropertyName); };
    }
 
    #region INotifyDataErrorInfo
 
    public IEnumerable GetErrors(string propertyName)
    {
        if (_errorMessages.ContainsKey(propertyName))
            return _errorMessages[propertyName];
 
        return new string[0];
    }
 
    public bool HasErrors
    {
        get { return _errorMessages.Any(); }
    }
 
    public Dictionary<string, List<string>> ErrorMessages
    {
        get { return _errorMessages; }
    }
 
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged = delegate { };
 
    private void OnErrorsChanged(string propertyName)
    {
        ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }
 
    #endregion
 
    #region INotifyPropertyChanged
 
    public event PropertyChangedEventHandler PropertyChanged = delegate { };
 
    protected void RaisePropertyChanged([CallerMemberName]string propertyName=null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
 
    #endregion
 
    protected void RaisePropertyChanged(Expression<Func<object>> expression)
    {
eSharper disable ExplicitCallerInfoArgument
        if (expression != null) RaisePropertyChanged(GetPropertyName(expression));
eSharper restore ExplicitCallerInfoArgument
    }
 
 
    protected PropertyValidation<TBindingModel> AddValidationFor(Expression<Func<object>> expression)
    {
        return AddValidationFor(GetPropertyName(expression));
    }
 
    protected PropertyValidation<TBindingModel> AddValidationFor(string propertyName)
    {
        var validation = new PropertyValidation<TBindingModel>(propertyName);
        _validations.Add(validation);
 
        return validation;
    }
 
 
    public void ValidateAll()
    {
        var propertyNamesWithValidationErrors = _errorMessages.Keys;
 
        _errorMessages = new Dictionary<string, List<string>>();
 
        _validations.ForEach(PerformValidation);
 
        var propertyNamesThatMightHaveChangedValidation =
            _errorMessages.Keys.Union(propertyNamesWithValidationErrors).ToList();
 
        propertyNamesThatMightHaveChangedValidation.ForEach(OnErrorsChanged);
 
        RaisePropertyChanged(() => HasErrors);
        RaisePropertyChanged(() => ErrorMessages);
    }
 
    public void ValidateProperty(Expression<Func<object>> expression)
    {
        ValidateProperty(GetPropertyName(expression));
    }
 
    private void ValidateProperty(string propertyName)
    {
        _errorMessages.Remove(propertyName);
 
        _validations.Where(v => v.PropertyName == propertyName).ToList().ForEach(PerformValidation);
        OnErrorsChanged(propertyName);
        RaisePropertyChanged(() => HasErrors);
        RaisePropertyChanged(() => ErrorMessages);
    }
 
    private void PerformValidation(PropertyValidation<TBindingModel> validation)
    {
        if (validation.IsInvalid((TBindingModel)this))
        {
            AddErrorMessageForProperty(validation.PropertyName, validation.GetErrorMessage());
        }
    }
 
    private void AddErrorMessageForProperty(string propertyName, string errorMessage)
    {
        if (_errorMessages.ContainsKey(propertyName))
        {
            _errorMessages[propertyName].Add(errorMessage);
        }
        else
        {
            _errorMessages.Add(propertyName, new List<string> { errorMessage });
        }
    }
 
    private static string GetPropertyName(Expression<Func<object>> expression)
    {
        if (expression == null)
            throw new ArgumentNullException("expression");
 
        MemberExpression memberExpression;
 
        if (expression.Body is UnaryExpression)
            memberExpression = ((UnaryExpression)expression.Body).Operand as MemberExpression;
        else
            memberExpression = expression.Body as MemberExpression;
 
        if (memberExpression == null)
            throw new ArgumentException("The expression is not a member access expression", "expression");
 
        var property = memberExpression.Member as PropertyInfo;
        if (property == null)
            throw new ArgumentException("The member access expression does not access a property", "expression");
 
        var getMethod = property.GetGetMethod(true);
        if (getMethod.IsStatic)
            throw new ArgumentException("The referenced property is a static property", "expression");
 
        return memberExpression.Member.Name;
    }
}

 

Show the errors

To show the errors at the user interface I used the ErrorMessages and HasError property. At my View I only have to attach a BindingValidationError event handler which calls an extension method to mark the properties which raises an error.

this.BindingValidationError += TimeSheetEntryPageBindingValidationError;
 
void TimeSheetEntryPageBindingValidationError(object sender, ValidationErrorEventArgs e)
{
    this.HandleBindingValidationError(sender, e);
}
 
public static void HandleBindingValidationError(this PhoneApplicationPage phoneApplicationPage, object sender, ValidationErrorEventArgs e)
{
    var control = e.OriginalSource as Control;
 
    if (control == null) return;
 
    if (e.Action == ValidationErrorEventAction.Added)
    {
 
        control.BorderBrush = new SolidColorBrush(Colors.Red);
 
    }
    else if (e.Action == ValidationErrorEventAction.Removed)
    {
 
        if (control is ListPicker || control is DatePicker || control is TimePicker)
        {
            control.BorderBrush = phoneApplicationPage.Resources["PhoneForegroundBrush"] as SolidColorBrush;
        }
        else
        {
            control.BorderBrush = new SolidColorBrush(Color.FromArgb(0xb, 255, 255, 255));
        }
    }
}

But how to show a list of the error messages for the corresponding fields?

To achieve this, I made a converter (our swiss knife :-) which extracts the error messages from the ErrorMessages property and populate that to a user control which shows the error list for one property (the parameter of the converter contains the bounded property name for which I want to display the error messages).

<toolkit:TimePicker  Grid.Row="1" Grid.Column="0" 
      Margin="0,-6,0,0"
      Style="{StaticResource TimePickerErrorStyle}"
      Value="{Binding SelectedEntry.Start, Mode=TwoWay,  
      ValidatesOnDataErrors=true, NotifyOnValidationError=True}"/>
<userControl:ErrorMessageList Items="{Binding SelectedEntry.ErrorMessages,
             Converter={StaticResource ErrorMessagesConverter}
            ,ConverterParameter=Start}"/>

The converter code:

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
   var errors = value as Dictionary<string, List<string>>;
   var errorList = new List<string>();
   if (errors != null && errors.ContainsKey(parameter.ToString()))
   {
       errorList.AddRange(errors[parameter.ToString()]);
   }
 
   return errorList;
}

3 Kommentare:

  1. Do you have src for an example? I'm just starting to work with windows phone and have no experience with silverlight.

    AntwortenLöschen
  2. Hi, Can you please share Source file for this sample?...

    AntwortenLöschen
  3. Does this application work for windows 6.5? might love to have this sort of application for my htc telephone iPhone app builder // iphone applications // Android application development

    AntwortenLöschen