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