I have a data collection of type ObservableCollection (say instance as myClassTypes). After some user operation, this myClassTypes populated with values in ViewModel. In view, there is a TextBox where user can enter text. I need to validate textbox data against myClassTypes values. So if myClassTypes contains the text inserted by User in textbox, Validation is passed otherwise it will fail. My code snippet is: ViewModel:
public ObservableCollection < MyClassType > ViewModelClassTypes {
get {
return _myClassTypes;
}
set {
_myClassTypes = value;
NotifyOfPropertyChange(() = >MyClassTypes);
}
}
public class TestValidationRule: ValidationRule {
public ObservableCollection < MyClassType > MyClassTypes {
get = >(ObservableCollection < MyClassType > ) GetValue(MyClassTypesProperty);
set = >SetValue(MyClassTypesProperty, value);
}
}
FYI : MyClassTypesProperty is a dependency property
My View.xaml is :
<TextBox>
<TextBox.Text>
<Binding UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<validationRules:TestValidationRule MyClassTypes="{Binding ViewModelClassTypes}"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
I am not able to get ViewModelClassTypes populated value in MyClassTypes. Can anyone please suggest what's wrong I am doing ?
Since .Net 4.5, the preferred way to implement data validation is to implement INotifyDataErrorInfo
(example from Technet, example from MSDN (Silverlight)).
Note: INotifyDataErrorInfo
replaces the obsolete IDataErrorInfo
.
The new framework infrastructure related to the INotifyDataErrorInfo
interface provides many advantages like
INotifyDataErrorInfo
worksWhen the ValidatesOnNotifyDataErrors
property of Binding
is set to true
, the binding engine will search for an INotifyDataErrorInfo
implementation on the binding source and subscribe to the INotifyDataErrorInfo.ErrorsChanged
event.
If the ErrorsChanged
event of the binding source is raised and INotifyDataErrorInfo.HasErrors
evaluates to true
, the binding engine will invoke the INotifyDataErrorInfo.GetErrors(propertyName)
method for the actual source property to retrieve the corresponding error message and then apply the customizable validation error template to the target control to visualize the validation error.
By default, a red border is drawn around the element that has failed to validate.
In case of an error, which is when INotifyDataErrorInfo.HasErrors
returns true
, the binding engine will also set the attached Validation
properties on the binding target, for example Validation.HasError
and Validation.ErrorTemplate
.
To customize the visual error feedback, we can override the default template provided by the binding engine, by overriding the value of the attached Validation.ErrorTemplate
property (see example below).
The described validation procedure only executes when Binding.ValidatesOnNotifyDataErrors
is set to true
on the particular data binding and the Binding.Mode
is set to either BindingMode.TwoWay
or BindingMode.OneWayToSource
.
Note that in order to enable visual validation error feedback, the Binding.Mode
must be set to BindingMode.TwoWay
.
INotifyDataErrorInfo
The following examples show three variations using:
ValidationRule
(class to encapsulate the actual data validation implementation)Of course, you can combine all three variations.
A fully working example, with a reusable base class ViewModel
, can be found at GitHub: Data_Validation_Example
MainWindow.xaml
To enable the visual data validation feedback, the Binding.ValidatesOnNotifyDataErrors
property must be set to true
on each relevant Binding
i.e. where the source of the Binding
is a validated property. The WPF framework will then show the control's default error feedback.
Note: to make this work, the Binding.Mode
must be either OneWayToSource
or TwoWay
(which is the default for the TextBox.Text
property):
<Window>
<Window.DataContext>
<ViewModel />
</Window.DataContext>
<!-- Important: set ValidatesOnNotifyDataErrors to true to enable visual feedback -->
<TextBox Text="{Binding UserInput, ValidatesOnNotifyDataErrors=True}"
Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}" />
</Window>
The following is an example of a custom validation error template.
The default visual error feedback is a simple red border around the validated element. In case you like to customize the visual feedback e.g., to allow showing error messages to the user, you can define a custom ControlTemplate
and assign it to the validated element (in this case the TextBox
) via the attached property Validation.ErrorTemplate
(see above).
The following ControlTemplate
enables showing a list of error messages that are associated with the validated property:
<ControlTemplate x:Key="ValidationErrorTemplate">
<StackPanel>
<Border BorderBrush="Red"
BorderThickness="1">
<!-- Placeholder for the TextBox itself -->
<AdornedElementPlaceholder x:Name="AdornedElement" />
</Border>
<Border Background="White"
BorderBrush="Red"
Padding="4"
BorderThickness="1,0,1,1"
HorizontalAlignment="Left">
<ItemsControl ItemsSource="{Binding}" HorizontalAlignment="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent}"
Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</StackPanel>
</ControlTemplate>
The view model is responsible for validating its own properties to ensure the data integrity of the model.
I recommend moving the implementation of INotifyDataErrorInfo
into a base class (e.g. an abstract ViewModel
class) together with the INotifyPropertyChanged
implementation and letting all your view models inherit it. This makes the validation logic reusable and keeps your view model classes clean.
ValidationRule
ViewModel.cs
When using ValidationRule
, the key is to have separate ValidationRule
implementations for each property or rule.
Extending ValidationRule
is optional. I chose to extend ValidationRule
because it the implementations can be reused with binding validation if necessary.
Basically, the result of the property validation should be a bool
to indicate fail or success of the validation and a message that can be displayed to the user to help him to fix his input.
When implementing the MVVM design pattern it is recommended to create a custom
ValidationRule
base class and a customValidationResult
.
This is because the framework'sValidationRule
and itsValidationResult
are both located in theSystem.Windows.Controls
namespace that requires a reference to the PresentationFramework.dll assembly.
You may want to avoid this reference.
public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
// Example property, which validates its value before applying it
private string userInput;
public string UserInput
{
get => this.userInput;
set
{
// Validate the value
bool isValueValid = IsPropertyValid(value);
// Optionally reject value if validation has failed
if (isValueValid)
{
this.userInput = value;
OnPropertyChanged();
}
}
}
// Constructor
public ViewModel()
{
this.Errors = new Dictionary<string, IList<object>>();
this.ValidationRules = new Dictionary<string, HashSet<ValidationRule>>();
// Create a Dictionary of validation rules for fast lookup.
// Each property name of a validated property maps to one or more ValidationRule.
RegisterValidationRule(nameof(this.UserInput), new UserInputValidationRule());
}
protected void RegisterValidationRule(string propertyName, IEnumerable<ValidationRule> validationRules)
{
foreach (ValidationRule validationRule in validationRules)
{
RegisterValidationRule(propertyName, validationRule);
}
}
protected void RegisterValidationRule(string propertyName, ValidationRule validationRule)
{
if (!this.ValidationRules.TryGetValue(propertyName, out HashSet<ValidationRule> validationRulesForProperty))
{
validationRulesForProperty = new HashSet<ValidationRule>();
this.ValidationRules.Add(propertyName, validationRulesForProperty);
}
validationRulesForProperty.Add(validationRule);
}
// Validation method.
// Is called from each property which needs to validate its value.
// Because the parameter 'propertyName' is decorated with the 'CallerMemberName' attribute.
// this parameter is automatically generated by the compiler.
// The caller only needs to pass in the 'propertyValue', if the caller is the target property's set method.
public bool IsPropertyValid<TValue>(TValue propertyValue, [CallerMemberName] string propertyName = null)
{
// Clear previous errors of the current property to be validated
_ = ClearErrors(propertyName);
if (this.ValidationRules.TryGetValue(propertyName, out HashSet<ValidationRule> propertyValidationRules))
{
// Apply all the rules that are associated with the current property
// and validate the property's value
IEnumerable<object> errorMessages = propertyValidationRules
.Select(validationRule => validationRule.Validate(propertyValue, CultureInfo.CurrentCulture))
.Where(result => !result.IsValid)
.Select(invalidResult => invalidResult.ErrorContent);
AddErrorRange(propertyName, errorMessages);
return !errorMessages.Any();
}
// No rules found for the current property
return true;
}
// Adds the specified errors to the errors collection if it is not
// already present, inserting it in the first position if 'isWarning' is
// false. Raises the ErrorsChanged event if the Errors collection changes.
// A property can have multiple errors.
private void AddErrorRange(string propertyName, IEnumerable<object> newErrors, bool isWarning = false)
{
if (!newErrors.Any())
{
return;
}
if (!this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors))
{
propertyErrors = new List<object>();
this.Errors.Add(propertyName, propertyErrors);
}
if (isWarning)
{
foreach (object error in newErrors)
{
propertyErrors.Add(error);
}
}
else
{
foreach (object error in newErrors)
{
propertyErrors.Insert(0, error);
}
}
OnErrorsChanged(propertyName);
}
// Removes all errors of the specified property.
// Raises the ErrorsChanged event if the Errors collection changes.
protected bool ClearErrors(string propertyName)
{
if (this.Errors.Remove(propertyName))
{
OnErrorsChanged(propertyName);
return true;
}
return false;
}
// Optional method to check if a particular property has validation errors
public bool PropertyHasErrors(string propertyName) => this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors) && propertyErrors.Any();
#region INotifyDataErrorInfo implementation
// The WPF binding engine will listen to this event
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
// This implementation of GetErrors returns all errors of the specified property.
// If the argument is 'null' instead of the property's name,
// then the method will return all errors of all properties.
// This method is called by the WPF binding engine when ErrorsChanged event was raised and HasErrors return true
public System.Collections.IEnumerable GetErrors(string propertyName)
=> string.IsNullOrWhiteSpace(propertyName)
? this.Errors.SelectMany(entry => entry.Value)
: this.Errors.TryGetValue(propertyName, out IList<object> errors)
? (IEnumerable<object>)errors
: new List<object>();
public bool HasErrors => this.Errors.Any();
#endregion
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
#endregion
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnErrorsChanged(string propertyName)
{
this.ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
// Maps a property name to a list of errors that belong to this property
private Dictionary<string, IList<object>> Errors { get; }
// Maps a property name to a list of ValidationRules that belong to this property
private Dictionary<string, HashSet<ValidationRule>> ValidationRules { get; }
}
UserInputValidationRule.cs
This example validation rule extends ValidationRule
and checks if the input starts with the '@' character. If not, it returns an invalid ValidationResult
with an error message that is automatically displayed as feedback to the user (with help of the previouisly defined error template).
public class UserInputValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (!(value is string userInput))
{
return new ValidationResult(false, "Value must be of type string.");
}
return userInput.StartsWith("@")
? ValidationResult.ValidResult
: new ValidationResult(false, "Input must start with '@'.");
}
}
As an alternative approach, the ValidationRule
can be replaced (or combined) with delegates to enable the use of Lambda expressions or Method Groups.
The validation expressions in this example return a tuple containing a boolean to indicate the validation state and a collection of string
error objects for the actual messages. Since all error object related properties are of type object
, the expressions can return any custom data type, in case you need more advanced error feedback and string
is not a sufficient. In this case, we would have to adjust the validation error template, to enable it to handle the data type.
ViewModel.cs
// Example uses System.ValueTuple
public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
// This property is validated using a lambda expression
private string userInput;
public string UserInput
{
get => this.userInput;
set
{
// Validate the new property value.
bool isValueValid = IsPropertyValid(value,
newValue => newValue.StartsWith("@")
? (true, Enumerable.Empty<object>())
: (false, new[] { "Value must start with '@'." }));
// Optionally, reject value if validation has failed
if (isValueValid)
{
// Accept the valid value
this.userInput = value;
OnPropertyChanged();
}
}
}
// Alternative usage example property, which validates its value
// before applying it, using a Method Group.
private string userInputAlternative;
public string UserInputAlternative
{
get => this.userInputAlternative;
set
{
// Use Method Group
if (IsPropertyValid(value, IsUserInputValid))
{
this.userInputAlternative = value;
OnPropertyChanged();
}
}
}
// Constructor
public ViewModel()
{
this.Errors = new Dictionary<string, IList<object>>();
}
// The validation method for the UserInput property
private (bool IsValid, IEnumerable<object> ErrorMessages) IsUserInputValid(string value)
{
return value.StartsWith("@")
? (true, Enumerable.Empty<object>())
: (false, new[] { "Value must start with '@'." });
}
// Example uses System.ValueTuple
public bool IsPropertyValid<TValue>(
TValue value,
Func<TValue, (bool IsValid, IEnumerable<object> ErrorMessages)> validationDelegate,
[CallerMemberName] string propertyName = null)
{
// Clear previous errors of the current property to be validated
_ = ClearErrors(propertyName);
// Validate using the delegate
(bool IsValid, IEnumerable<object> ErrorMessages) validationResult = validationDelegate?.Invoke(value) ?? (true, Enumerable.Empty<object>());
if (!validationResult.IsValid)
{
AddErrorRange(propertyName, validationResult.ErrorMessages);
}
return validationResult.IsValid;
}
// Adds the specified errors to the errors collection if it is not
// already present, inserting it in the first position if 'isWarning' is
// false. Raises the ErrorsChanged event if the Errors collection changes.
// A property can have multiple errors.
private void AddErrorRange(string propertyName, IEnumerable<object> newErrors, bool isWarning = false)
{
if (!newErrors.Any())
{
return;
}
if (!this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors))
{
propertyErrors = new List<object>();
this.Errors.Add(propertyName, propertyErrors);
}
if (isWarning)
{
foreach (object error in newErrors)
{
propertyErrors.Add(error);
}
}
else
{
foreach (object error in newErrors)
{
propertyErrors.Insert(0, error);
}
}
OnErrorsChanged(propertyName);
}
// Removes all errors of the specified property.
// Raises the ErrorsChanged event .
protected bool ClearErrors(string propertyName)
{
if (this.Errors.Remove(propertyName))
{
OnErrorsChanged(propertyName);
return true;
}
return false;
}
// Optional method to check if a particular property has validation errors
public bool PropertyHasErrors(string propertyName) => this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors) && propertyErrors.Any();
#region INotifyDataErrorInfo implementation
// The WPF binding engine will listen to this event
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
// This implementation of GetErrors returns all errors of the specified property.
// If the argument is 'null' instead of the property's name,
// then the method will return all errors of all properties.
// This method is called by the WPF binding engine when ErrorsChanged event was raised and HasErrors return true
public System.Collections.IEnumerable GetErrors(string propertyName)
=> string.IsNullOrWhiteSpace(propertyName)
? this.Errors.SelectMany(entry => entry.Value)
: this.Errors.TryGetValue(propertyName, out IList<object> errors)
? (IEnumerable<object>)errors
: new List<object>();
public bool HasErrors => this.Errors.Any();
#endregion
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
#endregion
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnErrorsChanged(string propertyName)
{
this.ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
// Maps a property name to a list of errors that belong to this property
private Dictionary<string, IList<object>> Errors { get; }
}
ValidationAttribute
This is an example implementation of INotifyDataErrorInfo
with ValidationAttribute
support e.g., MaxLengthAttribute
.
This solution combines the previous Lambda version to additionally support validation using a Lambda expression/delegate simultaneously.
While validation using a lambda expression or a delegate must be explicitly invoked by calling the TryValidateProperty
method in the property's setter, the attribute validation is executed implicitly from the OnPropertyChanged
event invocator (as soon the property was decorated with validation attributes):
ViewModel.cs
// Example uses System.ValueTuple
public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
private string userInput;
// Validate property using validation attributes
[MaxLength(Length = 5, ErrorMessage = "Only five characters allowed.")]
public string UserInput
{
get => this.userInput;
set
{
// Optional call to 'IsPropertyValid' to combine with attribute validation
// with a delegate
bool isValueValid = IsPropertyValid(value, newValue => newValue.StartsWith("@")
? (true, Enumerable.Empty<object>())
: (false, new[] { "Value must start with '@'." }));
this.userInput = value;
// Triggers checking for validation attributes and their validation,
// if any validation attributes were found (like 'MaxLength' in this example)
OnPropertyChanged();
}
}
// Constructor
public ViewModel()
{
this.Errors = new Dictionary<string, IList<object>>();
// Collect all properties that are decorated with validation attributes
this.DecoratedPropertyInfoMap = GetType().GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public)
.Where(entry => entry.Value.GetCustomAttributes(typeof(ValidationAttribute)).Any())
.ToDictionary(propertyInfo => propertyInfo.Name);
}
// Validate property using attributes.
// Is invoked by 'OnPropertyChanged' (see below).
protected bool IsAttributedPropertyValid<TValue>(string propertyName)
{
// The result flag
bool isValueValid = true;
if (!this.DecoratedPropertyInfoMap.TryGetValue(propertyName, out PropertyInfo propertyInfo))
{
return true;
}
object value = propertyInfo.GetValue(this);
var validationContext = new ValidationContext(this, null, null) { MemberName = propertyName };
var validationResults = new List<ValidationResult>();
if (!Validator.TryValidateProperty(value, validationContext, validationResults))
{
isValueValid = false;
AddErrorRange(propertyName, validationResults.Select(attributeValidationResult => attributeValidationResult.ErrorMessage));
}
return isValueValid;
}
public bool IsPropertyValid<TValue>(
TValue value,
Func<TValue, (bool IsValid, IEnumerable<object> ErrorMessages)> validationDelegate = null,
[CallerMemberName] string propertyName = null)
{
// Clear previous errors of the current property to be validated
ClearErrors(propertyName);
// Validate using the delegate
(bool IsValid, IEnumerable<object> ErrorMessages) validationResult = validationDelegate?.Invoke(value) ?? (true, Enumerable.Empty<object>());
if (!validationResult.IsValid)
{
// Store the error messages of the failed validation
AddErrorRange(propertyName, validationResult.ErrorMessages);
}
return validationResult.IsValid;
}
// Adds the specified errors to the errors collection if it is not
// already present, inserting it in the first position if 'isWarning' is
// false. Raises the ErrorsChanged event if the Errors collection changes.
// A property can have multiple errors.
private void AddErrorRange(string propertyName, IEnumerable<object> newErrors, bool isWarning = false)
{
if (!newErrors.Any())
{
return;
}
if (!this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors))
{
propertyErrors = new List<object>();
this.Errors.Add(propertyName, propertyErrors);
}
if (isWarning)
{
foreach (object error in newErrors)
{
propertyErrors.Add(error);
}
}
else
{
foreach (object error in newErrors)
{
propertyErrors.Insert(0, error);
}
}
OnErrorsChanged(propertyName);
}
// Removes all errors of the specified property.
protected bool ClearErrors(string propertyName)
{
if (this.Errors.Remove(propertyName))
{
OnErrorsChanged(propertyName);
return true;
}
return false;
}
// Optional
public bool PropertyHasErrors(string propertyName) => this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors) && propertyErrors.Any();
#region INotifyDataErrorInfo implementation
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
// Returns all errors of a property. If the argument is 'null' instead of the property's name,
// then the method will return all errors of all properties.
public System.Collections.IEnumerable GetErrors(string propertyName)
=> string.IsNullOrWhiteSpace(propertyName)
? this.Errors.SelectMany(entry => entry.Value)
: this.Errors.TryGetValue(propertyName, out IList<object> errors)
? (IEnuemrable<object>)errors
: new List<object>();
public bool HasErrors => this.Errors.Any();
#endregion
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
#endregion
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (this.DecoratedPropertyInfoMap.ContainsKey(propertyName))
{
_ = IsAttributedPropertyValid(propertyName);
}
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnErrorsChanged(string propertyName)
{
this.ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
// Maps a property name to a list of errors that belong to this property
private Dictionary<string, IList<object>> Errors { get; }
// List of property names of properties that are decorated with a ValidationAttribute
// to improve performance by avoiding unnecessary reflection.
private readonly Dictionary<string, PropertyInfo> DecoratedPropertyInfoMap { get; }
}