【问题标题】:Adding validation in WinForms without IDataErrorInfo在没有 IDataErrorInfo 的 WinForms 中添加验证
【发布时间】:2013-05-28 10:47:39
【问题描述】:

我正在开发一个(旧版)WinForms 应用程序,我喜欢以我习惯使用 MVC 的方式向用户提供错误信息的更动态的方式。

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

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

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

例如,有没有办法连接到 ErrorProvider 并将验证委托给 DataAnnotations 的 Validate 类?

【问题讨论】:

    标签: c# .net winforms validation data-annotations


    【解决方案1】:

    诀窍是查看控件的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;
            }
        }
    }
    

    【讨论】:

      【解决方案2】:

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

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

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

      【讨论】:

        猜你喜欢
        • 2010-11-26
        • 2012-11-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-05-20
        相关资源
        最近更新 更多