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

Silverlight 基础:验证

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.89/5 (5投票s)

2010年12月9日

Ms-PL

11分钟阅读

viewsIcon

49901

downloadIcon

945

一篇关于 Silverlight 验证的文章:DataAnnotations 和 ValidatesOnExceptions,IDataErrorInfo 和 INotifyDataErrorInfo。

引言

Silverlight 4 提供了一些验证输入值的新方法(在您的应用程序中实现验证的一些新方法)。第一种方法是 DataAnnotation。在这种情况下,您应该使用属性来描述验证规则。另外两种方法(都随 Silverlight 4 推出)是:您应该为您的 ViewModel 实现以下接口之一:IDataErrorInfoINotifyDataErrorInfo。我想讨论所有这些方法,以及使用它们各自的优缺点。本文的目标是找到在我的和您的应用程序中实现输入值验证的最佳方法。

背景

我有一个例子。我想在一个简单的控件中描述所有这些方法:“更改密码”。

Capture_1F726807.png

它有两个PasswordBox 控件、一个按钮和一个ValidationSummary。每个示例都有自己的 ViewModel,但 UserControl 的 XAML 将是相同的。

<UserControl x:Class="SilverlightValidation.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" 
    xmlns:DataAnnotations="clr-namespace:SilverlightValidation.DataAnnotations" 
    xmlns:SilverlightValidation="clr-namespace:SilverlightValidation"    
    mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
 
    <UserControl.DataContext>
        <DataAnnotations:BindingModel />
    </UserControl.DataContext>
    
    <Grid x:Name="LayoutRoot" 
            Background="White" Width="500">
 
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
 
        <TextBlock HorizontalAlignment="Right">New Password:</TextBlock>
 
        <PasswordBox Grid.Column="1" 
            Password="{Binding Path=NewPassword, Mode=TwoWay, 
                      ValidatesOnNotifyDataErrors=True, ValidatesOnExceptions=True, 
                      ValidatesOnDataErrors=True, NotifyOnValidationError=True}" />
 
        <TextBlock Grid.Row="1" 
           HorizontalAlignment="Right">New Password Confirmation:</TextBlock>
 
        <PasswordBox Grid.Row="1"  Grid.Column="1" 
            Password="{Binding Path=NewPasswordConfirmation, Mode=TwoWay, 
                      ValidatesOnNotifyDataErrors=True, ValidatesOnExceptions=True, 
                      ValidatesOnDataErrors=True, NotifyOnValidationError=True}"  />
 
        <Button Grid.Row="2" Grid.ColumnSpan="2" 
                HorizontalAlignment="Right"  Content="Change" 
                Command="{Binding Path=ChangePasswordCommand}" />
 
        <sdk:ValidationSummary Grid.Row="3" Grid.ColumnSpan="2" />
        
    </Grid>
</UserControl>

我已经在密码框的绑定中设置了四个与验证相关的属性。目前,我只谈论 NotifyOnValidationError 属性。我使用它来通知 ValidationSummary 存在一些验证错误。

PasswordBox 只有一个从控件到源的 OneWay 绑定(出于安全原因)。绑定仅在焦点更改时起作用(与 TextBox 相同)。在 WPF 中,您可以更改此行为;您可以设置在用户按下某个键时(按键事件)发生绑定。在 Silverlight 中,您可以使用附加属性完成同样的操作。

public static class UpdateSourceTriggerHelper
{
    public static readonly DependencyProperty UpdateSourceTriggerProperty =
        DependencyProperty.RegisterAttached("UpdateSourceTrigger", 
            typeof(bool), typeof(UpdateSourceTriggerHelper),
            new PropertyMetadata(false, OnUpdateSourceTriggerChanged));
 
    public static bool GetUpdateSourceTrigger(DependencyObject d)
    {
        return (bool)d.GetValue(UpdateSourceTriggerProperty);
    }
 
    public static void SetUpdateSourceTrigger(DependencyObject d, bool value)
    {
        d.SetValue(UpdateSourceTriggerProperty, value);
    }
 
    private static void OnUpdateSourceTriggerChanged(DependencyObject d, 
                        DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue is bool && d is PasswordBox)
        {
            PasswordBox textBox = d as PasswordBox;
            textBox.PasswordChanged -= PassportBoxPasswordChanged;
 
            if ((bool)e.NewValue)
                textBox.PasswordChanged += PassportBoxPasswordChanged;
        }
    }
 
    private static void PassportBoxPasswordChanged(object sender, RoutedEventArgs e)
    {
        var frameworkElement = sender as PasswordBox;
        if (frameworkElement != null)
        {
            BindingExpression bindingExpression = 
              frameworkElement.GetBindingExpression(PasswordBox.PasswordProperty);
            if (bindingExpression != null)
                bindingExpression.UpdateSource();
        }
    }
}

如果您想使用它,应该为密码框设置此附加属性。

<PasswordBox Grid.Column="1" 
  Password="{Binding Path=NewPassword, Mode=TwoWay, ValidatesOnNotifyDataErrors=True, 
            ValidatesOnExceptions=True, ValidatesOnDataErrors=True, 
            NotifyOnValidationError=True}"
    SilverlightValidation:UpdateSourceTriggerHelper.UpdateSourceTrigger="True"/>

我将不使用任何框架,因此我需要自己的 DelegateCommand(一个实现 ICommand 接口的类)。

public class DelegateCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Func<object, bool> _canExecute;
 
    public DelegateCommand(Action<object> execute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");
        _execute = execute;
    }
 
    public DelegateCommand(Action<object> execute, 
                           Func<object, bool> canExecute)
        : this(execute)
    {
        if (canExecute == null)
            throw new ArgumentNullException("canExecute");
        _canExecute = canExecute;
    }
 
    public bool CanExecute(object parameter)
    {
        if (_canExecute != null)
            return _canExecute(parameter);
        return true;
    }
 
    public void Execute(object parameter)
    {
        _execute(parameter);
    }
 
    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged(this, EventArgs.Empty);
    }
 
    public event EventHandler CanExecuteChanged = delegate {};
}

这**不好**,因为它可能导致内存泄漏(DelegateCommand.CanExecuteChanged 事件引起的内存泄漏),因此最好**使用 Prism 2.1 或更高版本**。

在我的示例中,我需要这些验证规则:

  • 新密码是必填字段;
  • 新密码长度限制为 20 个字符(可悲,但许多开发人员会忘记这一点,并从数据库收到生产错误,如“字符串截断”);
  • 新密码确认应与新密码相同。

#1 DataAnnotations 和 ValidatesOnExceptions

我们在 Silverlight 4 之前就有这种方法。我只从第三个版本开始使用 Silverlight,所以可以说在第三个版本中,我们有这种方法。这种验证的唯一优点是大多数 .NET 技术都有它(ASP.NET、WPF、WinForms)。基本思想是——通过抛出异常来显示验证错误。在 Silverlight 3 中,除了您自己的实现之外,通过抛出异常进行验证是实现验证的唯一方法。如果您想使用这种类型的验证,您应该在绑定中将 ValidatesOnExceptions 设置为 true。

让我们创建我们的绑定模型。所有示例都将从实现 BindingModel 类开始,该类实现了 INotifyPropertyChanged 接口,并包含三个字段:两个字符串字段用于存储密码,以及一个命令。我将此命令绑定到用户界面中的一个按钮;此命令将执行更改密码的操作(我将在构造函数中初始化此命令)。

public class BindingModel : INotifyPropertyChanged
{
    private string _newPassword;
    private string _newPasswordConfirmation;
 
    public DelegateCommand ChangePasswordCommand { get; private set; }
 
    #region INotifyPropertyChanged
 
    public event PropertyChangedEventHandler PropertyChanged = delegate { };
 
    private void OnPropertyChanged(string propertyName)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
 
    #endregion
}

我还需要两个辅助方法。

private bool IsValidObject()
{
    ICollection<ValidationResult> results = new Collection<ValidationResult>();
    return Validator.TryValidateObject(this, new ValidationContext(this, null, null), 
                     results, true) && results.Count == 0;
}
 
private void ValidateProperty(string propertyName, object value)
{
    Validator.ValidateProperty(value, 
      new ValidationContext(this, null, null) { MemberName = propertyName });
}

首先,我们将检查整个对象 BindingModel 是否有效。第二个方法检查某个属性是否有效。这两种方法都使用 Validator 类,这是 Silverlight 的基础结构。此类获取属性的所有验证规则并检查规则是否处于有效状态。IsValidObject 方法返回布尔结果。ValidateProperty 方法在验证规则状态无效时抛出异常。您可以使用继承自 ValidationAttribute 的属性来描述验证规则(您可以创建继承自此的自己的属性):StringLengthAttributeRequiredAttributeRegularExpressionAttributeRangeAttributeDataTypeAttributeCustomValidationAttribute。您可以从它们的链接中了解所有这些属性。我将描述我的属性的规则。

[Required]
[StringLength(20)]
[Display(Name = "New password")]
public string NewPassword
{
    get { return _newPassword; }
    set
    {
        _newPassword = value;
        OnPropertyChanged("NewPassword");
        ChangePasswordCommand.RaiseCanExecuteChanged();
        ValidateProperty("NewPassword", value);
    }
}
 
[CustomValidation(typeof(BindingModel), "CheckPasswordConfirmation")]
[Display(Name = "New password confirmation")]
public string NewPasswordConfirmation
{
    get { return _newPasswordConfirmation; }
    set
    {
        _newPasswordConfirmation = value;
        OnPropertyChanged("NewPasswordConfirmation");
        ChangePasswordCommand.RaiseCanExecuteChanged();
        ValidateProperty("NewPasswordConfirmation", value);
    }
}

NewPassword 属性很简单。它使用 RequiredStringLength 两个属性实现了两个验证规则。它还有一个DisplayAttribute,它为 ValidationSummary 等控件设置属性名称。如果属性没有当前属性,那么我们将在 ValidationSummary 中看到“NewPassword”。两个属性在设置方法中都具有相同的操作数序列:设置值、通知值已更改、触发命令的 CanExecute 方法(如果使用它),以及最后一个操作数检查当前属性的验证规则。NewPassword 属性具有 CustomValidationAttribute。此属性包含有关包含该方法的类类型和方法的​​信息。该方法应执行验证。该属性期望一个公共静态方法,并应返回一个 ValidationResult;第一个参数应具有属性类型。您可以将第二个参数设置为 ValidationContext。在我的情况下,我有

public static ValidationResult CheckPasswordConfirmation(string value, 
                               ValidationContext context)
{
    var bindingModel = context.ObjectInstance as BindingModel;
    if (bindingModel == null)
        throw new NotSupportedException("ObjectInstance must be BindingModel");
 
    if (string.CompareOrdinal(bindingModel._newPassword, value) != 0)
        return new ValidationResult("Password confirmation not equal to password.");
 
    return ValidationResult.Success;
}

我们还应该实现一个命令和命令的方法。

public BindingModel()
{
    ChangePasswordCommand = 
       new DelegateCommand(ChangePassword, CanChangePassword);
}
 
private bool CanChangePassword(object arg)
{
    return IsValidObject();
}
 
private void ChangePassword(object obj)
{
    if (ChangePasswordCommand.CanExecute(obj))
    {
        MessageBox.Show("Bingo!");
    }
}

对于两个 PasswordBox 控件,我在 XAML 中设置了 UpdateSourceTrigger="True"。您可以在我的博客上尝试此示例。

让我们来谈谈这种方法的缺点。主要问题是——我们在 set 方法中抛出异常。这可能会很混乱。如果您使用调试器运行应用程序,您将看到大量多余的信息(尤其是在为每个按键事件设置绑定时)。此外,您无法在没有后端属性或方法的情况下将 nullstring.Empty(空属性)设置为 NewPassword 属性(您无法使用默认属性,因为它会抛出异常)。在我的情况下,我无法在控件加载时将 null 设置为 NewPasswordNewPasswordConfirmation 属性,因为我将收到异常。因此,我需要为每个控件编写单独的方法,例如 Set[Property]Value。因此,这些属性只能用于绑定。实际上,这很混乱。

另一个问题是实现。我不喜欢每次设置属性时都触发 CanExecute 事件。如果验证失败,我不喜欢将按钮设置为禁用状态。我认为这不明智。按钮始终激活总是更好。这样,当用户单击按钮且所有控件都为空时,他将收到有关控件上所有错误的​​信息,并且他可以逐步解决问题。但是使用 DataAnnotation 和通过异常进行验证,您无法轻松做到这一点(我不知道如何轻松做到)。您无法验证所有 ViewModel 对象并将验证错误通知 ValidationSummary 和控件。您只能费力地做到:您可以在您的控件上为每个控件触发 UpdateBinding,并使用一些特殊类,如 ValidationScope,您应该自己编写。

此外,我在我的示例中使用了 UpdateSourceTrigger,因为绑定仅在焦点更改时才起作用。因此,当用户输入 NewPassword 然后输入 NewPasswordConfirmation 时,他会看到按钮仍然被禁用(因为 NewPasswordConfirmation 的绑定仅在他更改焦点时发生)。这很混乱。但是当我使用 UpdateSourceTrigger 时,我可以看到每个按键事件上的任何验证错误 - 这也不好。

关于 ValidatesOnExceptions 的几句话

忘了告诉您,如果您想通过异常实现验证,您可能不使用 DataAnnotations;您可以从 set 方法抛出自己的异常。例如,您可以实现密码确认检查,如下所示。

[Display(Name = "New password confirmation")]
public string NewPasswordConfirmation
{
    get { return _newPasswordConfirmation; }
    set
    {
        _newPasswordConfirmation = value;
        OnPropertyChanged("NewPasswordConfirmation");
        ChangePasswordCommand.RaiseCanExecuteChanged();
        if (string.CompareOrdinal(_newPassword, value) != 0)
            throw new Exception("Password confirmation not equal to password.");
    }
}

看起来比 CustomValidationAttribute 更好。

IDataErrorInfo

IDataErrorInfo 接口随 Silverlight 4 一起推出。如果我们想用此接口实现验证,我们应该在 ViewModel 中实现它(一个属性和一个方法)。通常,开发人员会编写他们自己的类处理程序来存储验证错误)。

public class ValidationHandler
{
    private Dictionary<string, string> BrokenRules { get; set; }
 
    public ValidationHandler()
    {
        BrokenRules = new Dictionary<string, string>();
    }
 
    public string this[string property]
    {
        get { return BrokenRules[property]; }
    }
 
    public bool BrokenRuleExists(string property)
    {
        return BrokenRules.ContainsKey(property);
    }
 
    public bool ValidateRule(string property, 
           string message, Func<bool> ruleCheck)
    {
        bool check = ruleCheck();
        if (!check)
        {
            if (BrokenRuleExists(property))
                RemoveBrokenRule(property);
 
            BrokenRules.Add(property, message);
        }
        else
        {
            RemoveBrokenRule(property);
        }
        return check;
    }
 
    public void RemoveBrokenRule(string property)
    {
        if (BrokenRules.ContainsKey(property))
        {
            BrokenRules.Remove(property);
        }
    }
 
    public void Clear()
    {
        BrokenRules.Clear();
    }
}

现在,让我们重写我们的 BindingModel 类;我们将它继承自 IDataErrorInfo 接口。

public class BindingModel : INotifyPropertyChanged, IDataErrorInfo
{
    private string _newPassword;
    private string _newPasswordConfirmation;
    private readonly ValidationHandler _validationHandler = new ValidationHandler();
 
    #region INotifyPropertyChanged
 
    public event PropertyChangedEventHandler PropertyChanged = delegate { };
 
    private void OnPropertyChanged(string propertyName)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
 
    #endregion
 
    #region IDataErrorInfo
 
    public string this[string columnName]
    {
        get
        {
            if (_validationHandler.BrokenRuleExists(columnName))
            {
                return _validationHandler[columnName];
            }
            return null;
        }
    }
 
    public string Error
    {
        get { return throw new NotImplementedException(); }
    }
 
    #endregion
}

我们 BindingModel 的主要属性将如下所示:

[Display(Name = "New password")]
public string NewPassword
{
    get { return _newPassword; }
    set
    {
        _newPassword = value;
        OnPropertyChanged("NewPassword");
 
        if (_validationHandler.ValidateRule("NewPassword", 
                   "New password required", 
                   () => !string.IsNullOrEmpty(value)))
        {
            _validationHandler.ValidateRule("NewPassword", 
                "Max length of password is 80 symbols.", 
                () => value.Length < 80);
        }
 
        ChangePasswordCommand.RaiseCanExecuteChanged();
    }
}
 
[Display(Name = "New password confirmation")]
public string NewPasswordConfirmation
{
    get { return _newPasswordConfirmation; }
    set
    {
        _newPasswordConfirmation = value;
        OnPropertyChanged("NewPasswordConfirmation");
 
        _validationHandler.ValidateRule("NewPasswordConfirmation", 
           "Password confirmation not equal to password.",
           () => string.CompareOrdinal(_newPassword, value) == 0);
 
        ChangePasswordCommand.RaiseCanExecuteChanged();
    }
}

每次调用 VaidationRule 都会检查某个条件,如果不满足,则会在 Errors 集合中写入有关验证错误的信息。绑定后,Silverlight 基础结构将调用索引属性 this[string columnName] 的 get 方法,它将返回此属性的错误信息(columnName)。如果我们想使用这种类型的验证,我们应该在绑定中将 ValidatesOnDataErrors 属性设置为 true。属性错误会抛出 NotImplementedException,因为 Silverlight 不使用它。来自MSDN 的引用:“请注意,绑定引擎永远不会使用 Error 属性,尽管您可以在自定义错误报告中使用它来显示对象级别的错误。”

最后,我们应该实现命令的方法。

public BindingModel()
{
    ChangePasswordCommand = 
      new DelegateCommand(ChangePassword, CanChangePassword);
}
 
public DelegateCommand ChangePasswordCommand { get; private set; }
 
private bool CanChangePassword(object arg)
{
    return !string.IsNullOrEmpty(_newPassword) 
     && string.CompareOrdinal(_newPassword, _newPasswordConfirmation) == 0;
}
 
private void ChangePassword(object obj)
{
    if (ChangePasswordCommand.CanExecute(obj))
    {
        MessageBox.Show("Bingo!");
    }
}

再次,我们应该使用 CanChangePassword 方法,因为当对象无效时,我们需要将按钮设置为禁用状态。在绑定发生之前,我们无法检查整个对象的有效状态。此实现中的另一个问题:我们应该编写两次验证规则:在属性 set 方法和 CanChangePassword 方法中。但这是此实现的问题;您可以通过另一种方法解决它:您可以编写一个 ValidationHandler 类,该类不仅存储验证错误,还存储验证规则,因此您可以在 CanChangePassword 方法中触发验证检查。但我们仍然有一个问题,即我们无法在绑定之前通知 ValidationSummary 或我们界面中的其他控件发生了或消失了某些验证错误。

此外,您可以使用 DataAnnotation 来实现此方法,但您应该为此编写一些辅助方法;我将在下一个示例中告诉您如何操作。

IDataErrorInfo 实现的结果(Silverlight 示例)可以在我博客的这篇文章中看到。

我认为此示例的行为与上一部分相同。我还想说这个示例有一个 bug:如果用户先输入密码确认,然后输入新密码,他将看到关于密码确认不相等的验证错误,因为此检查仅在 NewPasswordConfirmation 绑定中发生。

INotifyDataErrorInfo

INotifyDataErrorInfo 接口也随 Silverlight 4 一起推出。此接口的主要优点是您可以执行同步(如在前几个示例中)和异步验证。您可以等待来自服务器的验证,然后才告诉接口一切正常或发生了某些验证错误。我比其他方法更喜欢这种验证方式。我将使用Davy Brion 关于“MVP In Silverlight/WPF Series”的文章中的一些类和实现。

首先,我得到了 PropertyValidation 类;使用它,我们将存储属性的验证规则以及发生验证错误时应显示的​​消息。

public class PropertyValidation<TBindingModel>
       where TBindingModel : BindingModelBase<TBindingModel>
{
    private Func<TBindingModel, bool> _validationCriteria;
    private string _errorMessage;
    private readonly string _propertyName;
 
    public PropertyValidation(string propertyName)
    {
        _propertyName = propertyName;
    }
 
    public PropertyValidation<TBindingModel> 
           When(Func<TBindingModel, bool> validationCriteria)
    {
        if (_validationCriteria != null)
            throw new InvalidOperationException(
              "You can only set the validation criteria once.");
 
        _validationCriteria = validationCriteria;
        return this;
    }
 
    public PropertyValidation<TBindingModel> Show(string errorMessage)
    {
        if (_errorMessage != null)
            throw new InvalidOperationException(
              "You can only set the message once.");
 
        _errorMessage = errorMessage;
        return this;
    }
 
    public bool IsInvalid(TBindingModel presentationModel)
    {
        if (_validationCriteria == null)
            throw new InvalidOperationException(
                "No criteria have been provided for this " + 
                "validation. (Use the 'When(..)' method.)");
 
        return _validationCriteria(presentationModel);
    }
 
    public string GetErrorMessage()
    {
        if (_errorMessage == null)
            throw new InvalidOperationException(
                "No error message has been set for " + 
                "this validation. (Use the 'Show(..)' method.)");
 
        return _errorMessage;
    }
 
    public string PropertyName
    {
        get { return _propertyName; }
    }
}

当我们开始在示例中实现验证规则时,您将理解它是如何工作的。此类有一个泛型参数,其基类是 BindingModelBase<T>,我们的主 BindingModel 类将继承自该基类。

让我们实现 BindingModelBase 类;我们将它继承自 INotifyPropertyChangedINotifyDataErrorInfo 接口,并添加两个字段:一个用于存储验证规则,另一个用于存储验证错误。

public abstract class BindingModelBase<TBindingModel> : 
    INotifyPropertyChanged, INotifyDataErrorInfo
    where TBindingModel : BindingModelBase<TBindingModel>
{
    private readonly List<PropertyValidation<TBindingModel>> 
      _validations = new List<PropertyValidation<TBindingModel>>();
    private Dictionary<string, List<string>> 
      _errorMessages = new Dictionary<string, List<string>>();
 
    #region INotifyDataErrorInfo
 
    public IEnumerable GetErrors(string propertyName)
    {
        if (_errorMessages.ContainsKey(propertyName)) 
            return _errorMessages[propertyName];
 
        return new string[0];
    }
 
    public bool HasErrors
    {
        get { return _errorMessages.Count > 0; }
    }
 
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged = delegate { };
 
    private void OnErrorsChanged(string propertyName)
    {
        ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }
 
    #endregion
 
    #region INotifyPropertyChanged
 
    public event PropertyChangedEventHandler PropertyChanged = delegate { };
 
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
 
    #endregion
}

我想为 OnPropertyChanged 添加一个辅助方法,它以 Expression 作为参数,并允许我们像这样使用它:

public string NewPassword
{
    get { return _newPassword; }
    set { _newPassword = value; OnCurrentPropertyChanged(); }
}

这个方法非常有用。这是该方法的实现。

public string NewPassword
{
    get { return _newPassword; }
    set { _newPassword = value; OnPropertyChanged(() => NewPassword); }
}

更多实现。

protected void OnPropertyChanged(Expression<Func<object>> expression)
{
    OnPropertyChanged(GetPropertyName(expression));
}
 
private static string GetPropertyName(Expression<Func<object>> expression)
{
    if (expression == null)
        throw new ArgumentNullException("expression");
 
    MemberExpression memberExpression;
 
    if (expression.Body is UnaryExpression)
        memberExpression = 
          ((UnaryExpression)expression.Body).Operand as MemberExpression;
    else
        memberExpression = expression.Body as MemberExpression;
    
    if (memberExpression == null)
        throw new ArgumentException("The expression is not " + 
          "a member access expression", "expression");
 
    var property = memberExpression.Member as PropertyInfo;
    if (property == null)
        throw new ArgumentException("The member access expression " + 
          "does not access a property", "expression");
 
    var getMethod = property.GetGetMethod(true);
    if (getMethod.IsStatic)
        throw new ArgumentException("The referenced property " + 
          "is a static property", "expression");
 
    return memberExpression.Member.Name;
}

该方法从表达式中获取属性名称。接下来,让我们添加一些将执行验证的方法。

public void ValidateProperty(Expression<Func<object>> expression)
{
    ValidateProperty(GetPropertyName(expression));
}
 
private void ValidateProperty(string propertyName)
{
    _errorMessages.Remove(propertyName);
 
    _validations.Where(v => v.PropertyName == 
       propertyName).ToList().ForEach(PerformValidation);
    OnErrorsChanged(propertyName);
    OnPropertyChanged(() => HasErrors);
}
 
private void PerformValidation(PropertyValidation<TBindingModel> validation)
{
    if (validation.IsInvalid((TBindingModel) this))
    {
        AddErrorMessageForProperty(validation.PropertyName, 
                                   validation.GetErrorMessage());
    }
}
 
private void AddErrorMessageForProperty(string propertyName, string errorMessage)
{
    if (_errorMessages.ContainsKey(propertyName))
    {
        _errorMessages[propertyName].Add(errorMessage);
    }
    else
    {
        _errorMessages.Add(propertyName, new List<string> {errorMessage});
    }
}

ValidateProperty 方法会删除有关当前属性发生的所有验证错误的​​信息,然后检查当前属性的每个规则,如果规则为 false,则将错误添加到 Errors 集合中。此外,我们可以通过 PropertyChanged 事件自动触发每个属性的验证检查。

protected BindingModelBase()
{
    PropertyChanged += (s, e) => { if (e.PropertyName 
      != "HasErrors") ValidateProperty(e.PropertyName); };
}

为了方便将验证规则添加到我们的集合中,我们将添加此方法。

protected PropertyValidation<TBindingModel> 
    AddValidationFor(Expression<Func<object>> expression)
{
    var validation = 
      new PropertyValidation<TBindingModel>(GetPropertyName(expression));
    _validations.Add(validation);
    return validation;
}

现在我们可以实现我们将在最后一个示例中使用的 BindingModel 类。如果我们想使用 INotifyDataErrorInfo 接口实现验证,我们应该在绑定中将 ValidatesOnNotifyDataErrors 属性设置为 true

这是 BindingModel 的实现。

public class BindingModel : BindingModelBase<BindingModel>
{
    private string _newPassword;
    private string _newPasswordConfirmation;
 
    public DelegateCommand ChangePasswordCommand { get; private set; }
 
    public BindingModel()
    {
        ChangePasswordCommand = new DelegateCommand(ChangePassword);
 
        AddValidationFor(() => NewPassword)
            .When(x => string.IsNullOrEmpty(x._newPassword))
            .Show("New password required field.");
 
        AddValidationFor(() => NewPassword).When(
          x => !string.IsNullOrEmpty(x._newPassword) && 
          x._newPassword.Length > 80).Show(
          "New password must be a string with maximum length of 80.");
 
        AddValidationFor(() => NewPasswordConfirmation).When(
          x => !string.IsNullOrEmpty(x._newPassword) && 
              string.CompareOrdinal(x._newPassword, 
              x._newPasswordConfirmation) != 0).Show(
              "Password confirmation not equal to password.");
    }
 
    [Display(Name = "New password")]
    public string NewPassword
    {
        get { return _newPassword; }
        set
        {
            _newPassword = value;
            OnPropertyChanged(() => NewPassword);
        }
    }
    
    [Display(Name = "New password confirmation")]
    public string NewPasswordConfirmation
    {
        get { return _newPasswordConfirmation; }
        set
        {
            _newPasswordConfirmation = value;
            OnPropertyChanged(() => NewPasswordConfirmation);
        }
    }
 
    private void ChangePassword(object obj)
    {
        throw new NotImplementedException();
    }
}

在构造函数中,我们为属性描述了所有三个验证规则。看起来非常好(感谢 Davy Brion!)。我告诉过您我不喜欢设置禁用按钮,所以从现在开始,它将始终启用。为了实现命令的方法 ChangePassword,我需要一个方法来检查当前对象的所有验证规则。这将是 ValidateAll 方法,我将在 BindingModelBase 类中实现它。

public void ValidateAll()
{
    var propertyNamesWithValidationErrors = _errorMessages.Keys;
 
    _errorMessages = new Dictionary<string, List<string>>();
 
    _validations.ForEach(PerformValidation);
 
    var propertyNamesThatMightHaveChangedValidation =
        _errorMessages.Keys.Union(propertyNamesWithValidationErrors).ToList();
 
    propertyNamesThatMightHaveChangedValidation.ForEach(OnErrorsChanged);
 
    OnPropertyChanged(() => HasErrors);
}

此方法会删除集合中所有验证错误。然后它检查验证规则,在规则为 false 时写入错误,然后为更改了验证状态的每个属性触发 OnErrorsChanged 事件。

这是 ChangePassword 方法的实现。

private void ChangePassword(object obj)
{
    ValidateAll();
 
    if (!HasErrors)
    {
        MessageBox.Show("Bingo!");
    }
}

结果(Silverlight 应用程序)可以在我博客的这篇文章中看到。

我喜欢这个实现。它更加灵活,并且可以利用前几种方法的​​所有优点。DataAnnotation 怎么样?如果您喜欢使用 DataAnnotation 属性描述验证规则,我可以为您提供一个额外的辅助方法。此方法将从属性获取所有规则,并将它们转换为 PropertyValidation

protected PropertyValidation<TBindingModel> 
      AddValidationFor(Expression<Func<object>> expression)
{
    return AddValidationFor(GetPropertyName(expression));
}
 
protected PropertyValidation<TBindingModel> AddValidationFor(string propertyName)
{
    var validation = new PropertyValidation<TBindingModel>(propertyName);
    _validations.Add(validation);
 
    return validation;
}
 
protected void AddAllAttributeValidators()
{
    PropertyInfo[] propertyInfos = 
      GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
 
    foreach (PropertyInfo propertyInfo in propertyInfos)
    {
        Attribute[] custom = Attribute.GetCustomAttributes(propertyInfo, 
                                       typeof(ValidationAttribute), true);
        foreach (var attribute in custom)
        {
            var property = propertyInfo;
            var validationAttribute = attribute as ValidationAttribute;
 
            if (validationAttribute == null)
                throw new NotSupportedException("validationAttribute " + 
                  "variable should be inherited from ValidationAttribute type");
 
            string name = property.Name;
 
            var displayAttribute = Attribute.GetCustomAttributes(propertyInfo, 
                typeof(DisplayAttribute)).FirstOrDefault() as DisplayAttribute;
            if (displayAttribute != null)
            {
                name = displayAttribute.GetName();
            }
 
            var message = validationAttribute.FormatErrorMessage(name);
 
            AddValidationFor(propertyInfo.Name)
                .When(x =>
                {
                    var value = property.GetGetMethod().Invoke(this, new object[] { });
                    var result = validationAttribute.GetValidationResult(value,
                                 new ValidationContext(this, null, null)
                                 { MemberName = property.Name });
                    return result != ValidationResult.Success;
                })
                .Show(message);
         }
    }
}

以及最后一个 BindingModel 变体。

public class BindingModel : BindingModelBase<BindingModel>
{
    private string _newPassword;
    private string _newPasswordConfirmation;
 
    public DelegateCommand ChangePasswordCommand { get; private set; }
 
    public BindingModel()
    {
        ChangePasswordCommand = new DelegateCommand(ChangePassword);
 
        AddAllAttributeValidators();
 
        AddValidationFor(() => NewPasswordConfirmation).When(
           x => !string.IsNullOrEmpty(x._newPassword) && 
           string.CompareOrdinal(x._newPassword, 
           x._newPasswordConfirmation) != 0).Show(
           "Password confirmation not equal to password.");
    }
 
    [Display(Name = "New password")]
    [Required]
    [StringLength(80, ErrorMessage = 
      "New password must be a string with maximum length of 80.")]
    public string NewPassword
    {
        get { return _newPassword; }
        set
        {
            _newPassword = value;
            OnPropertyChanged(() => NewPassword);
        }
    }
 
    [Display(Name = "New password confirmation")]
    public string NewPasswordConfirmation
    {
        get { return _newPasswordConfirmation; }
        set
        {
            _newPasswordConfirmation = value;
            OnPropertyChanged(() => NewPasswordConfirmation);
        }
    }
 
    private void ChangePassword(object obj)
    {
        ValidateAll();
 
        if (!HasErrors)
        {
            MessageBox.Show("Bingo!");
        }
    }
}

这些示例的源代码可以从我assembla.com 的存储库下载,或者使用本文顶部的链接。

© . All rights reserved.