Proper validation with MVVM

Warning: Long answer also

I use the IDataErrorInfo interface for validation, but I have customised it to my needs. I think that you’ll find that it solves some of your problems too. One difference to your question is that I implement it in my base data type class.

As you pointed out, this interface just deals with one property at a time, but clearly in this day and age, that’s no good. So I just added a collection property to use instead:

protected ObservableCollection<string> errors = new ObservableCollection<string>();

public virtual ObservableCollection<string> Errors
{
    get { return errors; }
}

To address your problem of not being able to display external errors (in your case from the view, but in mine from the view model), I simply added another collection property:

protected ObservableCollection<string> externalErrors = new ObservableCollection<string>();

public ObservableCollection<string> ExternalErrors
{
    get { return externalErrors; }
}

I have an HasError property which looks at my collection:

public virtual bool HasError
{
    get { return Errors != null && Errors.Count > 0; }
}

This enables me to bind this to Grid.Visibility using a custom BoolToVisibilityConverter, eg. to show a Grid with a collection control inside that shows the errors when there are any. It also lets me change a Brush to Red to highlight an error (using another Converter), but I guess you get the idea.

Then in each data type, or model class, I override the Errors property and implement the Item indexer (simplified in this example):

public override ObservableCollection<string> Errors
{
    get
    {
        errors = new ObservableCollection<string>();
        errors.AddUniqueIfNotEmpty(this["Name"]);
        errors.AddUniqueIfNotEmpty(this["EmailAddresses"]);
        errors.AddUniqueIfNotEmpty(this["SomeOtherProperty"]);
        errors.AddRange(ExternalErrors);
        return errors;
    }
}

public override string this[string propertyName]
{
    get
    {
        string error = string.Empty;
        if (propertyName == "Name" && Name.IsNullOrEmpty()) error = "You must enter the Name field.";
        else if (propertyName == "EmailAddresses" && EmailAddresses.Count == 0) error = "You must enter at least one e-mail address into the Email address(es) field.";
        else if (propertyName == "SomeOtherProperty" && SomeOtherProperty.IsNullOrEmpty()) error = "You must enter the SomeOtherProperty field.";
        return error;
    }
}

The AddUniqueIfNotEmpty method is a custom extension method and ‘does what is says on the tin’. Note how it will call each property that I want to validate in turn and compile a collection from them, ignoring duplicate errors.

Using the ExternalErrors collection, I can validate things that I can’t validate in the data class:

private void ValidateUniqueName(Genre genre)
{
    string errorMessage = "The genre name must be unique";
    if (!IsGenreNameUnique(genre))
    {
        if (!genre.ExternalErrors.Contains(errorMessage)) genre.ExternalErrors.Add(errorMessage);
    }
    else genre.ExternalErrors.Remove(errorMessage);
}

To address your point regarding the situation where a user enters an alphabetical character into a int field, I tend to use a custom IsNumeric AttachedProperty for the TextBox, eg. I don’t let them make these kinds of errors. I always feel that it’s better to stop it, than to let it happen and then fix it.

Overall I’m really happy with my validation ability in WPF and am not left wanting at all.

To end with and for completeness, I felt that I should alert you to the fact that there is now an INotifyDataErrorInfo interface which includes some of this added functionality. You can find out more from the INotifyDataErrorInfo Interface page on MSDN.


UPDATE >>>

Yes, the ExternalErrors property just let’s me add errors that relate to a data object from outside that object… sorry, my example wasn’t complete… if I’d have shown you the IsGenreNameUnique method, you would have seen that it uses LinQ on all of the Genre data items in the collection to determine whether the object’s name is unique or not:

private bool IsGenreNameUnique(Genre genre)
{
    return Genres.Where(d => d.Name != string.Empty && d.Name == genre.Name).Count() == 1;
}

As for your int/string problem, the only way I can see you getting those errors in your data class is if you declare all your properties as object, but then you’d have an awful lot of casting to do. Perhaps you could double your properties like this:

public object FooObject { get; set; } // Implement INotifyPropertyChanged

public int Foo
{
    get { return FooObject.GetType() == typeof(int) ? int.Parse(FooObject) : -1; }
}

Then if Foo was used in code and FooObject was used in the Binding, you could do this:

public override string this[string propertyName]
{
    get
    {
        string error = string.Empty;
        if (propertyName == "FooObject" && FooObject.GetType() != typeof(int)) 
            error = "Please enter a whole number for the Foo field.";
        ...
        return error;
    }
}

That way you could fulfil your requirements, but you’ll have a lot of extra code to add.

Leave a Comment