2

I'm working on an (legacy) WinForms application and I like to do a more dynamic way of supplying the users with error information in a way I'm used to do with MVC.

Validation in WinForms however seems to work around the IDataErrorInfo interface, but I don't like to implement this interface on the objects I use for binding. I often can bind my command objects to the interface. Commands are DTOs that describe the business operation and are defined in the domain layer (the logic for executing those commands is defined in the business layer).

Since commands are part of the domain, I don't want to implement IDataErrorInfo on them, because this would couple them directly with the validation logic (since calling one of the IDataErrorInfo methods assumes validation). The only thing I want to do is mark my command properties with DataAnnotation attributes.

So my question is: how do I enable validation in WinForms (using the ErrorProvider) but without having to implement IDataErrorInfo on the classes that I use to bind?

For instance, is there a way to hook onto the ErrorProvider and delegate the validation of to DataAnnotations' Validate class?

4

2 回答 2

3

诀窍是查看控件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;
        }
    }
}
于 2013-09-24T12:14:26.393 回答
1

我认为要走的路是为Validating表单上的每个控件挂钩事件。然后,在这些处理程序中,实现您的自定义验证,例如调用 DataAnnotations Validator。

如果验证返回失败,则引发错误标志就像调用 ErrorProvider 的 SetError 方法一样简单。

另外,我确信通过您的一些巧妙的编码,您可以将所有控件集中到单个 Validating 事件处理程序,这样您就可以避免为您拥有的每个控件创建单独的事件处理程序。

于 2013-05-28T14:12:08.943 回答