Hello Devz,

This post will describe how to do a simple MVVM TextBox validation with IDataErrorInfo.

UI Data Validation is an important part of the FrontEnd creation. The FrontEnd should always be backed up by the BackEnd validation! But here we will focus only on the FE part.

Before showing the code, you have to know that I’m using PRISM Template Pack by Brian Lagunas which speeds up a lot the DataContext binding, INotifyPropertyChanged implementation (BindableBase), … If you don’t know it, have a look, it will save you a lot of time for your future developments.

So, here we just want a simple form to add a new customer and have a good UI validation, MVVM style.

IDataErrorInfo is according to me the way to manage this.

Here is the XAML part:

<Window x:Class="WpfValidation.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        Title="{Binding Title}" Height="200" Width="300">

    <Window.Resources>
        
        <Style TargetType="{x:Type Label}">
            <Setter Property="Margin" Value="5,0,5,0" />
            <Setter Property="HorizontalAlignment" Value="Right" />
        </Style>

        <Style TargetType="{x:Type TextBox}">
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="Margin" Value="0,2,40,2" />
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <DockPanel LastChildFill="true">
                            <Border Background="Red" DockPanel.Dock="right" Margin="5,0,0,0" Width="20" Height="20" CornerRadius="10"
                                    ToolTip="{Binding ElementName=customAdorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
                                <TextBlock Text="!" VerticalAlignment="center" HorizontalAlignment="center" FontWeight="Bold" Foreground="white">
                                </TextBlock>
                            </Border>
                            <AdornedElementPlaceholder Name="customAdorner" VerticalAlignment="Center" >
                                <Border BorderBrush="red" BorderThickness="1" />
                            </AdornedElementPlaceholder>
                        </DockPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        
    </Window.Resources>

    <Grid Margin="0,25,0,0">

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
        </Grid.RowDefinitions>
        
        <Label Content="First Name:" Grid.Column="0" Grid.Row="0" />
        <TextBox Grid.Row="0" Grid.Column="1"
                 Text="{Binding NewCustomer.FirstName, UpdateSourceTrigger=PropertyChanged,
                        ValidatesOnDataErrors=true, NotifyOnValidationError=true}" />

        <Label Content="Last Name:" Grid.Column="0" Grid.Row="1" />
        <TextBox Grid.Row="1" Grid.Column="1"
                 Text="{Binding NewCustomer.LastName, UpdateSourceTrigger=PropertyChanged, 
                        ValidatesOnDataErrors=true, NotifyOnValidationError=true}" />

        <Label Content="Age:" Grid.Column="0" Grid.Row="2" />
        <TextBox Grid.Row="2" Grid.Column="1" Width="50" HorizontalAlignment="left" MaxLength="3"
                 Text="{Binding NewCustomer.Age, UpdateSourceTrigger=PropertyChanged,
                        ValidatesOnDataErrors=true, NotifyOnValidationError=true}" />

        <Button Grid.ColumnSpan="2" Grid.Row="3" Grid.Column="1" Margin="0,0,10,0"
                Content="Add" ToolTip="Add Customer"
                HorizontalAlignment="right" VerticalAlignment="Center" 
                Command="{Binding AddCommand}"  />
    </Grid>

</Window>

Please observe the AdornedElement.(Validation.Errors)[0].ErrorContent in the Style resource and the ValidatesOnDataErrors=true in the binding attributes of all the TextBoxes.

No code behind at all!  🙂

Here is the ViewModel:

using Prism.Commands;
using Prism.Mvvm;
using System.Windows;

namespace WpfValidation.ViewModels
{
    public class MainWindowViewModel : BindableBase
    {
        private string _title = "Add Customer";
        public string Title
        {
            get { return _title; }
            set { SetProperty(ref _title, value); }
        }

        private Customer _newCustomer;
        public Customer NewCustomer
        {
            get { return _newCustomer ?? (_newCustomer = new Customer()); }
            set { SetProperty(ref _newCustomer, value); }
        }

        private DelegateCommand _addCommand;
        public DelegateCommand AddCommand => _addCommand ?? (_addCommand = new DelegateCommand(AddCustomer, CanAddCustomer));

        public MainWindowViewModel()
        {
            this.NewCustomer.PropertyChanged += NewCustomer_PropertyChanged;
        }

        private bool CanAddCustomer()
        {
            var isValid = this.NewCustomer.IsValid();
            return isValid;
        }

        private void NewCustomer_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            //If a property of our new Customer changed, we want to check the CanExecute of the Add button
            this.AddCommand.RaiseCanExecuteChanged();
        }

        private void AddCustomer()
        {
            var msg = $"FirstName: {this.NewCustomer.FirstName} - LastName: {this.NewCustomer.LastName} - Age: {this.NewCustomer.Age}";
            MessageBox.Show(msg, "New customer to add", MessageBoxButton.OK, MessageBoxImage.Information);

            //TODO: save the new customer to DB...

            this.NewCustomer.Reset();
        }
    }
}

And finally, our Customer class:

using Prism.Mvvm;
using System;
using System.ComponentModel;

namespace WpfValidation
{
    public class Customer : BindableBase, IDataErrorInfo
    {
        #region Properties

        private string _firstName;
        public string FirstName
        {
            get { return _firstName; }
            set { SetProperty(ref _firstName, value); }
        }

        private string _lastName;
        public string LastName
        {
            get { return _lastName; }
            set { SetProperty(ref _lastName, value); }
        }

        private int _age;
        public int Age
        {
            get { return _age; }
            set { SetProperty(ref _age, value); }
        }

        #endregion

        #region IDataErrorInfo Members

        public string Error
        {
            get { throw new NotImplementedException(); }
        }

        public string this[string columnName]
        {
            get
            {
                string result = null;

                switch (columnName)
                {
                    case "FirstName":
                        result = this.FirstNameValidation();
                        break;

                    case "LastName":
                        result = this.LastNameValidation();
                        break;

                    case "Age":
                        result = this.AgeValidation();
                        break;

                    default:
                        break;
                }

                return result;
            }
        }

        #endregion

        #region Methods

        public void Reset()
        {
            this.FirstName = default(string);
            this.LastName = default(string);
            this.Age = default(int);
        }

        public bool IsValid()
        {
            var firstNameValid = this.FirstNameValidation();
            var lastNameValid = this.LastNameValidation();
            var ageValid = this.AgeValidation();

            var result = firstNameValid == null && lastNameValid == null && ageValid == null;
            return result;
        }

        private string FirstNameValidation()
        {
            string result = null;

            if (string.IsNullOrEmpty(this.FirstName))
                result = "Please enter a First Name";
            else if (this.FirstName.Length > 5)
                result = "The First Name is too long";

            return result;
        }

        private string LastNameValidation()
        {
            string result = null;

            if (string.IsNullOrEmpty(this.LastName))
                result = "Please enter a Last Name";

            return result;
        }

        private string AgeValidation()
        {
            string result = null;

            if (Age <= 0 || Age >= 150)
                result = "Please enter a valid age";

            return result;
        }

        #endregion
    }
}

Please observe the IDataErrorInfo implementation with the public string this[string columnName], which is simply an indexer on the properties names of our class.

Happy validation! 🙂