Sunday, September 19, 2010

Silverlight 4 validation using INotifyDataErrorInfo

Validation of your data is a requirement for almost every application. By using validation, you make sure that no invalid data is (eventually) persisted in your data store. When you don't implement validation, there's a risk that a user will input wrongly formatted or plain incorrect data on the screen and even persist this data in your data store. This is something you should definitely avoid.
In Silverlight 4, a new way of validating your data is possible by using IDataErrorInfo or INotifyDataErrorInfo. This allows us to invalidate the properties without throwing exceptions and the validation code doesn't have to reside in the set accessor of the property. It can be called whenever it's needed. In this post we'll see how to implement validation on the bound fields in the UI using INotifyDataErrorInfo interface methods.

The XAML for the data bindings are implemented as..
<TextBox Style="{StaticResource commonTextBoxStyle}" Text="{Binding UserName, Mode=TwoWay, NotifyOnValidationError=true}" />
<TextBox Style="{StaticResource commonTextBoxStyle}" Text="{Binding EmailAddress, Mode=TwoWay, NotifyOnValidationError=true}" Grid.Row="1" />
<PasswordBox Style="{StaticResource commonPasswordStyle}" Password="{Binding Password, Mode=TwoWay, NotifyOnValidationError=true}" Grid.Row="2" />

I have the viewmodel implementation as. I have used FluentValidation for implementing validation checks in the code.
[Export(typeof(ViewModelBase))]
public class ValidationNotificationViewModel  : ViewModelBase, INotifyDataErrorInfo
{
    public ValidationNotificationViewModel()
    {
        ValidationErrors = new Dictionary<string, List<string>>();
    }

    public string EmailAddress
    {
        get { return _emailAddress; }
        set
        {
            _emailAddress = value;
            RaisePropertyChanged(EMAIL_KEY);
        }
    }

    public string UserName
    {
        get { return _userName; }
        set
        {
            _userName = value;
            RaisePropertyChanged(USERNAME_KEY);
        }
    }

    public string Password
    {
        get { return _password; }
        set
        {
            _password = value;
            RaisePropertyChanged(PASSWORD_KEY);
        }
    }

    public ICommand ValidateCommand
    {
        get
        {
            if (_validateCommand == null)
                _validateCommand = new RelayCommand(() => ValidateUser());
            return _validateCommand;
        }
    }

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public System.Collections.IEnumerable GetErrors(string propertyName)
    {
        if (ValidationErrors.ContainsKey(propertyName))
            return ValidationErrors[propertyName];
        else
            return ValidationErrors.Values;
    }

    public bool HasErrors
    {
        get { return ValidationErrors.Count > 0; }
    }

    void ValidateUser()
    {
        ValidateEmail();
        ValidateUsername();
        ValidatePassword();
    }

    void ValidateEmail()
    {
        List<ValidationFailure> results = new List<ValidationFailure>();
        results.AddRange(new EmailValidator().Validate(new PropertyValidatorContext("Email address", this, EmailAddress, EMAIL_KEY)));
        results.AddRange(new NotEmptyValidator(default(string)).Validate(new PropertyValidatorContext("Email address", this, EmailAddress, EMAIL_KEY)));
           
        PopulateValidationInfoFromValidationResults(EMAIL_KEY, results);
    }

    void ValidateUsername()
    {
        List<ValidationFailure> results = new List<ValidationFailure>();
        results.AddRange(new LengthValidator(8, 20).Validate(new PropertyValidatorContext("User name", this, UserName, USERNAME_KEY)));
        results.AddRange(new NotEmptyValidator(default(string)).Validate(new PropertyValidatorContext("User name", this, UserName, USERNAME_KEY)));

        PopulateValidationInfoFromValidationResults(USERNAME_KEY, results);
    }

    void ValidatePassword()
    {
        List<ValidationFailure> results = new List<ValidationFailure>();
        results.AddRange(new LengthValidator(8, 20).Validate(new PropertyValidatorContext("Password", this, Password, PASSWORD_KEY)));
        results.AddRange(new NotEmptyValidator(default(string)).Validate(new PropertyValidatorContext("Password", this, Password, PASSWORD_KEY)));
        results.AddRange(new NotNullValidator().Validate(new PropertyValidatorContext("Password", this, Password, PASSWORD_KEY)));

        PopulateValidationInfoFromValidationResults(PASSWORD_KEY, results);
    }

    void PopulateValidationInfoFromValidationResults(string key, IList<ValidationFailure> validationFailureCollection)
    {
        RemoveValidationErrors(key);
        if (validationFailureCollection.Count > 0)
        {
            List<string> errorMessages = new List<string>();
            validationFailureCollection.ToList().ForEach(failure => errorMessages.Add(failure.ErrorMessage));
            ValidationErrors.Add(key, errorMessages);
            NotifyErrorsChanged(key);
        }                         
    }

    void NotifyErrorsChanged(string propertyName)
    {
        if (ErrorsChanged != null)
            ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }

    void RemoveValidationErrors(string propertyName)
    {
        if (ValidationErrors.ContainsKey(propertyName)) ValidationErrors.Remove(propertyName);
    }

    string _emailAddress;
    string _userName;
    string _password;
    string EMAIL_KEY = "EmailAddress";
    string USERNAME_KEY = "UserName";
    string PASSWORD_KEY = "Password";
    ICommand _validateCommand;
    Dictionary<string, List<string>> ValidationErrors { get; set; }
}