Simple validation in WPF MvvM using IDataErrorInfo

By Mirek on (tags: IDataErrorInfo, mvvm, validation, WPF, categories: code)

In this post I will try to demonstrate the simplest validation approach for Windows Presentation Foundation application designed in Model View View-Model pattern.

We will be using data binding and IDataErrorInfo implementation.

Ok. Let’s assume we have simple data model ProductModel

   1: public class ProductModel : NotifyPropertyChangeBase
   2: {
   3:      private String name;
   4:      public String Name
   5:      {
   6:          get
   7:          {
   8:              return name;
   9:          }
  10:          set
  11:          {
  12:              name = value;
  13:              NotifyPropertyChanged(() => Name);
  14:          }
  15:      }
  16:  
  17:      private decimal price;
  18:      public decimal Price
  19:      {
  20:          get
  21:          {
  22:              return price;
  23:          }
  24:          set
  25:          {
  26:              price = value;
  27:              NotifyPropertyChanged(() => Price);
  28:          }
  29:      }
  30:  
  31:      private int amount;
  32:      public int Amount
  33:      {
  34:          get
  35:          {
  36:              return amount;
  37:          }
  38:          set
  39:          {
  40:              amount = value;
  41:              NotifyPropertyChanged(() => Amount);
  42:          }
  43:      }     
  44: }

NotifyPropertyChangeBase is just a base class which implements INotifyPropertyChanged according to this guide.
To display this model we use simple form

   1: <UserControl x:Class="WPFValidationApplication.Views.ProductForm"
   2:              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
   5:              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   6:              xmlns:my="clr-namespace:WPFValidationApplication.Utils"
   7:              d:DesignHeight="300"
   8:              d:DesignWidth="448"
   9:              mc:Ignorable="d">
  10:  
  11:     <Grid>
  12:         <Grid.RowDefinitions>
  13:             <RowDefinition Height="40" />
  14:             <RowDefinition Height="40" />
  15:             <RowDefinition Height="40" />
  16:             <RowDefinition Height="40" />
  17:             <RowDefinition Height="259*" />
  18:         </Grid.RowDefinitions>
  19:         <Grid.ColumnDefinitions>
  20:             <ColumnDefinition Width="140*" />
  21:             <ColumnDefinition Width="308*" />
  22:         </Grid.ColumnDefinitions>
  23:         <Label Grid.Row="0"
  24:                Height="28"
  25:                Margin="0,0,10,0"
  26:                HorizontalAlignment="Right"
  27:                VerticalAlignment="Center"
  28:                Content="Name:" />
  29:         <Label Grid.Row="1"
  30:                Height="28"
  31:                Margin="0,0,10,0"
  32:                HorizontalAlignment="Right"
  33:                VerticalAlignment="Center"
  34:                Content="Price:" />
  35:         <Label Grid.Row="2"
  36:                Height="28"
  37:                Margin="0,0,10,0"
  38:                HorizontalAlignment="Right"
  39:                VerticalAlignment="Center"
  40:                Content="Amount:" />
  41:  
  42:  
  43:         <TextBox Name="txtName"
  44:                  Grid.Row="0"
  45:                  Grid.Column="1"
  46:                  Width="200"
  47:                  HorizontalAlignment="Left"
  48:                  VerticalAlignment="Center"
  49:                  Text="{Binding Model.Name,
  50:                                 ValidatesOnDataErrors=True}" />
  51:  
  52:         <TextBox Name="txtPrice"
  53:                  Grid.Row="1"
  54:                  Grid.Column="1"
  55:                  Width="200"
  56:                  HorizontalAlignment="Left"
  57:                  VerticalAlignment="Center"
  58:                  Text="{Binding Model.Price,
  59:                                 ValidatesOnDataErrors=True}" />
  60:  
  61:         <TextBox Name="txtAmount"
  62:                  Grid.Row="2"
  63:                  Grid.Column="1"
  64:                  Width="200"
  65:                  HorizontalAlignment="Left"
  66:                  VerticalAlignment="Center"
  67:                  Text="{Binding Model.Amount,
  68:                                 ValidatesOnDataErrors=True}" />
  69:  
  70:         <Button Grid.Row="3"
  71:                 Grid.Column="1"
  72:                 Width="75"
  73:                 Height="23"
  74:                 Margin="0,0,20,0"
  75:                 HorizontalAlignment="Right"
  76:                 VerticalAlignment="Center"
  77:                 Command="{Binding SaveCommand}"
  78:                 Content="Save">
  79:         </Button>
  80:     </Grid>
  81: </UserControl>

which looks like this

Clipboard01

So we have Name, Price and Amount text boxes, which we want to bind to the ProductModel, and which we want to be validated according to some rules. To define those rules we must implement IDataErrorInfo interface on binded model.

   1: public class ProductModel : NotifyPropertyChangeBase , IDataErrorInfo
   2: {
   3:     [Properties...]
   4:  
   5:     public string Error
   6:     {
   7:         get { throw new NotImplementedException(); }
   8:     }
   9:  
  10:     public string this[string columnName]
  11:     {
  12:         get
  13:         {
  14:             string result = string.Empty;
  15:             switch (columnName)
  16:             {
  17:                 case "Name": if (string.IsNullOrEmpty(Name)) result = "Name is required!"; break;
  18:                 case "Price": if ((Price < 10) || (Price > 1000)) result = "Price must be between 10 and 1000"; break;
  19:                 case "Amount": if ((Amount < 1) || (Amount > 100)) result = "Amount must be between 1 and 100"; break;
  20:             };
  21:             return result;
  22:         }
  23:     }
  24:  
  25: }

The crucial here is the default collection type property which takes the column name as a parameter and return the error message when specific value is incorrect otherwise returns empty string when value is valid. This property is invoked by the framework when data validation is enabled on binded control (ValidatesOnDataErrors=True).

Ok, having that all in place we only need a appropriate ModelView which exposes our ProductModel as a property

   1: public class ProductFormViewModel : NotifyPropertyChangeBase
   2: {
   3:     public ProductModel Model { get; set; }
   4:  
   5:     public ICommand SaveCommand { get; set; }
   6:  
   7:     public ProductFormViewModel()
   8:     {
   9:         Model = new ProductModel();
  10:         SaveCommand = new DelegateCommand(SaveExecute);
  11:     }
  12:  
  13:     private void SaveExecute()
  14:     {
  15:        //dummy
  16:     }    
  17: }

Then we hook up the ModelView to the View DataContext

   1: public partial class ProductForm : UserControl
   2:    {
   3:        public ProductForm()
   4:        {
   5:            InitializeComponent();
   6:            this.DataContext = new ProductFormViewModel();
   7:        }
   8:    }

And we have our form with validation

Clipboard02

As you can see the error message is displayed as a TextBox tooltip. This can be easily achieved by simple styling and use of attached property Validation.HasError

 

   1: <Style TargetType="TextBox">
   2:             <Style.Triggers>
   3:                 <Trigger Property="Validation.HasError" Value="true">
   4:                     <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors).CurrentItem.ErrorContent}" />
   5:                 </Trigger>
   6:             </Style.Triggers>
   7:         </Style>

The style is applied for all TextBoxes in the scope and sets the error message to the Textbox’s ToolTip whenever Validation.HasError is true.