Ioannis Panagopoulos blog

Tutorials on HTML5, Javascript, WinRT and .NET

Data Validation in WPF

by Ioannis Panagopoulos

In the previous post, I have presented a basic intro to DataBinding in WPF. This post, extends the previous one by adding a brief introduction on data validation using WPF.

When it comes to data validations, one is faced with the following concerns:

  1. Where the validation logic will reside? Will it be part of the GUI logic or the Business object?
  2. How is the user going to be informed on validation errors?


 

In this post, I will try to tackle those concerns. For this reason, we will create a small window that allows the user to edit the FullName and Rating properties of a Client object. The Client class is implemented in the code-behind file and supports the INotifyPropertyChanged interface in order for the GUI to reflect immediately property value changes that occur in the code. The initial class code is given below:

public class Client : System.ComponentModel.INotifyPropertyChanged
    {
        private string _fullName;
        public string FullName
        {
            get { return _fullName; }
            set { _fullName = value
                  if (PropertyChanged != null
                      PropertyChanged(thisnew System.ComponentModel.PropertyChangedEventArgs("FullName")); }
        }

        private int _rating;
        public int Rating
        {
            get { return _rating; }
            set { _rating = value
                  if (PropertyChanged != null
                      PropertyChanged(thisnew System.ComponentModel.PropertyChangedEventArgs("Rating")); }
        }

        #region INotifyPropertyChanged Members
            public event 
                   System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
        #endregion
    }
 


 

The implemented window that supports the aforementioned functionality is given below:

The XAML code that creates this window and is ready to support Data Binding with a Client business object is as follows:

<Window x:Class="ValidationDemo.WindowValidationDemo"

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

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

Title="Validation Demo" Height="150" Width="432">

<Grid>

<Grid.ColumnDefinitions>

<ColumnDefinition Width="80"/>

<ColumnDefinition/>

</Grid.ColumnDefinitions>

<Grid.RowDefinitions>

<RowDefinition Height="30"/>

<RowDefinition Height="30"/>

<RowDefinition/>

</Grid.RowDefinitions>

<TextBlock Grid.Column="0" Grid.Row="0" Text="FullName:" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 5 0"/>

<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Path=FullName}" VerticalAlignment="Center"/>

<TextBlock Grid.Column="0" Grid.Row="1" Text="Rating:" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 5 0"/>

<StackPanel Grid.Column="1" Grid.Row="1" Orientation="Horizontal">

<TextBox Text="{Binding Path=Rating}" VerticalAlignment="Center" Width="30" HorizontalAlignment="Left" TextAlignment="Center"/>

<Button Content="+" VerticalAlignment="Center" Margin="10 0 0 0" Click="Button_Click"/>

</StackPanel>

<UniformGrid Columns="2" Rows="1" Grid.Column="1" Grid.Row="2" Height="30" VerticalAlignment="Top" Margin="5">

<Button Content="Save" Margin="3"/>

<Button Content="Cancel" Margin="3"/>

</UniformGrid>

</Grid>

</Window>


 

We create a new Client object to bind to the Window. The creation and binding happens on the window's contructor, just below the InitializeComponent() method:

private Client _testClient;
public WindowValidationDemo()
{
      InitializeComponent();
       _testClient = new Client { FullName="Ioannis Panagopoulos", Rating=0 };
       this.DataContext = _testClient;
}
 


 

There are two things worth mentioning here. The one is related to the way the object is initialized. Note that there is no constructor in the Client class.In C#3.0 you can initialize the properties of an object during construction in blocks of code enclosed by {}. Neat isn't it? Also note that due to the hierarchical nature of the DataContext property, we can set the Window's DataContext property to the Client Business Object and be sure that the bindings will find their paths ;)

Finally, we implement the handler for the Click event of the button as follows:

private void Button_Click(object sender, RoutedEventArgs e)
{
      _testClient.Rating++;
}
 


 

It's time now to start dealing with the issues concerning Data Validation. First of all, we need to decide where the validation logic's implementation resedes. Should it be completely decoupled from the business object or not? Initially, we distinguish Data Validation into three main categories:

  1. Data need to be validated for type conversion errors (eg writing an alphanumeric string in a textbox which is databound to an integer).
  2. Data need to be validated for values that do not make sense in their own context (eg. A rating property can only take positive values up to 10).
  3. Data need to be validated for values that do not make sense in the application's context (eg. the departure date of a trip should be smaller than the arrival date unless you are implementing a time machine!)


 

Type conversion errors (case 1) must throw exceptions and the exceptions must be caught by the GUI. Type conversion errors are serious and they are justified to be raising exceptions. In WPF, a type conversion error exception of this kind is raised by default in a data bound TextBox Control and by default it is ignored. In order to catch the exception you need to set the property ValidatesOnExceptions to True (here it is done for the Rating textbox):

 <TextBox Text="{Binding Path=Rating,ValidatesOnExceptions=True}">

This will display a red border around the TextBox when the entered value cannot be converted to the type of the Rating property (integer)- that is - an exception has been thrown. So far so good. But we may need to inform the user by providing a more descriptive message. For this reason, we hook up to the Validation.Error event and set the tooltip's value to the reported error in the event handler. We also need to explicitely state that the Binding object should be raising such events by setting the NotifyOnVaidationError property to true.

So we set the NotifyOnValidationError property to true and set the event handler for the Validation.Error event:

<TextBox Text="{Binding Path=Rating,ValidatesOnExceptions=True,NotifyOnValidationError=true}" Validation.Error="TextBox_Error"/>

And the implementation of the event handler is as follows:

private void TextBox_Error(object sender, ValidationErrorEventArgs e)
{
    if (e.Action == ValidationErrorEventAction.Added)
    {
        ((Control)sender).ToolTip = e.Error.ErrorContent.ToString();
    }
    else
    {
        ((Control)sender).ToolTip = "";
    }
}
 


 

This will display the tooltip with a description of the validation error:

Now how about cases 2,3 which are related to data that do not make sense? Well those validations are usually related to the "meaning" of our application and our business objects and therefore they should be decoupled from the GUI. Such validations can be implememented in the following ways:

You can raise exceptions at the Setters of the data bound business object. For example, the following code in the FullName setter raises and exception when the FullName contains the name "John":

public string FullName
{
    get { return _fullName; }
    setx
    {
        if (_fullName != value)
        {
            _fullName = value;
            if (_fullName!=null && _fullName.Contains("John")) throw new ApplicationException("FullName cannot be John!");
            if (PropertyChanged != null)
                PropertyChanged(thisnew System.ComponentModel.PropertyChangedEventArgs("FullName"));
           
        }
    }
}
 


 

The XAML code for the data bound TextBox will be like this:

<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Path=FullName, ValidatesOnExceptions=true, ValidatesOnDataErrors=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Validation.Error="TextBox_Error"/>


 

Again we are notified for the exception and the handler is called with the same code as before. But there is a problem. The error message displayed in the tooltip says "Exception has been thrown by a target of an Invocation" although we have supplied a different message in the exception. To show the correct message you need to display the message in e.Error.Exception.InnerException.

Raising exceptions in the Setters is not a very good approach since those properties are also set by code and sometimes it is ok to temporarily leave them with error values.

So another approach is to implement the IDataErrorInfo Interface. This interface is used by the Data Binding mechanism to validate error. For example in the following, we restrict the Rating value to be below 10 (Of course we state that our class implements the Systems.ComponentModel.IDataErrorInfo interface):

#region IDataErrorInfo Members

// This is not used in XAML (was used in WinForms)
public string Error
{
    get { throw new NotImplementedException(); }
}

public string this[string propertyName]
{
    get
    {
        if (propertyName == "Rating")
        {
            if (_rating > 10) return "Value must be smaller than 10!";
        }
        return "";
    }
}

#endregion



The XAML code should be altered to support those validation errors as follows:

<TextBox Text="{Binding Path=Rating, ValidatesOnExceptions=true, ValidatesOnDataErrors=true, NotifyOnValidationError=true}" Validation.Error="TextBox_Error"/>


 

This approach is a better alternative to the previous one. It is useful in cases where we want the Business Object to carry the validation logic if it is going to be used in different forms and all of those have the same validation needs for he object.

The third and the last approach is one that is totally decoupled both from the GUI and the Business Object. It is based on implementing custom Validators. For example this is a validator that provides an error when the value is smaller than 0 (note that you need to use the System.Windows.Controls assembly apart from the default ones):

    public class DemoValidator : ValidationRule
    {
        public override ValidationResult Validate(object value,
            System.Globalization.CultureInfo cultureInfo)
        {
            int IntValue=0;
            ValidationResult result = null;
            try
            {
               IntValue=Convert.ToInt16(value);
            }
            catch (Exception)
            {
                return new ValidationResult(false"Please provide a number");
            }

            if (IntValue < 0) return new ValidationResult(false"Please provide a number greater than 0");
            
            return new ValidationResult(truenull);
        }
    }
 


 

In the XAML file, in order to use this validator we first declare the namespace it resides:

<Window x:Class="ValidationDemo.WindowValidationDemo"

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

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

xmlns:local="clr-namespace:ValidationDemo"

Title="Validation Demo" Height="150" Width="432">

And then apply it to the Rating TextBox as follows:


 

 

A few things here. First, we have changed the XAML syntax (nesting the Binding element) to request the use of our custom validator. Second,since we are using all of the possible validators in this scenario, a question is raised about the sequence they are called. In other words, do I need to check whether the value is a number in the custom validator? For answers to those questions check out in here at the end of the article, the sequence of validation and also here for the property ValidationStep that controls when the custom validators will be executed. Finally for information on how to change the red box around the textbox and provide your own, read a nice article by Paul Stovell here.

That summarizes a brief introduction to Data Validation in WPF. As it seems in large scale applications the most flexible and useful approach is to use custom validators in conjunction with the native exception system of WPF.

The demo application can be downloaded here: ValidationDemo.zip (87,28 kb)

kick it on DotNetKicks.com

<TextBox VerticalAlignment="Center" Width="30" HorizontalAlignment="Left" TextAlignment="Center" Validation.Error="TextBox_Error">

<Binding Path="Rating" ValidatesOnExceptions="true" ValidatesOnDataErrors="True" NotifyOnValidationError="True">

<Binding.ValidationRules>

<local:DemoValidator/>

</Binding.ValidationRules>

</Binding>

</TextBox>

blog comments powered by Disqus
hire me