Validation Rule
简单示例
class ViewModel
{
public int Number { get; set; }
}
<TextBox>
<TextBox.Text>
<Binding Path="Number" UpdateSourceTrigger="PropertyChanged" Delay="200">
</Binding>
</TextBox.Text>
</TextBox>
示例中由于绑定的属性Number是int型(int在绑定到界面上时会自动添加int⇋string的converter),所以当我们输入非数字时,converter抛出的转换异常会被捕获到,此时Textbox上会出现红色边框。
描述错误的实际消息存储在 System.Windows.Controls.ValidationError 对象的ErrorContent属性中,该对象在运行时由绑定引擎添加到绑定元素的Validation.Errors集合中。当附加的属性Validation.Errors中具有 ValidationError 对象时,另一个附加的名为Validation.HasError的属性将返回true。
WPF提供的Validation Rule
- ExceptionValidationRule
ExceptionValidationRule会捕获源属性抛出异常,默认情况下发生验证错误时将显示红色边框,可编写自定义ErrorTemplate来显示(通知)用户。
class ViewModel
{
private int _number;
public int Number
{
get => _number;
set
{
if (value < 20 || value > 50)
{
throw new ArgumentException("The number must be between 20 and 50");
}
_number = value;
}
}
}
<TextBox >
<TextBox.Text>
<Binding Path="Number" UpdateSourceTrigger="PropertyChanged" Delay="200" >
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
- DataErrorValidationRule与NotifyDataErrorValidationRule
DataErrorValidationRule和NotifyDataErrorValidationRule将分别对应检查由IDataErrorInfo与INotifyDataErrorInfo接口引起的错误。
自定义Validation Rule
通过继承ValidationRule接口可自定义用于验证的方法。
public class EmailValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
Regex email = new Regex(@"[\w\.+-]+@[\w\.-]+\.[\w\.-]+", RegexOptions.Compiled);
bool valid = email.IsMatch(value.ToString());
return new ValidationResult(valid, valid ? null : "Input not match Email.");
}
}
public class UrlValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
Regex url = new Regex(@"[\w]+://[^/\s?#]+[^\s?#]+(?:\?[^\s#]*)?(?:#[^\s]*)?", RegexOptions.Compiled);
bool valid = url.IsMatch(value.ToString());
return new ValidationResult(valid, valid ? null : "Input not match Url.");
}
}
public class IPValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
Regex ip = new Regex(@"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9])", RegexOptions.Compiled);
bool valid = ip.IsMatch(value.ToString());
return new ValidationResult(valid, valid ? null : "Input not match IP.");
}
}
<StackPanel Margin="10" >
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Label>Email:</Label>
<TextBox >
<TextBox.Text>
<Binding Path="Email" UpdateSourceTrigger="PropertyChanged" Delay="200" >
<Binding.ValidationRules>
<local:EmailValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10">
<Label>Url:</Label>
<TextBox >
<TextBox.Text>
<Binding Path="Url" UpdateSourceTrigger="PropertyChanged" Delay="200" >
<Binding.ValidationRules>
<local:UrlValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Label>IP:</Label>
<TextBox >
<TextBox.Text>
<Binding Path="IP" UpdateSourceTrigger="PropertyChanged" Delay="200" >
<Binding.ValidationRules>
<local:IPValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
</StackPanel>
自定义ErrorTemplate
通过编写ErrorTemplate来自定义验证不通过时通知的样式。可以直接在Textbox中编写或者在Style中编写以应用到所有Textbox中。
<Style TargetType="TextBox">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<AdornedElementPlaceholder/>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
IDataErrorInfo与INotifyDataErrorInfo
IDataErrorInfo与INotifyDataErrorInfo上行为基本一致,IDataErrorInfo是初始的错误跟踪接口,INotifyDataErrorInfo包含了更多功能,如异步验证等,下面将以INotifyDataErrorInfo进行演示。
INotifyDataErrorInfo
public interface INotifyDataErrorInfo
{
//用于指示类中是否包含错误
bool HasErrors { get; }
//添加或删除错误时触发
event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
//用于获取错误
IEnumerable GetErrors(string propertyName);
}
由于INotifyDataErrorInfo要求将错误链接到特定属性,而每个属性可能包含多个错误,最简单的方式是定义一个Dictionary<T,K>来存储错误列表:
private readonly Dictionary<string, ICollection<string>>
_validationErrors = new Dictionary<string, ICollection<string>>();
同时添加方法用以触发错误变更
private void RaiseErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
private void SetErrors(string propertyName, List<string> errors)
{
_validationErrors.Remove(propertyName);
_validationErrors.Add(propertyName,errors);
RaiseErrorsChanged(propertyName);
}
private void ClearErrors(string propertyName)
{
_validationErrors.Remove(propertyName);
RaiseErrorsChanged(propertyName);
}
完整示例如下(包含Username属性):
class ViewModel : INotifyDataErrorInfo
{
public IEnumerable GetErrors(string propertyName)
{
if (string.IsNullOrWhiteSpace(propertyName) || !_validationErrors.ContainsKey(propertyName))
return null;
return _validationErrors[propertyName];
}
public bool HasErrors => _validationErrors.Count > 0;
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
private readonly Dictionary<string, ICollection<string>>
_validationErrors = new Dictionary<string, ICollection<string>>();
private void RaiseErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
private void SetErrors(string propertyName, List<string> errors)
{
_validationErrors.Remove(propertyName);
_validationErrors.Add(propertyName, errors);
RaiseErrorsChanged(propertyName);
}
private void ClearErrors(string propertyName)
{
_validationErrors.Remove(propertyName);
RaiseErrorsChanged(propertyName);
}
private string _username;
public string Username
{
get => _username;
set
{
_username = value;
List<string> errors = new List<string>();
if (string.IsNullOrWhiteSpace(value))
{
errors.Add("Username不能为空");
}
if (!Regex.IsMatch(value, @"^[a-zA-Z]+$"))
{
errors.Add("Username只能包含字母a-zA-Z");
}
SetErrors(nameof(Username), errors);
}
}
}
同时我们需要改造一下ErrorTemplate以适应多条错误信息,并将绑定的ValidatesOnDataErrors属性设置为True。
<TextBox >
<TextBox.Text>
<Binding Path="Username" UpdateSourceTrigger="PropertyChanged" ValidatesOnDataErrors="True" Delay="200" >
</Binding>
</TextBox.Text>
<Validation.ErrorTemplate>
<ControlTemplate>
<StackPanel>
<AdornedElementPlaceholder/>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
</TextBox>
ValidationAttribute
通过标记属性特性可以方便的指定验证规则,从而将验证逻辑从控制器转移到模型上。
下面使用了内置的特性标记了属性,可以在msdn上找到更多内置特性。还可以从继承 System.ComponentModel.DataAnnotations.ValidationAttribute 类来自定义验证特性。
class ViewModel
{
[Required(ErrorMessage = "必填项")]
[StringLength(10, MinimumLength = 4, ErrorMessage = "字符长度必须在4-10")]
[RegularExpression(@"^[a-zA-Z]+$", ErrorMessage = "只能包含字母a-zA-Z.")]
public string Username { get; set; }
}
这里我们依旧继承INotifyDataErrorInfo触发验证错误通知,将刚才的代码进行一点改造,使用 System.ComponentModel.DataAnnotations 命名空间下的Validator类来验证属性(当然也可以使用反射获取所有Attribute进行验证)。
class ViewModel : INotifyDataErrorInfo
{
public IEnumerable GetErrors(string propertyName)
{
if(string.IsNullOrWhiteSpace(propertyName) || !_validationErrors.ContainsKey(propertyName))
return null;
return _validationErrors[propertyName];
}
public bool HasErrors => _validationErrors.Count > 0;
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
private readonly Dictionary<string, IEnumerable<string>>
_validationErrors = new Dictionary<string, IEnumerable<string>>();
//传入变更的数值和调用的属性名进行验证
private void RaiseErrorsChanged(object value, [CallerMemberName] string propertyName = null)
{
List<ValidationResult> results = new List<ValidationResult>();
bool valid = Validator.TryValidateProperty(value, new ValidationContext(this){MemberName = propertyName}, results);
_validationErrors.Remove(propertyName);
if (!valid)
{
_validationErrors.Add(propertyName, results.Select(x => x.ErrorMessage));
}
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
private string _username;
[Required(ErrorMessage = "必填项")]
[StringLength(10, MinimumLength = 4, ErrorMessage = "字符长度必须在4-10")]
[RegularExpression(@"^[a-zA-Z]+$", ErrorMessage = "只能包含字母a-zA-Z.")]
public string Username
{
get => _username;
set
{
_username = value;
RaiseErrorsChanged(value);
}
}
}