Validating mutually dependent properties in WPF

In WPF the Validate method of the ValidationRule class takes as parameter a single value provided by the GUI. This is fine if you want to do a typecheck or a rangecheck on an isolated field, but what if you want to validate two -or more- mutually dependent values (e.g. an end date versus a begin date) ?

Let's say you have a Person class that exposes DateOfBirth and DateOfDeath properties. In most religions these two DateTime properties have the canonical chronologic dependency between them. So if you type an illegal value in the date of death textbox, you would expect a warning like this:

Likewise if the death date was already filled and you're entering the date of birth, you would expect an appropriate warning:


Unfortunately there's nothing in WPF like the ASP.NET CompareValidator class, so you have to come up with some coding yourself. Writing a custom validation rule that compares two entered values is quite cumbersome (but still possible: you find an example here).

If you're developing against WPF v3.5 or higher, then there is an elegant alternative. First you make the class to which the GUI controls are bound implement the good old IDataErrorInfo interface. You're specifically interested in the indexer on property name. This is were you write the validation code. Here's an example of the Person class, implementing the interface:

//-----------------------------------------------------------------------
// <copyright file="Person.cs" company="DockOfTheBay">
//     http://www.dotbay.be
// </copyright>
// <summary>Defines the Person class.</summary>
//-----------------------------------------------------------------------
 
namespace DockOfTheBay
{
    using System;
    using System.ComponentModel; // Home of the IDataErrorInfo interface.
 
    /// <summary>
    /// Sample class with mutually dependent properties.
    /// </summary>
    public class Person : IDataErrorInfo
    {
        /// <summary>
        /// Gets or sets the date of birth.
        /// </summary>
        public DateTime? DateOfBirth { get; set; }
 
        /// <summary>
        /// Gets or sets the date of death.
        /// </summary>
        public DateTime? DateOfDeath { get; set; }
 
        /// <summary>
        /// Gets an error message indicating what is wrong with this object. 
        /// The default is an empty string (""). 
        /// </summary>
        /// <remarks>
        /// Member of IDataErrorInfo.
        /// </remarks>
        public string Error
        {
            get
            {
                return string.Empty;
            }
        }
 
        /// <summary>
        /// Gets the error message for the property with the given name.
        /// </summary>
        /// <param name="propertyName">
        /// The name of the property whose error message to get.
        /// </param>
        /// <returns>
        /// The error message for the property. 
        /// The default is an empty string ("").
        /// </returns>
        /// <remarks>
        /// Member of IDataErrorInfo.
        /// </remarks>
        public string this[string propertyName]
        {
            get
            {
                if (propertyName == "DateOfBirth")
                {
                    DateTime birth = this.DateOfBirth ?? DateTime.MinValue;
                    DateTime death = this.DateOfDeath ?? DateTime.MaxValue;
 
                    if (birth > death)
                    {
                        return "This date lies after the date of death.";
                    }
                }
                else if (propertyName == "DateOfDeath")
                {
                    DateTime birth = this.DateOfBirth ?? DateTime.MinValue;
                    DateTime death = this.DateOfDeath ?? DateTime.MaxValue;
 
                    if (birth > death)
                    {
                        return "This date lies before the date of birth.";
                    }
                }
 
                return string.Empty;
            }
        }
    }
}


All you need to do to trigger this code from XAML is use the DataErrorValidationRule inside the Binding.ValidationRules, or alternatively use the Binding.ValidatesOnDataErrors property. Here's an example of the first approach; it's the content of the form that I used for the above screenshots:

<Grid Margin="4 3">
    <!-- Resources -->
    <Grid.Resources>
        <local:Person x:Key="Person" x:Name="Person" />
        <Style TargetType="TextBox">
            <Setter Property="Margin" Value="4 3" />
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource={RelativeSource self}, 
                                            Path=(Validation.Errors)[0].ErrorContent}" />
                    <Setter Property="Foreground" Value="Red" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </Grid.Resources>
    <!-- Binding Context -->
    <Grid.DataContext>
        <Binding Source="{StaticResource Person}"/>
    </Grid.DataContext>
    <!-- Structure -->
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="auto" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto" />
        <RowDefinition Height="auto" />
    </Grid.RowDefinitions>
    <!-- Content -->
    <Label Content="Date of birth:"
           Grid.Row="0" Grid.Column="0" 
           HorizontalAlignment="Right" />
    <TextBox x:Name="BirthTextBox"
             Grid.Row="0" Grid.Column="1" >
        <Binding Path="DateOfBirth" 
                 UpdateSourceTrigger="PropertyChanged" 
                 TargetNullValue="{x:Static sys:String.Empty }" >
            <Binding.ValidationRules>
                <DataErrorValidationRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox>
    <Label Content="Date of death:"
           Grid.Row="1" Grid.Column="0" 
           HorizontalAlignment="Right" />
    <TextBox x:Name="DeathTextBox"
             Grid.Row="1" Grid.Column="1">
        <Binding Path="DateOfDeath" 
                 UpdateSourceTrigger="PropertyChanged" 
                 TargetNullValue="{x:Static sys:String.Empty }" >
            <Binding.ValidationRules>
                <DataErrorValidationRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox>
</Grid>

4 comments:

  1. Does this work for the following scenario:

    Let's say control values are
    DateOfBirth => 1.1.2000
    DateOfDeath => 1.1.2100

    everything is fine.

    Then you set DateOfBirth to 1.1.2200 and get an error.
    Then you set DateOfDeath to 1.1.2300, you fixed the error but the DateOfBirth is still red.

    ReplyDelete
    Replies
    1. I would love to know this too, I currently don't think it will work. My current solution gives me the functionality you are describing and I'm not fond of it.

      Furthermore because the value of the second date is not edited, the red box isn't updated and the error isn't re-evaluated

      Delete
  2. Trying to find the Ultimate Dating Website? Join and find your perfect match.

    ReplyDelete
  3. If you want your ex-girlfriend or ex-boyfriend to come crawling back to you on their knees (no matter why you broke up) you have to watch this video
    right away...

    (VIDEO) Have your ex CRAWLING back to you...?

    ReplyDelete