在没有IDataErrorInfo的WinForms中添加验证

本文关键字:添加 验证 WinForms IDataErrorInfo | 更新日期: 2023-09-27 18:15:36

我正在开发一个(遗留的)WinForms应用程序,我喜欢用一种更动态的方式向用户提供错误信息,就像我习惯用MVC做的那样。

WinForms中的验证似乎围绕IDataErrorInfo接口工作,但我不喜欢在我用于绑定的对象上实现这个接口。我经常可以将命令对象绑定到接口。命令是描述业务操作并在域层中定义的dto(执行这些命令的逻辑在业务层中定义)。

由于命令是域的一部分,我不想在它们上实现IDataErrorInfo,因为这会将它们直接与验证逻辑耦合(因为调用IDataErrorInfo方法之一假定验证)。我唯一想做的就是用DataAnnotation属性标记我的命令属性。

所以我的问题是:我如何在WinForms中启用验证(使用ErrorProvider),但不必在我用来绑定的类上实现IDataErrorInfo ?

例如,是否有一种方法挂钩到ErrorProvider和委托的验证DataAnnotations' Validate类?

在没有IDataErrorInfo的WinForms中添加验证

技巧是查看控件的DataBindings,以确定控件绑定的类型和属性。有了这些信息,验证就可以被钩住了。

public static void RegisterBindingSourceValidations(Form form, 
    ErrorProvider errorProvider)
{
    Requires.IsNotNull(form, "form");
    Requires.IsNotNull(errorProvider, "errorProvider");
    RegisterBindingSourceValidationsRecursive(form, errorProvider);
}
private static void RegisterBindingSourceValidationsRecursive(
    Control control, ErrorProvider provider)
{
    foreach (Control childControl in control.Controls)
    {
        RegisterBindingSourceValidationsForControl(childControl, provider);
        RegisterBindingSourceValidationsRecursive(childControl, provider);
    }
}
private static void RegisterBindingSourceValidationsForControl(
    Control control, ErrorProvider errorProvider)
{
    AddMaximumStringLengthToDataViewBoundTextBox(control);
    AddDataAnnotationsValidations(control, errorProvider);
}
private static void AddMaximumStringLengthToDataViewBoundTextBox(Control control)
{
    TextBox textBox = control as TextBox;
    if (textBox == null)
    {
        return;
    }
    int maximumTextLength = (
        from dataBinding in textBox.DataBindings.Cast<Binding>()
        where StringComparer.OrdinalIgnoreCase.Equals(dataBinding.PropertyName, "Text")
        let bindingSource = (BindingSource)dataBinding.DataSource
        where bindingSource.SyncRoot is DataView
        let view = (DataView)bindingSource.SyncRoot
        let bindingField = dataBinding.BindingMemberInfo.BindingField
        let maxLength = view.Table.Columns[bindingField].MaxLength
        where maxLength > 0
        select maxLength)
        .SingleOrDefault();
    if (maximumTextLength > 0)
    {
        textBox.MaxLength = maximumTextLength;
    }
}
private static void AddDataAnnotationsValidations(Control control, 
    ErrorProvider errorProvider)
{
    var binding = (
        from dataBinding in control.DataBindings.Cast<Binding>()
        where dataBinding.DataSource is BindingSource
        let bindingSource = (BindingSource)dataBinding.DataSource
        where !string.IsNullOrEmpty(dataBinding.BindingMemberInfo.BindingMember)
        let modelType = bindingSource.GetEnumerableElementType()
        where modelType != null
        let controlProperty = control.GetType().GetProperty(dataBinding.PropertyName)
        let boundPropertyName = dataBinding.BindingMemberInfo.BindingMember
        select new { bindingSource, modelType, controlProperty, boundPropertyName })
        .FirstOrDefault();
    if (binding != null)
    {
        RegisterValidator(control, binding.controlProperty, 
            binding.modelType, binding.boundPropertyName, 
            () => binding.bindingSource.Current, errorProvider);
        if (control is TextBox)
        {
            SetMaximumTextLength((TextBox)control, binding.modelType, 
                binding.boundPropertyName);
        }
    }
}
private static void SetMaximumTextLength(TextBox textBoxToValidate, 
    Type modelType, string modelPropertyName)
{
    var propertyChain = GetPropertyChain(modelType, modelPropertyName).ToArray();
    ApplyMaximumStringLength(textBoxToValidate, propertyChain.Last());
}
private static void ApplyMaximumStringLength(TextBox textBoxToValidate, 
    PropertyInfo property)
{
    var maximumLength = (
        from attribute in property.GetCustomAttributes(
            typeof(StringLengthAttribute), true)
            .OfType<StringLengthAttribute>()
        select attribute.MaximumLength)
        .FirstOrDefault();
    if (maximumLength > 0)
    {
        textBoxToValidate.MaxLength = maximumLength;
    }
}
private static Type GetEnumerableElementType(
    this BindingSource bindingSource)
{
    return (
        from intf in bindingSource.DataSource.GetType()
            .GetInterfaces()
        where intf.IsGenericType
        where intf.GetGenericTypeDefinition() == typeof(IEnumerable<>)
        let type = intf.GetGenericArguments().Single()
        where type != typeof(object)
        select type)
        .SingleOrDefault();
}
public static void RegisterValidator(Control controlToValidate, 
    PropertyInfo controlProperty,
    Type modelType, string modelPropertyName, 
    Func<object> instanceSelector, ErrorProvider errorProvider)
{
    Requires.IsNotNull(controlToValidate, "controlToValidate");
    Requires.IsNotNull(controlProperty, "controlProperty");
    Requires.IsNotNull(modelType, "modelType");
    Requires.IsNotNull(instanceSelector, "instanceSelector");
    Requires.IsNotNull(errorProvider, "errorProvider");
    controlToValidate.CausesValidation = true;
    var propertyChain = GetPropertyChain(modelType, modelPropertyName).ToArray();
    PropertyInfo targetProperty = propertyChain.Last();
    var validator = new ControlValidator
    {
        ControlToValidate = controlToValidate,
        ControlProperty = controlProperty,
        PropertyChain = propertyChain,
        InstanceSelector = instanceSelector,
        ErrorProvider = errorProvider,
        ValidationAttributes = 
            targetProperty.GetCustomAttributes<ValidationAttribute>().ToArray(),
        Converter = TypeDescriptor.GetConverter(targetProperty.PropertyType),
    };
    if (validator.ValidationAttributes.Any())
    {
        controlToValidate.CausesValidation = true;
        // This check seems redundant, since WinForms doesn't allow you to 
        // leave a form field when the value can't be converted, which 
        // means the validator will not go off.
        if (validator.Converter == null) 
        {
            throw GetTypeConverterMissingExcpetion(targetProperty);
        }
        controlToValidate.Validating += (s, e) => validator.Validate();
    }
}
private static Exception GetTypeConverterMissingExcpetion(
    PropertyInfo modelProperty)
{
    return new InvalidOperationException(string.Format(
        "Property '{0}' declared on type {1} cannot be used for validation. " +
        "There is no TypeConverter for type {2}.", 
        modelProperty.Name, 
        modelProperty.DeclaringType, 
        modelProperty.PropertyType));
}
private static IEnumerable<PropertyInfo> GetPropertyChain(
    Type modelType, string modelPropertyName)
{
    foreach (string propertyName in modelPropertyName.Split('.'))
    {
        var property = modelType.GetProperty(propertyName);
        if (property == null)
        {
            throw new InvalidOperationException(string.Format(
                "Property with name '{0}' could not be found on type {1}.",
                propertyName, modelType.FullName));
        }
        modelType = property.PropertyType;
        yield return property;
    }
}
private class ControlValidator
{
    public PropertyInfo[] PropertyChain { get; set; }
    public ValidationAttribute[] ValidationAttributes { get; set; }
    public TypeConverter Converter { get; set; }
    public Func<object> InstanceSelector { get; set; }
    public ErrorProvider ErrorProvider { get; set; }
    public Control ControlToValidate { get; set; }
    public PropertyInfo ControlProperty { get; set; }
    public void Validate()
    {
        ModelPropertyPair pair = this.GetModelPropertyChain().Last();
        object value = this.GetValueToValidate();
        object convertedValue;
        if (!this.TryConvertValue(value, out convertedValue))
        {
            this.ErrorProvider.SetError(this.ControlToValidate, 
                "Value is invalid.");
            return;
        }
        string errorMessage = this.GetValidationErrorOrNull(pair, convertedValue);
        this.ErrorProvider.SetError(this.ControlToValidate, errorMessage);
    }
    private IEnumerable<ModelPropertyPair> GetModelPropertyChain()
    {
        var model = this.InstanceSelector();
        foreach (var property in this.PropertyChain)
        {
            yield return new ModelPropertyPair(model, property);
            model = model == null ? null : property.GetValue(model);
        }
    }
    private object GetValueToValidate()
    {
        return this.ControlProperty.GetValue(this.ControlToValidate);
    }
    [DebuggerStepThrough]
    private string GetValidationErrorOrNull(ModelPropertyPair pair, object value)
    {
        var context = new ValidationContext(pair.Model) { MemberName = pair.Property.Name };
        try
        {
            Validator.ValidateValue(value, context, this.ValidationAttributes);
            return null;
        }
        catch (ValidationException ex)
        {
            return ex.Message;
        }
    }
    [DebuggerStepThrough]
    private bool TryConvertValue(object rawValue, out object convertedValue)
    {
        if (rawValue != null && 
            rawValue.GetType() == this.PropertyChain.Last().PropertyType)
        {
            convertedValue = rawValue;
            return true;
        }
        try
        {
            convertedValue = this.Converter.ConvertFrom(rawValue);
            return true;
        }
        catch (Exception ex)
        {
            // HACK: There is a bug in the .NET framework BaseNumberConverter class. 
            // The class throws an Exception base class, and therefore we must catch 
            // the 'Exception' base class :-(.
            convertedValue = null;
            return false;
        }
    }
    private class ModelPropertyPair
    {
        public readonly object Model;
        public readonly PropertyInfo Property;
        public ModelPropertyPair(object model, PropertyInfo property)
        {
            this.Model = model;
            this.Property = property;
        }
    }
}

我认为应该为窗体上的每个控件挂钩Validating事件。然后,在这些处理程序中实现您的自定义验证,例如调用DataAnnotations Validator。

如果验证返回失败,那么抛出错误标志就像调用ErrorProvider的SetError方法一样简单。

同时,我相信在你的部分有一些聪明的编码,你可以漏斗你所有的控件到一个单一的验证事件处理程序,所以你可能能够避免创建一个单独的事件处理程序为每个控件,你有