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

使用自定义绑定和属性自动验证 WPF 中的业务实体

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (15投票s)

2009年8月21日

CPOL

4分钟阅读

viewsIcon

72404

downloadIcon

1401

以一种可维护的方式验证您的业务实体。

引言

我喜欢 WPF。

我喜欢 MVVM。

我喜欢无重复代码。

我不喜欢 WPF 的验证方式如何融入其中。

验证实现 1:XAML 中的验证规则

<TextBox Name="textSandrino">
  <Binding Path="Name">
    <Binding.ValidationRules>
      <MustBeANumberValidationRule />
    </Binding.ValidationRules>
  </Binding>
</TextBox>

是的,很好……才怪。

这有几个缺点

  • 您的业务逻辑泄露到 UI。非常糟糕!
  • 验证规则继承自 System.Windows.Controls.ValidationRule。即使我可以在 BLL 中创建一些验证规则,这仍然会基于 UI 对象。不干净。
  • 如果我正在迁移,现有的验证代码怎么办?我必须重写所有内容以适应 ValidationRule 逻辑吗?疯狂……
  • 而且,这些代码很难管理。

验证实现 2:setter 中的异常

public class User
{
    private int _age;
    public int Age
    {
        get { return _age; }
        set
        {
            if (value < 21)
                throw new ArgumentException("Kid!");
            _age = value;
        }
    }
}

HTML

<TextBox Name="textSandrino">
  <Binding Path="Age">
    <Binding.ValidationRules>
      <ExceptionValidationRule />
    </Binding.ValidationRules>
  </Binding>
</TextBox>

我几乎可以肯定 getter 和 setter 不应该抛出异常(我是在 Framework Design Guidelines 中读到的吗?)。就个人而言,我没关系,方法会抛出异常。但 setter 呢?它们应该只设置值,并且处理逻辑最少(如 INotifyPropertyChanged)。

这是另一个缺点,您的业务逻辑的大部分将位于 setter 中。对我来说,这不够干净。

验证实现 3:IDataErrorInfo

public class User : IDataErrorInfo
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
 
    public string Error
    {
        get
        {
            return this[string.Empty];
        }
    }
 
    public string this[string propertyName]
    {
        get
        {
            string result = string.Empty;
            if (propertyName == "Name")
            {
                if (string.IsNullOrEmpty(this.Name))
                    result = "Name cannot be empty!";
            }
            return result;
        }
    }
}

你在开玩笑吗?

  • 基于属性名?这太老套了,一点也不利于重构。
  • 如果我从 WinForms 项目迁移,这如何与我当前的验证逻辑集成?
  • 这段代码看起来很乱。我想要干净的代码。

我看到 Josh Smith 尝试在 MVVM 中实现这一点。抱歉 Josh,我喜欢你的作品,但这甚至更糟。我无法接受我应该在 Model 和 ViewModel 中以这种方式实现我的验证。编写这种类型的验证一次就够多了!想象一下写两次。

更干净的解决方案

好的。这可以说是我心目中的干净解决方案。

public class Person
{
    [TextValidation(MinLength=5)]
    public string Name { get; set; }
 
    [NumberValidation]
    public string Age { get; set; }
}

此解决方案应提供什么

  • 将所有验证逻辑集中在一处。
  • 能够在客户端(WPF UI)和服务器端(BLL)使用它。
  • 无重复代码。
  • 不基于字符串。

我们将使用 Philipp Sumi 的 BindingDecoratorBase。这个类比 .NET 中原生的 BindingBase 要好得多。干得好!

编写我们自己的验证逻辑

interface IValidationRule
{
    void Validate(object value, out bool isValid, out string errorMessage);
}

这个接口没有什么好说的。它将是我们验证逻辑的基础。

创建属性

public class NumberValidationAttribute : Attribute, IValidationRule
{
    public NumberValidationAttribute()
    {
    }
 
    public void Validate(object value, out bool isValid, out string errorMessage)
    {
        double result = 0;
        isValid = double.TryParse(value.ToString(), out result);
        errorMessage = "";
 
        if (!isValid)
            errorMessage = value + " is not a valid number";
    }
}
 
public class TextValidationAttribute : Attribute, IValidationRule
{
    public int MinLength { get; set; }
 
    public TextValidationAttribute()
    {
 
    }
 
    public void Validate(object value, out bool isValid, out string errorMessage)
    {
        isValid = false;
        errorMessage = "";
 
        if (value != null && value.ToString().Length >= MinLength)
            isValid = true;
 
        if (!isValid)
            errorMessage = value + " is not equal to or longer than " + MinLength;
    }
}

这里也没有什么神奇之处。这两个属性类使得我们可以用规则来修饰我们的模型属性。但是,它也使得配置验证属性成为可能(参见 TextValidationAttribute 中的 MinLength)。

使用验证属性

public class Personn
{
    [TextValidation(MinLength=5)]
    public string Name { get; set; }
 
    [NumberValidation]
    public string Age { get; set; }
 
    [NumberValidation]
    [TextValidation(MinLength=2)]
    public string Money { get; set; }
}

这有多干净?我们可以

  • 以非常干净的方式应用验证。
  • 只编写一次验证属性,并重复使用多次。
  • 以干净的方式配置验证属性!
  • 每个属性使用多个验证属性。

我喜欢!

但是,我们如何将自定义验证属性与 WPF 集成呢?

与 WPF 集成

您应该首先看看 Philipp 的文章。有了他的 BindingDecoratorBase,我们将在我们的解决方案中添加一些自定义绑定。

但首先,我们将创建一个 WPF ValidationRule,它可以利用我们自己的验证属性。

class GenericValidationRule : ValidationRule
{
    private IValidationRule ValidationRule;
 
    public GenericValidationRule(IValidationRule validationRule)
    {
        this.ValidationRule = validationRule;
        this.ValidatesOnTargetUpdated = true;
    }
 
    public override ValidationResult Validate(object value, 
                    System.Globalization.CultureInfo cultureInfo)
    {
        bool isValid = false;
        string errorMessage = "";
 
        ValidationRule.Validate(value, out isValid, out errorMessage); 
        ValidationResult result = new ValidationResult(isValid, errorMessage);
        return result;
    }
}

好的,关于这个类,只说一小句。

仔细看,这个 WPF ValidationRule 将吸收我们自己的 IValidationRule(由属性使用)。它将使用接口中的 Validate 方法,并返回一个 WPF 兼容的响应。

另外,您看到 ValidatesOnTargetUpdated 了吗?这是一个很好的属性。这将导致控件在数据绑定时立即进行验证。这意味着即使在用户输入任何内容之前。

现在我们有了这个规则,让我们自动地将我们的 IValidationRule 与 WPF 控件绑定!

/// <summary>
/// Binding that will automatically implement the validation
/// </summary>
public class ValidationBinding : BindingDecoratorBase
{
    public ValidationBinding()
        : base()
    {
        Binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
    }
 
    /// <summary>
    /// This method is being invoked during initialization.
    /// </summary>
    /// <param name="provider">Provides access to the bound items.</param>
    /// <returns>The binding expression that is created by the base class.</returns>
    public override object ProvideValue(IServiceProvider provider)
    {
        // Get the binding expression
        object bindingExpression = base.ProvideValue(provider);
 
        // Bound items
        DependencyObject targetObject;
        DependencyProperty targetProperty;
 
        // Try to get the bound items
        if (TryGetTargetItems(provider, out targetObject, out targetProperty))
        {
            if (targetObject is FrameworkElement)
            {
                // Get the element and implement datacontext changes
                FrameworkElement element = targetObject as FrameworkElement;
                element.DataContextChanged += 
                  new DependencyPropertyChangedEventHandler(element_DataContextChanged);
 
                // Set the template
                ControlTemplate controlTemplate = 
                  element.TryFindResource("validationTemplate") as ControlTemplate;
                if (controlTemplate != null)
                    Validation.SetErrorTemplate(element, controlTemplate);
            }
        }
 
        // Go on with the flow
        return bindingExpression;
    }
 
    /// <summary>
    /// Datacontext of the control has changed
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void element_DataContextChanged(object sender, 
                         DependencyPropertyChangedEventArgs e)
    {
        object datacontext = e.NewValue;
        if (datacontext != null)
        {
            PropertyInfo property = datacontext.GetType().GetProperty(Binding.Path.Path);
            if (property != null)
            {
                IEnumerable<object> attributes = 
                  property.GetCustomAttributes(true).Where(o => o is IValidationRule);
                foreach (IValidationRule validationRule in attributes)
                    ValidationRules.Add(new GenericValidationRule(validationRule));
            }
        }
    }
}

好的,让我快速过一遍代码。

  • 继承自 Philipp 的类。
  • 验证应该在属性更改时发生(即时验证)。
  • ProvideValue 将帮助我们检测接收绑定的控件。在该控件上,我们将观察 DataContext 何时更改。

现在,一旦 DataContext 被设置到控件上,我们将开始。使用 Binding.Path.Path,我们知道我们绑定到 DataContext 的属性是什么。

想象一下,我将 Person 设置为 Window 的 DataContext。然后,DataContextChanged 也会在 TextBox 等控件上触发。而且,如果该 TextBox 的绑定被设置为该类的属性,我们就可以获取到属性!

这就是在 element_DataContextChanged 中发生的事情。我们获取与 DataContext 属性匹配的属性。一旦我们有了那个属性,我们就尝试找到我们想要的属性,并将它们作为验证规则添加到控件中。

另外请注意,我自动设置了控件的验证模板。这再次减少了丑陋的重复代码。这也可以用样式来实现。

实现它

没有什么可以做的。

public class Person
{
   [TextValidation(MinLength=5)]
    public string Name { get; set; }
 
    [NumberValidation]
    public string Age { get; set; }
 
    [NumberValidation]
    [TextValidation(MinLength=2)]
    public string Money { get; set; }
}

HTML

<TextBox Height="23" Margin="38,34,0,0" Name="textBox1" 
  Text="{local:ValidationBinding Path=Name}" VerticalAlignment="Top" 
  HorizontalAlignment="Left" Width="170" />

就是这样!

唯一的小缺点是,如果您使用 MVVM,您必须在 Model 和 ViewModel 中都添加属性。但也有办法解决这个问题。

附件中的项目包含了源代码和一个可用的示例。

© . All rights reserved.