Silverlight – About validation when binding to custom forms
I have notice that some people have problems about how to implement User Input validation when they don’t use controls like the DataForm or DataGrid etc, and instead bind directly to a TextBox or other user input controls. In this post I will write about different ways to handle validations. In this post I will work with a simple View. I will later add a post where I will use some new Validation features in Siverlight 4 and where I also use the MVVM pattern and Commanding.
The following is the XAML of the View I will use in this post:
<UserControl ... > <Grid x:Name="LayoutRoot"> <StackPanel Width="400" Margin="50"> <TextBox
x:Name="myTextBox"
Text="{Binding Name, Mode=TwoWay, ValidatesOnExceptions=true}"/>
<Button Margin="0,10,0,0" Click="Button_Click" Content="Save"/> </StackPanel> </Grid> </UserControl>
Here is the View in runtime when a validation fails:
Hmm, is the template of the Validation tooltip changed? ;)
I have created a simple class called Customer which I bind
to the LayoutRoot’s DataContext property in the code-behind.
Here is the Customer class:
public class Customer { private string _name = null; public string Name { get { return _name; } set { if (string.IsNullOrEmpty(value)) throw new ValidationException("The Name field is required"); _name = value; } } }
Here is the code-behind where I set the LayoutRoot’s
DataContext to a new empty Customer class:
public partial class MainPage : UserControl { public MainPage() { InitializeComponent(); LayoutRoot.DataContext = new Customer(); ; } }
When the Customer’s Name property is set a validation
will take place to check if the value is null or empty. If
the value is empty a ValidationException will be thrown (You
can use any kind of exception class here, but I use the
ValidationException in this example). When a TwoWay binding
is used on a TextBox, the set property of the bounded
property will only be set when the TextBox lost focus (this
is by default and can be changed); BUT! Only as long as the
TextBox value is changed from its previous value. If the
TextBox value is an empty string when the application is
started, the validation will not take place if we left the
TextBox blank, even if the TextBox will lost is focus the
set property will not be set. To make sure so it will be
set, the TextBox value must be changed. I have seen several
questions on forums where developers have problem with a
required value validation because of this. So how to handle
this? One way is to change the Binding’s UpdateSourceTrigger
to Explicit and then manually trigger the update of the
source with the BindingExpression, for example when the
TextBox will lost is focus or when the user click’s on a
Button. Here is an example using the LostFocus event:
private void TextBox_LostFocus(object sender, RoutedEventArgs e) { BindingExpression expression = myTextBox.GetBindingExpression(TextBox.TextProperty); expression.UpdateSource(); }
By doing the UpdateSource manually you can make sure
the set property bound to the TextBox will always be set
even if the value isn’t changed from its previous value.
To make sure the nice built in validation tooltip will
appear when a bounded property will throw an exception, the
Binding expression’s ValidatesOnExceptions must be set to
true, if not, the tooltip will not be displayed and the user
will not know if the validation failed.
<TextBox x:Name="myTextBox"Text="{Binding Name, Mode=TwoWay, ..., ValidatesOnExceptions=true}">
Instead of throwing exceptions and add validation code
to the set property, annotation can also be used.
Using Validation Annotation
Here is an example of the Customer class using validation
annotation instead of an if statement:
public class Customer { private string _name = null; [Required] public string Name { get { return _name; } set { //if (string.IsNullOrEmpty(value)) // throw new ValidationException("The Name field is required"); Validator.ValidateProperty(value, new ValidationContext(this, null, null) { MemberName = "Name" }); _name = value; } } }
The code above only uses the Require validation attribute but there are more attributes, like the RegularExpression-, Range- and StringLengthAttribute etc. You can find them in the System.ComponentModel.DataAnnotations namespace. If you want to make sure the validation annotation should be used the Validator’s ValidateProperty method must called within the set method of the property with the annotation. The ValidateProperty method will throw a ValidationException if the validation is invalid.
If you don’t want to use the explicit handling of updating a
source with the BindingExpression’s UpdateSource() method,
you can validate the whole Customer object when you for
example press a button. This will also reduce codes both
within the code-behind and XAML. If you have several
TextBoxes in the View, it will be really boring to hook up
to the LostFocus event and manually update the source with
the BindingExpression’s UpdateSource method. By validating a
whole object you can use the Validator.TryValidateObject
method. Here is an example where the whole Customer object
is validated when a Button is clicked:
private void Button_Click(object sender, RoutedEventArgs e) { var validationContext = new ValidationContext(LayoutRoot.DataContext, null, null); List<ValidationResult> validationResults = new List<ValidationResult>(); if (!Validator.TryValidateObject(LayoutRoot.DataContext, validationContext, validationResults)) { //Validation failed } else { //Valdation passed } }
The validationResults will contain a list of the
validation results when the validation failed. The problem
now is to make sure the TextBox’s validation tooltip will be
visible if the validation fails and also show the validation
error message. After some tries I managed to make it work,
and also as a result I managed to change the ControlTemplate
for the validation tooltip. The following is the code to
show the validation tooltip for the TextBox (myTextBox) added in this post example and also set the validation
message for the tooltip:
private void Button_Click(object sender, RoutedEventArgs e) { var validationContext = new ValidationContext(LayoutRoot.DataContext, null, null); List<ValidationResult> validationResults = new List<ValidationResult>(); if (!Validator.TryValidateObject(LayoutRoot.DataContext, validationContext, validationResults)) { var border = ((Border)VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(myTextBox, 0), 3)); var tooltip = border.GetValue(ToolTipService.ToolTipProperty) as ToolTip; tooltip.DataContext = validationResults[0].ErrorMessage; tooltip.Template = this.Resources["ValidationToolTipTemplate"] as ControlTemplate; if (!myTextBox.Focus()) VisualStateManager.GoToState(myTextBox, "InvalidUnfocused", true); else VisualStateManager.GoToState(myTextBox, "InvalidFocused", true); } else VisualStateManager.GoToState(myTextBox, "Valid", true); }
Note: I haven’t done any refactoring here and the code
is not generic, the myTextBox is hardcoded in the code
above. The code is only to show the concept.
By using the VisualTreeHelper I managed to get the ToolTip showing the validation error message. I changed the ToolTip’s template to my own and also changed the DataContext of the tooltip to the validation error message. By default the TextBlock used to show the error message within the ToolTip Template is bound to the Validation.Errors attached property, but the constructor of the ValidationError class used by the Errors collection is internal; so I couldn’t use it and add ValidationErrors to the attached property, so I just use the ToolTip’s DataContext instead. The default TextBox template has some visual states to make a border and the the tooltip visible or not, InvalidUnfocused, InvalidFocused and Valid, I decided to reuse them.
The ValidationToolTipTemplate I use for the tooltip is here:
<UserControl.Resources> <ControlTemplate x:Key="ValidationToolTipTemplate"> <Grid x:Name="Root" Margin="5,0" RenderTransformOrigin="0,0" Opacity="0"> <Grid.RenderTransform> <TranslateTransform x:Name="xform" X="-25"/> </Grid.RenderTransform> <VisualStateManager.VisualStateGroups> <VisualStateGroup Name="OpenStates"> <VisualStateGroup.Transitions> <VisualTransition GeneratedDuration="0"/> <VisualTransition To="Open" GeneratedDuration="0:0:0.2"> <Storyboard> <DoubleAnimation Storyboard.TargetName="xform" Storyboard.TargetProperty="X" To="0" Duration="0:0:0.2"> <DoubleAnimation.EasingFunction> <BackEase Amplitude=".3" EasingMode="EaseOut"/> </DoubleAnimation.EasingFunction> </DoubleAnimation> <DoubleAnimation Storyboard.TargetName="Root" Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.2"/> </Storyboard> </VisualTransition> </VisualStateGroup.Transitions> <VisualState x:Name="Closed"> <Storyboard> <DoubleAnimation Storyboard.TargetName="Root" Storyboard.TargetProperty="Opacity" To="0" Duration="0"/> </Storyboard> </VisualState> <VisualState x:Name="Open"> <Storyboard> <DoubleAnimation Storyboard.TargetName="xform" Storyboard.TargetProperty="X" To="0" Duration="0"/> <DoubleAnimation Storyboard.TargetName="Root" Storyboard.TargetProperty="Opacity" To="1" Duration="0"/> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Border Background="#FF000045" CornerRadius="2"/> <Border CornerRadius="2"> <StackPanel Orientation="Horizontal" Margin="8,4,8,4"> <Image Source="validation.png" Height="20" VerticalAlignment="Center"/> <TextBlock UseLayoutRounding="false" Foreground="White" VerticalAlignment="Center" FontWeight="Bold" MaxWidth="250" TextWrapping="Wrap" Text="{Binding}"/> </StackPanel> </Border> <Grid.Effect> <DropShadowEffect ShadowDepth="1"/> </Grid.Effect> </Grid> </ControlTemplate> </UserControl.Resources>
I think I made some people happy now when they can see
how they can change the ToolTip template for a TextBox
without adding the Template for the TextBox itself ;)
Summary
In this post you have seen different validation solutions like throwing exception, using annotation, solve the Required field validation and how to change the Validation Tooltip template.
If you want to know when I publish new blog posts you can follow me on twitter: http://www.twitter.com/fredrikn