1、WPF包含的验证方式
说明:
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上显示错误信息
<ControlTemplate x:Key="validationTemplate">
<DockPanel>
<TextBlock Foreground="Red" FontSize="20">!</TextBlock>
<AdornedElementPlaceholder/>
</DockPanel>
</ControlTemplate>
<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中
验证流程(如果有一个错误发生,验证流程终止)
-
绑定引擎(Binding engine)检查ValidationStep设置为RawProposedValue的任意自定义ValidationRule
-
绑定引擎调用Converter(如果有Converter)
-
如果转换成功,绑定引擎检查ValidationStep设置为ConvertedProposedValue的任意自定义ValidationRule
-
绑定引擎设置source属性
-
绑定引擎调用Validate方法,检查ValidationStep设置为UpdatedValue的任意自定义ValidaRule。
-
如果Binding中包含DataErrorValidationRule,并且ValidationStep设置为Default(UpdatedValue),此时检查DataErrorValidationRule
-
此时也是ValidatesOnDataErrors设置为true的默认检查时机
-
绑定引擎调用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程序的性能问题(本身就够臃肿了……)