WPF MVVM 使用 IDataErrorInfo 进行验证 ViewModel






4.68/5 (19投票s)
实现 IDataErrorInfo 的基础 ViewModel
引言
在编写 WPF 应用程序时,MVVM 中的验证主要通过 IDataErrorInfo
接口完成。数据通过实现 IDataErrorInfo
接口的 viewmodel
绑定到控件。
我们将介绍一个基础 ViewModel 的一些概念,称之为 ViewModelBase
,并将其扩展到 ValidationViewModelBase
。
Using the Code
实现 IDataErrorInfo
所涉及的大部分样板代码是评估单个属性的错误,以及查看整个对象的状态并将其判定为有效或无效。
我们构建一个示例,该示例包含
- 用户输入为
string
,其长度遵循 3 条简单的业务规则- 必须是 2 的倍数
- 大于 10 位
- 小于 32 位
- 仅当用户输入遵循规则(有效)时,才可以单击“确定”按钮。
无效状态将禁用“确定”按钮。
一旦用户输入正确,错误就会清除,“确定”按钮就会启用。
实现基于我们稍后将解释的基础类 ValidationViewModel.cs。UI 包含一个常规的 TextBox
和一个 Button
。
DataContext
被设置为绑定到 TextBox Text
属性,如下所示:
<TextBox Text="{Binding Aid,UpdateSourceTrigger=PropertyChanged,Mode=TwoWay,ValidatesOnDataErrors=True}"
重写默认的 ErrorTemplate
以更改 Background
颜色
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Pink"/>
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
在示例 viewmodel
上实现 ValidationViewModel
可以如下实现,对应于我们最初的两个用例。
1. 实现业务规则
该规则使用 AddRule()
方法以 Func<bool>
的形式添加到规则字典中。
public ViewModel()
{
base.AddRule(() => Aid, () =>
Aid.Length >= (5 * 2) &&
Aid.Length <= (16 * 2) &&
Aid.Length % 2 == 0, "Invalid AID.");
}
2. 定义“确定”按钮的行为
这是通过使用 RelayCommand
实现的,该命令使用 HasErrors
来评估 ICommand.CanExecute
。
public ICommand OkCommand
{
get
{
return Get(()=>OkCommand, new RelayCommand(
()=> MessageBox.Show("Ok pressed"),
()=> !base.HasErrors));
}
}
另外,顺便说一句,不需要命令的 private
字段,因为结果会被缓存,并且每次调用 getter 时都会返回相同的命令。
实现 ViewModelBase
首先是通用的 ViewModelBase
,它将实现 INotifyPropertyChanged
。此外,在基础类中,我们处理了很多通用问题。
-
移除 PropertyChanged 事件中的“魔术字符串”
这是一个常见问题,使用
Expression
可以消除对属性string
的需求,这是一个非常好的解决方案。这很好,因为它消除了输入错误,并使重构变得容易。代码主要是 PRISM 库中的
NotificationObject
。protected static string GetPropertyName<T>(Expression<Func<T>> expression) { if (expression == null) throw new ArgumentNullException("expression"); Expression body = expression.Body; MemberExpression memberExpression = body as MemberExpression; if (memberExpression == null) { memberExpression = (MemberExpression)((UnaryExpression)body).Operand; } return memberExpression.Member.Name; }
-
通用 Getter
我们有一个属性名到值的映射,用于映射相应属性的最后已知值。
private Dictionary<string, object> propertyValueMap; protected ViewModelBase() { propertyValueMap = new Dictionary<string, object>(); }
我们有一个
Get
方法,它接受一个Expression
,用于提取属性名和默认值。protected T Get<T>(Expression<Func<T>> path) { return Get(path, default(T)); } protected virtual T Get<T>(Expression<Func<T>> path, T defaultValue) { var propertyName = GetPropertyName(path); if (propertyValueMap.ContainsKey(propertyName)) { return (T)propertyValueMap[propertyName]; } else { propertyValueMap.Add(propertyName, defaultValue); return defaultValue; } }
-
通用 Setter
在属性映射的基础上,我们有一个通用的 setter,用于引发
PropertyChanged
事件。protected void Set<T>(Expression<Func<T>> path, T value) { Set(path, value, false); } protected virtual void Set<T>(Expression<Func<T>> path, T value, bool forceUpdate) { var oldValue = Get(path); var propertyName = GetPropertyName(path); if (!object.Equals(value, oldValue) || forceUpdate) { propertyValueMap[propertyName] = value; OnPropertyChanged(path); } }
实现 ValidationViewModel
在之前的 ViewModelBase
的基础上,我们在 ValidationViewModel
上实现了 IDataErrorInfo
接口。它暴露的功能是:
1. 添加对应特定属性的规则的方法
该类公开了一个 AddRule()
方法,该方法接受属性、一个求值为 bool
的委托函数以及规则失败时显示的错误消息 string
。此委托被添加到对应属性名的 ruleMap
中。
为同一属性添加多个规则的功能留给客户端自行决定,如果属性名(键)已存在,AddRule()
将抛出 ArgumentException。
private Dictionary<string, Binder> ruleMap = new Dictionary<string, Binder>();
public void AddRule<T>(Expression<Func<T>> expression, Func<bool> ruleDelegate, string errorMessage)
{
var name = GetPropertyName(expression);
ruleMap.Add(name, new Binder(ruleDelegate, errorMessage));
}
Binder
类的实现很简单,它仅用于封装数据验证的功能。
Binder
类有一个 IsDirty
属性,用于判断当前值是否已更改。每当属性值更新时,都会设置此属性。还有一个 Update()
方法,用于评估注册规则时传递的规则。
internal string Error { get; set; }
internal bool HasError { get; set; }
internal bool IsDirty { get; set; }
internal void Update()
{
if (!IsDirty)
return;
Error = null;
HasError = false;
try
{
if (!ruleDelegate())
{
Error = message;
HasError = true;
}
}
catch (Exception e)
{
Error = e.Message;
HasError = true;
}
}
Update()
方法进行了一些优化,当属性未更改时,不重新评估 ruleDelegate
。
2. 重写 Set 方法以设置 IsDirty 标志
protected override void Set<T>(Expression<Func<T>> path, T value, bool forceUpdate)
{
ruleMap[GetPropertyName(path)].IsDirty = true;
base.Set<T>(path, value, forceUpdate);
}
3. 全局 HasErrors,用于检查整个 ViewModel 状态的有效性
public bool HasErrors
{
get
{
var values = ruleMap.Values.ToList();
values.ForEach(b => b.Update());
return values.Any(b => b.HasError);
}
}
4. IDataErrorInfo 的实现。Error 属性将错误消息连接成一条消息。
public string Error
{
get
{
var errors = from b in ruleMap.Values where b.HasError select b.Error;
return string.Join("\n", errors);
}
}
public string this[string columnName]
{
get
{
if (ruleMap.ContainsKey(columnName))
{
ruleMap[columnName].Update();
return ruleMap[columnName].Error;
}
return null;
}
}
这就是我对 WPF 验证的全部介绍。
整个代码本质上是聚合信息,并为您提供一个封装好的基类来处理您的自定义业务规则。
希望有人觉得有用。
请留下您的评论……
历史
- 2014 年 9 月 25 日 - 添加了依赖 DLL
- 2014 年 6 月 10 日 - 初稿