DATABASE

Silverlight : Data Binding - Validating Input for Bound Data

1/28/2013 6:31:09 PM

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.

Figure 1. TextBox control displaying validation error using default error UI

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.

Figure 2. Input validation error display with validation summary
Other  
  •  Western Digital Black 4TB - One Of The Quickest Physical Drives
  •  Samsung 840 SSD 250GB - Most Of The Good Things
  •  SQL Server 2005 : Working with SQL Server Management Objects in Visual Studio (part 3) - Creating Backup-and-Restore Applications, Performing Programmatic DBCC Commands with SMO
  •  SQL Server 2005 : Working with SQL Server Management Objects in Visual Studio (part 2) - Retrieving Server Settings
  •  SQL Server 2005 : Working with SQL Server Management Objects in Visual Studio (part 1) - Iterating Through Available Servers
  •  SQL Server 2005 : Advanced OLAP - Roles
  •  SQL Server 2005 : Advanced OLAP - Translations
  •  SQL Server 2005 : Advanced OLAP - Perspectives
  •  Oracle Database 11g : Installing Oracle - Install the Oracle Software
  •  Oracle Database 11g : Installing Oracle - Configure Kernel Parameters, Get Familiar with Linux
  •  
    Video
    Top 10
    SG50 Ferrari F12berlinetta : Prancing Horse for Lion City's 50th
    The latest Audi TT : New angles for TT
    Era of million-dollar luxury cars
    Game Review : Hearthstone - Blackrock Mountain
    Game Review : Battlefield Hardline
    Google Chromecast
    Keyboards for Apple iPad Air 2 (part 3) - Logitech Ultrathin Keyboard Cover for iPad Air 2
    Keyboards for Apple iPad Air 2 (part 2) - Zagg Slim Book for iPad Air 2
    Keyboards for Apple iPad Air 2 (part 1) - Belkin Qode Ultimate Pro Keyboard Case for iPad Air 2
    Michael Kors Designs Stylish Tech Products for Women
    REVIEW
    - First look: Apple Watch

    - 3 Tips for Maintaining Your Cell Phone Battery (part 1)

    - 3 Tips for Maintaining Your Cell Phone Battery (part 2)
    Popular Tags
    Video Tutorail Microsoft Access Microsoft Excel Microsoft OneNote Microsoft PowerPoint Microsoft Project Microsoft Visio Microsoft Word Active Directory Exchange Server Sharepoint Sql Server Windows Server 2008 Windows Server 2012 Windows 7 Windows 8 Adobe Flash Professional Dreamweaver Adobe Illustrator Adobe Photoshop CorelDRAW X5 CorelDraw 10 windows Phone 7 windows Phone 8 Iphone