深入剖析WPF数据验证

1、WPF包含的验证方式

方式

父类

对应接口

处理方式

简便写法

默认值

ExceptionValidationRule 

ValidationRule

检查在更新数据源(Source)时抛出的异常

ValidatesOnExceptions=True

false

DataErrorValidationRule

ValidationRule

IDataErrorInfo

检查实现了IDataError接口的对象生成的错误

ValidatesOnDataErrors=True

false

NotifyDataErrorValidationRule

ValidationRule

INotifyDataErrorInfo

检查实现了INotifyDataErrorInfo接口的对象生成的错误

ValidatesOnNotifyDataErrors=True

true

说明:

1、如果一个Binding的ValidationRules设置了ExceptionValidationRule或在Binding中直接指定ValidateOnException=true,那么它捕获属性中抛出的异常,参照下面的详细示例

2、对应的如果一个Binding的ValidationRules设置了DataErrorValidationRule或NotifyDataErrorValidationRule,或直接在Binding中指定ValidateOnDataError或ValidateOnNotifyDataErrors,那么它捕获实现对应接口的错误

2、ValidationStep属性,默认是:RawProposedValue,对于DataErrorValidationRule来说,默认是UpdatedValue

<ExceptionValidationRule ValidationStep="RawProposedValue" />
    //
    // Summary:
    //     确定何时执行ValidationRule
    public enum ValidationStep
    {
        //
        // Summary:
        //     在数据没有转换之前(应用Converter)之前执行ValidationRule
        RawProposedValue = 0,
        //
        // Summary:
        //     在数据转换发生之后,赋值给source之前执行ValidationRule
        ConvertedProposedValue = 1,
        //
        // Summary:
        //     在source的值更新之后执行ValidationRule
        UpdatedValue = 2,
        //
        // Summary:
        //     在数据提交之后执行ValidationRule
        CommittedValue = 3
    }

3、在UI上显示错误信息

  • 指定Validation.ErrorTemplate(AdornedElementPlaceholder是输入控件,如TextBox的占位符)

        <ControlTemplate x:Key="validationTemplate">
            <DockPanel>
                <TextBlock Foreground="Red"  FontSize="20">!</TextBlock>
                <AdornedElementPlaceholder/>
            </DockPanel>
        </ControlTemplate>
  • 使用Style指定错误样式
        <Style x:Key="textStyleTextBox" TargetType="TextBox">
            <Setter Property="Foreground" Value="#333333" />
            <Setter Property="MaxLength" Value="40" />
            <Setter Property="Width" Value="392" />
            <Style.Triggers>
                <Trigger Property="Validation.HasError"  Value="true">
                    <Setter Property="ToolTip" Value="{Binding  RelativeSource={RelativeSource Self},  Path=(Validation.Errors)[0].ErrorContent}" />
                </Trigger>
            </Style.Triggers>
        </Style>

4、Validation处理过程

数据验证发生在TwoWay和OneWayToSource的Binding中

验证流程(如果有一个错误发生,验证流程终止)

  1. 绑定引擎(Binding engine)检查ValidationStep设置为RawProposedValue的任意自定义ValidationRule

  2. 绑定引擎调用Converter(如果有Converter)

  3. 如果转换成功,绑定引擎检查ValidationStep设置为ConvertedProposedValue的任意自定义ValidationRule

  4. 绑定引擎设置source属性

  5. 绑定引擎调用Validate方法,检查ValidationStep设置为UpdatedValue的任意自定义ValidaRule。

    1. 如果Binding中包含DataErrorValidationRule,并且ValidationStep设置为Default(UpdatedValue),此时检查DataErrorValidationRule

    2. 此时也是ValidatesOnDataErrors设置为true的默认检查时机

  6. 绑定引擎调用Validate方法检查ValidationStep设置为CommittedValue的ValidationRule

 

5、说明:

  • 如果一个ValidationRule在某一个时机/步骤没有检查通过,绑定引擎创建一个ValidationError对象,并添加到绑定元素的Errors集合,在此之前,绑定引擎会移除之前此步骤添加的ValidationError对象。例如:如果一个ValidationRule在UpdatedValue步骤没有检查通过,那么下次检查UpdatedValue时机的任意ValidationRule之前,会把之前添加的ValidationError对象移除

  • Errros不为空时,元素的附加属性HasError会设置为true,如果Binding的NotifyOnValidationError=true,那么绑定引擎会触发元素的附加事件Validation.Error

  • 如果一个验证通过的值从source传递到target(或从target传递到source)时,Errors集合会清空

6、ValidationRule.ValidatesOnTargetUpdated属性(默认false)

<local:ValueIsNotNullValidationRule  ValidatesOnTargetUpdated="True" />
        private RelayCommand loadCommand;
        /// <summary>
        /// Gets the LoadCommand.
        /// </summary>
        public RelayCommand LoadCommand
        {
            get
            {
                return loadCommand
                    ?? (loadCommand = new RelayCommand(
                    () =>
                    {
                        UserName = null;
                    }));
            }
        }

如果直接给source赋值时(更新到target),会触发检查ValidatioRule。

 

7、当使用ExceptionValidationRule或ValidateOnExceptions=true时,你可以使用 UpdateSourceExceptionFilter处理验证发出的异常,如果没有 UpdateSourceExceptionFilter,Binding引擎会创建一个ValidationError。

示例:

BindingExpression myBindingExpression =  textBox.GetBindingExpression(TextBox.TextProperty);
Binding myBinding =  myBindingExpression.ParentBinding;
myBinding.UpdateSourceExceptionFilter = new  UpdateSourceExceptionFilterCallback(ReturnExceptionHandler);
myBindingExpression.UpdateSource();
        object ReturnExceptionHandler(object bindingExpression,  Exception exception)
        {
            return "This is from the  UpdateSourceExceptionFilterCallBack.";
        }

8、ExceptionValidationRule 详细示例:它的简便写法是直接在binding设置ValidatesOnExceptions为True,即:下面两种写法效果相同

        <TextBox
            Grid.Column="1"
            Width="200"
            HorizontalAlignment="Left">
            <TextBox.Text>
                <Binding
                    Mode="TwoWay"
                    Path="UserName"
                    UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <ExceptionValidationRule />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
         <TextBox
            Grid.Row="1"
            Grid.Column="1"
            Width="200"
            HorizontalAlignment="Left"
            Text="{Binding Email, Mode=TwoWay,  ValidatesOnExceptions=True,  UpdateSourceTrigger=PropertyChanged}" />

详细示例:

        <Style x:Key="validationTextBoxStyle"  TargetType="TextBox">
            <Setter Property="VerticalAlignment" Value="Center"  />
            <Style.Triggers>
                <Trigger Property="Validation.HasError"  Value="true">
                    <Setter Property="ToolTip" Value="{Binding  (Validation.Errors)[0].ErrorContent,  RelativeSource={RelativeSource Mode=Self}}" />
                </Trigger>
            </Style.Triggers>
        </Style>
        <TextBox
            x:Name="tbUserName"
            Grid.Column="1"
            Width="200"
            HorizontalAlignment="Left"
            Style="{DynamicResource validationTextBoxStyle}">
            <TextBox.Text>
                <Binding
                    Mode="TwoWay"
                    Path="UserName"
                    UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <ExceptionValidationRule  ValidationStep="RawProposedValue" />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        private string userName;
        public string UserName
        {
            get { return userName; }
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new Exception("用户名不能为空");
                }
                if (value.Length < 5)
                {
                    throw new Exception("用户名长度不能小于5个字符");
                }
                if (value != userName)
                {
                    userName = value;
                    RaisePropertyChanged(() => this.UserName);
                }
            }
        }

9、IDataErrorInfo示例

    public class BaseViewModel : ViewModelBase, IDataErrorInfo
    {
        protected Dictionary<string, string> errorList = new  Dictionary<string, string>();
        public string this[string columnName] =>  errorList.ContainsKey(columnName) ? errorList[columnName] :  string.Empty;
        public string Error => errorList.Count > 0 ?  errorList.FirstOrDefault().Value : string.Empty;
    }
        private string email;
        public string Email
        {
            get { return email; }
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    errorList.Add("Email", "Email不能为空");
                    return;
                }
                if (value != email)
                {
                    email = value;
                    RaisePropertyChanged(() => this.Email);
                }
            }
        }
        <TextBox
            Grid.Row="1"
            Grid.Column="1"
            Width="200"
            HorizontalAlignment="Left"
            Style="{DynamicResource validationTextBoxStyle} "
            Text="{Binding Email, Mode=TwoWay,  ValidatesOnDataErrors=True,  UpdateSourceTrigger=PropertyChanged}" />

10、INotifyDataErrorInfo示例

    public class BaseViewModel1 : ViewModelBase,  INotifyDataErrorInfo
    {
        protected Dictionary<string, string> errorList = new  Dictionary<string, string>();
        public bool HasErrors => errorList.Count > 0;
        public event EventHandler<DataErrorsChangedEventArgs>  ErrorsChanged;
        public IEnumerable GetErrors(string propertyName)
        {
            if (errorList.ContainsKey(propertyName))
            {
                yield return errorList[propertyName];
            }
        }
    }

 

        private string email;
        public string Email
        {
            get { return email; }
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    errorList.Add("Email", "Email不能为空");
                    return;
                }
                if (value != email)
                {
                    email = value;
                    RaisePropertyChanged(() => this.Email);
                }
            }
        }
        <TextBox
            Grid.Row="1"
            Grid.Column="1"
            Width="200"
            HorizontalAlignment="Left"
            Style="{DynamicResource validationTextBoxStyle}"
            Text="{Binding Email, Mode=TwoWay,  ValidatesOnNotifyDataErrors=True,  UpdateSourceTrigger=PropertyChanged}" />

11、INotifyDataErrorInfo高级示例(使用特性——Attribute实现验证)

        <TextBox
            Grid.Row="1"
            Grid.Column="1"
            Width="200"
            HorizontalAlignment="Left"
            Style="{DynamicResource validationTextBoxStyle}"
            Text="{Binding Email, Mode=TwoWay,  ValidatesOnNotifyDataErrors=True,  UpdateSourceTrigger=PropertyChanged}" />

其实ValidatesOnNotifyDataErrors默认是true,不用设置也可以,下面的RequiredLooseAttribute继承自:ValidationAttribute,在

System.ComponentModel.DataAnnotations命名空间下,需要引用该dll。
    /// <summary>
    /// 默认允许为null和string.empty,不允许为空白字符
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, AllowMultiple =  false)]
    public class EmailAddressEmptyAttribute :  RequiredLooseAttribute
    {
        protected override bool IsValidString(string  inputString)
        {
            string regPattern =  @"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$";
            Regex reg = new Regex(regPattern);
            return reg.IsMatch(inputString);
        }
    }


    public class ExceptionValidationViewModel :  BaseViewModelWithValidation
    {
        private string email = "11";
        [EmailAddressEmpty(AllowNullOrEmptyString = false,  AllowWhiteSpace = false, ErrorMessage = "请输入合法的Email")]
        public string Email
        {
            get { return email; }
            set
            {
                if (value != email)
                {
                    email = value;
                    RaisePropertyChanged(() => this.Email);
                }
            }
        }
    }
    public abstract class BaseViewModelWithValidation :  BaseViewModel, INotifyDataErrorInfo
    {
        private Dictionary<string, List<string>> allErrorList =  new Dictionary<string, List<string>>();
        public bool HasErrors => allErrorList != null &&  allErrorList.Count > 0;
        public event EventHandler<DataErrorsChangedEventArgs>  ErrorsChanged;
        protected void NotifyErrorsChanged(string propName)
        {
            ErrorsChanged?.Invoke(this, new  DataErrorsChangedEventArgs(propName));
        }

        public IEnumerable GetErrors(string propertyName)
        {
            if (allErrorList != null &&  allErrorList.ContainsKey(propertyName))
            {
                List<string> errors = allErrorList[propertyName];
                foreach (var item in errors)
                {
                    yield return item;
                }
            }
            yield break;
        }

        public override void  RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)
        {
            base.RaisePropertyChanged(propertyExpression);
            ValidateProperty(propertyExpression);
        }

        protected bool ValidateProperty<T>(Expression<Func<T>>  propExpression)
        {
            string propName = GetPropertyName(propExpression);
            object propValue =  GetType().GetProperty(propName).GetValue(this);
            bool isValid = ValidateProperty(propName, propValue);
            return isValid;
        }

        protected bool ValidateProperty(string propName, object  value)
        {
            var validationResults = new List<ValidationResult>();
            ValidationContext context = new  ValidationContext(this) { MemberName = propName };
            bool isValid = Validator.TryValidateProperty(value,  context, validationResults);
            if (isValid)
            {
                RemoveErrorsForProperty(propName);//移除所有的Error
            }
            else
            {
                AddErrorsForProperty(propName,  validationResults);//添加Error
            }
            return isValid;
        }
    }

12、总结

通过三种验证方式的比较,实际项目中用的最多的就是后两种,IDataErrorInfo和INotifyDataErrorInfo,即实现好通用的ViewModel基类之后,使用特性Attribute来给Property校验。

值得一提的是,IDataErrorInfo也可以通过自定义Attribute来实现验证,通过实现IDataErrorInfo的BaseViewModel中通过反射获取属性的Attribute实现

var attr = Attribute.GetCustomAttributes(propInfo,  typeof(ValidationBaseAttribute))
string error = attr.Validate(value);
    [AttributeUsage(AttributeTargets.Property, AllowMultiple  =true)]
    public class ValidationBaseAttribute : Attribute
    {
        public ValidationBaseAttribute()
        {
            
        }
        public string ErrorMessage{ get; set; }
        
        public virtual string Validate(object value)
        {
            return string.Empty;
        }
    }

因为Exception方式要对每个属性都写单独的验证代码(set方法中或者写ValidationRule),而且抛出异常本身就会影响性能,加剧WPF程序的性能问题(本身就够臃肿了……)


版权声明:本文为coaxhappy原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。