WPF Validation
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 **