自 .Net 4.5 以来实现数据验证的首选方式是让您的视图模型实现INotifyDataErrorInfo
(来自Technet的示例,来自MSDN (Silverlight)的示例)。
注意:INotifyDataErrorInfo
替换过时的IDataErrorInfo
.
INotifyDataErrorInfo
工作原理
当 的ValidatesOnNotifyDataErrors
属性Binding
设置为true
时,绑定引擎将在绑定源上搜索INotifyDataErrorInfo
实现并订阅INotifyDataErrorInfo.ErrorsChanged
事件。
如果ErrorsChanged
绑定源的事件被引发并INotifyDataErrorInfo.HasErrors
评估为true
,则绑定引擎将调用INotifyDataErrorInfo.GetErrors(propertyName)
实际源属性的方法以检索相应的错误消息,然后将可自定义的验证错误模板应用于目标控件以可视化验证错误。
默认情况下,在验证失败的元素周围会绘制一个红色边框。
此验证反馈可视化过程仅在特定数据绑定上设置为 且Binding.ValidatesOnNotifyDataErrors
设置为或时执行。true
Binding.Mode
BindingMode.TwoWay
BindingMode.OneWayToSource
如何实施INotifyDataErrorInfo
以下示例显示了属性验证的三种变体,使用
- a
ValidationRule
(封装实际数据验证实现的类)
- Lambdas(或委托)
- 验证属性(用于装饰已验证的属性)。
代码未经测试。这些片段应该都可以工作,但可能由于输入错误而无法编译。此代码旨在提供有关如何实现INotifyDataErrorInfo
接口的简单示例。
视图模型.cs
视图模型负责验证自己的属性以确保模型的数据完整性。
从 .NET 4.5 开始,推荐的方式是让视图模型实现INotifyDataErrorInfo
接口。关键是为每个属性或规则
分别实现。ValidationRule
扩展ValidationRule
是可选的。我选择扩展ValidationRule
是因为它已经提供了一个完整的验证 API,并且如果需要,可以通过绑定验证重用实现。
基本上,属性验证的结果应该是bool
指示验证失败或成功的结果,以及可以显示给用户以帮助他修复输入的消息。
如果出现验证错误,我们所要做的就是生成错误消息,将其添加到私有字符串集合中,以允许我们的INotifyDataErrorInfo.GetErrors(propertyName)
实现从该集合返回正确的错误消息并引发INotifyDataErrorInfo.ErrorChanged
事件以通知 WPF 绑定引擎有关错误:
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
ValidateProperty(value);
this.userInput = value;
OnPropertyChanged();
}
}
// Constructor
public ViewModel()
{
this.Errors = new Dictionary<string, List<string>>();
this.ValidationRules = new Dictionary<string, List<ValidationRule>>();
// Create a Dictionary of validation rules for fast lookup.
// Each property name of a validated property maps to one or more ValidationRule.
this.ValidationRules.Add(nameof(this.UserInput), new List<ValidationRule>() {new UserInputValidationRule()});
}
// 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 ValidateProperty<TValue>(TValue propertyValue, [CallerMemberName] string propertyName = null)
{
// Clear previous errors of the current property to be validated
this.Errors.Remove(propertyName);
OnErrorsChanged(propertyName);
if (this.ValidationRules.TryGetValue(propertyName, out List<ValidationRule> propertyValidationRules))
{
// Apply all the rules that are associated with the current property
// and validate the property's value
propertyValidationRules
.Select(validationRule => validationRule.Validate(propertyValue, CultureInfo.CurrentCulture))
.Where(result => !result.IsValid)
.ToList()
.ForEach(invalidResult => AddError(propertyName, invalidResult.ErrorContent as string));
return !PropertyHasErrors(propertyName);
}
// No rules found for the current property
return true;
}
// Adds the specified error 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.
public void AddError(string propertyName, string errorMessage, bool isWarning = false)
{
if (!this.Errors.TryGetValue(propertyName, out List<string> propertyErrors))
{
propertyErrors = new List<string>();
this.Errors[propertyName] = propertyErrors;
}
if (!propertyErrors.Contains(errorMessage))
{
if (isWarning)
{
// Move warnings to the end
propertyErrors.Add(errorMessage);
}
else
{
propertyErrors.Insert(0, errorMessage);
}
OnErrorsChanged(propertyName);
}
}
// Optional method to check if a certain property has validation errors
public bool PropertyHasErrors(string propertyName) => this.Errors.TryGetValue(propertyName, out List<string> propertyErrors) && propertyErrors.Any();
#region INotifyDataErrorInfo implementation
// The WPF binding engine will listen to this event
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
// This implementatio 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 retirn true
public System.Collections.IEnumerable GetErrors(string propertyName)
=> string.IsNullOrWhiteSpace(propertyName)
? this.Errors.SelectMany(entry => entry.Value)
: this.Errors.TryGetValue(propertyName, out List<string> errors)
? errors
: new List<string>();
// Returns 'true' if the view model has any invalid property
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, List<String>> Errors { get; }
// Maps a property name to a list of ValidationRules that belong to this property
private Dictionary<String, List<ValidationRule>> ValidationRules { get; }
}
UserInputValidationRule.cs
此示例验证规则扩展ValidationRule
并检查输入是否以“@”字符开头。如果不是,它返回一个无效ValidationResult
的错误消息,可以显示给用户以帮助他修复他的输入。
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.");
}
if (!userInput.StartsWith("@"))
{
return new ValidationResult(false, "Input must start with '@'.");
}
return ValidationResult.ValidResult;
}
}
主窗口.xaml
要启用可视数据验证反馈,Binding.ValidatesOnNotifyDataErrors
必须将属性设置为true
每个相关Binding
的,即其中的源Binding
是经过验证的属性。然后 WPF 框架将显示控件的默认错误反馈。
注意使这项工作Binding.Mode
必须是OneWayToSource
或TwoWay
(这是TextBox.Text
属性的默认值):
<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>
以下是自定义验证错误模板的示例。
默认的视觉错误反馈是经过验证的元素周围的简单红色边框。如果您想自定义视觉反馈,例如,允许向用户显示错误消息,您可以定义一个自定义并通过附加属性(见上文)ControlTemplate
将其分配给经过验证的元素(在本例中为)。
以下启用显示与已验证属性关联的错误消息列表:TextBox
Validation.ErrorTemplate
ControlTemplate

<ControlTemplate x:Key="ValidationErrorTemplate">
<StackPanel>
<Border BorderBrush="Red"
BorderThickness="1">
<!-- Placeholder for the DataGridTextColumn 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>
我建议将 的实现与实现一起移动INotifyDataErrorInfo
到基类(例如抽象ViewModel
类)中,INotifyPropertyChanged
并让所有视图模型继承它。这使验证逻辑可重用并保持您的视图模型类干净。
您可以更改示例的实现细节INotifyDataErrorInfo
以满足需求。
评论
作为替代方法,ValidationRule
可以用委托替换以启用 Lambda 表达式或方法组:
// Example uses System.ValueTuple
public bool ValidateProperty<TValue>(
TValue value,
Func<TValue, (bool IsValid, IEnumerable<string> ErrorMessages)> validationDelegate,
[CallerMemberName] string propertyName = null)
{
// Clear previous errors of the current property to be validated
this.Errors.Remove(propertyName);
OnErrorsChanged(propertyName);
// Validate using the delegate
(bool IsValid, IEnumerable<string> ErrorMessages) validationResult = validationDelegate?.Invoke(value) ?? (true, string.Empty);
if (!validationResult.IsValid)
{
// Store the error messages of the failed validation
foreach (string errorMessage in validationResult.ErrorMessages)
{
// See previous example for implementation of AddError(string,string):void
AddError(propertyName, errorMessage);
}
}
return validationResult.IsValid;
}
private string userInput;
public string UserInput
{
get => this.userInput;
set
{
// Validate the new property value before it is accepted
if (ValidateProperty(value,
newValue => newValue.StartsWith("@")
? (true, new List<string>())
: (false, new List<string> {"Value must start with '@'."})))
{
// Accept the valid value
this.userInput = value;
OnPropertyChanged();
}
}
}
// Alternative usage example property which validates its value
// before applying it using a Method Group.
// Example uses System.ValueTuple.
private string userInputAlternativeValidation;
public string UserInputAlternativeValidation
{
get => this.userInputAlternativeValidation;
set
{
// Use Method group
if (ValidateProperty(value, AlternativeValidation))
{
this.userInputAlternativeValidation = value;
OnPropertyChanged();
}
}
}
private (bool IsValid, string ErrorMessage) AlternativeValidation(string value)
{
return value.StartsWith("@")
? (true, string.Empty)
: (false, "Value must start with '@'.");
}
数据验证使用ValidationAttribute
这是一个INotifyDataErrorInfo
带有ValidationAttribute
支持的示例实现,例如MaxLengthAttribute
. 该解决方案结合了之前的 Lamda 版本,另外还支持同时使用 Lambda 表达式/委托进行验证:
public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
private string userInputAttributeValidation;
[MaxLength(Length = 5, ErrorMessage = "Only five characters allowed.")]
public string UserInputAttributeValidation
{
get => this.userInputAttributeValidation;
set
{
// Use only the attribute (can be combined with a Lambda or Method group)
if (ValidateProperty(value))
{
this.userInputAttributeValidation = value;
OnPropertyChanged();
}
}
}
// Constructor
public ViewModel()
{
this.Errors = new Dictionary<string, List<string>>();
}
// Validate properties using decorated attributes and/or a validation delegate.
// The validation delegate is optional.
public bool ValidateProperty<TValue>(
TValue value,
Func<TValue, (bool IsValid, IEnumerable<string> ErrorMessages)> validationDelegate = null,
[CallerMemberName] string propertyName = null)
{
// Clear previous errors of the current property to be validated
this.Errors.Remove(propertyName);
OnErrorsChanged(propertyName);
bool isValueValid = ValidatePropertyUsingAttributes(value, propertyName);
if (validationDelegate != null)
{
isValueValid |= ValidatePropertyUsingDelegate(value, validationDelegate, propertyName);
}
return isValueValid;
}
// Validate properties using decorated attributes.
public bool ValidatePropertyUsingAttributes<TValue>(TValue value, string propertyName)
{
// The result flag
bool isValueValid = true;
// Check if property is decorated with validation attributes
// using reflection
IEnumerable<Attribute> validationAttributes = GetType()
.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
?.GetCustomAttributes(typeof(ValidationAttribute)) ?? new List<Attribute>();
// Validate using attributes if present
if (validationAttributes.Any())
{
var validationContext = new ValidationContext(this, null, null) { MemberName = propertyName };
var validationResults = new List<ValidationResult>();
if (!Validator.TryValidateProperty(value, validationContext, validationResults))
{
isValueValid = false;
foreach (ValidationResult attributeValidationResult in validationResults)
{
AddError(propertyName, attributeValidationResult.ErrorMessage);
}
}
}
return isValueValid;
}
// Validate properties using the delegate.
public bool ValidatePropertyUsingDelegate<TValue>(
TValue value,
Func<TValue, (bool IsValid, IEnumerable<string> ErrorMessages)> validationDelegate,
string propertyName)
{
// The result flag
bool isValueValid = true;
// Validate using the delegate
(bool IsValid, IEnumerable<string> ErrorMessages) validationResult = validationDelegate.Invoke(value);
if (!validationResult.IsValid)
{
isValueValid = false;
// Store the error messages of the failed validation
foreach (string errorMessage in validationResult.ErrorMessages)
{
AddError(propertyName, errorMessage);
}
}
return isValueValid;
}
// Adds the specified error 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.
public void AddError(string propertyName, string errorMessage, bool isWarning = false)
{
if (!this.Errors.TryGetValue(propertyName, out List<string> propertyErrors))
{
propertyErrors = new List<string>();
this.Errors[propertyName] = propertyErrors;
}
if (!propertyErrors.Contains(errorMessage))
{
if (isWarning)
{
// Move warnings to the end
propertyErrors.Add(errorMessage);
}
else
{
propertyErrors.Insert(0, errorMessage);
}
OnErrorsChanged(propertyName);
}
}
public bool PropertyHasErrors(string propertyName) => this.Errors.TryGetValue(propertyName, out List<string> 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 IEnumerable<string> errors)
? errors
: new List<string>();
// Returns if the view model has any invalid property
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, List<String>> Errors { get; }
}