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.

posted @ 2013-01-03 23:29  guogangj  阅读(6152)  评论(1编辑  收藏  举报