Download the code: http://gallery.technet.microsoft.com/WPF-Validation-d5d87476


Introduction


This article is aimed at those developers who want, and need to take control of their validation. WPF provides us with a rich validation framework but unfortunately in some advanced scenarios the validation framework can leave your mind twisted into a pretzel. Today, I’m going to help you take control of this monster and put forward some suggestions on how to deal with validation.

Let us take a quick look at a scenario that causes a problem when we are trying to apply validation. Say you have a data entry form that has five fields and all five fields require validation. If you only enter data into the first two fields but tab through the other three fields and click save, then the remaining three fields will not be validated. Sure, you probably are thinking that you can apply ValidatesOnTargetUpdated but do you really want your validation to fire before you have started adding any data.

Also, when you fire UpdateSource through the BindingExpression when using the Entity Framework as a source, the Entity will still try and update even if a validation has returned and error. This can be really frustrating as the Entity Frameworks exception still halts execution even if your code is encapsulated in a Try/Catch!

Furthermore, you do have the option of using ValidateWithoutUpdate on the BindingExpression but as mentioned earlier, if you haven’t entered data and changed the field value in any way, that field will not be processed by the validation framework.

Therefore, I will show you how to deal with all of these frustrating scenarios which you may have thought that you have no control over.

Setting up Validation – CustomerView.xaml


As a demonstration, we will create a data entry form that has five fields. Although we will have Bindings set on these fields we won’t actually bind to any data. The validation logic will still fire as usual.

 

The validation logic we are about to implement ties into the current WPF validation framework so you can go about your business creating ErrorTemplates as usual though there are not any defined in this demonstration.

Firstly, lets create the data entry form as shown below:



Figure 1:
Customer Data Entry Window
 

Now, let’s define the Text Property for the Firstname using a Binding in XAML. All other fields will be defined in pretty much the same way apart from the ValidationRule:

<TextBox Name=”textboxFirstname”
         Height=”25”
         Width=”155”
         Margin=”0,0,0,3”
         LostFocus=”Field_LostFocus”>
    <TextBox.Text>
        <Binding Path=”Firstname”
                 ValidatesOnExceptions=”True”
                 NotifyOnValidationError=”True”
                 UpdateSourceTrigger=”Explicit” >
            <Binding.ValidationRules>
               <local:ValidateFirstname ValidationStep=”ConvertedProposedValue” />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

The things we need to take note in the above code snippet are as follows:

Attribute

Value

ValidationsOnException

Ties the Text Property to a ExceptionValidationRule

NotifyOnValidationError

Fires an event when a validation error is fired. This is the event that we will be listening to.

UpdateSourceTrigger

Determines when the source will be updated. Setting this value to Explicit means that we have to call UpdateSource on the BindingExpression

ValidationStep

Determines when the ValidationRule will be executed. By using ConvertedProposedValue will can validate the data after any converter has been applied but before the source is updated

LostFocus

Defined on the TextBox itself, This will determine when to validate

Table 1: Binding Attributes

The ValidationRule we are using for the Firstname field in the above code snippet is:


<
local:ValidateFirstname ValidationStep=”ConvertedProposedValue” />

Implementing the Validation Rules – Validation.vb


The ValidationRules are implemented in a loose class file as follows:

 

Public Class ValidateFirstname
    Inherits ValidationRule
 
    Public Overrides Function Validate(value As Object, cultureInfo As System.Globalization.CultureInfo) As System.Windows.Controls.ValidationResult
 
        If value Is Nothing Then
            Return New ValidationResult(False, "Firstname must be entered")
        End If
        If CStr(value).Length = 0 Then
            Return New ValidationResult(False, "Firstname must be entered")
        End If
        If CStr(value).Length < 5 Then
            Return New ValidationResult(False, "Firstname must be at least 5 characters")
        End If
        If CStr(value).Length > 15 Then
            Return New ValidationResult(False, "Firstname must be no greater than 15 characters")
        End If
        Return ValidationResult.ValidResult
 
    End Function
 
End Class

As you can see above, we are testing value against some criteria. You can test value against whatever you like. What is import here is that when value fails to meet a critieria you must return a new ValidationResult with the IsValid parameter being false. If you reach the end of the validation method without errors, you simply return ValidationResult.ValidResult.


The first point of order in our CustomerView.xaml code behind find is to create an instance of our ValidationBase class. The ValidationBase class is used to perform all the validation for the fields that we created on the data entry form. Let’s take a look at the implementation:

Public Class ValidationBase
 
    Private errorCount As Integer
 
    Public ReadOnly Property HasError As Boolean
        Get
            If errorCount = 0 Then Return False Else Return True
        End Get
    End Property
 
    Public Sub AddErrorHandler(control As Object)
 
        Validation.AddErrorHandler(control, AddressOf Me.OnValidationError)
 
    End Sub
 
    Public Sub OnValidationError(sender As Object, e As ValidationErrorEventArgs)
        If e.Action = ValidationErrorEventAction.Added Then errorCount += 1
        If e.Action = ValidationErrorEventAction.Removed Then errorCount -= 1
    End Sub
 
    Public Sub ValidateTextBox(textBox As TextBox, ruleIndex As Integer)
        If Not BindingOperations.GetBinding(textBox, textBox.TextProperty) _
            .ValidationRules.Item(ruleIndex) _
                        .Validate(textBox.Text, _
            System.Threading.Thread.CurrentThread.CurrentCulture).IsValid Then
 
            Validation.MarkInvalid(textBox.GetBindingExpression(textBox.TextProperty), _
                New ValidationError(BindingOperations.GetBinding(textBox, _
                 textBox.TextProperty).ValidationRules.Item(0), textBox))
        Else
            Validation.ClearInvalid(textBox.GetBindingExpression(textBox.TextProperty))
        End If
 
    End Sub
 
    Public Sub ValidateComboBox(comboBox As ComboBox, ruleIndex As Integer)
        If Not BindingOperations.GetBinding(comboBox, comboBox.SelectedValueProperty) _
        .ValidationRules.Item(ruleIndex) _
                .Validate(comboBox.SelectedValue, _
        System.Threading.Thread.CurrentThread.CurrentCulture).IsValid Then
            Validation.MarkInvalid(comboBox.GetBindingExpression(comboBox.SelectedValueProperty), _
                New ValidationError(BindingOperations.GetBinding(comboBox, _
        comboBox.SelectedValueProperty).ValidationRules.Item(0), comboBox))
        Else
            Validation.ClearInvalid(comboBox.GetBindingExpression(comboBox.SelectedValueProperty))
        End If
 
    End Sub
 
    Public Sub ValidateDatePicker(datePicker As DatePicker, ruleIndex As Integer)
        If Not BindingOperations.GetBinding(datePicker, datePicker.SelectedDateProperty) _
        .ValidationRules.Item(ruleIndex) _
                .Validate(datePicker.SelectedDate, _
        System.Threading.Thread.CurrentThread.CurrentCulture).IsValid Then
            Validation.MarkInvalid(datePicker.GetBindingExpression(datePicker.SelectedDateProperty), _
        New ValidationError(BindingOperations.GetBinding(datePicker, _
        datePicker.SelectedDateProperty).ValidationRules.Item(0), datePicker))
        Else
            Validation.ClearInvalid(datePicker.GetBindingExpression(datePicker.SelectedDateProperty))
 
        End If
    End Sub 
 
End Class

Name

Type

Description

errorCount

Integer

Counter for the errors generated. If for example two controls have errors, errorCount will be 2

HasError

Property

Used by the class that instantiates a ValidationBase class instance to test for errors

AddErrorHandler

Method

Adds a handler for a control that will fire when an error occurs. This is defined in the XAML through NotifyOnValidationError

OnValidationError

Method

Processes the validation error for a control

ValidateTextBox

Method

Validates TextBox data

ValidateComboBox

Method

Validates ComboBox data

ValidateDatePicker

Method

Validates DatePicker data


You can add or remove any other controls that are not currently implemented into the ValidationBase class. Now that we have a description of the ValidationBase class, let’s look closer at the implementation of the ValidateTextBox method.

Firstly, we make a call to BindingOperations.GetBinding which contains a list of all of our ValidationRules as a collection of Item. Using the ruleIndex we can call Validate passing in the value we want validated and a reference to the CurrentCulture. We test IsValid for true to determine if there is a validation error.


BindingOperations
.GetBinding(target, DependencyProperty).ValidationRules.Item(Integer).Validate(Object, CultureInfo).

Implementing the code behind – CustomerView.xaml.vb


We have done all the ground work so it’s time to wire up the controls with the ValidationBase class. First of all, we create an instance of the ValidationBase class as a private member of the CustomerView class
.

 

Private validation As New ValidationBase

Next, we add the validation handlers for our TextBox controls in the constructor of the CustomerView window.


Public Sub New()
    InitializeComponent()
    validation.AddErrorHandler(textboxFirstname)
    validation.AddErrorHandler(textboxLastname)
    validation.AddErrorHandler(textboxAddress)
    validation.AddErrorHandler(textboxCity)
    validation.AddErrorHandler(textboxPhone)
End Sub

Then, we add the Field_LostFocus event which is the handler for TextBox_LostFocus event that fires on our TextBox’s. We set this up in the XAML code earlier.
Private Sub Field_LostFocus(sender As Object, e As RoutedEventArgs)
    validation.ValidateTextBox(sender, 0)
End Sub

Lastly, we call the handler again when the Save button is clicked. We do this so controls that have not been updated but need to be validated are validated before the data source is updated. Remember, this deals with the problem that if fields need to be validated but the fields have not been modified. We also check the ValidationBase class for HasError before calling UpdateSource on the BindingExpression.


Private Sub buttonSave_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles buttonSave.Click
    validation.ValidateTextBox(textboxFirstname, 0)
    validation.ValidateTextBox(textboxLastname, 0)
    validation.ValidateTextBox(textboxAddress, 0)
    validation.ValidateTextBox(textboxCity, 0)
    validation.ValidateTextBox(textboxPhone, 0)
    If validation.HasError Then Exit Sub
    textboxFirstname.GetBindingExpression(TextBox.TextProperty).UpdateSource()
    textboxLastname.GetBindingExpression(TextBox.TextProperty).UpdateSource()
    textboxAddress.GetBindingExpression(TextBox.TextProperty).UpdateSource()
    textboxCity.GetBindingExpression(TextBox.TextProperty).UpdateSource()
    textboxPhone.GetBindingExpression(TextBox.TextProperty).UpdateSource()
End Sub

There you have it. When you now enter data into the first two fields and skip over the last three fields by pressing the Save button, the last three fields will be validated.



Figure 2:
Customer Data Entry Window after validation

Conclusion


Today we have simply learnt how not to be pushed around by the WPF Validation system but rather how to take control and make validation a pleasure.

Other resources