Bindings are a major part of WPF, and a big part of what makes it quick
to create user interfaces in. They can be a little wordy and hard to
debug sometimes, but overall they are extremely useful. Hopefully, after
today's tutorial, you will find them even more useful - because today we
are going to talk about building validation rules right into your
bindings. Yup, you heard right - bindings in WPF have built in support
for validation, and a number of the common controls have built in ways
for displaying validation errors.
Today we are going to build a simple little WPF application that has a
TextBox
in which the user is supposed to enter an IP Address (a v4
address - I was lazy and didn't want to write a validator for ipv6 as
well :P). Below you can see a screenshot of the the app in action:
So the first thing that we need to do to add validation to a binding is
to create a validation rule. To do this, you have to create a class that
derives from
ValidationRule
and implements the abstract method
Validate
. There is actually only
one validation rule built into the .NET framework, the
ExceptionValidationRule,
so in most cases you will probably be writing the rules yourself.
Below you can see the code for our rule, the
IPv4ValidationRule
:using System;
using System.Globalization;
using System.Windows.Controls;
namespace SOTCBindingValidation
{
public class IPv4ValidationRule : ValidationRule
{
public override ValidationResult Validate(object value,
CultureInfo cultureInfo)
{
var str = value as string;
if (String.IsNullOrEmpty(str))
{
return new ValidationResult(false,
"Please enter an IP Address.");
}
var parts = str.Split('.');
if (parts.Length != 4)
{
return new ValidationResult(false,
"IP Address should be four octets, seperated by decimals.");
}
foreach (var p in parts)
{
int intPart;
if (!int.TryParse(p, NumberStyles.Integer,
cultureInfo.NumberFormat, out intPart))
{
return new ValidationResult(false,
"Each octet of an IP Address should be a number.");
}
if (intPart < 0 || intPart > 255)
{
return new ValidationResult(false,
"Each octet of an IP Address should be between 0 and 255.");
}
}
return new ValidationResult(true, null);
}
}
}
Before you comment on it, yes I know that you can check if a string is
an IP Address by using the
IPAddress.Parse
method. For this example, though, using that wasn't nearly as
interesting - plus, unlike this validation rule, it doesn't give very
informative errors as to why the given string does not parse as an IP
address.
Giving informative errors are one of the reasons to use validation rules
- you get to send back information about the rule failure, instead of
just a true/false. The
Validate
method always returns a
ValidationResult,
which is made up of two main parts - a boolean for if the rule
passed/didn't pass, and an object holding extra information about the
error. In our case here, that object is always an error string that we
are later going to display to the user.
OK, now that we have a validation rule, how do we use it? Below you can
see the C# and XAML code for the main window of our application:
using System.Windows;
namespace SOTCBindingValidation
{
public partial class Window1 : Window
{
public static readonly DependencyProperty IPAddressProperty =
DependencyProperty.Register("IPAddress", typeof(string),
typeof(Window1), new UIPropertyMetadata("1.1.1.1"));
public string IPAddress
{
get { return (string)GetValue(IPAddressProperty); }
set { SetValue(IPAddressProperty, value); }
}
public Window1()
{ InitializeComponent(); }
}
}
<Window x:Class="SOTCBindingValidation.Window1" x:Name="This"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SOTCBindingValidation"
Title="SOTC Validation Test" Height="150" Width="400">
<Window.Resources>
<local:ErrorsToMessageConverter x:Key="eToMConverter" />
</Window.Resources>
<StackPanel Margin="5">
<TextBlock Margin="2">Enter An IPv4 Address:</TextBlock>
<TextBox x:Name="AddressBox">
<TextBox.Text>
<Binding ElementName="This" Path="IPAddress"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:IPv4ValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock Margin="2" Foreground="Red" FontWeight="Bold"
Text="{Binding ElementName=AddressBox,
Path=(Validation.Errors),
Converter={StaticResource eToMConverter}}" />
</StackPanel>
</Window>
In terms of the C# code, there really isn't much to look at - we just
have a dependency property for the IP address. It is the XAML that we
are really interested in here. First thing you probably noticed is that
we are declaring the binding for the
TextBox
using the full binding
tags and syntax. This is because once you start adding things like
validation rules, we can't use the markup extension anymore.
Inside the
<Binding.ValidationRules>
tags, we can list out all the
rules that we want to run against this binding. In our case, we only
have the one rule, but you can potentially have as many as you want. If
you have more than one rule, all the rules get run (it doesn't short
circuit after the first failure), so you get the accumulation of all the
rule errors.
The rules get run every time the binding propagates changes from the
target (in this case the
TextBox
) to the source (in this case the
backing dependency property). Rules do not get run in the other
direction - so if something invalid was pushed directly into the source,
the target would show the value without running any rules. Because of
this, validation rules only make sense if the BindingMode of the
binding is OneWayToSource
or TwoWay
(the default).
By default, a
TextBox
binding only commits changes when it loses
focus, but that is not nearly as interesting for this example. To get
the validation rules to run as the user types in the TextBox
, we set
the UpdateSourceTrigger
binding property to
PropertyChanged.
This means that the binding will be updated (and the rules will
therefore be run) every time the target property is changed.
Looking at the XAML, you are probably wondering where the red outline
comes from on the
TextBox
when there is an error. Well, it turns out
that most of the built in WPF controls have a built in
ErrorTemplate.
This template defines what happens to the control in the case of a
validation rule failure. I actually think the default error template is
pretty good, but if you need to change it, you can treat it like any
other control template, and set it in a style.
That about covers the
TextBox
, but now we need to take a look at how
we are displaying the error message. Validation rule errors are held in
an attached property called
Validation.Errors.
This is a collection of all the errors accumulated from all the rules
(remember, we can have more than one), so we need a converter to convert
that collection into a nice string to display:using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Data;
namespace SOTCBindingValidation
{
public class ErrorsToMessageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
var sb = new StringBuilder();
var errors = value as ReadOnlyCollection<ValidationError>;
if (errors != null)
{
foreach(var e in errors.Where(e => e.ErrorContent != null))
{ sb.AppendLine(e.ErrorContent.ToString()); }
}
return sb.ToString();
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{ throw new NotImplementedException(); }
}
}
A pretty simple converter - takes the error collection, and for each
error that has an error message, append it to the accumulating string
and then return the string. In the case were there are no errors, we
will just get an empty string back.
The only trick to actually binding to the error collection is that it is
an attached property, and so you need to use the attached property path
syntax to reference it. That is why the
Validation.Errors
has
parenthesis around it when it is set as the path of the binding. It is
essentially saying to take the Errors
attached property from the
Validation
class and find the value on the TextBox
. Oh, the wonders
and confusion of dependency properties!
Well, that is it for this introduction to adding validation rules to
your bindings! You can grab a zip of the source code for the example we
built here below.
Source Files:
Comments
Post a Comment