How do we handle validation in WPF ?
Before we begin…
Let’s start by looking at the view model we’re going to work on and write our validation code for – this is a very simple model with a single property “Number” which could ofcourse represent anything you like.
public class NumberViewModel : INotifyPropertyChanged { // standard implementation of INotifyPropertyChanged removed private int number; public int Number { get { return number; } set { if (number != value) { number = value; OnPropertyChanged("Number"); } } } }
You can assume that OnPropertyChanged fires the INotifyPropertyChanged.PropertyChanged event, but I’ve removed the implementation for brevity.
We’ll be equally simple with our UI, which has the following XAML
<TextBox Text="{Binding Number}" />
Good old IDataErrorInfo
So if you’ve used IDataErrorInfo in WinForms, this will be very familiar to you. We can simple implement the IDataErrorInfo interface in our NumberViewModel and add something like the following
string IDataErrorInfo.Error { get { return null; } } string IDataErrorInfo.this[string columnName] { get { if (columnName == "Number") { if (number < 0) return "Number must be greater or equal to 0"; } return null; } }
To get the WPF binding to actually interact with the IDataErrorInfo we need to change the XAML to look like this
<TextBox Text="{Binding Number, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
In the above our IDataErrorInfo.this indexer will be called each time the property changes, at which time we can handle the validation either within the view model or ofcourse via some other validation rule class.
ValidationRule validation
An alternative to the IDataErrorInfo route for validation are ValidationRules. A ValidateRule implementation of the previous validator is listed below
public class PostiveValidationRule : ValidationRule { public override ValidationResult Validate(object value, CultureInfo cultureInfo) { int result; if (value != null && Int32.TryParse(value.ToString(), out result)) { if (result < 0) return new ValidationResult( false, "Number must be greater or equal to 0"); } return ValidationResult.ValidResult; } }
To use the above rule in XAML we write the following
<TextBox> <TextBox.Text> <Binding Path="Number" ValidatesOnDataErrors="True" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <validators:PostiveValidationRule /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox>
The ValidationRules (as the pluralism suggests) can have multiple rules listed.
INotifyDataErrorInfo
.NET 4.5 included the INotifyDataErrorInfo. This allows us to validate in a more asynchronous way in that we can raise the ErrorsChanged event when we have errors and report them so that the binding engine can then call the GetErrors method to get the list of errors.
BindingGroup
The previously highlighted validation methods tend to be aimed more at a specific view model, but what if our view is made up of multiple view models and we want to validate across them all. Then we can look to use the BindingGroup.
To put it another way, the BindingGroup allows us to validate a group of bindings at the same time. Our sample only has a view model but if we had multiple view models it could validate all the items that make up a BindingGroup. Let’s look at how we could create a BindingGroup.
<StackPanel> <StackPanel.BindingGroup> <BindingGroup Name="ValidDataGroup"> <BindingGroup.ValidationRules> <validators:ValidDataValidationRule /> </BindingGroup.ValidationRules> </BindingGroup> </StackPanel.BindingGroup> <TextBox Text="{Binding Text, ValidatesOnDataErrors=True, BindingGroupName=ValidDataGroup}" />
We give the BindingGroup a name and then we can assign this name to the various bindings – in this example we don’t actually need to use the group name on the textbox as it will be used within the validation, but hopefully you can see how the syntax would look.
Now let’s take a look at the ValidDataValidationRule code
public class ValidDataValidationRule : ValidationRule { public override ValidationResult Validate(object value, CultureInfo cultureInfo) { var bindingGroup = (BindingGroup)value; if (bindingGroup != null) { var numberViewModel = bindingGroup.Items[0] as NumberViewModel; if (numberViewModel != null) { if (numberViewModel.Number < 0) { return new ValidationResult( false, "Number must be greater or equal to 0"); } } } return ValidationResult.ValidResult; } }
The Items collection will contain the various groups that have been assigned the BindingGroup name and then we can validate across all the data contexts.
Unfortunately, this doesn’t just happen magically. Instead, we need to invoke it. If we give the name DataElement to the StackPanel and for the sake of simplicity we add a button with a Click handler, we can then call the BindingGroup’s CommitEdit method to force validation, i.e.
DataElement.BindingGroup.CommitEdit();
Note: The default style is to draw a read line around the control which fails validation, if you’ve placed the StackPanel as the top level Window you might need to add a margin to see the red border.
ValidationStep
On both the BindingGroup and ValidationRule we can set the ValidationStep property. This allows us to tell the binding mechanism at what stage it should invoke the validation rule.
This can be set to one of four values
CommittedValue: Runs the ValidationRule after the value has been committed to the source.
ConvertedProposedValue: Runs the Validation rule after a value is converetd.
RawProposedValue: Runs the validation before any conversion occurs.
UpdatedValue: Runs the ValidationRule after the source is updated.
Note: The above definitions were taken from ValidationStep Enumeration
Data annotations
Finally, let’s take a look at data annotations or more specifically validation attributes that are part of the System.ComplonentMode.DataAnnotations namespace.
With the data annotations we can apply attributes to a class or its members which denote the validation rules to be used. For example
public class ValidationModel { [Required(ErrorMessage = "First name is a required field")] public string FirstName { get; set; } }
In this example we’ve removed the get/set actual implementation, for brevity.
Now this code requires use to write code to validation the property, for example in our setter we might have code like this
var validationContext = new ValidationContext(this, null, null); validationContext.MemberName = nameof(FirstName); Validator.ValidateProperty(value, validationContext);
Enhancing the user interface
So far, I’ve not done anything to make the user experience any better regarding validation, i.e. you will only see a red border around our text box, but this doesn’t really help the user identify what’s gone wrong, so now let’s look at how we might change our UI to better reflect the validation failures.
Ofcourse, we’ve been busy creating error messages, but so far not shown them in the UI, so we could use something like the following
<TextBox Text="{Binding Number, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"> <Validation.ErrorTemplate> <ControlTemplate> <StackPanel> <AdornedElementPlaceholder /> <TextBlock Text="{Binding [0].ErrorContent}" Foreground="Red"/> </StackPanel> </ControlTemplate> </Validation.ErrorTemplate> </TextBox>
This will display the first error message just beneath the TextBox.
Another alternative is to style the text box with a trigger against the Validation.HasError property
<Style TargetType="{x:Type TextBox}"> <Style.Triggers> <Trigger Property="Validation.HasError" Value="True"> <Setter Property="Background" Value="Red" /> <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/> </Trigger> </Style.Triggers> </Style>
References
Data validation in WPF
Using BindingGroups For Greater Control Over Input Validation
BindingGroups For Total View Validation
WPF 3.5 SP1 Feature: BindingGroups with Item-level Validation
Validation in Windows Presentation Foundation