65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF MVVM 使用 IDataErrorInfo 进行验证 ViewModel

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (19投票s)

2014年6月10日

CPOL

3分钟阅读

viewsIcon

105409

downloadIcon

5996

实现 IDataErrorInfo 的基础 ViewModel

引言

在编写 WPF 应用程序时,MVVM 中的验证主要通过 IDataErrorInfo 接口完成。数据通过实现 IDataErrorInfo 接口的 viewmodel 绑定到控件。

我们将介绍一个基础 ViewModel 的一些概念,称之为 ViewModelBase ,并将其扩展到 ValidationViewModelBase

Using the Code

实现 IDataErrorInfo 所涉及的大部分样板代码是评估单个属性的错误,以及查看整个对象的状态并将其判定为有效或无效。

我们构建一个示例,该示例包含

  1. 用户输入为 string,其长度遵循 3 条简单的业务规则
    1. 必须是 2 的倍数
    2. 大于 10 位
    3. 小于 32 位
  2. 仅当用户输入遵循规则(有效)时,才可以单击“确定”按钮。

无效状态将禁用“确定”按钮。

一旦用户输入正确,错误就会清除,“确定”按钮就会启用。

实现基于我们稍后将解释的基础类 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。此外,在基础类中,我们处理了很多通用问题。

  1. 移除 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;
            }
  2. 通用 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;
        }
    }
  3. 通用 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 日 - 初稿
© . All rights reserved.