WPF model validation
This post is rooted in this: http://stackoverflow.com/questions/14023552/how-to-use-idataerrorinfo-error-in-a-wpf-program
As many people know, WPF uses IDataErrorInfo interface for validation. This is IDataErrorInfo's definition:
// Summary: // Provides the functionality to offer custom error information that a user // interface can bind to. public interface IDataErrorInfo { // Summary: // Gets an error message indicating what is wrong with this object. // // Returns: // An error message indicating what is wrong with this object. The default is // an empty string (""). string Error { get; } // Summary: // Gets the error message for the property with the given name. // // Parameters: // columnName: // The name of the property whose error message to get. // // Returns: // The error message for the property. The default is an empty string (""). string this[string columnName] { get; } }
It's quite simple and self-explanatory. For examle:
class Person : IDataErrorInfo { public string PersonName { get; set; } public int Age { get; set; } string IDataErrorInfo.this[string propertyName] { get { if(propertyName=="PersonName") { if(PersonName.Length>30 || PersonName.Length<1) { return "Name is required and less than 30 characters."; } } else if (propertyName == "Age") { if (Age<10 || Age>50) { return "Age must between 10 and 50."; } } return null; } } string IDataErrorInfo.Error { get { if(PersonName=="Tom" && Age!=30) { return "Tom must be 30."; } return null; } } }
In order to make this model observable, I also implement the INotifyPropertyChanged interface. (For more information about INotifyPropertyChanged: http://www.codeproject.com/Articles/165368/WPF-MVVM-Quick-Start-Tutorial )
class Person : IDataErrorInfo, INotifyPropertyChanged { private string _PersonName; private int _Age; public string PersonName { get { return _PersonName; } set { _PersonName = value; NotifyPropertyChanged("PersonName"); } } public int Age { get { return _Age; } set { _Age = value; NotifyPropertyChanged("Age"); } } string IDataErrorInfo.this[string propertyName] { get { if(propertyName=="PersonName") { if(PersonName.Length>30 || PersonName.Length<1) { return "Name is required and less than 30 characters."; } } else if (propertyName == "Age") { if (Age<10 || Age>50) { return "Age must between 10 and 50."; } } return null; } } string IDataErrorInfo.Error { get { if(PersonName=="Tom" && Age!=30) { return "Tom must be 30."; } return null; } } public event PropertyChangedEventHandler PropertyChanged; protected void NotifyPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } }
It's not a grace implementation but enough to illustrate this case. I will show a better solution later in this article. Now we create a user interface like that:
<Window x:Class="WpfModelValidation.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="165" Width="395" TextOptions.TextFormattingMode="Display"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="176*" /> <ColumnDefinition Width="327*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="40" /> <RowDefinition Height="40" /> <RowDefinition Height="40" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Label Content="Person Name" Height="28" HorizontalAlignment="Right" /> <Label Content="Age" Grid.Row="1" Height="28" HorizontalAlignment="Right" /> <TextBox Grid.Column="1" Height="23" HorizontalAlignment="Left" Margin="5,0,0,0" Name="textboxPersonName" VerticalAlignment="Center" Width="120" Text="{Binding PersonName, ValidatesOnDataErrors=True}" /> <TextBox Grid.Column="1" Grid.Row="1" Height="23" HorizontalAlignment="Left" Margin="5,0,0,0" Name="textBox1" VerticalAlignment="Center" Width="50" Text="{Binding Age, ValidatesOnDataErrors=True}" /> <StackPanel Grid.Column="0" Grid.Row="2" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right"> <TextBlock Name="textblockError" Text="{Binding Error, ValidatesOnDataErrors=True}" /> <Button Content="Test" Grid.Column="1" Grid.Row="2" Height="23" HorizontalAlignment="Right" VerticalAlignment="Center" Name="buttonTest" Width="75" /> </StackPanel> </Grid> </Window>
And the CSharp code:
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); Person person = new Person { PersonName = "Tom", Age = 31 //A model error should be occurred because Tom must be 30 }; DataContext = person; } }
Run the program.
No error displays? (You can download the code here) That is not we are expecting. After searching a lot of material, I finally realize that the Error property of IDataErrorInfo is useless in WPF! That's a little weird, how should I do model level validation? At last, I found a workaround, I made a special property named ModelError and a method named IsValid. The model is changed to:
class Person : IDataErrorInfo, INotifyPropertyChanged { private string _PersonName; private int _Age; public string PersonName { get { return _PersonName; } set { _PersonName = value; NotifyPropertyChanged("PersonName"); } } public int Age { get { return _Age; } set { _Age = value; NotifyPropertyChanged("Age"); } } public string ModelError { get { return ModelValidation(); } } private string ModelValidation() { if (PersonName == "Tom" && Age != 30) { return "Tom must be 30."; } return null; } //Call this to trigger the model validation. public void Validate() { NotifyPropertyChanged(""); } string IDataErrorInfo.this[string propertyName] { get { if (propertyName=="ModelError") { string strValidationMessage = ModelValidation(); if (!string.IsNullOrEmpty(strValidationMessage)) { return strValidationMessage; } } if(propertyName=="PersonName") { if(PersonName.Length>30 || PersonName.Length<1) { return "Name is required and less than 30 characters."; } } else if (propertyName == "Age") { if (Age<10 || Age>50) { return "Age must between 10 and 50."; } } return null; } } string IDataErrorInfo.Error { get { throw new NotImplementedException(); } } public event PropertyChangedEventHandler PropertyChanged; protected void NotifyPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } }
And bind the model validation error like this:
<TextBlock Name="textblockError" Text="{Binding ModelError, ValidatesOnDataErrors=True}" />
This time, it works.
Download the code here.
Obviously, the code above is not good. How about making a base class? That sounds like a ideal. I made the model like that:
class Person : ViewModelBase { [Required(ErrorMessage = "Cannot be null.")] [StringLength(30, ErrorMessage = "Name is required and less than 30 characters.")] public string PersonName { get { return GetValue(() => PersonName); } set { SetValue(() => PersonName, value); } } [Range(10, 50, ErrorMessage = "Age must between 10 and 50.")] public int Age { get { return GetValue(() => Age); } set { SetValue(() => Age, value); } } public override string ModelValidate() { if (PersonName == "Tom" && Age!=30) { return "Tom must be 30"; } return null; } }
Then, add some style to make the error message more informative.
<Style TargetType="{x:Type TextBox}"> <Setter Property="Height" Value="23" /> <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>
It works again.
Quite familiar with ASP.net MVCer? Download the code here.