Saturday, July 26, 2014

More about Validation

WPF 4.0 has some powerful validation features which are readily declared in XAML. I have put together a small project that demonstrates some validation techniques and also highlights a curious issue.

Let's start by defining a very simple window with one text box that only accepts letters. If the user enters a non-letter the textbox validation will trigger an error which will cause the textbox to get a red outline. We will write a custom validator to do this.

Start a new WPF Application project in C# and call it "SimpleValidator". Change the XAML and code behind to look like this...

<Window x:Class="SimpleValidator.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:SimpleValidator" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <local:User x:Key="theUser"/> </Window.Resources> <StackPanel Orientation="Horizontal" Height="30"> <Label>First Name:</Label> <TextBox Name="NameTextBox" Width="200"> <TextBox.Text> <Binding Source="{StaticResource theUser}" Path="Name" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <local:OnlyLetterValidation/> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> </StackPanel> </Window> ---------------------------------------------------------------------------------------------- using System; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Media; namespace SimpleValidator {
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } public class User { public string Name { get; set; } } public class OnlyLetterValidation : ValidationRule { public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo) { if (value.ToString().All(char.IsLetter)) return new ValidationResult(true, ""); else return new ValidationResult(false, "Only letters allowed"); } } }
Note the use of the String.All method (a linq extension). This is a cool technique for testing every character in a string. Also check out the Any method.

If you run this project you will see that as soon as you enter a non-letter the textbox border goes red to indicate that it has failed validation. If you put a breakpoint on the OldLetterValidation.Validate method you will see it is called whenever the user changes the value in the textbox. This is because UpdateSourceTrigger is set to "PropertyChanged" in the XAML. Validation occurs when the Source is updated. If you want to only validate when the user exits the field you should set UpdateSourceTrigger to "LostFocus".

At the moment we cannot tell the user why the textbox failed validation. There are at least three ways to do this.
  1. Bind a control to the Textbox's Validation.Errors collection. This is an attached property which exposes a collection of ValidationError objects. You could attach a list box's ItemsSource to the collection or attach a tooltip or textblock to it using a converter to convert the collection to a string.
  2. Mark the Textbox to raise Validation events when validation errors are added or removed and consume those events.
  3. Create a custom ErrorTemplate for the textbox. This is what we are going to do now.
Insert the following XAML after the </TextBox.Text> tag. Note the AdornedElementPlaceholder simply creates a placeholder for the UIElement that is being templated.

<Validation.ErrorTemplate> <ControlTemplate> <StackPanel Orientation="Horizontal"> <AdornedElementPlaceholder/> <TextBlock Text="{Binding [0].ErrorContent}" Foreground="Red"/> </StackPanel> </ControlTemplate> </Validation.ErrorTemplate>

Now run the application again and type a non-letter into the textbox. See how the error message is now displayed to the right of the text box.

Now let's add another validation rule. This will be a minimum length rule and will demonstrate how to create a validation with a parameter and how multiple validations work.

Start by adding a second validation class to the code behind. Add this underneath the existing OnlyLettersValidation class.

public class MinLengthValidation : ValidationRule { public int MinLength { get; set; } public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo) { if (value.ToString().Length < MinLength) return new ValidationResult(false, string.Format("At least {0} letters are required", MinLength)); else return new ValidationResult(true, ""); } }

This class exposes a public property which means the XAML can easily access that property, thus allowing parameters to be passed to validators. Note this is not a DependencyProperty so you cannot bind to it in XAML, although it can be done with more work. This is outside the scope of this blog entry.

Using the validator in XAML couldn't be much easier. Just add the following line at the top of the list of ValidationRules.

<Binding.ValidationRules> <local:MinLengthValidation MinLength="5"/> <local:OnlyLetterValidation/> </Binding.ValidationRules>

Run the project again and you will see that as soon as you start typing you see the minimum length error displayed. It only goes away when you have entered at least five letters.

But how do we tell the user when they first see the text box that it requires at least five characters. It should start in an error state because it is empty. We fix this with one line of code that explicitly initializes the text property of the textbox. Because we have two-way binding this causes the source to be updated. Validation occurs whenever the source is updated. Add the following line of code after the InitializeComponents line in the window's constructor.

public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); NameTextBox.Text = ""; } }

Run the project now and you will see the text box get validated before the page displays.

Now we have two validators. If you recall the textbox has an attached property called Validation.Errors which exposes a collection of ValidationError objects. However, if you put a breakpoint on each of our validators and type a value that fails both rules ie a space, you will see that validation stops at the first error. I cannot actually find a way to get more than one validation error in the Validation.Errors collection. Oh well!

There's one more thing I want to demonstrate in this blog entry. Sometimes we would like to change the color of another field, normally the prompt field, when validation fails. We can do this with binding although we do need a converter to convert from a boolean to a brush.

Let's start by adding a simply converter to our code behind.

public class ValidationConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value is Boolean) { if ((Boolean)value) return new SolidColorBrush(Colors.Red); else return new SolidColorBrush(Colors.Black); } return DependencyProperty.UnsetValue; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } }

Now we need to add a reference to the converter in the Windows.Resources section like this.

<Window.Resources> <local:User x:Key="theUser"/> <local:ValidationConverter x:Key="ValidationConverter"/> </Window.Resources>

And finally we bind the foreground property of our label using this rather intimidating binding clause. Notice the Path attribute has parentheses around it because Validation is an attached property.

<Label Grid.Row="0" Grid.Column="0" Foreground="{Binding ElementName=NameTextBox, Path=(Validation.HasError), Converter={StaticResource ValidationConverter}}">First Name: </Label>

Now run the project one more time and you will see the foreground color of the label turns red whenever the textbox fails validation. You could just have easily have changed the tooltip using a converter that grabbed the text of the first validation error, etc.

No comments:

Post a Comment