INotifyDataErrorInfo 和绑定异常
INotifyDataErrorInfo and binding exceptions
我正在使用 INotifyDataErrorInfo 接口来实现通用的 MVVM 验证机制。我通过调用 OnValidate 而不是 OnPropertyChanged:
来实现接口
public void OnValidate(dynamic value, [CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
Validate(propertyName, value);
}
在我生成验证错误的验证方法中,将它们添加到字典并在发现或清除验证错误时引发 ErrorsChanged 事件:
if (entry.Validate(strValue, out errorNumber, out errorString) == false)
{
_validationErrors[propertyName] = new List<string> {errorString};
RaiseErrorsChanged(propertyName);
}
else if (_validationErrors.ContainsKey(propertyName))
{
_validationErrors.Remove(propertyName);
RaiseErrorsChanged(propertyName);
}
HasErrors 属性 是通过查看错误字典实现的:
public bool HasErrors
{
get { return _validationErrors.Any(kv => kv.Value != null
&& kv.Value.Count > 0); }
}
防止在出现验证错误时启用保存按钮 - 保存命令 canExecuteMethod 查看 HasErrors 属性:
private bool IsSaveEnabled()
{
return HasErrors == false;
}
一切正常,除了我遇到绑定错误的情况 - 如果绑定值是(例如)整数,则输入非整数 - 文本框的 ErrorContent 更新为错误字符串:“无法转换值 'something'”。
但是 INotifyDataErrorInfo 机制并未对此进行更新。尽管视图中存在错误,但 HasErrors 仍然为假并启用保存。
我想找到一种方法将绑定异常传播到 INotifyDataErrorInfo 机制,这样我就能够:
- 禁用保存按钮(必须)。
- 将验证错误消息更改为更有意义的错误字符串(很高兴)。
我想找到一个通用的 MVVM 解决方案,而无需在视图中添加代码。
感谢您的帮助
设置
在您的绑定表达式中。
string int 案例不适用于 MVVM,因为您的视图模型由于绑定异常而未获取任何信息。
我看到有两种方法可以获得您想要的验证:
- 只需在您的视图模型中使用字符串属性,当您必须转到您的模型时,只需将字符串转换为您的模型类型即可。
- 创建行为或 "special" 控件,以便您视图中的输入始终 "convertible" 您的视图模型类型。
顺便说一句,我使用第二种方法,因为我必须 :) 但第一种方法总是有效,而且对我来说似乎更容易。
这是我找到的解决方案。它使 INotifyDataErrorInfo 在 ViewModel 中表现正确(当存在任何验证错误时 – HasError 为真),并且它允许从 viewModel 添加验证错误。除此之外,它不需要更改视图、更改绑定或转换器。
此解决方案涉及:
- 添加自定义验证规则。
- 添加基本用户控件(所有视图都必须派生自该控件)。
- 在 ViewModel 基础中添加一些代码。
添加自定义验证规则 – 执行实际验证并在验证更改时引发事件的验证实体:
class ValidationEntity : ValidationRule
{
public string Key { get; set; }
public string BaseName = "Base";
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
var fullPropertyName = BaseName + "." + Key;
ValidationEntry entry;
var validationResult = new ValidationResult(true, null);
if ((entry = ValidationManager.Instance.FindValidation(fullPropertyName)) != null)
{
int errorNumber;
string errorString;
var strValue = (value != null) ? value.ToString() : string.Empty;
if (entry.Validate(strValue, out errorNumber, out errorString) == false)
{
validationResult = new ValidationResult(false, errorString);
}
}
if (OnValidationChanged != null)
{
OnValidationChanged(Key, validationResult);
}
return validationResult;
}
public event Action<string, ValidationResult> OnValidationChanged;
}
添加一个保留活动文本框列表的基本用户控件,并将验证规则添加到每个文本框绑定:
这是用户控制库中的代码:
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
_textBoxes = FindAllTextBoxs(this);
var vm = DataContext as ViewModelBase;
if (vm != null) vm.UpdateAllValidationsEvent += OnUpdateAllValidationsEvent;
foreach (var textbox in _textBoxes)
{
var binding = BindingOperations.GetBinding(textbox, TextBox.TextProperty);
if (binding != null)
{
var property = binding.Path.Path;
var validationEntity = new ValidationEntity {Key = property};
binding.ValidationRules.Add(validationEntity);
validationEntity.ValidationChanged += OnValidationChanged;
}
}
}
private List<TextBox> FindAllTextBoxs(DependencyObject fe)
{
return FindChildren<TextBox>(fe);
}
private List<T> FindChildren<T>(DependencyObject dependencyObject)
where T : DependencyObject
{
var items = new List<T>();
if (dependencyObject is T)
{
items.Add(dependencyObject as T);
return items;
}
var count = VisualTreeHelper.GetChildrenCount(dependencyObject);
for (var i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(dependencyObject, i);
var children = FindChildren<T>(child);
items.AddRange(children);
}
return items;
}
当 ValidationChange 事件发生时——调用视图以通知验证错误:
private void OnValidationChanged(string propertyName, ValidationResult validationResult)
{
var vm = DataContext as ViewModelBase;
if (vm != null)
{
if (validationResult.IsValid)
{
vm.ClearValidationErrorFromView(propertyName);
}
else
{
vm.AddValidationErrorFromView(propertyName, validationResult.ErrorContent as string);
}
}
}
ViewModel 基础保留两个列表:
- _notifyvalidationErrors,INotifyDataErrorInfo 接口使用它来显示验证错误。
- _privateValidationErrors,用于向用户显示Validation规则产生的错误。
从视图中添加验证错误时 – _notifyvalidationErrors 更新为空值(仅表示存在验证错误)错误字符串未添加到 _notifyvalidationErrors。如果我们将它添加到那里,我们将在文本框 ErrorContent 中获得两次验证错误字符串。
验证错误字符串也被添加到 _privateValidationErrors (因为我们希望能够将其保留在视图模型中)
这是 ViewModel 基础上的代码:
private readonly Dictionary<string, List<string>> _notifyvalidationErrors =
new Dictionary<string, List<string>>();
private readonly Dictionary<string, List<string>> _privateValidationErrors =
new Dictionary<string, List<string>>();
public void AddValidationErrorFromView(string propertyName, string errorString)
{
_notifyvalidationErrors[propertyName] = new List<string>();
// Add the error to the private dictionary
_privateValidationErrors[propertyName] = new List<string> {errorString};
RaiseErrorsChanged(propertyName);
}
public void ClearValidationErrorFromView(string propertyName)
{
if (_notifyvalidationErrors.ContainsKey(propertyName))
{
_notifyvalidationErrors.Remove(propertyName);
}
if (_privateValidationErrors.ContainsKey(propertyName))
{
_privateValidationErrors.Remove(propertyName);
}
RaiseErrorsChanged(propertyName);
}
视图中的 INotifyDataErrorInfo 实现:
public bool HasErrors
{
get { return _notifyvalidationErrors.Any(kv => kv.Value != null); }
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public void RaiseErrorsChanged(string propertyName)
{
var handler = ErrorsChanged;
if (handler != null)
handler(this, new DataErrorsChangedEventArgs(propertyName));
}
public IEnumerable GetErrors(string propertyName)
{
List<string> errorsForProperty;
_notifyvalidationErrors.TryGetValue(propertyName, out errorsForProperty);
return errorsForProperty;
}
用户可以选择通过调用 ViewModelBase AddValidationError 和 ClearValidationError 方法从视图中添加验证错误。
public void AddValidationError(string errorString, [CallerMemberName] string propertyName = null)
{
_notifyvalidationErrors[propertyName] = new List<string>{ errorString };
RaiseErrorsChanged(propertyName);
}
public void ClearValidationError([CallerMemberName] string propertyName = null)
{
if (_notifyvalidationErrors.ContainsKey(propertyName))
{
_notifyvalidationErrors.Remove(propertyName);
RaiseErrorsChanged(propertyName);
}
}
视图可以通过调用 GetValidationErrors 和 GetValidationErrorsString 方法从 ViewModel 库中获取所有当前验证错误的列表。
public List<string> GetValidationErrors()
{
var errors = new List<string>();
foreach (var key in _notifyvalidationErrors.Keys)
{
errors.AddRange(_notifyvalidationErrors[key]);
if (_privateValidationErrors.ContainsKey(key))
{
errors.AddRange(_privateValidationErrors[key]);
}
}
return errors;
}
public string GetValidationErrorsString()
{
var errors = GetValidationErrors();
var sb = new StringBuilder();
foreach (var error in errors)
{
sb.Append("● ");
sb.AppendLine(error);
}
return sb.ToString();
}
我正在使用 INotifyDataErrorInfo 接口来实现通用的 MVVM 验证机制。我通过调用 OnValidate 而不是 OnPropertyChanged:
来实现接口public void OnValidate(dynamic value, [CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
Validate(propertyName, value);
}
在我生成验证错误的验证方法中,将它们添加到字典并在发现或清除验证错误时引发 ErrorsChanged 事件:
if (entry.Validate(strValue, out errorNumber, out errorString) == false)
{
_validationErrors[propertyName] = new List<string> {errorString};
RaiseErrorsChanged(propertyName);
}
else if (_validationErrors.ContainsKey(propertyName))
{
_validationErrors.Remove(propertyName);
RaiseErrorsChanged(propertyName);
}
HasErrors 属性 是通过查看错误字典实现的:
public bool HasErrors
{
get { return _validationErrors.Any(kv => kv.Value != null
&& kv.Value.Count > 0); }
}
防止在出现验证错误时启用保存按钮 - 保存命令 canExecuteMethod 查看 HasErrors 属性:
private bool IsSaveEnabled()
{
return HasErrors == false;
}
一切正常,除了我遇到绑定错误的情况 - 如果绑定值是(例如)整数,则输入非整数 - 文本框的 ErrorContent 更新为错误字符串:“无法转换值 'something'”。 但是 INotifyDataErrorInfo 机制并未对此进行更新。尽管视图中存在错误,但 HasErrors 仍然为假并启用保存。 我想找到一种方法将绑定异常传播到 INotifyDataErrorInfo 机制,这样我就能够:
- 禁用保存按钮(必须)。
- 将验证错误消息更改为更有意义的错误字符串(很高兴)。
我想找到一个通用的 MVVM 解决方案,而无需在视图中添加代码。
感谢您的帮助
设置
在您的绑定表达式中。
string int 案例不适用于 MVVM,因为您的视图模型由于绑定异常而未获取任何信息。
我看到有两种方法可以获得您想要的验证:
- 只需在您的视图模型中使用字符串属性,当您必须转到您的模型时,只需将字符串转换为您的模型类型即可。
- 创建行为或 "special" 控件,以便您视图中的输入始终 "convertible" 您的视图模型类型。
顺便说一句,我使用第二种方法,因为我必须 :) 但第一种方法总是有效,而且对我来说似乎更容易。
这是我找到的解决方案。它使 INotifyDataErrorInfo 在 ViewModel 中表现正确(当存在任何验证错误时 – HasError 为真),并且它允许从 viewModel 添加验证错误。除此之外,它不需要更改视图、更改绑定或转换器。
此解决方案涉及:
- 添加自定义验证规则。
- 添加基本用户控件(所有视图都必须派生自该控件)。
- 在 ViewModel 基础中添加一些代码。
添加自定义验证规则 – 执行实际验证并在验证更改时引发事件的验证实体:
class ValidationEntity : ValidationRule
{
public string Key { get; set; }
public string BaseName = "Base";
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
var fullPropertyName = BaseName + "." + Key;
ValidationEntry entry;
var validationResult = new ValidationResult(true, null);
if ((entry = ValidationManager.Instance.FindValidation(fullPropertyName)) != null)
{
int errorNumber;
string errorString;
var strValue = (value != null) ? value.ToString() : string.Empty;
if (entry.Validate(strValue, out errorNumber, out errorString) == false)
{
validationResult = new ValidationResult(false, errorString);
}
}
if (OnValidationChanged != null)
{
OnValidationChanged(Key, validationResult);
}
return validationResult;
}
public event Action<string, ValidationResult> OnValidationChanged;
}
添加一个保留活动文本框列表的基本用户控件,并将验证规则添加到每个文本框绑定: 这是用户控制库中的代码:
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
_textBoxes = FindAllTextBoxs(this);
var vm = DataContext as ViewModelBase;
if (vm != null) vm.UpdateAllValidationsEvent += OnUpdateAllValidationsEvent;
foreach (var textbox in _textBoxes)
{
var binding = BindingOperations.GetBinding(textbox, TextBox.TextProperty);
if (binding != null)
{
var property = binding.Path.Path;
var validationEntity = new ValidationEntity {Key = property};
binding.ValidationRules.Add(validationEntity);
validationEntity.ValidationChanged += OnValidationChanged;
}
}
}
private List<TextBox> FindAllTextBoxs(DependencyObject fe)
{
return FindChildren<TextBox>(fe);
}
private List<T> FindChildren<T>(DependencyObject dependencyObject)
where T : DependencyObject
{
var items = new List<T>();
if (dependencyObject is T)
{
items.Add(dependencyObject as T);
return items;
}
var count = VisualTreeHelper.GetChildrenCount(dependencyObject);
for (var i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(dependencyObject, i);
var children = FindChildren<T>(child);
items.AddRange(children);
}
return items;
}
当 ValidationChange 事件发生时——调用视图以通知验证错误:
private void OnValidationChanged(string propertyName, ValidationResult validationResult)
{
var vm = DataContext as ViewModelBase;
if (vm != null)
{
if (validationResult.IsValid)
{
vm.ClearValidationErrorFromView(propertyName);
}
else
{
vm.AddValidationErrorFromView(propertyName, validationResult.ErrorContent as string);
}
}
}
ViewModel 基础保留两个列表:
- _notifyvalidationErrors,INotifyDataErrorInfo 接口使用它来显示验证错误。
- _privateValidationErrors,用于向用户显示Validation规则产生的错误。
从视图中添加验证错误时 – _notifyvalidationErrors 更新为空值(仅表示存在验证错误)错误字符串未添加到 _notifyvalidationErrors。如果我们将它添加到那里,我们将在文本框 ErrorContent 中获得两次验证错误字符串。 验证错误字符串也被添加到 _privateValidationErrors (因为我们希望能够将其保留在视图模型中) 这是 ViewModel 基础上的代码:
private readonly Dictionary<string, List<string>> _notifyvalidationErrors =
new Dictionary<string, List<string>>();
private readonly Dictionary<string, List<string>> _privateValidationErrors =
new Dictionary<string, List<string>>();
public void AddValidationErrorFromView(string propertyName, string errorString)
{
_notifyvalidationErrors[propertyName] = new List<string>();
// Add the error to the private dictionary
_privateValidationErrors[propertyName] = new List<string> {errorString};
RaiseErrorsChanged(propertyName);
}
public void ClearValidationErrorFromView(string propertyName)
{
if (_notifyvalidationErrors.ContainsKey(propertyName))
{
_notifyvalidationErrors.Remove(propertyName);
}
if (_privateValidationErrors.ContainsKey(propertyName))
{
_privateValidationErrors.Remove(propertyName);
}
RaiseErrorsChanged(propertyName);
}
视图中的 INotifyDataErrorInfo 实现:
public bool HasErrors
{
get { return _notifyvalidationErrors.Any(kv => kv.Value != null); }
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public void RaiseErrorsChanged(string propertyName)
{
var handler = ErrorsChanged;
if (handler != null)
handler(this, new DataErrorsChangedEventArgs(propertyName));
}
public IEnumerable GetErrors(string propertyName)
{
List<string> errorsForProperty;
_notifyvalidationErrors.TryGetValue(propertyName, out errorsForProperty);
return errorsForProperty;
}
用户可以选择通过调用 ViewModelBase AddValidationError 和 ClearValidationError 方法从视图中添加验证错误。
public void AddValidationError(string errorString, [CallerMemberName] string propertyName = null)
{
_notifyvalidationErrors[propertyName] = new List<string>{ errorString };
RaiseErrorsChanged(propertyName);
}
public void ClearValidationError([CallerMemberName] string propertyName = null)
{
if (_notifyvalidationErrors.ContainsKey(propertyName))
{
_notifyvalidationErrors.Remove(propertyName);
RaiseErrorsChanged(propertyName);
}
}
视图可以通过调用 GetValidationErrors 和 GetValidationErrorsString 方法从 ViewModel 库中获取所有当前验证错误的列表。
public List<string> GetValidationErrors()
{
var errors = new List<string>();
foreach (var key in _notifyvalidationErrors.Keys)
{
errors.AddRange(_notifyvalidationErrors[key]);
if (_privateValidationErrors.ContainsKey(key))
{
errors.AddRange(_privateValidationErrors[key]);
}
}
return errors;
}
public string GetValidationErrorsString()
{
var errors = GetValidationErrors();
var sb = new StringBuilder();
foreach (var error in errors)
{
sb.Append("● ");
sb.AppendLine(error);
}
return sb.ToString();
}