gpt4 book ai didi

wpf - 如何添加验证以查看模型属性或如何实现 INotifyDataErrorInfo

转载 作者:行者123 更新时间:2023-12-04 13:43:07 26 4
gpt4 key购买 nike

我有一个 ObservableCollection 类型的数据集合(比如实例作为 myClassTypes)。在一些用户操作之后,这个 myClassTypes 填充了 ViewModel 中的值。在 View 中,有一个文本框,用户可以在其中输入文本。我需要根据 myClassTypes 值验证文本框数据。因此,如果 myClassTypes 包含用户在文本框中插入的文本,则验证通过,否则将失败。
我的代码片段是:
View 模型:

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 以来,实现数据验证的首选方法是让您的 View 模型实现 INotifyDataErrorInfo (来自 Technet 的示例,来自 MSDN (Silverlight) 的示例)。
注意: INotifyDataErrorInfo 替换过时的 IDataErrorInfo
INotifyDataErrorInfo 的工作原理
ValidatesOnNotifyDataErrorsBinding 属性设置为 true 时,绑定(bind)引擎将在绑定(bind)源上搜索 INotifyDataErrorInfo 实现并订阅 INotifyDataErrorInfo.ErrorsChanged 事件。
如果绑定(bind)源的 ErrorsChanged 事件被引发并且 INotifyDataErrorInfo.HasErrors 评估为 true ,则绑定(bind)引擎将调用实际源属性的 INotifyDataErrorInfo.GetErrors() 方法以检索相应的错误消息,然后将可自定义的验证错误模板应用于目标控件以可视化验证错误。
默认情况下,会在验证失败的元素周围绘制红色边框。
此验证反馈可视化过程仅在特定数据绑定(bind)上将 Binding.ValidatesOnNotifyDataErrors 设置为 true 并且 Binding.Mode 设置为 BindingMode.TwoWayBindingMode.OneWayToSource 时才执行。
如何实现INotifyDataErrorInfo以下示例显示使用 ValidationRule(用于封装实际数据验证实现)和 Lambda(或委托(delegate))的默认验证。最后一个示例展示了如何使用验证属性实现数据验证。
代码没有经过测试。这些片段应该都可以工作,但由于输入错误可能无法编译。此代码旨在提供一个关于如何实现 INotifyDataErrorInfo 接口(interface)的简单示例。

ViewModel.cs
View 模型负责验证其属性以确保模型的数据完整性。
从 .NET 4.5 开始,推荐的方式是让 View 模型实现 INotifyDataErrorInfo 接口(interface)。
关键是为每个属性或规则有单独的 ValidationRule 实现:

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);
}
}

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 List<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; }

// 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;
}
}
MainWindow.xaml
要启用可视化数据验证反馈,必须在每个相关 Binding.ValidatesOnNotifyDataErrors 上将 true 属性设置为 Binding 。然后 WPF 框架将显示控件的默认错误反馈。请注意,要使此工作正常运行, Binding.Mode 必须是 OneWayToSourceTwoWay(这是 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>
以下是验证错误模板,以防您想自定义可视化表示(可选)。它通过附加属性 TextBox (见上文)设置在经过验证的元素(在本例中为 Validation.ErrorTemplate )上:
<ControlTemplate x:Key=ValidationErrorTemplate>
<StackPanel>
<!-- Placeholder for the DataGridTextColumn itself -->
<AdornedElementPlaceholder />
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ControlTemplate>
</Validation.ErrorTemplate>

除了我提供的链接之外,您还可以在网上找到许多示例。
我建议将 INotifyDataErrorInfo 的实现移动到基类中(例如 BaseViewModel) together with INotifyPropertyChanged` 并让所有 View 模型继承它。这使得验证逻辑可重用并保持 View 模型类清洁。
您可以更改 INotifyDataErrorInfo 的实现细节以满足要求。
评论
作为替代方法,可以将 ValidationRule 替换为委托(delegate)以启用 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 表达式/委托(delegate)的验证:
public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
private string userInputAttributeValidation;

[Required(ErrorMessage = "Value is required.")]
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; }
}

关于wpf - 如何添加验证以查看模型属性或如何实现 INotifyDataErrorInfo,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56606441/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com