Stumbling Through: WPF (Databinding Part III)
Our project now has a listbox bound to the full name of a collection of people, and textboxes that allow the user to update the first or last name of the selected person and have that change reflected in the listbox. What I'd like to add now is a way to prevent the user from clearing out the first and last name, basically validating that the length of the first and last name is greater than zero and displaying an appropriate message if it is not. Additionally, I'd like the validation logic to reside within the person class itself; the form shouldn't have to worry about what is a valid value to put into a person's name. The key to this functionality lies in the 'IDataErrorInfo' interface, which resides in the System.ComponentModel namespace. When this interface is implemented in a bound object, it provides the functionality to communicate errors to the front-end. All we need to do to implement this interface is to provide the logic for an 'Error' property (returns the message for when anything in the object is invalid) and a default property with a 'name' parameter, which is the error message for the property with the given name. There are a number of different ways to implement these methods, they aren't new to WPF so I'm not going to cover them in detail here. All I am going to do is hardcode a little something for the error message by property name and leave the generic error as null. It will look a little something like this:
public string Error
{
get
{
return null;
}
}
public string this[string name]
{
get
{
string result = null;
if (name.Equals("FirstName"))
{
if (this._firstName.Trim().Length == 0)
result = "Please specify a first name";
}
else if (name.Equals("LastName"))
{
if (this._lastName.Trim().Length == 0)
result = "Please specify a last name";
}
return result;
}
}
Let's just take a shot in the dark here and run the project, maybe it'll just work! Er... no, it doesn't. In fact, if I put a breakpoint in either of these new IDataErrorInfo properties, we see that the code path never even reaches these guys. What gives? How do we tell our window to try and use the Data Error Info? I'm sad to find out that the answer to this question involves changing the way I bound the text boxes to the object. In order for a binding to actively 'listen' for data errors, the 'ValidatesOnDataErrors' property must be set to true. Since this property is a part of the binding, there is no way to set it from our TextBox tag as it is currently defined... we'll need to set up the binding tag as a child of our textbox tags, like this:
<TextBox Height="21" HorizontalAlignment="Right" Margin="0,73,10,0" Name="txtFirstName" VerticalAlignment="Top" Width="120" >
<Binding Path="FirstName" ValidatesOnDataErrors="True"/>
</TextBox>
<TextBox Height="21" HorizontalAlignment="Right" Margin="0,104,10,0" Name="txtLastName" VerticalAlignment="Top" Width="120" >
<Binding Path="FirstName" ValidatesOnDataErrors="True"/>
</TextBox>
I guess that's ok, it is fairly logical and maybe we can use a style in some way so that ValidatesOnDataErrors is always set to true, kind of like that 'IsSynchronizedWithCurrentItem' property of the listbox discussed in a previous post. If we run the application now, and enter in a blank for first or last name, we see the following:
Not Incredibly informative, but at least it knows that the field had an error. I'd like to at least display the error message in a tool tip so the user can see what is wrong (and we can verify our validation logic works). To change the tooltip when a validation error occurs, we can resort back to triggers, which can conveniently handle the 'HasError' event that gets raised by our object. The following XAML will apply a style to all textboxes that will display the error message in its tooltip whenever the HasError event is handled:
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
Now when we run the application and enter a blank first name, we get this:
The screenshot got cut-off a bit, but it does say 'Please specify a first name', which is the error message we provided. Here is the full XAML of our window as it exists now:
<Window x:Class="StumblingThroughWPFPartI.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:StumblingThroughWPFPartI="clr-namespace:StumblingThroughWPFPartI"
Title="Window1" Height="300" Width="300" Loaded="Window_Loaded">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/VisualFoundation;component/HotTrackListBox.xaml"/>
</ResourceDictionary.MergedDictionaries>
<StumblingThroughWPFPartI:PersonCollection x:Key="_personCollection"/>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</Window.Resources>
<Window.Background>
<ImageBrush ImageSource="Greenstone.bmp" />
</Window.Background>
<Grid DataContext="{Binding Source={StaticResource _personCollection}}">
<ListBox Margin="22,73,136,89" Name="listBox1" FontSize="16" Style="{StaticResource HotTrackListBoxStyle}" DisplayMemberPath="FullName" ItemsSource="{Binding}" />
<TextBox Height="21" HorizontalAlignment="Right" Margin="0,73,10,0" Name="txtFirstName" VerticalAlignment="Top" Width="120" >
<Binding Path="FirstName" ValidatesOnDataErrors="True"/>
</TextBox>
<TextBox Height="21" HorizontalAlignment="Right" Margin="0,104,10,0" Name="txtLastName" VerticalAlignment="Top" Width="120" >
<Binding Path="FirstName" ValidatesOnDataErrors="True"/>
</TextBox>
</Grid>
</Window>
And here is the person class:
using System;
using System.ComponentModel;
namespace StumblingThroughWPFPartI
{
public class Person : INotifyPropertyChanged, IDataErrorInfo
{
String _firstName;
String _lastName;
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(System.ComponentModel.PropertyChangedEventArgs e)
{
PropertyChanged(this, e);
}
public Person(String firstName, String lastName)
{
_firstName = firstName;
_lastName = lastName;
}
public String FirstName
{
get
{
return _firstName;
}
set
{
_firstName = value;
this.OnPropertyChanged(new PropertyChangedEventArgs("FullName"));
}
}
public String LastName
{
get
{
return _lastName;
}
set
{
_lastName = value;
this.OnPropertyChanged(new PropertyChangedEventArgs("FullName"));
}
}
public String FullName
{
get
{
return _firstName + " " + _lastName;
}
}
public string Error
{
get
{
return null;
}
}
public string this[string name]
{
get
{
string result = null;
if (name.Equals("FirstName"))
{
if (this._firstName.Trim().Length == 0)
result = "Please specify a first name";
}
else if (name.Equals("LastName"))
{
if (this._lastName.Trim().Length == 0)
result = "Please specify a last name";
}
return result;
}
}
}
}
Next up, hierarchical data binding. I am going to add an 'Address' class and collection, where each person can have zero to many addresses associated with them. I'll display their addresses in another listbox as the people get selected from the current listbox... at least, that's my goal.