如何添加验证以查看模型属性或如何实现 INotifyDataErrorInfo
How to add validation to view model properties or how to implement INotifyDataErrorInfo
我有一个类型为 ObservableCollection 的数据集合(比如实例为 myClassTypes)。在一些用户操作之后,这个 myClassTypes 填充了 ViewModel 中的值。在视图中,有一个 TextBox,用户可以在其中输入文本。我需要根据 myClassTypes 值验证文本框数据。因此,如果 myClassTypes 包含用户在文本框中插入的文本,则验证通过,否则将失败。
我的代码片段是:
视图模型:
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);
}
}
仅供参考:MyClassTypesProperty 是一个依赖项 属性
我的 View.xaml 是:
<TextBox>
<TextBox.Text>
<Binding UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<validationRules:TestValidationRule MyClassTypes="{Binding ViewModelClassTypes}"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
我无法在 MyClassTypes 中获取 ViewModelClassTypes 填充值。谁能告诉我我做错了什么?
自 .Net 4.5 以来实现数据验证的首选方法是让您的视图模型实现 INotifyDataErrorInfo
(example from Technet, example from MSDN (Silverlight)).
注意:INotifyDataErrorInfo
替换了过时的 IDataErrorInfo
。
与 INotifyDataErrorInfo
接口相关的新框架基础设施提供了许多优势,例如
- 支持每个 属性
多个错误
- 自定义错误对象和视觉错误反馈的自定义(例如,使视觉提示适应自定义错误对象)
- 异步验证使用async/await
INotifyDataErrorInfo
的工作原理
当 Binding
的 ValidatesOnNotifyDataErrors
属性 设置为 true
时,绑定引擎将在绑定源上搜索 INotifyDataErrorInfo
实现并订阅 INotifyDataErrorInfo.ErrorsChanged
活动。
如果引发绑定源的 ErrorsChanged
事件并且 INotifyDataErrorInfo.HasErrors
求值为 true
,绑定引擎将为实际源调用 INotifyDataErrorInfo.GetErrors(propertyName)
方法 属性 检索相应的错误消息,然后将可自定义的验证错误模板应用到目标控件以可视化验证错误。
默认情况下,在验证失败的元素周围绘制红色边框。
如果出现错误,即 INotifyDataErrorInfo.HasErrors
returns true
,绑定引擎还将在绑定目标上设置附加的 Validation
属性,例如 Validation.HasError
和 Validation.ErrorTemplate
.
要自定义视觉错误反馈,我们可以覆盖绑定引擎提供的默认模板,方法是覆盖附加的 Validation.ErrorTemplate
属性 的值(参见下面的示例)。
仅当特定数据绑定的 Binding.ValidatesOnNotifyDataErrors
设置为 true
且 Binding.Mode
设置为 BindingMode.TwoWay
或 [=40 时,才会执行所描述的验证过程=].
如何实现INotifyDataErrorInfo
以下示例显示了使用
进行 属性 验证的三种变体
- a
ValidationRule
(class封装实际的数据验证实现)
- lambda 表达式(或委托)
- 验证属性(用于装饰已验证的属性)。
当然,您可以组合所有三种变体以提供最大的灵活性。
代码未经测试。这些片段应该都可以工作,但可能由于输入错误而无法编译。此代码旨在提供有关如何实现 INotifyDataErrorInfo
接口的简单示例。
正在准备视图
MainWindow.xaml
要启用视觉数据验证反馈,Binding.ValidatesOnNotifyDataErrors
属性 必须在每个相关 Binding
上设置为 true
,即 [=22] 的来源=] 是经过验证的 属性。然后 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
的实现与 INotifyPropertyChanged
实现一起移动到基础 class(例如抽象 ViewModel
class)中,并让所有视图模型继承它。这使得验证逻辑可重用并保持您的视图模型 classes 干净。
您可以更改 INotifyDataErrorInfo
示例的实现细节以满足要求。
1 使用 ValidationRule
进行数据验证
ViewModel.cs
使用 ValidationRule
时,关键是要为每个 属性 或规则提供单独的 ValidationRule
实现。
扩展 ValidationRule
是可选的。我选择扩展 ValidationRule
因为它已经提供了完整的验证 API 并且因为如果需要可以通过绑定验证重用这些实现。
基本上,属性 验证的结果应该是 bool
以指示验证失败或成功,并且可以向用户显示一条消息以帮助他修复输入。
如果出现验证错误,我们所要做的就是生成一条错误消息,将其添加到私有字符串集合中,以允许我们的 INotifyDataErrorInfo.GetErrors(propertyName)
实现 return 来自的正确错误消息此集合并引发 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
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, IList<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 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 List<ValidationRule> propertyValidationRules))
{
// Apply all the rules that are associated with the current property
// and validate the property's value
IEnumerable<string> 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.
public bool ClearErrors(string propertyName)
{
this.ValidatedAttributedProperties.Remove(propertyName);
if (this.Errors.Remove(propertyName))
{
OnErrorsChanged(propertyName);
return true;
}
return false;
}
// Optional method to check if a certain 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 retirn 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>();
// 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, IList<object>> Errors { get; }
// Maps a property name to a list of ValidationRules that belong to this property
private Dictionary<string, IList<ValidationRule>> ValidationRules { get; }
}
UserInputValidationRule.cs
此示例验证规则扩展 ValidationRule
并检查输入是否以“@”字符开头。如果不是,它 return 是一个无效的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;
}
}
2 使用 lambda 表达式和委托进行数据验证
作为替代方法,可以将 ValidationRule
替换(或组合)为委托以启用 Lambda 表达式或方法组的使用。
此示例中的验证表达式 return 一个包含布尔值的元组,用于指示验证状态和 string
实际消息的错误对象集合。由于所有与错误对象相关的属性都是 object
类型,因此表达式可以 return 任何自定义数据类型,以防您需要更高级的错误反馈并且 string
不是足够的错误对象。在这种情况下,我们还必须调整验证错误模板,使其能够处理数据类型。
ViewModel.cs
public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
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.
// Example uses System.ValueTuple.
private string userInputAlternativeValidation;
public string UserInputAlternativeValidation
{
get => this.userInputAlternativeValidation;
set
{
// Use Method group
if (IsPropertyValid(value, AlternativeValidation))
{
this.userInputAlternativeValidation = value;
OnPropertyChanged();
}
}
}
// Constructor
public ViewModel()
{
this.Errors = new Dictionary<string, IList<object>>();
}
// The validation handler
private (bool IsValid, IEnumerable<object> ErrorMessages) AlternativeValidation(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 if the Errors collection changes.
public bool ClearErrors(string propertyName)
{
this.ValidatedAttributedProperties.Remove(propertyName);
if (this.Errors.Remove(propertyName))
{
OnErrorsChanged(propertyName);
return true;
}
return false;
}
// Optional method to check if a certain 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 retirn 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>();
// 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, IList<object>> Errors { get; }
}
3 使用 ValidationAttribute
进行数据验证
这是 INotifyDataErrorInfo
与 ValidationAttribute
support e.g., MaxLengthAttribute
的示例实现。该解决方案结合了以前的 Lambda 版本,以额外支持同时使用 Lambda expression/delegate 进行验证。
虽然必须通过调用属性 setter 中的 TryValidateProperty
方法来显式调用使用 lambda 表达式或委托的验证,但属性验证是从 OnPropertyChanged
事件调用器隐式执行的(一旦属性 用验证属性装饰):
ViewModel.cs
public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
private string userInputAttributeValidation;
// Validate property using validation attributes
[MaxLength(Length = 5, ErrorMessage = "Only five characters allowed.")]
public string UserInputAttributeValidation
{
get => this.userInputAttributeValidation;
set
{
// Optional call to 'IsPropertyValid' to combine attribute validation
// with a delegate
bool isValueValid = IsPropertyValid(value, newValue => newValue.StartsWith("@")
? (true, Enumerable.Empty<object>())
: (false, new[] { "Value must start with '@'." }));
// Optionally reject value if validation h as failed
if (isValueValid)
{
this.userInputAttributeValidation = 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>>();
this.ValidatedAttributedProperties = new HashSet<string>();
}
// Validate property using decorating attributes.
// Is invoked by 'OnPropertyChanged' (see below).
private bool IsAttributedPropertyValid<TValue>(TValue value, string propertyName)
{
this.ValidatedAttributedProperties.Add(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;
AddErrorRange(validationResults.Select(attributeValidationResult => attributeValidationResult.ErrorMessage));
}
}
return isValueValid;
}
// Example uses System.ValueTuple
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(validationResult.ErrorMessages);
}
bool isAttributedPropertyValid = IsAttributedPropertyValid(value, propertyName);
return isAttributedPropertyValid && 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 if the Errors collection changes.
public bool ClearErrors(string propertyName)
{
this.ValidatedAttributedProperties.Remove(propertyName);
if (this.Errors.Remove(propertyName))
{
OnErrorsChanged(propertyName);
return true;
}
return false;
}
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>();
// 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)
{
// Check if IsAttributedPropertyValid Property was already called by 'IsValueValid'.
if (!this.ValidatedAttributedProperties.Contains(propertyName))
{
_ = IsAttributedPropertyValid(value, 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; }
// Track attribute validation calls
private HashSet<string> ValidatedAttributedProperties { get; }
}
我有一个类型为 ObservableCollection 的数据集合(比如实例为 myClassTypes)。在一些用户操作之后,这个 myClassTypes 填充了 ViewModel 中的值。在视图中,有一个 TextBox,用户可以在其中输入文本。我需要根据 myClassTypes 值验证文本框数据。因此,如果 myClassTypes 包含用户在文本框中插入的文本,则验证通过,否则将失败。 我的代码片段是: 视图模型:
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);
}
}
仅供参考:MyClassTypesProperty 是一个依赖项 属性
我的 View.xaml 是:
<TextBox>
<TextBox.Text>
<Binding UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<validationRules:TestValidationRule MyClassTypes="{Binding ViewModelClassTypes}"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
我无法在 MyClassTypes 中获取 ViewModelClassTypes 填充值。谁能告诉我我做错了什么?
自 .Net 4.5 以来实现数据验证的首选方法是让您的视图模型实现 INotifyDataErrorInfo
(example from Technet, example from MSDN (Silverlight)).
注意:INotifyDataErrorInfo
替换了过时的 IDataErrorInfo
。
与 INotifyDataErrorInfo
接口相关的新框架基础设施提供了许多优势,例如
- 支持每个 属性 多个错误
- 自定义错误对象和视觉错误反馈的自定义(例如,使视觉提示适应自定义错误对象)
- 异步验证使用async/await
INotifyDataErrorInfo
的工作原理
当 Binding
的 ValidatesOnNotifyDataErrors
属性 设置为 true
时,绑定引擎将在绑定源上搜索 INotifyDataErrorInfo
实现并订阅 INotifyDataErrorInfo.ErrorsChanged
活动。
如果引发绑定源的 ErrorsChanged
事件并且 INotifyDataErrorInfo.HasErrors
求值为 true
,绑定引擎将为实际源调用 INotifyDataErrorInfo.GetErrors(propertyName)
方法 属性 检索相应的错误消息,然后将可自定义的验证错误模板应用到目标控件以可视化验证错误。
默认情况下,在验证失败的元素周围绘制红色边框。
如果出现错误,即 INotifyDataErrorInfo.HasErrors
returns true
,绑定引擎还将在绑定目标上设置附加的 Validation
属性,例如 Validation.HasError
和 Validation.ErrorTemplate
.
要自定义视觉错误反馈,我们可以覆盖绑定引擎提供的默认模板,方法是覆盖附加的 Validation.ErrorTemplate
属性 的值(参见下面的示例)。
仅当特定数据绑定的 Binding.ValidatesOnNotifyDataErrors
设置为 true
且 Binding.Mode
设置为 BindingMode.TwoWay
或 [=40 时,才会执行所描述的验证过程=].
如何实现INotifyDataErrorInfo
以下示例显示了使用
进行 属性 验证的三种变体- a
ValidationRule
(class封装实际的数据验证实现) - lambda 表达式(或委托)
- 验证属性(用于装饰已验证的属性)。
当然,您可以组合所有三种变体以提供最大的灵活性。
代码未经测试。这些片段应该都可以工作,但可能由于输入错误而无法编译。此代码旨在提供有关如何实现 INotifyDataErrorInfo
接口的简单示例。
正在准备视图
MainWindow.xaml
要启用视觉数据验证反馈,Binding.ValidatesOnNotifyDataErrors
属性 必须在每个相关 Binding
上设置为 true
,即 [=22] 的来源=] 是经过验证的 属性。然后 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
的实现与 INotifyPropertyChanged
实现一起移动到基础 class(例如抽象 ViewModel
class)中,并让所有视图模型继承它。这使得验证逻辑可重用并保持您的视图模型 classes 干净。
您可以更改 INotifyDataErrorInfo
示例的实现细节以满足要求。
1 使用 ValidationRule
进行数据验证
ViewModel.cs
使用 ValidationRule
时,关键是要为每个 属性 或规则提供单独的 ValidationRule
实现。
扩展 ValidationRule
是可选的。我选择扩展 ValidationRule
因为它已经提供了完整的验证 API 并且因为如果需要可以通过绑定验证重用这些实现。
基本上,属性 验证的结果应该是 bool
以指示验证失败或成功,并且可以向用户显示一条消息以帮助他修复输入。
如果出现验证错误,我们所要做的就是生成一条错误消息,将其添加到私有字符串集合中,以允许我们的 INotifyDataErrorInfo.GetErrors(propertyName)
实现 return 来自的正确错误消息此集合并引发 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
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, IList<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 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 List<ValidationRule> propertyValidationRules))
{
// Apply all the rules that are associated with the current property
// and validate the property's value
IEnumerable<string> 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.
public bool ClearErrors(string propertyName)
{
this.ValidatedAttributedProperties.Remove(propertyName);
if (this.Errors.Remove(propertyName))
{
OnErrorsChanged(propertyName);
return true;
}
return false;
}
// Optional method to check if a certain 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 retirn 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>();
// 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, IList<object>> Errors { get; }
// Maps a property name to a list of ValidationRules that belong to this property
private Dictionary<string, IList<ValidationRule>> ValidationRules { get; }
}
UserInputValidationRule.cs
此示例验证规则扩展 ValidationRule
并检查输入是否以“@”字符开头。如果不是,它 return 是一个无效的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;
}
}
2 使用 lambda 表达式和委托进行数据验证
作为替代方法,可以将 ValidationRule
替换(或组合)为委托以启用 Lambda 表达式或方法组的使用。
此示例中的验证表达式 return 一个包含布尔值的元组,用于指示验证状态和 string
实际消息的错误对象集合。由于所有与错误对象相关的属性都是 object
类型,因此表达式可以 return 任何自定义数据类型,以防您需要更高级的错误反馈并且 string
不是足够的错误对象。在这种情况下,我们还必须调整验证错误模板,使其能够处理数据类型。
ViewModel.cs
public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
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.
// Example uses System.ValueTuple.
private string userInputAlternativeValidation;
public string UserInputAlternativeValidation
{
get => this.userInputAlternativeValidation;
set
{
// Use Method group
if (IsPropertyValid(value, AlternativeValidation))
{
this.userInputAlternativeValidation = value;
OnPropertyChanged();
}
}
}
// Constructor
public ViewModel()
{
this.Errors = new Dictionary<string, IList<object>>();
}
// The validation handler
private (bool IsValid, IEnumerable<object> ErrorMessages) AlternativeValidation(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 if the Errors collection changes.
public bool ClearErrors(string propertyName)
{
this.ValidatedAttributedProperties.Remove(propertyName);
if (this.Errors.Remove(propertyName))
{
OnErrorsChanged(propertyName);
return true;
}
return false;
}
// Optional method to check if a certain 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 retirn 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>();
// 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, IList<object>> Errors { get; }
}
3 使用 ValidationAttribute
进行数据验证
这是 INotifyDataErrorInfo
与 ValidationAttribute
support e.g., MaxLengthAttribute
的示例实现。该解决方案结合了以前的 Lambda 版本,以额外支持同时使用 Lambda expression/delegate 进行验证。
虽然必须通过调用属性 setter 中的 TryValidateProperty
方法来显式调用使用 lambda 表达式或委托的验证,但属性验证是从 OnPropertyChanged
事件调用器隐式执行的(一旦属性 用验证属性装饰):
ViewModel.cs
public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
private string userInputAttributeValidation;
// Validate property using validation attributes
[MaxLength(Length = 5, ErrorMessage = "Only five characters allowed.")]
public string UserInputAttributeValidation
{
get => this.userInputAttributeValidation;
set
{
// Optional call to 'IsPropertyValid' to combine attribute validation
// with a delegate
bool isValueValid = IsPropertyValid(value, newValue => newValue.StartsWith("@")
? (true, Enumerable.Empty<object>())
: (false, new[] { "Value must start with '@'." }));
// Optionally reject value if validation h as failed
if (isValueValid)
{
this.userInputAttributeValidation = 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>>();
this.ValidatedAttributedProperties = new HashSet<string>();
}
// Validate property using decorating attributes.
// Is invoked by 'OnPropertyChanged' (see below).
private bool IsAttributedPropertyValid<TValue>(TValue value, string propertyName)
{
this.ValidatedAttributedProperties.Add(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;
AddErrorRange(validationResults.Select(attributeValidationResult => attributeValidationResult.ErrorMessage));
}
}
return isValueValid;
}
// Example uses System.ValueTuple
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(validationResult.ErrorMessages);
}
bool isAttributedPropertyValid = IsAttributedPropertyValid(value, propertyName);
return isAttributedPropertyValid && 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 if the Errors collection changes.
public bool ClearErrors(string propertyName)
{
this.ValidatedAttributedProperties.Remove(propertyName);
if (this.Errors.Remove(propertyName))
{
OnErrorsChanged(propertyName);
return true;
}
return false;
}
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>();
// 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)
{
// Check if IsAttributedPropertyValid Property was already called by 'IsValueValid'.
if (!this.ValidatedAttributedProperties.Contains(propertyName))
{
_ = IsAttributedPropertyValid(value, 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; }
// Track attribute validation calls
private HashSet<string> ValidatedAttributedProperties { get; }
}