Using INotifyDataErrorInfo for validation in MVVM with Silverlight

In this post we will be looking at how validation can be done by implementing the INotifyDataErrorInfo interface for a calculator we have been building as part of the Silverlight refactoring series.

Like the IDataErrorInfo interface, the INotifyDataErrorInfo interface gives you the ability to do validation without throwing exceptions.

The full solution for this post can be downloaded here.

Note to WPF developers – INotifyDataErrorInfo isn’t available in WPF (yet)

But you can vote for INotifyDataErrorInfo to be in a future release of WPF.

Pre INotifyDataErrorInfo

As you can see in the code below we are throwing exceptions in the setters for the two values we want to add together.

public string  FirstValue
{
    get { return _firstValue; }
    set
    {
        _firstValue = value;
        try
        {
            int.Parse(_firstValue);
        }
        catch (Exception)
        {
            throw new Exception("That's not a number");
        }
        OnPropertyChanged("FirstValue");
    }
}
public string  SecondValue
{
    get { return _secondValue; }
    set
    {
        _secondValue = value;
        try
        {
            int.Parse(_secondValue);
        }
        catch (Exception)
        {
            throw new Exception("That's not a number");
        }
        OnPropertyChanged("SecondValue");
    }
}

Implementing the INotifyDataErrorInfo interface

As we only want the calculate button to be enabled when the user has entered valid numbers to add together, we need to keep track of any validation errors.

Similar to how we have done in other posts, we will use a class for storing validation errors instead of making it the responsibility of the ViewModel.

And that class is the EntityBase class, which I borrowed from Mike Taulty. Cheers Mike!

The code below shows how the EntityBase class implements the ErrorsChanged event, GetErrors method and the HasErrors property defined in the INotifyDataErrorInfo interface.

public class EntityBase : INotifyDataErrorInfo
{
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    readonly Dictionary<string, List<string>> _currentErrors;

    public EntityBase()
    {
        _currentErrors = new Dictionary<string, List<string>>();
    }

    public IEnumerable GetErrors(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName))
        {
            return (_currentErrors.Values);
        }

        MakeOrCreatePropertyErrorList(propertyName);
        return _currentErrors[propertyName];
    }

    public bool HasErrors
    {
        get
        {
            return (_currentErrors.Where(c => c.Value.Count > 0).Count() > 0);
        }
    }

    void FireErrorsChanged(string property)
    {
        if (ErrorsChanged != null)
        {
            ErrorsChanged(this, new DataErrorsChangedEventArgs(property));
        }
    }
    public void ClearErrorFromProperty(string property)
    {
        MakeOrCreatePropertyErrorList(property);
        _currentErrors[property].Clear();
        FireErrorsChanged(property);
    }
    public void AddErrorForProperty(string property, string error)
    {
        MakeOrCreatePropertyErrorList(property);
        _currentErrors[property].Add(error);
        FireErrorsChanged(property);
    }

    void MakeOrCreatePropertyErrorList(string propertyName)
    {
        if (!_currentErrors.ContainsKey(propertyName))
        {
            _currentErrors[propertyName] = new List<string>();
        }
    }
}

The EnitityBase class can be unit tested like this

[TestFixture]
public class When_using_the_EntityBase
{
    private EntityBase _entityBase;

    [SetUp]
    public void SetUp()
    {
        _entityBase = new EntityBase();
    }

    [Test]
    public void HasErrors_should_return_true_when_errors_exist()
    {
        // Arrange
        _entityBase.AddErrorForProperty("X", "X");

        // Act
        var result = _entityBase.HasErrors;

        // Assert
        result.ShouldBeTrue();
    }

    [Test]
    public void HasErrors_should_return_true_when_no_errors_exist()
    {
        // Arrange
        // collection will be empty at this point

        // Act
        var result = _entityBase.HasErrors;

        // Assert
        result.ShouldBeFalse();
    }

    [Test]
    public void Should_be_able_to_clear_error_from_property()
    {
        // Arrange
        _entityBase.AddErrorForProperty("X", "X");

        // Act
        _entityBase.ClearErrorFromProperty("X");
        var result = _entityBase.GetErrors("X") as List<string>; 

        // Assert
        result.Count().ShouldEqual(0);

    }

    [Test]
    public void Should_be_able_to_get_error_for_property()
    {
        // Arrange
        const string errorMessage = "ErrorMessage";
        _entityBase.AddErrorForProperty("X", "ErrorMessage");

        // Act
        var result =  _entityBase.GetErrors("X") as List<string>;

        // Assert
        result[0].ShouldEqual(errorMessage);

    }
}

How the ViewModel validates user input

We need to validate the users input each time the value of a property changes.

If you have been following the series you’ll know the benefits of injecting/importing a class responsible for validation, the CalculatorValidator, into the ViewModel and to validate the users input.

In the code for the CalculatorValidator class, the Validate method could have returned a collection of error messages, but to keep this example simple we’ll just return the first validation error we encounter.

[Export(typeof(ICalculatorValidator))]
public class CalculatorValidator : ICalculatorValidator
{
    [ImportMany]
    public IEnumerable<ICalculatorValidationRule> CalculatorValidationRules { get; set; }

    public ValidationResult Validate(string value)
    {
        List<ValidationResult> validationResults = new List<ValidationResult>();
        foreach (var calculatorValidationRule in CalculatorValidationRules)
        {
            if (!calculatorValidationRule.IsValid(value))
            {
                return new ValidationResult()
                            {
                                IsValid = false,
                                ErrorMessage = calculatorValidationRule.ErrorMessage

                            };
            }
        }
        return new ValidationResult() { IsValid = true };
    }
}

public class ValidationResult
{
    public bool IsValid { get; set; }
    public string ErrorMessage { get; set; }
}

If you’re new to the series the validation rules for calculator are being imported by MEF. Read how and why we are doing this in the ‘Applying the Open Closed Principle in Silverlight and WPF using MEF‘ post.

Using the INotifyDataErrorInfo interface in the ViewModel

In the code for the ViewModel shown below take note of how:

  • the ViewModel inherits from the EntityBase class
  • the CheckIfNumberIsValid method is called whenever a property value changes
  • the INotifyDataErrorInfo HasErrors property is used to determine if the button should be enabled in the CalculatorViewModel_ErrorsChanged handler for the ErrorsChanged event. This is one of the advantages INotifyDataErrorInfo has over IDataErrorInfo.
[Export]
public class CalculatorViewModel : EntityBase, INotifyPropertyChanged
{
    private string _firstValue;
    private string _secondValue;
    private string _result;       

    private readonly ICalculator _calculator;
    private readonly RelayCommand _calculateCommand;

    public event PropertyChangedEventHandler PropertyChanged;
    public ObservableCollection<string> FirstValueErrors { get; set; }
    private readonly ICalculatorValidator _calculatorValidator;

    [ImportingConstructor]
    public CalculatorViewModel(ICalculator calculator, ICalculatorValidator calculatorValidator)
    {
        _calculator = calculator;
        _calculatorValidator = calculatorValidator;
        _calculateCommand = new RelayCommand(Calculate) { IsEnabled = true };
        ErrorsChanged += new EventHandler<DataErrorsChangedEventArgs>(CalculatorViewModel_ErrorsChanged);

        _firstValue = "0";
        _secondValue = "0";
    }

    void CalculatorViewModel_ErrorsChanged(object sender, DataErrorsChangedEventArgs e)
    {
        _calculateCommand.IsEnabled = HasErrors == false;
    }

    public void Calculate()
    {
        Result = _calculator.Add(Convert.ToInt32(FirstValue), Convert.ToInt32(SecondValue)).ToString();
    }

    public string FirstValue
    {
        get { return _firstValue; }
        set
        {
            _firstValue = value;
            CheckIfNumberIsValid("FirstValue", value);
        }
    }

    public string SecondValue
    {
        get { return _secondValue; }
        set
        {
            _secondValue = value;
            CheckIfNumberIsValid("SecondValue", value);
        }
    }

    public void CheckIfNumberIsValid(string propertyName, string value)
    {
        ClearErrorFromProperty(propertyName);

        var validationResult = _calculatorValidator.Validate(value);
        if (validationResult.IsValid  == false)
        {
            AddErrorForProperty(propertyName, validationResult.ErrorMessage);
        }
        OnPropertyChanged(propertyName);
    }

    public string Result
    {
        get { return _result; }
        private set
        {
            _result = value;
            OnPropertyChanged("Result");
        }
    }

    public RelayCommand CalculateCommand
    {
        get { return _calculateCommand; }
    }

    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this,
                new PropertyChangedEventArgs(propertyName));
        }
    }
}

XAML changes for INotifyDataErrorInfo

The final change required to implement INotifyDataErrorInfo interface is to change the text box bindings and set ValidatesOnNotifyDataErrors=True

<TextBox Text="{Binding FirstValue, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}"/>
<TextBox Text="{Binding SecondValue, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}"/>

So what have we achieved?

In this post we have refactored the code to use the INotifyDataErrorInfo interface and are no longer throwing exceptions to do validation.

As well as allowing you return multiple validation errors for a property, the INotifyDataErrorInfo interface can be used for asynchronous data validation as demonstrated by Jesse Liberty in this video.

Comments

  • Pingback: Dew Drop – July 16, 2010 | Alvin Ashcraft's Morning Dew

  • Graham Hobson

    Just a trivial question. I was working on a Silverlight solution and came to a point where I wanted to start implementatin validation techniques.

    I had many common chunks of functionality in a Silverlight Class Library (Common) after following a solution by Microsoft’s John Papa in the recent Silverlight Firestarte event. The EntityBase class seemed to belong in there but I couldn’t reference it in my Unit Testing project because “it targeted a different framework family”.

  • http://www.arrangeactassert.com Jag Reehal

    Graham,

    Have you tried adding the dll using

    Add Reference > Browse

    instead of adding the project via

    Add Reference > Projects

    as suggested on stackoverflow?

    Cheers Jag

  • Graham Hobson

    Jag: I was able to refernce the DLL that way – thanks, didn’t think that would work.

    In the Dictionary object _currentErrors how do you remove a specific error that was added before. For instance a Surname is invalidated as being empty and then the user types numbers. My example shows the non-numeric error message but keeps the empty message when the field is obviously no longer empty.

  • http://www.arrangeactassert.com Jag Reehal

    Graham,

    What about clearing all errors when a Surname changes and then validate it?

    Cheers,

    Jag

  • Edwin Raubenheimer

    Hi, thanks for the post. I’ve also implemented INotifyDataErrorInfo and works great except I can’t solve this one:

    Your Example:

    My Example:

    How do you call AddErrorForProperty?
    AddErrorForProperty(“Datacontract.FirstValue”
    or AddErrorForProperty(“FirstValue”

    Because both of them doesn’t work.

  • Anonymous

    Hi Edwin,

    Without seeing the rest of the code I can’t say, but in the example I have shown the calculator values are properties in the ViewModel itself.

    Cheers,

    Jag