Introduction
This article is about how to simplify the standard WPF validation process when working with databound objects that use the System.ComponentModel.IDataErrorInfo
interface. So if you are not concerned with either of these areas, this is probably a great place to stop reading.
This article also assumes you are familiar enough with some basic WPF principles such as Binding/Styles/Resources.
This article will cover the following areas:
WPF now supports the System.ComponentModel.IDataErrorInfo
interface for validation. If you want to read more about this, I would recommend reading the Windows Presentation Foundation SDK page, at the following URL: http://blogs.msdn.com/wpfsdk/archive/2007/10/02/data-validation-in-3-5.aspx.
This is achieved by implementing this interface for a business object class. Something like the following:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace BusinessLayerValidation
{
public class Person : IDataErrorInfo
{
#region Data
private int age;
public int Age
{
get { return age; }
set { age = value; }
}
#endregion
#region IDataErrorInfo implementation
public string Error
{
get
{
return null;
}
}
public string this[string name]
{
get
{
string result = null;
if (name == "Age")
{
if (this.age < 0 || this.age > 150)
{
result = "Age must not be less than 0 or greater than 150.";
}
}
return result;
}
}
#endregion
}
}
Now that's fine, but this is only part of the story when working with the standard WPF validation mechanism. It is quite common to find a Binding
per property when working with a bound data object, which would be something like this:
<TextBlock>Enter your age:</TextBlock>
<TextBox Style="{StaticResource textBoxInError}">
<TextBox.Text>
-->
<Binding Path="Age" Source="{StaticResource data}"
ValidatesOnDataErrors="True"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
-->
<ExceptionValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
This chunk of code needs to be repeated for each bound property that you would like to validate. There is a bit of shorthand syntax that you can use, which is as follows:
<Binding Source="{StaticResource data}" Path="Age"
UpdateSourceTrigger="PropertyChanged"
ValidatesOnDataErrors="True" />
ValidatesOnDataErrors="True"
is the magic part that really means using the System.ComponentModel.IDataErrorInfo
interface mechanism for validation.
The standard WPF validation story isn't over yet. We still have to create a Style
for the TextBox
es to use when they are invalid; this requires more XAML. Luckily, all TextBox
es that need validation styling can use this single Style
:
<!---->
<Style x:Key="textBoxInError" TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
Even though there is only one Style
section, I didn't/don't like the fact that for what is essentially a very simple binding operation, we ended up with quite a lot of bloating XAML. Imagine a Window that required 100s of these sort of Binding
s. It would get big quite fast.
To this end, I set up to develop a self binding/validating UserControl that would allow the user to specify the following:
- The underlying bound object property to bind to
- The label text
- The position of the label
- The update trigger to update the underlying data object property
- The type of text that is allowed
How these are realized is discussed below.
This section will outline how a self binding/validating UserControl works, which is the main idea behind this article.
The underlying bound object property to bind to
Before I dive in and explain what is essentially a very simple idea, I think it's important to at least understand how data binding and the magical DataContext
property works.
Each object in WPF has a DataContext
property, which accepts the object
Type as a value. So basically, it accepts anything. What you commonly see is that an element (within the Visual Tree) that contains child elements that want to bind to some object, will have its own DataContext
property set to some object. Now, if the object's (which has the DataContext
property set to some object) children do not overwrite their own DataContext
properties with a value, they will inherit the value from their parent (which in this case would be the actual DataContext
property of their parent within the Visual Tree) control.
This is pretty typical.
Now when a DataContext
property changes value, an event is raised called DataContextChanged
. Again, most WPF controls have this event. So by wiring up this event, we are able to create a data aware UserControl (CLRObjectBoundTextBox
) that can create a new Binding
whenever its DataContext
changes. The only thing we need to know is the name of the property of the actual business object that we wish to bind to. Once we know that, we can create a binding in code that will be replaced whenever the DataContextChanged
event is fired. This means that if we are using a data aware UserControl (CLRObjectBoundTextBox
) to monitor business object A, then the data aware UserControl (CLRObjectBoundTextBox
) is told to now look at another business object B (via a new DataContext
value, which it may actually inherit) and the associated Binding
will be created.
Ok so that's the idea, how about some code?
Well, if you get that, the code is pretty easy to understand actually:
public CLRObjectBoundTextBox()
{
InitializeComponent();
textBoxInErrorStyle =
Application.Current.TryFindResource("textBoxInError") as Style;
this.DataContextChanged +=
new DependencyPropertyChangedEventHandler(
CLRObjectBoundTextBox_DataContextChanged);
}
private void CLRObjectBoundTextBox_DataContextChanged(object sender,
DependencyPropertyChangedEventArgs e)
{
if (txtBoxInUse != null)
{
var exp = txtBoxInUse.GetBindingExpression(TextBox.TextProperty);
if (exp != null)
Validation.ClearInvalid(
txtBoxInUse.GetBindingExpression(TextBox.TextProperty));
BindingOperations.ClearAllBindings(txtBoxInUse);
Binding bind = new Binding();
bind.Source = this.DataContext;
bind.Path = new PropertyPath(BoundPropertyName);
bind.Mode = BindingMode.TwoWay;
bind.UpdateSourceTrigger = updateSourceTrigger;
bind.ValidatesOnDataErrors = true;
bind.ValidatesOnExceptions = true;
txtBoxInUse.SetBinding(TextBox.TextProperty, bind);
}
}
public string BoundPropertyName
{
private get { return boundPropertyName; }
set { boundPropertyName = value; }
}
The only things worth mentioning here are that the user is able to specify either in XAML/code what property to use from the underlying business object. Also worth a mention is the fact that I have enabled Exception based validation, using ValidatesOnExceptions = true
, which will show the Exception message if an Exception is raised whilst attempting to set the underlying business object's bound property. This gives the best of both worlds, using the System.ComponentModel.IDataErrorInfo
interface mechanism for validation, and also the Exception based validation.
And there is still a requirement for a Style
that styles the TextBox
when it is invalid. This can not be avoided, but at least this way, the XAML of the host for this data aware UserControl (CLRObjectBoundTextBox
) will be relatively clean. Also, this TextBox
style is declared exactly once within a ResourceDictionary
(AppStyles.xaml) which is part of the current application's MergedDictionaries
collection (App.xaml).
The label text
This is achieved via the use of a simple CLR property on this article's UserControl (CLRObjectBoundTextBox
) which allows the users to pick what text the label should display for the bound property for the underlying business object.
public string LabelToDisplay
{
private get { return labelToDisplay; }
set
{
labelToDisplay = value;
lbl.Content = labelToDisplay;
}
}
The position of the label
This is achieved via the use of a simple CLR property on this article's UserControl (CLRObjectBoundTextBox
) which allows the users to pick where they want the label's text to be:
public Dock LabelDock
{
set {
Dock dock = value;
switch (dock)
{
case Dock.Left:
lbl.SetValue(DockPanel.DockProperty, Dock.Left);
break;
case Dock.Top:
lbl.SetValue(DockPanel.DockProperty, Dock.Top);
break;
case Dock.Right:
lbl.SetValue(DockPanel.DockProperty, Dock.Right);
break;
case Dock.Bottom:
lbl.SetValue(DockPanel.DockProperty, Dock.Bottom);
break;
default:
lbl.SetValue(DockPanel.DockProperty, Dock.Left);
break;
}
}
}
The update trigger to update the underlying data object property
When using the Binding
class to bind to underlying objects, you will inevitably need to update the underlying business object at some point. The Binding
class offers four options for this, which can be set using the UpdateSourceTrigger
enumeration:
Default
: Uses PropertyChanged
by default
Explicit
: Updates must be manually performed using code-behind
LostFocus
: Updates the underlying business objects when the control with the Binding
loses focus
PropertyChanged
: Updates the underlying business objects when the control with the Binding
property value changes
Now these options are fine, but as this article's UserControl (CLRObjectBoundTextBox
) is aimed at providing something that is re-usable, I had to rule out being able to allow the user to use the Explicit
option. This would require code-behind, which would be different each time, as it would depend on the actual business object being used at that time. To this end, I have created a simple wrapper property which only allows the Binding
used to have a value of LostFocus
or PropertyChanged
.
This is done as follows:
public enum UpdateTrigger { PropertyChanged = 0, LostFocus = 1 };
...
...
public UpdateTrigger UpdateDataSourceTrigger
{
set
{
switch (value)
{
case UpdateTrigger.LostFocus:
updateSourceTrigger = UpdateSourceTrigger.LostFocus;
break;
case UpdateTrigger.PropertyChanged:
updateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
break;
default:
updateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
break;
}
}
}
The type of text that is allowed
I wanted the user to be able to specify, as part of the XAML, what sort of text should be allowed. Based on this user selected option, a specialized TextBox
would then be created which would be used to create a Binding
to the underlying business object.
This works by allowing the user to enter any one of four values from an enumeration of allowable TextBoxTypes
, which allows the correct type of TextBox
to be created.
The TextBoxTypes
enumeration currently contains the following possible values:
Standard
: Will use a bulk standard System.Windows.Controls.TextBox
.
NumericOnly
: Will use a specialized numeric-only TextBox
(though this is by no means a production quality numeric-only TextBox
, but it gives a starting place; for example, it doesn't cater for pasted text. This was not the point of this article, so coming up with robust TextBox
es that match your business requirements is left as an exercise for the reader.)
LettersOnly
: Will use a specialized letters-only TextBox
(though this is by no means a production quality letters-only TextBox
, but it gives a starting place; for example, it doesn't cater for pasted text. This was not the point of this article, so coming up with robust TextBox
es that match your business requirements is left as an exercise for the reader.)
Masked
: Will use a specialized masked TextBox
, which will make sure the text entered is only that is allowed by its Mask
property. In order for a user to specify a mask for the masked textbox, a Mask
property is available.
public string Mask
{
private get { return mask; }
set
{
mask = value;
if (txtBoxInUse is MaskedTextBox)
(txtBoxInUse as MaskedTextBox).Mask = mask;
}
}
public TextBoxTypes TextBoxTypeToUse
{
private get { return textBoxTypeToUse; }
set
{
textBoxTypeToUse = value;
switch (textBoxTypeToUse)
{
case TextBoxTypes.Standard:
txtBoxInUse = new TextBox();
txtBoxInUse.Width = double.NaN;
txtBoxInUse.Height = double.NaN;
txtBoxInUse.Style = textBoxInErrorStyle;
dpMain.Children.Add(txtBoxInUse);
break;
case TextBoxTypes.NumericOnly:
txtBoxInUse = new NumericOnlyTextBox();
txtBoxInUse.Width = double.NaN;
txtBoxInUse.Height = double.NaN;
txtBoxInUse.Style = textBoxInErrorStyle;
dpMain.Children.Add(txtBoxInUse);
break;
case TextBoxTypes.LettersOnly:
txtBoxInUse = new LettersOnlyTextBox();
txtBoxInUse.Width = double.NaN;
txtBoxInUse.Height = double.NaN;
txtBoxInUse.Style = textBoxInErrorStyle;
dpMain.Children.Add(txtBoxInUse);
break;
case TextBoxTypes.Masked:
txtBoxInUse = new MaskedTextBox();
if (Mask != string.Empty)
(txtBoxInUse as MaskedTextBox).Mask = Mask;
txtBoxInUse.Width = double.NaN;
txtBoxInUse.Height = double.NaN;
txtBoxInUse.Style = textBoxInErrorStyle;
dpMain.Children.Add(txtBoxInUse);
break;
default:
txtBoxInUse = new TextBox();
txtBoxInUse.Width = double.NaN;
txtBoxInUse.Height = double.NaN;
txtBoxInUse.Style = textBoxInErrorStyle;
dpMain.Children.Add(txtBoxInUse);
break;
}
}
}
I am not expecting that the NumericOnlyTextBox
/LettersOnlyTextBox
included with this article will be right for everyone's businesses. They simply demonstrate how you could extend this article's idea to suit your own business needs.
For the demo, assume the following data object:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace BoundTextBoxWithValidation
{
class Person : IDataErrorInfo
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string PhoneNumber { get; set; }
public string Error
{
get
{
return null;
}
}
public string this[string column]
{
get
{
string result = null;
if (column == "FirstName")
{
if (this.FirstName == string.Empty ||
this.FirstName == "SACHA")
{
result = "cant be empty or SACHA";
}
}
if (column == "LastName")
{
if (this.LastName == string.Empty ||
this.LastName == "BARBER")
{
result = "cant be empty or BARBER";
}
}
if (column == "Age")
{
if (this.Age < 0 || this.Age > 50)
{
result = "Age must be between 0 and 50";
}
}
if (column == "PhoneNumber")
{
if (!this.PhoneNumber.StartsWith("(01273"))
{
result = "PhoneNumber must start with (01273)";
}
}
return result;
}
}
}
}
Which can be used using this article's UserControl (CLRObjectBoundTextBox
) quite easily, as follows, in XAML:.
<Window x:Class="BoundTextBoxWithValidation.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local1="clr-namespace:BoundTextBoxWithValidation"
Title="Window1" Height="300" Width="300">
<StackPanel Orientation="Vertical">
<local1:CLRObjectBoundTextBox x:Name="txtFirstName"
Margin="5"
TextBoxTypeToUse="LettersOnly"
UpdateDataSourceTrigger="PropertyChanged"
BoundPropertyName="FirstName"
LabelToDisplay="First Name"
LabelDock="Top"/>
<local1:CLRObjectBoundTextBox x:Name="txtLastName"
Margin="5"
TextBoxTypeToUse="LettersOnly"
UpdateDataSourceTrigger="LostFocus"
BoundPropertyName="LastName"
LabelToDisplay="Last Name"
LabelDock="Top"/>
<local1:CLRObjectBoundTextBox x:Name="txtAgeName"
Margin="5"
TextBoxTypeToUse="NumericOnly"
UpdateDataSourceTrigger="PropertyChanged"
BoundPropertyName="Age"
LabelToDisplay="Age"
LabelDock="Top"/>
<local1:CLRObjectBoundTextBox x:Name="txtPhoneNumber"
Margin="5"
TextBoxTypeToUse="Masked"
UpdateDataSourceTrigger="PropertyChanged"
BoundPropertyName="PhoneNumber"
LabelToDisplay="Phone Number"
Mask="(99999) 000000"
LabelDock="Top"/>
</StackPanel>
</Window
I am fully aware that the UserControl (CLRObjectBoundTextBox
) contained within this article will not cover all eventualities, but should you wish to extend this idea, all you would have to do is create your own specialized TextBox
es and alter the CLRObjectBoundTextBox
TextBoxTypes
enumeration and TextBoxTypeToUse
properties to use your own specialized TextBox
es.
We're Done
Well, that's all I wanted to say this time. I think this article is pretty useful. If you like it, could you please leave a vote for it?