1. Problem
You need to capture data validation errors in your application code and provide visual indications of such errors if needed.
2. Solution
Attach handlers to the BindingValidationError event of the control in question, and ensure that the binding is set to raise the event on validation exceptions.
3. How It Works
As you create UIs that are data bound to various controls in a TwoWay
binding so that users of your application can edit the data, you often
have the need for those edits to pass validation checks. And in the
event one or more of those validations fail, you may want to capture the
errors and display them to your users in a meaningful way.
3.1. Validation Error Notification
There is built-in support for notification of
validation errors in the data binding subsystem within Silverlight. To
enable this support, the Binding.ValidatesOnExceptions property needs to be set to true
on the binding. This allows the framework to capture any exceptions
raised during the setting of a property on a data source or during a
type conversion and propagate them to your code as validation errors.
This prevents the otherwise normal flow of your application suffering a
crash from the exception being unhandled.
Most of the controls in the base class library that
may typically be used in two-way bindings provide a built-in user
interface to display the binding validation error to the user. The
built-in user interface usually provides a small error icon overlaid on
the control, hovering on which displays the error message in a tooltip
beside the control. The error message displayed is the Exception.Message
property value of the raised exception. Once the error is corrected,
the control logic automatically removes the error user interface.
Figure 1 shows a TextBox control displaying a validation error with the default error user interface.
3.2. Getting Error Information
In some cases, it may not be enough to simply display
the error message. You may want programmatic access to the error
information, for additional reasons like logging or some other custom
handling of the error beyond the display of the standard error user
interface.
To enable this, the FrameworkElement class (and, by inheritance, every control) can raise the BindingValidationError
event whenever an exception gets propagated as a validation error or an
existing validation error is removed. To instruct the binding subsystem
to raise this event, you need to set the Binding.NotifyOnValidationError property to true on the binding.
If you handle the BindingValidationError event, you can access detailed error information through the event argument of type ValidationErrorEventArgs. The ValidationErrorEventArgs.Action property, of type ValidationErrorEventAction, has two possible values—ValidationErrorEventAction.Added (indicating that a validation error has occurred) and ValidationErrorEventAction.Removed (indicating that an error was corrected). The ValidationErrorEventArgs.Exception property gives you access to the actual exception that caused the validation error.
3.3. Getting a Validation Error Summary
In many applications, it is common to show a summary
of all the errors that a user might have made in performing the
necessary data input. Typically, a summary display such as that will
point out the fields where the errors were made, the nature of the
error, and in some cases, will also include automatic navigation (click
the entry in the summary to navigate to the field). This feature is also
built into the Silverlight binding validation mechanism now and is
enabled through the System.Windows.Controls.ValidationSummary control and its related classes in the System.Windows.Controls.Data.Input assembly.
Once you place a ValidationSummary control
in your page, the binding subsystem automatically knows to populate it
with the error entries and then binding errors occur. There is no
additional wiring up that you have to perform. The following snippet
shows a sample declaration:
<input:ValidationSummary />
Also note that validation errors are bubbled up the visual tree by the binding subsystem. This means that the ValidationSummary
control can be placed anywhere in your page, as long as it is higher in
the visual tree than the control(s) whose validation errors it is
supposed to display.
Note that the default error and validation summary user interfaces can be customized using control templating.
Let's take a look at how all of this might work.
4. The Code
You'll modify Recipe in this article to add input validation. You remove the custom collection class in favor of using a simple ObservableCollection.
To add the validation on the data source, you adjust some of the
property setters to throw exceptions if certain validation rules are not
met. You also change the types of Employee.PhoneNum and Address.ZipCode
properties to string to simply the validation logic. The application
data classes with some of these modified property setters are shown in Listing 1.
Listing 1. Application Data Classes
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
namespace Recipe4_6
{
public class Employee : INotifyPropertyChanged {
//InotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
public Employee()
{
}
private string _FirstName;
public string FirstName
{
get { return _FirstName; }
set
{
string OldVal = _FirstName;
if (OldVal != value)
{
_FirstName = value;
RaisePropertyChanged(new PropertyChangedEventArgs("FirstName"));
}
}
}
private string _LastName;
public string LastName
{
get { return _LastName; }
set
{
string OldVal = _LastName;
if (OldVal != value)
{
_LastName = value;
RaisePropertyChanged(new PropertyChangedEventArgs("LastName"));
}
}
}
private string _PhoneNum;
public string PhoneNum
{
get { return _PhoneNum; }
set
{
string OldVal = _PhoneNum;
if (value.Length != 10)
throw new Exception("Phone Number has to be exactly 10 digits");
try
{
Convert.ToInt64(value);
}
catch
{
throw new Exception("Phone Number has to be exactly 10 digits");
}
if (OldVal != value)
{
_PhoneNum = value;
RaisePropertyChanged(new PropertyChangedEventArgs("PhoneNum"));
}
}
}
private Address _Address;
public Address Address
{
get { return _Address; }
set
{
Address OldVal = _Address;
if (OldVal != value)
{
_Address = value;
RaisePropertyChanged(new PropertyChangedEventArgs("Address"));
}
}
}
private bool _InError = default(bool);
public bool InError
{
get
{
return _InError;
}
set
{
if (value != _InError)
{
_InError = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("InError"));
}
}
}
}
public class Address : InotifyPropertyChanged
{
private static List<string> StateList =
new List<string>(){ "AL","AK","AS","AZ","AR","CA","CO","CT","DE","DC","FM",
"FL","GA","GU","HI","ID","IL","IN","IA","KS","KY","LA","ME","MH","MD","MA",
"MI","MN","MS","MO","MT","NE","NV","NH","NJ","NM","NY","NC","ND","MP","OH",
« OK », »OR », »PW », »PA », »PR », »RI », »SC », »SD », »TN », »TX », »UT », »VT », »VI »,
»VA », «WA »,
"WV","WI","WY" };
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
private string _Street;
public string Street
{
get { return _Street; }
set
{
string OldVal = _Street;
if (OldVal != value)
{
_Street = value;
RaisePropertyChanged(new PropertyChangedEventArgs("Street"));
}
}
}
private string _City;
public string City
{
get { return _City; }
set
{
string OldVal = _City;
if (OldVal != value)
{
_City = value;
RaisePropertyChanged(new PropertyChangedEventArgs("City"));
}
}
}
private string _State;
public string State
{
get { return _State; }
set
{
string OldVal = _State;
//length needs to be 2 characters
if (StateList.Contains(value) == false)
throw new Exception(
"State needs to be the 2 letter abbreviation for valid US State"
);
if (OldVal != value)
{
_State = value;
RaisePropertyChanged(new PropertyChangedEventArgs("State"));
}
}
}
private string _ZipCode;
public string ZipCode
{
get { return _ZipCode; }
set
{
string OldVal = _ZipCode;
//length needs to be 5 characters
if (value.Length != 5)
throw new Exception("Zipcode needs to be exactly 5 digits");
try
{
Convert.ToInt32(value);
}
catch
{
throw new Exception("Zipcode needs to be exactly 5 digits");
}
if (OldVal != value)
{
_ZipCode = value;
RaisePropertyChanged(new PropertyChangedEventArgs("ZipCode"));
}
}
}
}
}
|
As Listing 1 shows, Employee.PhoneNum validates a phone number if it has exactly ten digits in its setter and raises an Exception otherwise. Similarly, Address.State and Address.ZipCode check for a two-letter state abbreviation and a five-digit ZIP code, respectively, and raise Exceptions if those criteria are not met. Also note the new InError property on the Employee class; we will address its use in a little bit.
Listing 2 shows the complete XAML for the page.
Listing 2. XAML for the Page
<UserControl x:Class="Recipe4_6.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Recipe4_6"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:input=
"clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.Input"
mc:Ignorable="d"
Width="400"
Height="450">
<UserControl.Resources>
<local:BoolToVisibilityConverter x:Key="REF_BoolToVisibilityConverter" />
<DataTemplate x:Key="dtEmployee">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding FirstName}"/>
<TextBlock Text="{Binding LastName}"
Grid.Column="1"
Grid.Row="0"
Margin="5,0,0,0" />
<TextBlock Text=" -> Error!!" Foreground="Red"
Visibility=
"{Binding InError, Converter={StaticResource REF_BoolToVisibilityConverter}}"
Grid.Column="2" />
</Grid>
</DataTemplate>
</UserControl.Resources>
<Grid x:Name="LayoutRoot"
Background="White"
Margin="10,10,10,10">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
x:Name="lbx_Employees"
ItemTemplate="{StaticResource dtEmployee}"
SelectionChanged="lbx_Employees_SelectionChanged" />
<Grid x:Name="grid_NewButton"
Margin="0,2,0,0"
Grid.Row="1"
HorizontalAlignment="Right">
<Button x:Name="btn_New"
Click="btn_New_Click"
Content="New Employee" />
</Grid>
<input:ValidationSummary Grid.Row="2" Margin="0,10,0,5"/>
<Border Grid.Row="3"
Visibility="Collapsed"
x:Name="border_EmployeeForm"
Margin="0,2,0,0"
BorderBrush="Black"
BorderThickness="1"
Padding="1,1,1,1">
<Grid x:Name="grid_EmployeeForm">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.142*" />
<ColumnDefinition Width="0.379*" />
<ColumnDefinition Width="0.1*" />
<ColumnDefinition Width="0.097*" />
<ColumnDefinition Width="0.082*" />
<ColumnDefinition Width="0.2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="0.10*" />
<RowDefinition Height="0.15*" />
<RowDefinition Height="0.15*" />
<RowDefinition Height="0.15*" />
<RowDefinition Height="0.45*" />
</Grid.RowDefinitions>
<TextBox HorizontalAlignment="Stretch"
Margin="1,1,1,1"
x:Name="tbxFName"
VerticalAlignment="Stretch"
Text="{Binding FirstName, Mode=TwoWay}"
Grid.Row="1"
Width="Auto"
Grid.RowSpan="1"
Grid.ColumnSpan="2"
Grid.Column="1" />
<TextBox HorizontalAlignment="Stretch"
Margin="1,1,1,1"
x:Name="tbxLName"
VerticalAlignment="Stretch"
Text="{Binding LastName, Mode=TwoWay}"
Grid.Row="1"
Grid.Column="3"
Width="Auto"
Grid.RowSpan="1"
Grid.ColumnSpan="3" />
<TextBlock HorizontalAlignment="Stretch"
Margin="1,1,1,1"
VerticalAlignment="Stretch"
Text="Last"
TextWrapping="Wrap"
Grid.RowSpan="1"
Grid.Column="4"
Grid.ColumnSpan="2"
Height="Auto"
Width="Auto" />
<TextBlock HorizontalAlignment="Center"
Margin="1,1,1,1"
VerticalAlignment="Center"
Text="First"
TextWrapping="Wrap"
Grid.RowSpan="1"
Grid.Column="1"
Width="Auto"
Height="Auto" />
<TextBlock HorizontalAlignment="Center"
Margin="1,1,1,1"
VerticalAlignment="Stretch"
Text="Name"
TextWrapping="Wrap"
Grid.RowSpan="1"
Grid.Row="1"
Height="Auto"
Width="Auto" />
<TextBlock HorizontalAlignment="Center"
Margin="1,1,1,1"
VerticalAlignment="Stretch"
Text="Street"
TextWrapping="Wrap"
Grid.Row="2"
Width="Auto" />
<TextBox HorizontalAlignment="Stretch"
x:Name="tbxStreet"
VerticalAlignment="Stretch"
Text="{Binding Address.Street, Mode=TwoWay}"
Grid.Row="2"
Margin="1,1,1,1"
Grid.Column="1"
Grid.ColumnSpan="5"
Width="Auto" />
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Text="City"
TextWrapping="Wrap"
Margin="1,1,1,1"
Grid.Row="3" />
<TextBlock Text="State"
Margin="1,1,1,1"
TextWrapping="Wrap"
Grid.Column="2"
Grid.Row="3"
HorizontalAlignment="Center" />
<TextBlock Text="Zip"
Margin="1,1,1,1"
TextWrapping="Wrap"
Grid.Column="4"
Grid.Row="3"
HorizontalAlignment="Center" />
<TextBox HorizontalAlignment="Stretch"
x:Name="tbxCity"
Margin="1,1,1,1"
VerticalAlignment="Stretch"
Text="{Binding Address.City, Mode=TwoWay}"
Grid.Row="3"
Grid.Column="1" />
<TextBox Background="Transparent"
Grid.Column="3"
Margin="1,1,1,1"
Grid.Row="3"
Text="{Binding Address.State, Mode=TwoWay,
ValidatesOnExceptions=True,NotifyOnValidationError=True}"
x:Name="tbxState">
</TextBox>
<TextBox Background="Transparent"
Grid.Column="5"
Grid.Row="3"
Margin="1,1,1,1"
Text="{Binding Address.ZipCode, Mode=TwoWay ,
ValidatesOnExceptions=True,NotifyOnValidationError=True}"
x:Name="tbxZipCode" />
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Text="Phone"
Margin="1,1,1,1"
TextWrapping="Wrap"
Grid.Row="4" />
<TextBox Grid.Column="1"
Grid.Row="4"
Margin="1,1,1,1"
Text="{Binding PhoneNum, Mode=TwoWay ,
ValidatesOnExceptions=True,NotifyOnValidationError=True}"
x:Name="tbxPhoneNum" />
<Button Grid.Column="5"
Margin="1,1,1,1"
Grid.Row="4"
Height="30.911"
VerticalAlignment="Top"
Content="Close"
x:Name="btnClose"
Click="btnClose_Click" />
</Grid>
</Border>
</Grid>
</UserControl>
|
Note the binding expression for the TextBox. Text for displaying and editing a state, a ZIP code, and a phone number sets both ValidatesOnExceptions and NotifyOnValidationError to true. Also note that the dtEmployee data template now includes an extra TextBlock with its Visibility property bound to the InError property of the bound Employee instance. This TextBlock then displays an error string in red beside the Employee name when Employee.InError is set to true for the currently selected Employee instance in the lbx_Employees and hides it when not. Since the InError property is Boolean in type, you use a value converter to convert it to type Visibility for the binding to work.
And last, note the ValidationSummary control in the second row of the top level Grid. As validation errors are made, the ValidationSummary control gets populated with entries describing the error, and clicking one of the items positions you in the control in error.
Listing 3 shows the complete codebehind for the page.
Listing 3. MainPage Codebehind
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Collections.ObjectModel;
using System.Collections.Generic;
namespace Recipe4_6
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
//initialize the employee collection with some sample data
ObservableCollection<Employee> empColl = new ObservableCollection<Employee>();
empColl.Add(new Employee
{
FirstName = "Joe",
LastName = "Duffin",
PhoneNum = "2125551212",
Address = new Address
{
Street = "2000 Mott Street",
City = "New York",
State = "NY",
ZipCode = "10006"
}
});
empColl.Add(new Employee
{
FirstName = "Alex",
LastName = "Bleeker",
PhoneNum = "7185551212",
Address = new Address
{
Street = "11000 Clover Street",
City = "New York",
State = "NY",
ZipCode = "10007"
}
});
empColl.Add(new Employee
{
FirstName = "Nelly",
LastName = "Myers",
PhoneNum = "7325551212",
Address = new Address
{
Street = "12000 Fay Road",
City = "New York",
State = "NY",
ZipCode = "10016"
}
});
lbx_Employees.ItemsSource = empColl;
this.BindingValidationError +=
new System.EventHandler<ValidationErrorEventArgs>((s, e) =>
{
if (lbx_Employees.SelectedItem == null) return;
//change the InError property of the currently selected Employee
if(e.Action == ValidationErrorEventAction.Added)
(lbx_Employees.SelectedItem as Employee).InError = true;
else
(lbx_Employees.SelectedItem as Employee).InError = false;
});
}
private void btn_New_Click(object sender, RoutedEventArgs e)
{
//get the bound collection
ObservableCollection<Employee> empColl =
(ObservableCollection<Employee>)lbx_Employees.ItemsSource;
//create and initialize a new Employee
Employee newEmp = new Employee();
newEmp.Address = new Address();
//add it to the collection
empColl.Add(newEmp);
//set the current selection to the newly added employee.
//This will cause selection change to fire, and set the
//datacontext for the form appropriately
lbx_Employees.SelectedItem = newEmp;
}
private void lbx_Employees_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
//set the datacontext of the form to the selected Employee
grid_EmployeeForm.DataContext = (Employee)lbx_Employees.SelectedItem;
//show the form
border_EmployeeForm.Visibility = Visibility.Visible;
grid_NewButton.Visibility = Visibility.Collapsed;
}
private void btnClose_Click(object sender, RoutedEventArgs e)
{
//hide the form
if (lbx_Employees.SelectedItem != null)
(lbx_Employees.SelectedItem as Employee).InError = false;
border_EmployeeForm.Visibility = Visibility.Collapsed;
grid_NewButton.Visibility = Visibility.Visible;
}
}
}
|
As you can see in the C# codebehind, you don't need
to do anything special for the binding subsystem to display validation
errors. If you refer to the BindingValidationError event handler on the page, you will see that you handle the event to update the InError property of the currently selected Employee in the lbx_Employees. If a validation error has occurred, you set it to true, and to false otherwise. If you refer to the XAML in Listing 1, this is the property change that notifies the data template dtEmployee to change the visibility of the error indicator TextBlock.
Also note that you are able to handle the BindingValidationError
event on the page, even though the validation error happens at controls
that are contained further down in the visual tree. As mentioned
before, BindingValidationError events are bubbled all the way
up to the highest level container in the XAML, so you are free to handle
them anywhere in the visual tree, including in and higher than the
control where it happened.
NOTE
If you are in debug mode in Visual Studio, the
debugger will break at the exceptions raised in the property setters for
the data classes. This is normal, and if you continue with processing,
you will see the application behave the way it should. The Silverlight
runtime absorbs the unhandled exceptions because of the error handling
property settings on the Binding and translates them to notifications.
However, the Visual Studio debugger has no notion of this, so it breaks
on the exception if you are in debug mode.
Figure 2 illustrates the UI used to display a binding validation error, errors displayed in the ValidationSummary, a summary item selected, and the tooltip UI displaying the actual error message in the focused control that's in error.
|