Cross-Field Attribute Validation in WPF using MVVM and IDataErrorInfo

Binding / Coding / MVVM / Validation / WPF

At one point or another we’ve all had to do some sort of input validation.  In this post, I’m going to show you how to use the IDataErrorInfo interface along with INotifyPropertyChanged and the MVVM pattern to perform cross-field validation (e.g. field A should only be validated if field B is true) using nothing but attributes (and of course a little bit of helper code, which I’ll give you).  The idea behind this is to be able to do something like this:

public bool IsLastNameRequired { ... }

[RequiredIf("IsLastNameRequired", true, ErrorMessage = "Last name is required.")]
public string LastName
{
    get { return this.lastName; }
    set
    {
        this.lastName = value;
        this.OnPropertyChanged("LastName");
    }
}

Looking at the [RequiredIf(...)] attribute, simply put it means: “`LastName´ is required if the property `IsLastNameRequired´  equals true.  When validation fails, the error message to display is `Last name is required.´”.  Pretty simple, huh?  Now, let’s get our hands dirty…

Note! Before you get started,  you need to add a reference to the System.ComponentModel.DataAnnotations assembly (included in .NET).  This is where ValidationAttribute and the framework-provided implementations exist.

Overview

There is a little bit too much code to paste into this post, so I will only be posting snippets. You can find the full source attached. In the sample project, there are six files of interest:

  • ViewModel.cs – This is a boilerplate MVVM base class implementing INotifyPropertyChanged.
  • ValidationViewModel.cs – This class is where all of the validation logic lives and it will take care of passing information about validation errors to the framework for you (note that we’re not doing anything special here; by implementing IDataErrorInfo correctly we provide the framework all of the information it needs to know when a validation error has occurred).
  • MainWindowViewModel.cs – This is the view-model that we will use in binding for MainWindow.xaml (it will be MainWindow’s DataContext).
  • MainWindow.xaml – This file contains our XAML markup which defines the window contents and our bindings.
  • MainWindow.xaml.cs – This file contains one event for ‘Button_Click’, which forces validation to take place when in explicit mode (more on that later).  Note that this is anti-MVVM and I only use a click event here for simplicity in the example.  In a real-world application you should bind a command on the view-model to the button.

Now, I want to point out that I wrote the ValidationViewModel to work with two different methods of validation: “Implicit” and “Explicit” – implicit meaning that the validation is executed automatically on a control’s default validation check which occurs on binding, and explicit meaning that you need to explicitly call the Validate() method on the view-model to trigger validation.  To change the mode, you can set the property  `UseExplicitValidation´ in your view-model constructor (or change it at any time elsewhere).

The Basics

Okay, let’s have a look at our view-model and the XAML for the window we will be binding to the view-model:

MainWindowViewModel.cs

public class MainWindowViewModel : ValidationViewModel
{
    string
        firstName = null,
        lastName = null;
    bool
        isLastNameRequired = false;

    public MainWindowViewModel()
    {
        this.UseExplicitValidation = true;
    }

    [Required(ErrorMessage = "First name is required.")]
    public string FirstName
    {
        get { return this.firstName; }
        set
        {
            this.firstName = value;
            this.OnPropertyChanged("FirstName");
        }
    }

    public bool IsLastNameRequired
    {
        get { return this.isLastNameRequired; }
        set
        {
            this.isLastNameRequired = value;
            this.OnPropertyChanged("IsLastNameRequired");
        }
    }

    [RequiredIf("IsLastNameRequired", true, ErrorMessage = "Last name is required.")]
    public string LastName
    {
        get { return this.lastName; }
        set
        {
            this.lastName = value;
            this.OnPropertyChanged("LastName");
        }
    }
}

Note that in the constructor I am setting `UseExplicitValidation´ to true, and also that I am raising the `OnPropertyChanged´ event each time a property is set. Raising the PropertyChanged event is very important to ensure that binding works and for the framework to know when a property has changed.  Since we’re using some TextBox controls it might be worth mentioning that TextBox.Text binding does not occur on KeyUp or KeyDown, but instead when the TextBox has lost focus.

MainWindow.xaml

<Window.DataContext>
    <ViewModels:MainWindowViewModel />
</Window.DataContext>
<Grid>
    <StackPanel Orientation="Vertical" Margin="10">
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="First Name:" />
            <TextBox
                Text="{Binding Path=FirstName, Mode=TwoWay, ValidatesOnDataErrors=True}"
                Width="200" />
        </StackPanel>
        <CheckBox
            IsChecked="{Binding Path=IsLastNameRequired}"
            Content="Last name is required" />
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Last Name:" />
            <TextBox
                Text="{Binding Path=LastName, Mode=TwoWay, ValidatesOnDataErrors=True}"
                IsEnabled="{Binding Path=IsLastNameRequired}"
                Width="200" />
        </StackPanel>
        <Button Width="100" IsEnabled="{Binding Path=UseExplicitValidation}" Content="Validate" HorizontalAlignment="Left" Click="Button_Click" />
    </StackPanel>
</Grid>

Take note of how I set the DataContext of the Window to a new instance of MainWindowViewModel; this causes the instance of MainWindowViewModel to flow to the DataContext properties of all of the window’s child elements (unless explicitly set otherwise) such as our TextBox and CheckBox controls. That way, when we use our {Binding … } statements for the TextBox and CheckBox controls used in this example we know that we are binding directly to our MainWindowViewModel instance. In order to make the TextBoxes worry about validation, we need to use the `ValidatesOnDataErrors´ binding property as you can see above.

The ValidationViewModel and Cross-Field Validation

Moving on to the ValidationViewModel and cross-field validation, I want to bring special focus to the ValidationAttribute.GetValidationResult() method and the ValidationContext class. The combination of these two is what makes it possible to be aware of the view-model instance which is being validated, coincidentally providing us access to all of its properties as well. Almost all of the attribute validation examples that I have seen thus far make use of the ValidationAttribute.IsValid() method which is not recommended practice anymore and in-fact is internal in Silverlight.  Have a read about ValidationContext here.  Anyways, here’s the code for our Validate() method in our ValidationViewModel base class.  Note the use of ValidationAttribute.GetValidationResult():

private static string[] Validate(ValidationContext validationContext, string propertyName)
{
    if (validationContext == null || validationContext.ObjectInstance == null)
        throw new ArgumentNullException("validationContext", "Validation context must be provided and the ObjectInstance must be set.");

    return GetValidatorDescriptors(validationContext.ObjectType)
        .Where(v => v.Property.Name == propertyName)
        .Where(v => v.ValidationAttribute.GetValidationResult(v.GetValue(validationContext.ObjectInstance), validationContext) != ValidationResult.Success)
        .Select(v => v.ValidationAttribute.ErrorMessage).ToArray();
}

So, what we are doing here is iterating through all of the validators defined on properties in our view-model and passing each of them the current value of the property which we are validating as well as the ValidationContext, which in this example we have only set the ObjectInstance to `this´.  I chose to make this a property and set it in the ValidationViewModel constructor:

private ValidationContext @ValidationContext { get; set; }

protected ValidationViewModel()
{
    ...
    this.ValidationContext = new ValidationContext(this, null, null);
    ...
}

Now, for the actual cross-field validation itself let’s move on to our RequiredIfAttribute implementation of ValidationAttribute.  Here’s the code (comments are included in the sample project):

RequiredIfAttribute.cs

public sealed class RequiredIfAttribute : ValidationAttribute
{
    public string PropertyName { get; private set; }
    public object[] Values { get; private set; }

    public RequiredIfAttribute(string propertyName, params object[] equalsValues)
    {
        this.PropertyName = propertyName;
        this.Values = equalsValues;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        bool isRequired = this.IsRequired(validationContext);

        if (isRequired && string.IsNullOrEmpty(Convert.ToString(value)))
            return new ValidationResult(this.ErrorMessage);
        return ValidationResult.Success;
    }

    private bool IsRequired(ValidationContext validationContext)
    {
        var property = validationContext.ObjectType.GetProperty(this.PropertyName);
        var currentValue = property.GetValue(validationContext.ObjectInstance, null);

        foreach (var val in this.Values)
            if (object.Equals(currentValue, val))
                return true;
        return false;
    }
}

When we call ValidationAttribute.GetValidationResult() it internally calls IsValid(object value, ValidationContext validationContext) which we override here in our implementation.  Since the validationContext.ObjectInstance property is set to our instance of MainWindowViewModel, we have the possibility to check the values of other properties in the class and base our validation results on the outcome.  In our RequiredIfAttribute we provide the means to perform validation on property “A” only if the value of property “B” equals values “C”, “D” or “F”.  The IsRequired() method uses the ObjectInstance of the ValidationContext to determine if the value of the property with the name specified in PropertyName contains one of the values specified in Values.  If the property’s value exists in Values, then the `value´ parameter in our IsValid() implementation is subjected to validation.

Anyways, in an effort to stop making this post longer and longer I’m going to stop explaining at this point and give you the download link to the sample project.  Give it a try!  If there’s anything that you don’t understand, don’t hesitate to post a comment or e-mail me and I’ll try to explain further.  I only gave one example of a cross-field validator, but you can pretty much do anything you want.  One request I saw was to compare two date fields “start” and “end” date and to validate that the end date was later than the start date – that can easily be accomplished using this.  The source code is almost fully documented, so hopefully that clears any question marks you might have up :-)

Cyle


Programming enthusiast. I've been intrigued by computers since I was 12, staying in at recess to code QBASIC on the old Apple II. Background in the payment industry, particularly in card switching and related system architecture. Lover of high-performance distributed architecture. Huge fan of the new NoSQL wave. Open source fanatic.

1 Comment

  1. Luis
    27 March 13, 5:03pm

    Hi Cyle.

    Thanks for yout great post.

    How can i get the default error message from the Validate function if if the DataAnnotation has no a custom error message defined?

    It returns an empty string in your source code

Leave a Reply