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






4.75/5 (15投票s)
以一种可维护的方式验证您的业务实体。
引言
我喜欢 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 中都添加属性。但也有办法解决这个问题。
附件中的项目包含了源代码和一个可用的示例。