Diederik Krols

The XAML Brewer

Validation in a WPF DataGrid

In this article I will describe two options to implement validation on the cells and rows of a WPF DataGrid. On the architectural level you have indeed (at least) two options when it comes to validation. You can decide to validate through independent reusable logic captured in ValidationRules, or you can let the bound entities carry out the validation themselves. Let's first set up a mini-application, with a WPF DataGrid displaying a list of instances of a WeatherForecast class. It should look like this:

Here's the WeatherForecast class:

namespace U2UConsult.DockOfTheBay

{

    using System;

    using System.Collections.ObjectModel;

    using System.ComponentModel;

 

    /// <summary>

    /// Intergalactic Weather Forecast

    /// </summary>

    public class WeatherForecast

    {

        /// <summary>

        /// Gets the list of weather forecasts for tomorrow.

        /// </summary>

        public static ObservableCollection<WeatherForecast> TomorrowsForecast

        {

            get

            {

                return new ObservableCollection<WeatherForecast>()

                {

                    new WeatherForecast()

                    {

                        Planet = "Alderaan",

                        Conditions = "Risk of a severe lightning bolt",

                        LowestTemp = 0,

                        HighestTemp = 0

                    },

                    new WeatherForecast()

                    {

                        Planet = "Caprica",

                        Conditions = "Acid rain - toasters better stay inside",

                        LowestTemp = 24,

                        HighestTemp = 28

                    },

                    new WeatherForecast()

                    {

                        Planet = "Earth",

                        Conditions = "Globally warming"

                        LowestTemp = 6,

                        HighestTemp = 9

                    },

                    new WeatherForecast()

                    {

                        Planet = "Middle Earth",

                        Conditions = "Cloudy over Mordor",

                        LowestTemp = 24,

                        HighestTemp = 28

                    },

                    new WeatherForecast()

                    {

                        Planet = "Tatooine",

                        Conditions = "Heavy sand storms",

                        LowestTemp = 3,

                        HighestTemp = 8

                    }

                };

            }

        }

 

        /// <summary>

        /// Gets or sets the name of the planet.

        /// </summary>

        public string Planet { get; set; }

 

        /// <summary>

        /// Gets or sets the description of the weather conditions.

        /// </summary>

        public string Conditions { get; set; }

 

        /// <summary>

        /// Gets or sets the lowest temperature during an interval.

        /// </summary>

        /// <remarks>Unit measure is °C.</remarks>

        public int LowestTemp { get; set; }

 

        /// <summary>

        /// Gets or sets the highest temperature during an interval.

        /// </summary>

        /// <remarks>Unit measure is °C.</remarks>

        public int HighestTemp { get; set; }

    }

}

 

And here's the XAML for the form:

<Window x:Class="U2UConsult.DockOfTheBay.DataGridValidationSampleWindow"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:toolkit="http://schemas.microsoft.com/wpf/2008/toolkit"

    xmlns:local="clr-namespace:U2UConsult.DockOfTheBay"

    Icon="/DataGridValidationSample;component/dotbay.png"

    SizeToContent="WidthAndHeight"

    Title="DataGrid Validation Sample" >

    <StackPanel>

        <!-- Title -->

        <TextBlock

           Text="Intergalactic Weather Forecast"

           HorizontalAlignment="Center"

           FontWeight="Bold"

           Margin="5" />

        <!-- Weather Forecasts Grid -->

        <toolkit:DataGrid

            ItemsSource="{x:Static local:WeatherForecast.TomorrowsForecast}"

            AutoGenerateColumns="False"

            RowHeaderWidth="16">

            <toolkit:DataGrid.Columns>

                <toolkit:DataGridTextColumn

                   Header="Planet"

                   Binding="{Binding Path=Planet}"/>

                <toolkit:DataGridTextColumn

                   Header="Conditions"

                   Binding="{Binding Path=Conditions}"/>

                <toolkit:DataGridTextColumn

                   Header="Low Temperature"

                   Binding="{Binding Path=LowestTemp}"/>

                <toolkit:DataGridTextColumn

                   Header="High Temperature"

                   Binding="{Binding Path=HighestTemp}"/>

            </toolkit:DataGrid.Columns>

        </toolkit:DataGrid>

    </StackPanel>

</Window>

 

1. Validation through ValidationRules

A first way to perform validation is to store the rules in ValidationRule classes, and let the column or row bindings call that business logic at the appropriate moment (that moment is determined by the ValidationStep property of the rule). The bound entities themselves are unaware of this process.

Cell validation

You can build validation rule classes to validate a single value, e.g. a Temperature. First we check whether the input is a whole number (type check). Then we perform a range check to verify if the entered value is within the physical boundaries of temperature i.e. between absolute cold and absolute hot (no need to check the latter, because it's higher than Integer32.MaxValue anyway). Here's how this could look like:

namespace U2UConsult.DockOfTheBay

{

    using System.Windows.Controls;

 

    /// <summary>

    /// Validates a temperature in °C.

    /// </summary>

    internal class TemperatureValidationRule : ValidationRule

    {

        /// <summary>

        /// Validates the proposed value.

        /// </summary>

        /// <param name="value">The proposed value.</param>

        /// <param name="cultureInfo">A CultureInfo.</param>

        /// <returns>The result of the validation.</returns>

        public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)

        {

            if (value != null)

            {

                int proposedValue;

                if (!int.TryParse(value.ToString(), out proposedValue))

                {

                    return new ValidationResult(false, "'" + value.ToString() + "' is not a whole number.");

                }

 

                if (proposedValue < -273)

                {

                    // Something was wrong.

                    return new ValidationResult(false, "Temperature can not be below the absolute cold (-273°C).");

                }

            }

 

            // Everything OK.

            return new ValidationResult(true, null);

        }

    }

}

 

All you need to do to trigger the validation, is registering the validation rules in the binding of the corresponding bound columns, like this:

<toolkit:DataGridTextColumn Header="High Temperature">

    <toolkit:DataGridTextColumn.Binding>

        <Binding Path="HighestTemp">

            <!-- Validating through independent rules -->

            <Binding.ValidationRules>

                <local:TemperatureValidationRule />

            </Binding.ValidationRules>

        </Binding>

    </toolkit:DataGridTextColumn.Binding>

</toolkit:DataGridTextColumn>

 

If you run the application in this stage, you'll notice that the validation is executed. The invalid cell is presented through the default ErrorTemplate (a red border around the contents):

You have no idea what went wrong, until you add you own error template. Let's display an icon in the row header, with the error message as tooltip:

<!-- Validation Error Template for a DataGrid Row -->

<Style TargetType="{x:Type toolkit:DataGridRow}">

    <Setter Property="ValidationErrorTemplate">

        <Setter.Value>

            <ControlTemplate>

                <Image

                   Source="/DataGridValidationSample;component/error.png"

                   ToolTip="{Binding RelativeSource={

                                         RelativeSource FindAncestor,

                                         AncestorType={x:Type toolkit:DataGridRow}},

                                     Path=(Validation.Errors)[0].ErrorContent}"

                   Margin="0"

                   Width="11" Height="11" />

            </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

 

Now you get all the details when the validation rule is broken:

 

Row validation

WPF 3.5 SP1 introduced the BindingGroup class. A BindingGroup represents a set of related Bindings -e.g. the bindings to different properties of the same entity. A BindingGroup can also span bindings to multiple entities -e.g. a list of travellers reserving a group ticket. Here's a validation rule that validates a WheaterForecast:

namespace U2UConsult.DockOfTheBay

{

    using System.Windows.Controls;

    using System.Windows.Data;

 

    /// <summary>

    /// Validation of WeatherForecast instances.

    /// </summary>

    public class WeatherForecastValidationRule : ValidationRule

    {

        /// <summary>

        /// Validate a whole collection of binding sources.

        /// </summary>

        /// <param name="value">A BindingGroup.</param>

        /// <param name="cultureInfo">A CultureInfo.</param>

        /// <returns>The result of the validation.</returns>

        public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)

        {

            // Get binding group.

            BindingGroup bindingGroup = value as BindingGroup;

 

            if (bindingGroup != null)

            {

                // Loop through binding sources - could be multiple.

                foreach (var bindingSource in bindingGroup.Items)

                {

                    // Get object.

                    WeatherForecast forecast = bindingSource as WeatherForecast;

 

                    // Validate object.

                    if (forecast.HighestTemp < forecast.LowestTemp)

                    {

                        // Something was wrong.

                        return new ValidationResult(false, "Why do you think we called it 'High' and 'Low' ?");

                    }

                }

            }

 

            // Everything OK.

            return ValidationResult.ValidResult;

        }

    }

}

 

Note: this class is very tightly coupled to the WeatherForecast class itself.

The validation rule is called from the datagrid's bindings via RoleValidationRules:

<!-- Validating through independent rules -->

<toolkit:DataGrid.RowValidationRules>

    <local:WeatherForecastValidationRule ValidationStep="ConvertedProposedValue" />

</toolkit:DataGrid.RowValidationRules>

 

The Error Template that we defined earlier in this article, can be reused for row validation: 

2. Validation through IDataErrorInfo

A second way to perform validation is to store all the necessary business logic inside the entity, and expose it through the IDataErrorInfo interface. A DataErrorValidationRule at column and row level will call the methods on this interface.

Cell validation

An indexer with the property name as parameter will be called if a single property needs to be validated. Here's the code for the WeatherForecast class:

/// <summary>

/// Gets the result of a validation at Property level (from IDataErrorInfo).

/// </summary>

/// <param name="columnName">Name of the modified property.</param>

/// <returns>A validation error message.</returns>

/// <remarks>Called by DataErrorValidationRule at Column level.</remarks>

public string this[string columnName]

{

    get

    {

        // Temperature range checks.

        if ((columnName == "LowestTemp") || (columnName == "HighestTemp"))

        {

            if ((this.LowestTemp < -273) || (this.HighestTemp < -273))

            {

                return "Temperature can not be below the absolute cold (-273°C).";

            }

        }

 

        // All validations passed successfully.

        return null;

    }

}

 

An alternative is to do the validation in the setter of the properties, and only define an ExceptionValidationRule at column level.

Row validation

A DataErrorValidationRule can also be instantiated from the RowValidationRules of the DataGrid:

<!-- Validating through IDataErrorInfo -->

<toolkit:DataGrid.RowValidationRules>

    <DataErrorValidationRule />

</toolkit:DataGrid.RowValidationRules>

 

It will call the Error property on the bound entities:

/// <summary>

/// Gets the result of a validation at Entity level (from IDataErrorInfo).

/// </summary>

/// <remarks>Called by DataErrorValidationRule at Row level.</remarks>

public string Error

{

    get

    {

        // Compare temperatures.

        if (this.LowestTemp > this.HighestTemp)

        {

            return "Why do you think we called it 'High' and 'Low' ?";

        }

 

        // All validations passed successfully.

        return null;

    }

Comments are closed