다음을 통해 공유


Data Validation in MVVM

**You can download the whole sample from here:
**

Introduction

Validation is more important if are working on Data entry applications or any kind of form based application. But if the application also follows MVVM pattern there are a lot of ways to do validation in a more cleaner and effective way.

Even though there are a lot of ways to do the Validation. Data Annotations seems to be more effective compared to another ways. 

A simple Example of Data Annotations


[Required(ErrorMessage = "Email address is required")]
[EmailAddress(ErrorMessage = "Email Address is Invalid")]
public string  Email
{
    get { return GetValue(() => Email); }
    set { SetValue(() => Email, value); }
}

Through Data Annotations it’s very simple to mention the validation rule and appropriate error message. 

We can even create custom validation attributes if the built in Validation attribute like EmailAddress, MaxLength, MinLength doesn’t satisfy our needs. For Example if we want to validate the Name by not allowing special characters like *, #, @ there is no built-in attributes to do this. So we can define a custom attribute.

Adding Data Annotations to the Model

1.) Add a reference to System.ComponentModel.DataAnnotations

2.) Refer System.ComponentModel.DataAnnotations in your Model.

using System.ComponentModel.DataAnnotations;

3.) Annotate the properties of the Model with Validation Attributes

[Unique(ErrorMessage = "Duplicate Id. Id already exists")]
[Required(ErrorMessage = "Id is Required")]
public int  Id
{
    get { return GetValue(() => Id); }
    set { SetValue(() => Id, value); }
}
 
[Required(ErrorMessage = "Name is Required")]
[MaxLength(50, ErrorMessage = "Name exceeded 50 letters")]
[ExcludeChar("/.,!@#$%", ErrorMessage = "Name contains invalid letters")]
public string  Name
{
    get { return GetValue(() => Name); }
    set { SetValue(() => Name, value); }
}
 
[Range(1, 100, ErrorMessage = "Age should be between 1 to 100")]
public int  Age
{
    get { return GetValue(() => Age); }
    set { SetValue(() => Age, value); }
}
 
public Gender Gender
{
    get { return GetValue(() => Gender); }
    set { SetValue(() => Gender, value); }
}
 
[Required(ErrorMessage = "Email address is required")]
[EmailAddress(ErrorMessage = "Email Address is Invalid")]
public string  Email
{
    get { return GetValue(() => Email); }
    set { SetValue(() => Email, value); }
}
 
[Required(ErrorMessage = "Repeat Email address is required")]
[Compare("Email", ErrorMessage = "Email Address does not match")]
public string  RepeatEmail
{
    get { return GetValue(() => RepeatEmail); }
    set { SetValue(() => RepeatEmail, value); }
}

4.) In the above code there are validation attributes named ExculdeChar, Unique which are Custom Validation Attributes. ExcludeChar is used to check whether the name has any special characters like @,*, # etc. Unique is used to check whether the Id is unique just like a primary key in table to prevent the duplicate entries. 

[Required(ErrorMessage = "Name is Required")]
[MaxLength(50, ErrorMessage = "Name exceeded 50 letters")]
[ExcludeChar("/.,!@#$%", ErrorMessage = "Name contains invalid letters")]
public string  Name
{
    get { return GetValue(() => Name); }
    set { SetValue(() => Name, value); }
}
[Unqiue(ErrorMessage = "Duplicate Id. Id already exists")]
[Required(ErrorMessage = "Id is Required")]
public int  Id
{
    get { return GetValue(() => Id); }
    set { SetValue(() => Id, value); }
}

5.) To create a Custom Validation attribute. Create a class named ExcludeChar with a parameterized constructor to accept the Invalid characters and derive it from ValidationAttribute class.

public class  ExcludeChar : ValidationAttribute
    {
        string _characters;
        public ExcludeChar(string characters)
        {
            _characters = characters;
        }
    }

6.) Override the IsValid method of the ValidationAttribute Class to implement your validation Logic

public class  ExcludeChar : ValidationAttribute
   {
       string _characters;
       public ExcludeChar(string characters)
       {
           _characters = characters;
       }
       protected override  ValidationResult IsValid(object value, System.ComponentModel.DataAnnotations.ValidationContext validationContext)
       {
           if (value != null)
           {
               for (int i = 0; i < _characters.Length; i++)
               {
                   var valueAsString = value.ToString();
                   if (valueAsString.Contains(_characters[i]))
                   {
                       var errorMessage = FormatErrorMessage(validationContext.DisplayName);
                       return new  ValidationResult(errorMessage);
                   }
               }
           }
           return ValidationResult.Success;
       }
   }

7.) Similarly for Unique validation attribute.

public class  Unqiue : ValidationAttribute
    {
        protected override  ValidationResult IsValid(object value, ValidationContext validationContext)
        {
           var contains = CustomerViewModel.SharedViewModel().Customers.Select(x => x.Id).Contains(int.Parse(value.ToString()));
            
           if (contains)
               return new  ValidationResult(FormatErrorMessage(validationContext.DisplayName));
           else
               return ValidationResult.Success;
        }
    }

8.) The Whole Model Code with Data Annotations

public class  Customer : PropertyChangedNotification
    {
        [Unqiue(ErrorMessage = "Duplicate Id. Id already exists")]
        [Required(ErrorMessage = "Id is Required")]
        public int  Id
        {
            get { return GetValue(() => Id); }
            set { SetValue(() => Id, value); }
        }
 
        [Required(ErrorMessage = "Name is Required")]
        [MaxLength(50, ErrorMessage = "Name exceeded 50 letters")]
        [ExcludeChar("/.,!@#$%", ErrorMessage = "Name contains invalid letters")]
        public string  Name
        {
            get { return GetValue(() => Name); }
            set { SetValue(() => Name, value); }
        }
 
        [Range(1, 100, ErrorMessage = "Age should be between 1 to 100")]
        public int  Age
        {
            get { return GetValue(() => Age); }
            set { SetValue(() => Age, value); }
        }
 
        public Gender Gender
        {
            get { return GetValue(() => Gender); }
            set { SetValue(() => Gender, value); }
        }
 
        [Required(ErrorMessage = "Email address is required")]
        [EmailAddress(ErrorMessage = "Email Address is Invalid")]
        public string  Email
        {
            get { return GetValue(() => Email); }
            set { SetValue(() => Email, value); }
        }
 
        [Required(ErrorMessage = "Repeat Email address is required")]
        [Compare("Email", ErrorMessage = "Email Address does not match")]
        public string  RepeatEmail
        {
            get { return GetValue(() => RepeatEmail); }
            set { SetValue(() => RepeatEmail, value); }
        }
    }
 
    public enum  Gender
    {
        Male,
        Female
    }

In the above code you can see the Customer model derived from PropertyChangedNotification which is explained below

Merging Validation and PropertyChanged Notification

An application can have a lot of business entities so reusing the logic of Validation and PropertyChanged is necessary. It also remove the overhead of adding OnPropertyChanged() Notification each time when an value is set. We are going to move both Validation and PropertyChanged Notification in one common class named PropertyChangedNotification. This class implements both IDataErrorInfo and INotifyPropertyChanged Interfaces.

Let’s just look at the validation part of the Class PropertyChangedNotification

As you implement the Interface IDataErrorInfo you can see two properties created. One is Indexer which takes the PropertyName as index and the other is Error property.

string IDataErrorInfo.Error
  {
      get
      {
          throw new  NotSupportedException("IDataErrorInfo.Error is not supported, use IDataErrorInfo.this[propertyName] instead.");
      }
  }
 
  string IDataErrorInfo.this[string propertyName]
  {
      get
      {
          return OnValidate(propertyName);
      }
  }

The Indexer is the one which returns the validation messages once the properties changes. So we need to call our Validation Method in the get of the Indexer.

string IDataErrorInfo.this[string propertyName]
  {
      get
      {
          return OnValidate(propertyName);
      }
  }

The OnValidate Method Gets the value from the property name and validate it and sends the error messages to the UI.

protected virtual  string OnValidate(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                throw new  ArgumentException("Invalid property name", propertyName);
            }
 
            string error = string.Empty;
            var value = GetValue(propertyName);
            var results = new  List<System.ComponentModel.DataAnnotations.ValidationResult>(1);
            var result = Validator.TryValidateProperty(
                value,
                new ValidationContext(this, null, null)
                {
                    MemberName = propertyName
                },
                results);
 
            if (!result)
            {
                var validationResult = results.First();
                error = validationResult.ErrorMessage;
            }
 
            return error;
        }

The Whole Code for PropertyChangedNotification

public abstract  class PropertyChangedNotification : INotifyPropertyChanged, IDataErrorInfo
   {
       #region Fields
 
       private readonly  Dictionary<string, object> _values = new  Dictionary<string, object>();
 
       #endregion
 
       #region Protected
 
       /// <summary>
       /// Sets the value of a property.
       /// </summary>
       /// <typeparam name="T">The type of the property value.</typeparam>
       /// <param name="propertySelector">Expression tree contains the property definition.</param>
       /// <param name="value">The property value.</param>
       protected void  SetValue<T>(Expression<Func<T>> propertySelector, T value)
       {
           string propertyName = GetPropertyName(propertySelector);
 
           SetValue<T>(propertyName, value);
       }
 
       /// <summary>
       /// Sets the value of a property.
       /// </summary>
       /// <typeparam name="T">The type of the property value.</typeparam>
       /// <param name="propertyName">The name of the property.</param>
       /// <param name="value">The property value.</param>
       protected void  SetValue<T>(string  propertyName, T value)
       {
           if (string.IsNullOrEmpty(propertyName))
           {
               throw new  ArgumentException("Invalid property name", propertyName);
           }
 
           _values[propertyName] = value;
           NotifyPropertyChanged(propertyName);
       }
 
       /// <summary>
       /// Gets the value of a property.
       /// </summary>
       /// <typeparam name="T">The type of the property value.</typeparam>
       /// <param name="propertySelector">Expression tree contains the property definition.</param>
       /// <returns>The value of the property or default value if not exist.</returns>
       protected T GetValue<T>(Expression<Func<T>> propertySelector)
       {
           string propertyName = GetPropertyName(propertySelector);
 
           return GetValue<T>(propertyName);
       }
 
       /// <summary>
       /// Gets the value of a property.
       /// </summary>
       /// <typeparam name="T">The type of the property value.</typeparam>
       /// <param name="propertyName">The name of the property.</param>
       /// <returns>The value of the property or default value if not exist.</returns>
       protected T GetValue<T>(string propertyName)
       {
           if (string.IsNullOrEmpty(propertyName))
           {
               throw new  ArgumentException("Invalid property name", propertyName);
           }
 
           object value;
           if (!_values.TryGetValue(propertyName, out value))
           {
               value = default(T);
               _values.Add(propertyName, value);
           }
 
           return (T)value;
       }
 
       /// <summary>
       /// Validates current instance properties using Data Annotations.
       /// </summary>
       /// <param name="propertyName">This instance property to validate.</param>
       /// <returns>Relevant error string on validation failure or <see cref="System.String.Empty"/> on validation success.</returns>
       protected virtual  string OnValidate(string propertyName)
       {
           if (string.IsNullOrEmpty(propertyName))
           {
               throw new  ArgumentException("Invalid property name", propertyName);
           }
 
           string error = string.Empty;
           var value = GetValue(propertyName);
           var results = new  List<System.ComponentModel.DataAnnotations.ValidationResult>(1);
           var result = Validator.TryValidateProperty(
               value,
               new ValidationContext(this, null, null)
               {
                   MemberName = propertyName
               },
               results);
 
           if (!result)
           {
               var validationResult = results.First();
               error = validationResult.ErrorMessage;
           }
 
           return error;
       }
 
       #endregion
 
       #region Change Notification
 
       /// <summary>
       /// Raised when a property on this object has a new value.
       /// </summary>
       public event  PropertyChangedEventHandler PropertyChanged;
 
       /// <summary>
       /// Raises this object's PropertyChanged event.
       /// </summary>
       /// <param name="propertyName">The property that has a new value.</param>
       protected void  NotifyPropertyChanged(string propertyName)
       {
           this.VerifyPropertyName(propertyName);
 
           PropertyChangedEventHandler handler = this.PropertyChanged;
           if (handler != null)
           {
               var e = new  PropertyChangedEventArgs(propertyName);
               handler(this, e);
           }
       }
 
       protected void  NotifyPropertyChanged<T>(Expression<Func<T>> propertySelector)
       {
           var propertyChanged = PropertyChanged;
           if (propertyChanged != null)
           {
               string propertyName = GetPropertyName(propertySelector);
               propertyChanged(this, new  PropertyChangedEventArgs(propertyName));
           }
       }
 
       #endregion // INotifyPropertyChanged Members
 
       #region Data Validation
 
       string IDataErrorInfo.Error
       {
           get
           {
               throw new  NotSupportedException("IDataErrorInfo.Error is not supported, use IDataErrorInfo.this[propertyName] instead.");
           }
       }
 
       string IDataErrorInfo.this[string propertyName]
       {
           get
           {
               return OnValidate(propertyName);
           }
       }
 
       #endregion
 
       #region Privates
 
       private string  GetPropertyName(LambdaExpression expression)
       {
           var memberExpression = expression.Body as  MemberExpression;
           if (memberExpression == null)
           {
               throw new  InvalidOperationException();
           }
 
           return memberExpression.Member.Name;
       }
 
       private object  GetValue(string  propertyName)
       {
           object value;
           if (!_values.TryGetValue(propertyName, out value))
           {
               var propertyDescriptor = TypeDescriptor.GetProperties(GetType()).Find(propertyName, false);
               if (propertyDescriptor == null)
               {
                   throw new  ArgumentException("Invalid property name", propertyName);
               }
 
               value = propertyDescriptor.GetValue(this);
               _values.Add(propertyName, value);
           }
 
           return value;
       }
 
       #endregion
 
       #region Debugging
 
       /// <summary>
       /// Warns the developer if this object does not have
       /// a public property with the specified name. This 
       /// method does not exist in a Release build.
       /// </summary>
       [Conditional("DEBUG")]
       [DebuggerStepThrough]
       public void  VerifyPropertyName(string propertyName)
       {
           // Verify that the property name matches a real,  
           // public, instance property on this object.
           if (TypeDescriptor.GetProperties(this)[propertyName] == null)
           {
               string msg = "Invalid property name: " + propertyName;
 
               if (this.ThrowOnInvalidPropertyName)
                   throw new  Exception(msg);
               else
                   Debug.Fail(msg);
           }
       }
 
       /// <summary>
       /// Returns whether an exception is thrown, or if a Debug.Fail() is used
       /// when an invalid property name is passed to the VerifyPropertyName method.
       /// The default value is false, but subclasses used by unit tests might 
       /// override this property's getter to return true.
       /// </summary>
       protected virtual  bool ThrowOnInvalidPropertyName { get; private  set; }
 
       #endregion // Debugging Aides
   }

ViewModel

 Name  Type Description 
 Customers  ObservableCollection<Customer>  List of all customers
 NewCustomer  Customer  New customer to be added through form
 Errors  Int  Static property used to enable and disable the save button based on validation
 SaveCommand  RelayCommand  It is used to add the new customer to customers observable collection
 ClearCommand  RelayCommand  It is used to clear the form after adding a new customer
 SaveDataCommand  RelayCommand  It is used to save the data to the XML file so if the application is restarted the data is preserved.

The Whole ViewModel code

public class  CustomerViewModel : PropertyChangedNotification
    {
        private static  CustomerViewModel customerViewModel;
        private static  string Filename = Directory.GetCurrentDirectory() + "\\Output.xml";
 
        public ObservableCollection<Customer> Customers
        {
            get { return GetValue(() => Customers); }
            set { SetValue(() => Customers, value); }
        }
 
        public Customer NewCustomer
        {
            get { return GetValue(() => NewCustomer); }
            set { SetValue(() => NewCustomer, value); }
        }
 
        public RelayCommand SaveCommand { get; set; }
 
        public RelayCommand ClearCommand { get; set; }
 
        public RelayCommand SaveDataCommand { get; set; }
 
        public static  int Errors { get; set; }
 
        public static  CustomerViewModel SharedViewModel()
        {
            return customerViewModel ?? (customerViewModel = new CustomerViewModel());
        }
 
        private CustomerViewModel()
        {
            if (File.Exists(Filename))
            {
                Customers = Deserialize<ObservableCollection<Customer>>();
            }
            else
            {
                Customers = new  ObservableCollection<Customer>();
                Customers.Add(new Customer { Id = 1, Name = "William", Email = "william@hotmail.com", RepeatEmail = "william@hotmail.com", Age = 23, Gender = Gender.Male });
            }
            NewCustomer = new  Customer();
            SaveCommand = new  RelayCommand(Save, CanSave);
            ClearCommand = new  RelayCommand(Clear);
            SaveDataCommand = new  RelayCommand(SaveData);
        }
 
        public void  Save(object  parameter)
        {
            Customers.Add(NewCustomer);
            Clear(this);
        }
 
        public bool  CanSave(object  parameter)
        {
            if (Errors == 0)
                return true;
            else
                return false;
        }
 
        public void  Clear(object  parameter)
        {
            NewCustomer = new  Customer();
        }
 
        public void  SaveData(object  parameter)
        {
            var result = Serialize<ObservableCollection<Customer>>(Customers);
            if (result) MessageBox.Show("Data Saved Successfully", "Success", MessageBoxButton.OK, MessageBoxImage.Information);
            else MessageBox.Show("Data Not Saved", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
        }
 
        private bool  Serialize<T>(T value)
        {
            if (value == null)
            {
                return false;
            }
            try
            {
                XmlSerializer _xmlserializer = new  XmlSerializer(typeof(T));
                Stream stream = new  FileStream(Filename, FileMode.CreateNew);
                _xmlserializer.Serialize(stream, value);
                stream.Close();
                return true;
            }
            catch (Exception ex)
            {
                return false;
            }
        }
 
        private T Deserialize<T>()
        {
            if (string.IsNullOrEmpty(Filename))
            {
                return default(T);
            }
            try
            {
                XmlSerializer _xmlSerializer = new  XmlSerializer(typeof(T));
                Stream stream = new  FileStream(Filename, FileMode.Open, FileAccess.Read);
                var result = (T)_xmlSerializer.Deserialize(stream);
                stream.Close();
                return result;
            }
            catch (Exception ex)
            {
                return default(T);
            }
        }
    }

View

In order to show the validation messages in the view. We need to do two things.

I.          set NotifyOnDataErrors=true in binding

II.        Add a Textblock and bind the validation message of the Textbox in order to show the validation message.

<TextBlock Style="{StaticResource TextBlockStyle}" Grid.Column="0" Grid.Row="0" Text="Id : "/>
        
<TextBox x:Name="TxtId" Style="{StaticResource TextBoxStyle}" Grid.Column="1" Grid.Row="0" Text="{Binding Id, Mode=TwoWay, ValidatesOnDataErrors=True,NotifyOnValidationError=True}" Width="100" Validation.Error="Validation_Error" />
        
<TextBlock Style="{StaticResource TextBlockStyle}" Grid.Column="2" Grid.Row="0" Text="{Binding (Validation.Errors)[0].ErrorContent, ElementName=TxtId}" Foreground="Red" Margin="5,0,0,0"/>

The Most Important thing is to disable the Save button when the form is invalid. For this we had a static property in our view model named Errors. When the Error is occurred the Error count is increased by 1. When the error is corrected the error count is decreased by 1. So when the Count is 0 the save will be enabled or disabled.

To find whether an error is added or removed. We need to subscribe to an event Validation.Error this event is added to all textboxes where we need validation.

For Example,

<TextBox x:Name="TxtId" Style="{StaticResource TextBoxStyle}" Grid.Column="1" Grid.Row="0" Text="{Binding Id, Mode=TwoWay, ValidatesOnDataErrors=True,NotifyOnValidationError=True}" Width="100" Validation.Error="Validation_Error" />
private void  Validation_Error(object sender, ValidationErrorEventArgs e)
        {
            if (e.Action == ValidationErrorEventAction.Added) CustomerViewModel.Errors += 1;
            if (e.Action == ValidationErrorEventAction.Removed) CustomerViewModel.Errors -= 1;
        }

The Whole XAML

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MVVMValidation.Extensions"
        xmlns:Model="clr-namespace:MVVMValidation.Model"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        x:Class="MVVMValidation.MainWindow"
        Title="MainWindow" SizeToContent="WidthAndHeight">
    <Window.Resources>
        <Style x:Key="TextBlockStyle" TargetType="{x:Type TextBlock}">
            <Setter Property="FontSize" Value="16"/>
            <Setter Property="Margin" Value="0,5,0,0"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
        </Style>
        <Style x:Key="TextBoxStyle" TargetType="{x:Type TextBox}">
            <Setter Property="FontSize" Value="16"/>
            <Setter Property="Margin" Value="0,5,0,0"/>
        </Style>
        <Style TargetType="{x:Type ComboBox}">
            <Setter Property="FontSize" Value="16"/>
            <Setter Property="Margin" Value="0,5,0,0"/>
        </Style>
    </Window.Resources>
    <Grid Margin="10,5,10,5">
        <StackPanel>
            <DataGrid ItemsSource="{Binding Customers}" AutoGenerateColumns="False" HorizontalAlignment="Center" FontSize="16">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Id" Binding="{Binding Id}" Width="40"/>
                    <DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
                    <DataGridTextColumn Header="Age" Binding="{Binding Age}" Width="40"/>
                    <DataGridTextColumn Header="Gender" Binding="{Binding Gender}" Width="70"/>
                    <DataGridTextColumn Header="Email" Binding="{Binding Email}"/>
                </DataGrid.Columns>
            </DataGrid>
            <TextBlock Style="{StaticResource TextBlockStyle}" Text="Add New Customer" Margin="0,10,0,10"/>
            <Grid x:Name="GrdCustomer" DataContext="{Binding NewCustomer, ValidatesOnDataErrors=True}">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="120"/>
                    <ColumnDefinition Width="100"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition/>
                    <RowDefinition/>
                    <RowDefinition/>
                    <RowDefinition/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
                <TextBlock Style="{StaticResource TextBlockStyle}" Grid.Column="0" Grid.Row="0" Text="Id : "/>
                <TextBox x:Name="TxtId" Style="{StaticResource TextBoxStyle}" Grid.Column="1" Grid.Row="0" Text="{Binding Id, Mode=TwoWay, ValidatesOnDataErrors=True,NotifyOnValidationError=True}" Width="100" Validation.Error="Validation_Error" />
                <TextBlock Style="{StaticResource TextBlockStyle}" Grid.Column="2" Grid.Row="0" Text="{Binding (Validation.Errors)[0].ErrorContent, ElementName=TxtId}" Foreground="Red" Margin="5,0,0,0"/>
 
                <TextBlock Style="{StaticResource TextBlockStyle}" Grid.Column="0" Grid.Row="1" Text="Name : "/>
                <TextBox x:Name="TxtName" Style="{StaticResource TextBoxStyle}" Grid.Column="1" Grid.Row="1" Width="100" Text="{Binding Name, Mode=TwoWay, ValidatesOnDataErrors=True,NotifyOnValidationError=True}" Validation.Error="Validation_Error"  />
                <TextBlock Style="{StaticResource TextBlockStyle}" Grid.Column="2" Grid.Row="1" Text="{Binding (Validation.Errors)[0].ErrorContent, ElementName=TxtName}" Foreground="Red" Margin="5,0,0,0"/>
 
                <TextBlock Style="{StaticResource TextBlockStyle}" Grid.Column="0" Grid.Row="2" Text="Age : "/>
                <TextBox x:Name="TxtAge" Style="{StaticResource TextBoxStyle}" Grid.Column="1" Grid.Row="2" Width="100" Text="{Binding Age, Mode=TwoWay, ValidatesOnDataErrors=True,NotifyOnValidationError=True}" Validation.Error="Validation_Error"  />
                <TextBlock Style="{StaticResource TextBlockStyle}" Grid.Column="2" Grid.Row="2" Text="{Binding (Validation.Errors)[0].ErrorContent, ElementName=TxtAge}" Foreground="Red" Margin="5,0,0,0"/>
 
                <TextBlock Style="{StaticResource TextBlockStyle}" Grid.Column="0" Grid.Row="3" Text="Gender : "/>
                <ComboBox x:Name="CbxGender" Grid.Column="1" SelectedIndex="0" Grid.Row="3" Width="100" SelectedItem="{Binding Gender}" ItemsSource="{Binding Source={local:Enumeration {x:Type Model:Gender}}}" Validation.ErrorTemplate="{x:Null}"  DisplayMemberPath="Description" SelectedValuePath="Value"/>
 
                <TextBlock Style="{StaticResource TextBlockStyle}" Grid.Column="0" Grid.Row="4" Text="Email : "/>
                <TextBox x:Name="TxtEmail" Style="{StaticResource TextBoxStyle}" Grid.Column="1" Grid.Row="4" Width="100" Text="{Binding Email, Mode=TwoWay, ValidatesOnDataErrors=True,NotifyOnValidationError=True}" Validation.Error="Validation_Error" />
                <TextBlock Style="{StaticResource TextBlockStyle}" Grid.Column="2" Grid.Row="4" Text="{Binding (Validation.Errors)[0].ErrorContent, ElementName=TxtEmail}" Foreground="Red" Margin="5,0,0,0"/>
 
                <TextBlock Style="{StaticResource TextBlockStyle}" Grid.Column="0" Grid.Row="5" Text="Repeat Email : "/>
                <TextBox x:Name="TxtRepeatEmail" Style="{StaticResource TextBoxStyle}" Grid.Column="1" Grid.Row="5" Width="100" Text="{Binding RepeatEmail, Mode=TwoWay, ValidatesOnDataErrors=True,NotifyOnValidationError=True}" Validation.Error="Validation_Error"  />
                <TextBlock Style="{StaticResource TextBlockStyle}" Grid.Column="2" Grid.Row="5" Text="{Binding (Validation.Errors)[0].ErrorContent, ElementName=TxtRepeatEmail}" Foreground="Red" Margin="5,0,0,0"/>
            </Grid>
            <StackPanel Orientation="Horizontal">
                <Button Content="Save" Command="{Binding SaveCommand}" Width="60" Height="30" Margin="10,15,0,15" />
                <Button Content="Clear" Command="{Binding ClearCommand}" Width="60" Height="30" Margin="10,15,0,15" />
                <Button Content="Save Data" Command="{Binding SaveDataCommand}" Width="60" Height="30" Margin="10,15,0,15" />
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

Output

When the application launches, you can see the validation messages and the Save Button Disabled

Once the Valid input is given. You can see the validation messages getting disappeared and Save Button getting Enabled

After clicking the Save the customer will be added to the Datagrid and the form will be cleared and again validation messages appear since the data is invalid to save and Save Button is also disabled.

Source Code

You can download the whole sample from here