Using IDataErrorInfo for validation in MVVM with Silverlight and WPF

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

The IDataErrorInfo interface gives you the ability to do validation without throwing exceptions.

The full solution for this post can be downloaded here.

Pre IDataErrorInfo

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 IDataErrorInfo interface

The IDataErrorInfo interface consists of two properties.

string this[string columnName] {get;}
string Error {get;}

For the Error property we can just return null because we don’t want to return a single error message for the entire object.

public string Error
{
    get { return null; }
}

For the property which returns an error for a text box we could do the validation like this

public string this[string columnName]
{
    get
    {
        string error = null;
        switch (columnName)
        {
            case "FirstValue":
                try
                {
                    int.Parse(_firstValue);
                }
                catch (Exception)
                {
                    error = "That is not a number";
                }
                break;
            case "SecondValue":
                try
                {
                    int.Parse(_firstValue);
                }
                catch (Exception)
                {
                    error = "That is not a number";
                }
                break;       
        }
        return error;
    }
}

But as we saw in a previous post about using ValidatesOnExceptions to do validation it’s much easier to write unit tests when there are separation of concerns and the ViewModel is not responsible for validation.

So this means we need to create a class for storing the validation error for each textbox

public class ValidationBase
{
    public readonly Dictionary<string, string> Errors;

    public ValidationBase()
    {
        Errors = new Dictionary<string, string>();
    }

    public void AddError(string propertyName, string message)
    {
        if (!Errors.ContainsKey(propertyName))
        {
            Errors[propertyName] = message;
        }
    }

    public void RemoveErrors(string propertyName)
    {
        Errors.Remove(propertyName);
    }
   
    public string GetErrorMessageForProperty(string propertyName)
    {
        string message;
        Errors.TryGetValue(propertyName, out message);
        return message;
    }

    public bool HasErrors()
    {
        return Errors.Count != 0;
    }    
}

which is inherited by a CalculatorValidator class that returns a boolean value if the property value is not valid

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

    public bool IsPropertyValid(string propertyName, string value)
    {
        RemoveErrors(propertyName);
        foreach (var calculatorValidationRule in CalculatorValidationRules)
        {
            if (!calculatorValidationRule.IsValid(value))
            {
                AddError(propertyName, calculatorValidationRule.ErrorMessage);
                return false;
            }
        }
        return true;
    }
}

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.

Now we can inject/import the CalculatorValidator into the ViewModel and use it to validate the users input.

As we only want the user to be able to click the calculate button when the form is valid we call we the CheckIfCalculteButtonShouldBeEnabled method when a text box value has changed.

[Export]
public class CalculatorViewModel : INotifyPropertyChanged, IDataErrorInfo
{
    private string _firstValue;
    private string _secondValue;
    private string _result;

    private readonly ICalculator _calculator;
    private readonly RelayCommand _calculateCommand;

    public event PropertyChangedEventHandler PropertyChanged;
    private readonly ICalculatorValidator _calculatorValidator;

    [ImportingConstructor]
    public CalculatorViewModel(ICalculator calculator, ICalculatorValidator calculatorValidator)
    {
        _calculator = calculator;
        _calculatorValidator = calculatorValidator;
        _calculateCommand = new RelayCommand(Calculate) { IsEnabled = true };

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

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

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

    public void CheckIfCalculteButtonShouldBeEnabled()
    {
        _calculateCommand.IsEnabled = _calculatorValidator.HasErrors() == false;
    }

    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));
        }
    }

    public string this[string columnName]
    {
        get
        {
            string error = null;
            switch (columnName)
            {
                case "FirstValue":
                    error = ValidateNumber("FirstValue", _firstValue);
                    break;
                case "SecondValue":
                    error = ValidateNumber("SecondValue", _secondValue);
                    break;
            }

            CheckIfCalculteButtonShouldBeEnabled();
            return error;
        }
    }

    public string ValidateNumber(string propertyName, string value)
    {
        if (!_calculatorValidator.IsPropertyValid(propertyName, value))
        {
            return _calculatorValidator.GetErrorMessageForProperty(propertyName);
        }
        return null;
    }

    public string Error
    {
        get { return null; }
    }
}

The code below shows how we can unit test the ViewModel which implements the IDataErrorInfo interface.

[TestFixture]
public class When_using_the_CalculatorViewModel
{
    private Mock<ICalculator> _calculator;
    private Mock<ICalculatorValidator> _calculatorValidator;
    private CalculatorViewModel _calculatorViewModel;

    [SetUp]
    public void SetUp()
    {
        _calculator = new Mock<ICalculator>();
        _calculatorValidator = new Mock<ICalculatorValidator>();
        _calculatorViewModel = new CalculatorViewModel(_calculator.Object, _calculatorValidator.Object);
    }

    [Test]
    public void Initial_value_of_first_number_is_0()
    {
        // Arrange
        // checking initial value

        // Act
        var result = _calculatorViewModel.FirstValue;

        // Assert
        result.ShouldEqual("0");
    }

    [Test]
    public void Initial_value_of_second_number_is_0()
    {
        // Arrange
        // checking initial value

        // Act
        var result = _calculatorViewModel.SecondValue;

        // Assert
        result.ShouldEqual("0");
    }

    [Test]
    public void Initial_value_of_calculate_button_is_enabled()
    {
        // Arrange
        // checking initial value

        // Act
        var result = _calculatorViewModel.CalculateCommand.IsEnabled;

        // Assert
        result.ShouldBeTrue();
    }

    [Test]
    public void ValidateNumber_returns_null_if_value_is_valid()
    {
        // Arrange
        _calculatorValidator.Setup(c => c.IsPropertyValid("X","X")).Returns(true);

        // Act
        var result = _calculatorViewModel.ValidateNumber("X","X");

        // Assert 
        result.ShouldBeNull();
    }

    [Test]
    public void ValidateNumber_returns_error_message_if_value_is_not_valid()
    {
        // Arrange
        const string errorMessage = "ErrorMessageText";
        _calculatorValidator.Setup(c => c.IsPropertyValid("X", "X")).Returns(false);
        _calculatorValidator.Setup(c => c.GetErrorMessageForProperty("X")).Returns(errorMessage);

        // Act
        var result = _calculatorViewModel.ValidateNumber("X", "X");

        // Assert 
        result.ShouldEqual(errorMessage);
    }

    [Test]
    public void Calculate_command_should_not_be_enabled_if_ViewModel_is_not_valid()
    {
        // Arrange
        _calculatorValidator.Setup(c => c.HasErrors()).Returns(true);

        // Act
        _calculatorViewModel.CheckIfCalculteButtonShouldBeEnabled();

        // Assert 
        _calculatorViewModel.CalculateCommand.IsEnabled.ShouldBeFalse();
    }

    [Test]
    public void Calculate_command_should_be_enabled_if_ViewModel_is_valid()
    {
        // Arrange
        _calculatorValidator.Setup(c => c.HasErrors()).Returns(false);

        // Act
        _calculatorViewModel.CheckIfCalculteButtonShouldBeEnabled();

        // Assert 
        _calculatorViewModel.CalculateCommand.IsEnabled.ShouldBeTrue();
    }
}

The final change we need to make is to change the text box binding to ValidatesOnDataErrors=True in the XAML file.

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

So what have we achieved?

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

Whether you choose to use it or not depends if you want multiple errors for a single property to be combined into a single error message.

Comments

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

  • Todd

    This is nice but an insane amount of code for validation. As an asp.net dev, for the last 10 years, I have been doing validation on my forms in about 10 minutes by simply dragging some validation controls on the screen and setting some properties. This much coding for some simple validation seems like a huge step backwards.

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

    Todd if dragging validation controls onto a page works for you then that’s great.

    As you’re an developer with 10 years experience I would be interested to know if you understand why for some folks, including me this is a step forward.

    If you want a clue think about testability.

  • Sharjeel

    Thanks for this wonderful post, i have a scenario where my text boxes are empty on load (and not with ’0′). This means User can directly press Calculate button & the input will not be validated.
    How can i validate all ‘Not Set’ properties automatically when User clicks on the button?

    Thanks!
    Sharjeel

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

    Sharjeel,

    What about running validation when a user clicks on the button and only continue if there are no validation errors?

    Cheers,

    Jag

  • Qmikej

    I am having some real concerns about how to use this approach in a Silverlight DataGrid which is bound to a collection of objects. How does the ViewModel know which object in the collection has the error? Very confusing and I dont see the “light” at the end of the tunnel on this approach on a bound datagrid?

  • Qmikej

    FYI- Great blog…love the Agile, TDD and Silverlight stuff…Keep it coming cause we are reading it ! Going to put a link to your blog in my blog

  • Anonymous

    That’s a interesting question and not a problem I have encountered.

  • Anonymous

    Thanks I’m glad you’ve found them useful

  • http://www.facebook.com/Grishe4ka Grishechka Samoshkin

    super

  • Damian Sadowski

    Ok, I’ve got one nasty question.
    The code is very usefull and it let’s you to follow Open/Closed Principle but…

            foreach (var calculatorValidationRule in CalculatorValidationRules)        {            if (!calculatorValidationRule.IsValid(value))            {                AddError(propertyName, calculatorValidationRule.ErrorMessage);                return false;            }        }

    This piece of code checks all calculator validation rules for each supplied property. How about a situation when I want to validate a property against a certain validation rule. Is it should be done like below:

            if (propertyName == “firstNumber”)
            {
                ICalculatorValidationRule validationRule = CalculatorValidationRules.Where(a => a is ValidateValueIsANumber).First();
                if (!validationRule.IsValid(value))
                {
                    AddError(propertyName, validationRule.ErrorMessage);
                    return false;
                }
            }Does the above code doesn’t breake the O/C principle?Thank you for you answer

  • Anonymous

    Hi Damian,

    I think your code could become a bit of a mess if you started going down this route.

    Does it break the open closed principle – open for extension, but closed for modification – Yes.

    What you may want to do is have a groups of validators for properties depending on their validation requirements.

    Cheers,

    Jag