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>

ErrorTemplate

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>

file

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);
        }
    }
}