Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / WPF

WPF: Validation of business objects, a simpler approach

Rate me:
Please Sign up or sign in to vote.
4.91/5 (39 votes)
20 Jul 2008CPOL8 min read 146K   1.1K   79   34
An article on how to simplify the WPF validation process.

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:

Standard WPF Validation

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:

C#
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
        //Get the error
        public string Error
        {
            get
            {
                return null;
            }
        }

        /// <summary>
        /// Gets an error message for the bound column
        /// </summary>
        /// <param name="name">The column name to validate</param>
        /// <returns>An error message for the current column</returns>
        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:

XML
<TextBlock>Enter your age:</TextBlock>
<TextBox Style="{StaticResource textBoxInError}">
    <TextBox.Text>
        <!-- Setting the ValidatesOnDataErrors to true 
            enables to the Binding to check for
            errors raised by the IDataErrorInfo implementation.
            Alternatively, you can add DataErrorValidationRule 
            to <Binding.ValidationRules/>-->
        <Binding Path="Age" Source="{StaticResource data}"
                 ValidatesOnDataErrors="True"   
                 UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <!-- Checks for exceptions during 
                    the setting of the source property.
                    Alternatively, set ValidatesOnExceptions 
                    to True on the Binding.-->
                <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:

XML
<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 TextBoxes to use when they are invalid; this requires more XAML. Luckily, all TextBoxes that need validation styling can use this single Style:

XML
<!--The tool tip for the TextBox to display the validation error message.-->
<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 Bindings. 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.

Surely There is an Easier Way

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:

C#
public CLRObjectBoundTextBox()
{
    InitializeComponent();

    //Get Style for whatever textbox we end up using
    textBoxInErrorStyle = 
      Application.Current.TryFindResource("textBoxInError") as Style;

    //hook up the DataContextChanged, which will allow the validation to 
    //work when a new bound object is seen
    this.DataContextChanged += 
        new DependencyPropertyChangedEventHandler(
        CLRObjectBoundTextBox_DataContextChanged);
}

/// <summary>
/// Bind the textbox to the CLR object property, it is assumed that
/// there is a DataContext that is active that this control will be
/// using for the binding source
/// </summary>
private void CLRObjectBoundTextBox_DataContextChanged(object sender, 
        DependencyPropertyChangedEventArgs e)
{

    if (txtBoxInUse != null)
    {
        //clear validation because it is not automatically cleared
        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);
    }
}

/// <summary>
/// The underlying objects property to use for the Binding
/// </summary>
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.

C#
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:

C#
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:

C#
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 TextBoxes 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 TextBoxes 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.
C#
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.

Demo

For the demo, assume the following data object:

C#
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:.

XML
<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

Extending This Idea

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 TextBoxes and alter the CLRObjectBoundTextBox TextBoxTypes enumeration and TextBoxTypeToUse properties to use your own specialized TextBoxes.

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?

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
BugThanks! A little bug fix... Pin
Troy Tamas17-Oct-11 16:35
Troy Tamas17-Oct-11 16:35 
GeneralRe: Thanks! A little bug fix... Pin
Sacha Barber17-Oct-11 19:56
Sacha Barber17-Oct-11 19:56 
GeneralRe: Thanks! A little bug fix... Pin
Troy Tamas18-Oct-11 14:21
Troy Tamas18-Oct-11 14:21 
QuestionData Binding TwoWay Pin
rostrat25-Jun-09 6:03
rostrat25-Jun-09 6:03 
AnswerRe: Data Binding TwoWay Pin
Sacha Barber25-Jun-09 21:35
Sacha Barber25-Jun-09 21:35 
Questionpublic string this[string column] intrepetation from Person class Pin
rostrat12-Mar-09 3:36
rostrat12-Mar-09 3:36 
Questionhow can I trigger the adorner when the screen is first loaded Pin
rjempo26-Feb-09 17:04
rjempo26-Feb-09 17:04 
AnswerRe: how can I trigger the adorner when the screen is first loaded Pin
Sacha Barber1-Mar-09 11:02
Sacha Barber1-Mar-09 11:02 
QuestionWhat is data?? Pin
azamsharp15-Jan-09 11:31
azamsharp15-Jan-09 11:31 
AnswerRe: What is data?? Pin
Sacha Barber15-Jan-09 13:09
Sacha Barber15-Jan-09 13:09 
GeneralDesigner won't display [modified] Pin
dereklk9-Dec-08 17:41
dereklk9-Dec-08 17:41 
GeneralRe: Designer won't display Pin
Sacha Barber9-Dec-08 21:56
Sacha Barber9-Dec-08 21:56 
GeneralRe: Designer won't display Pin
dereklk10-Dec-08 6:44
dereklk10-Dec-08 6:44 
GeneralRe: Designer won't display Pin
Sacha Barber10-Dec-08 22:03
Sacha Barber10-Dec-08 22:03 
QuestionRe: Designer won't display Pin
rostrat11-Mar-09 17:30
rostrat11-Mar-09 17:30 
AnswerRe: Designer won't display Pin
Sacha Barber12-Mar-09 1:38
Sacha Barber12-Mar-09 1:38 
GeneralRe: Designer won't display Pin
rostrat12-Mar-09 3:28
rostrat12-Mar-09 3:28 
GeneralRe: Designer won't display Pin
Sacha Barber12-Mar-09 3:46
Sacha Barber12-Mar-09 3:46 
GeneralRe: Designer won't display Pin
rostrat12-Mar-09 14:11
rostrat12-Mar-09 14:11 
GeneralRe: Designer won't display Pin
Sacha Barber12-Mar-09 22:54
Sacha Barber12-Mar-09 22:54 
GeneralThanks again!!! Pin
VisualLive26-Nov-08 23:04
VisualLive26-Nov-08 23:04 
GeneralRe: Thanks again!!! Pin
Sacha Barber26-Nov-08 23:10
Sacha Barber26-Nov-08 23:10 
GeneralRe: Thanks again!!! Pin
VisualLive27-Nov-08 16:12
VisualLive27-Nov-08 16:12 
GeneralRe: Thanks again!!! Pin
Sacha Barber27-Nov-08 22:00
Sacha Barber27-Nov-08 22:00 
Generalxaml display in code project Pin
prasad0225-Oct-08 15:20
prasad0225-Oct-08 15:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.