实现WPF TextBoxes的验证

时间:2013-10-23 10:43:54

标签: c# wpf validation xaml data-binding

我有3个文本框(Id1NameSalary)。 IdSalary应包含整数,Name只应包含字符。我需要对TextBox进行验证,它应该在输入错误的字符或整数时显示错误。也可以只在没有代码隐藏的Xaml中完成吗?请帮我提供所需的代码

这是Xaml代码:

<TextBox Name="tb1" HorizontalAlignment="Left" Height="20" Margin="60,10,0,0" TextWrapping="NoWrap" Text="{Binding SelectedItem.Id,ElementName=dgsample}" VerticalAlignment="Top" Width="100" />
<TextBox Name="tb2" HorizontalAlignment="Left" Height="20" Margin="60,60,0,0" TextWrapping="NoWrap" Text="{Binding SelectedItem.Name, ElementName=dgsample}" VerticalAlignment="Top" Width="100"/>
<TextBox Name="tb3" HorizontalAlignment="Left" Height="20" Margin="60,110,0,0" TextWrapping="NoWrap" Text="{Binding SelectedItem.Salary, ElementName=dgsample}" VerticalAlignment="Top" Width="100"/>

7 个答案:

答案 0 :(得分:63)

有三种方法可以实现验证:

  1. 验证规则
  2. INotifyDataErrorInfo
  3. 的实现
  4. IDataErrorInfo
  5. 的实现

    验证规则示例

    public class NumericValidationRule : ValidationRule
    {
        public Type ValidationType { get; set; }
        public override ValidationResult Validate(object value, CultureInfo cultureInfo)
        {
            string strValue = Convert.ToString(value);
    
            if (string.IsNullOrEmpty(strValue))
                return new ValidationResult(false, $"Value cannot be coverted to string.");
            bool canConvert = false;
            switch (ValidationType.Name)
            {
    
                case "Boolean":
                    bool boolVal = false;
                    canConvert = bool.TryParse(strValue, out boolVal);
                    return canConvert ? new ValidationResult(true, null) : new ValidationResult(false, $"Input should be type of boolean");
                case "Int32":
                    int intVal = 0;
                    canConvert = int.TryParse(strValue, out intVal);
                    return canConvert ? new ValidationResult(true, null) : new ValidationResult(false, $"Input should be type of Int32");
                case "Double":
                    double doubleVal = 0;
                    canConvert = double.TryParse(strValue, out doubleVal);
                    return canConvert ? new ValidationResult(true, null) : new ValidationResult(false, $"Input should be type of Double");
                case "Int64":
                    long longVal = 0;
                    canConvert = long.TryParse(strValue, out longVal);
                    return canConvert ? new ValidationResult(true, null) : new ValidationResult(false, $"Input should be type of Int64");
                default:
                    throw new InvalidCastException($"{ValidationType.Name} is not supported");
            }
        }
    }
    

    XAML:

    非常重要:如果没有这个定义,请不要忘记设置ValidatesOnTargetUpdated="True"它不会工作。

     <TextBox x:Name="Int32Holder"
             IsReadOnly="{Binding IsChecked,ElementName=CheckBoxEditModeController,Converter={converters:BooleanInvertConverter}}"
             Style="{StaticResource ValidationAwareTextBoxStyle}"
             VerticalAlignment="Center">
        <!--Text="{Binding Converter={cnv:TypeConverter}, ConverterParameter='Int32', Path=ValueToEdit.Value, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"-->
        <TextBox.Text>
            <Binding Path="Name"
                     Mode="TwoWay"
                     UpdateSourceTrigger="PropertyChanged"
                     Converter="{cnv:TypeConverter}"
                     ConverterParameter="Int32"
                     ValidatesOnNotifyDataErrors="True"
                     ValidatesOnDataErrors="True"
                     NotifyOnValidationError="True">
                <Binding.ValidationRules>
                    <validationRules:NumericValidationRule ValidationType="{x:Type system:Int32}"
                                                           ValidatesOnTargetUpdated="True" />
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
        <!--NumericValidationRule-->
    </TextBox>
    

    INotifyDataErrorInfo示例

    public abstract class ViewModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
    {
    
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
    
        public void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
            ValidateAsync();
        }
        #endregion
    
    
        public virtual void OnLoaded()
        {
        }
    
        #region INotifyDataErrorInfo
        private ConcurrentDictionary<string, List<string>> _errors = new ConcurrentDictionary<string, List<string>>();
    
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    
        public void OnErrorsChanged(string propertyName)
        {
            var handler = ErrorsChanged;
            if (handler != null)
                handler(this, new DataErrorsChangedEventArgs(propertyName));
        }
    
        public IEnumerable GetErrors(string propertyName)
        {
            List<string> errorsForName;
            _errors.TryGetValue(propertyName, out errorsForName);
            return errorsForName;
        }
    
        public bool HasErrors
        {
            get { return _errors.Any(kv => kv.Value != null && kv.Value.Count > 0); }
        }
    
        public Task ValidateAsync()
        {
            return Task.Run(() => Validate());
        }
    
        private object _lock = new object();
        public void Validate()
        {
            lock (_lock)
            {
                var validationContext = new ValidationContext(this, null, null);
                var validationResults = new List<ValidationResult>();
                Validator.TryValidateObject(this, validationContext, validationResults, true);
    
                foreach (var kv in _errors.ToList())
                {
                    if (validationResults.All(r => r.MemberNames.All(m => m != kv.Key)))
                    {
                        List<string> outLi;
                        _errors.TryRemove(kv.Key, out outLi);
                        OnErrorsChanged(kv.Key);
                    }
                }
    
                var q = from r in validationResults
                        from m in r.MemberNames
                        group r by m into g
                        select g;
    
                foreach (var prop in q)
                {
                    var messages = prop.Select(r => r.ErrorMessage).ToList();
    
                    if (_errors.ContainsKey(prop.Key))
                    {
                        List<string> outLi;
                        _errors.TryRemove(prop.Key, out outLi);
                    }
                    _errors.TryAdd(prop.Key, messages);
                    OnErrorsChanged(prop.Key);
                }
            }
        }
        #endregion
    
    }
    

    查看模型实施:

    public class MainFeedViewModel : BaseViewModel//, IDataErrorInfo
    {
        private ObservableCollection<FeedItemViewModel> _feedItems;
        [XmlIgnore]
        public ObservableCollection<FeedItemViewModel> FeedItems
        {
            get
            {
                return _feedItems;
            }
            set
            {
                _feedItems = value;
                OnPropertyChanged("FeedItems");
            }
        }
        [XmlIgnore]
        public ObservableCollection<FeedItemViewModel> FilteredFeedItems
        {
            get
            {
                if (SearchText == null) return _feedItems;
    
                return new ObservableCollection<FeedItemViewModel>(_feedItems.Where(x => x.Title.ToUpper().Contains(SearchText.ToUpper())));
            }
        }
    
        private string _title;
        [Required]
        [StringLength(20)]
        //[CustomNameValidationRegularExpression(5, 20)]
        [CustomNameValidationAttribute(3, 20)]
        public string Title
        {
            get { return _title; }
            set
            {
                _title = value;
                OnPropertyChanged("Title");
            }
        }
        private string _url;
        [Required]
        [StringLength(200)]
        [Url]
        //[CustomValidation(typeof(MainFeedViewModel), "UrlValidation")]
        /// <summary>
        /// Validation of URL should be with custom method like the one that implemented below, or with 
        /// </summary>
        public string Url
        {
            get { return _url; }
            set
            {
                _url = value;
                OnPropertyChanged("Url");
            }
        }
    
        public MainFeedViewModel(string url, string title)
        {
            Title = title;
            Url = url;
        }
        /// <summary>
        /// 
        /// </summary>
        public MainFeedViewModel()
        {
    
        }
        public MainFeedViewModel(ObservableCollection<FeedItemViewModel> feeds)
        {
            _feedItems = feeds;
        }
    
    
        private string _searchText;
        [XmlIgnore]
        public string SearchText
        {
            get { return _searchText; }
            set
            {
                _searchText = value;
    
                OnPropertyChanged("SearchText");
                OnPropertyChanged("FilteredFeedItems");
            }
        }
    
        #region Data validation local
        /// <summary>
        /// Custom URL validation method
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public static ValidationResult UrlValidation(object obj, ValidationContext context)
        {
            var vm = (MainFeedViewModel)context.ObjectInstance;
            if (!Uri.IsWellFormedUriString(vm.Url, UriKind.Absolute))
            {
                return new ValidationResult("URL should be in valid format", new List<string> { "Url" });
            }
            return ValidationResult.Success;
        }
    
        #endregion
    }
    

    XAML:

    <UserControl x:Class="RssReaderTool.Views.AddNewFeedDialogView"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                 mc:Ignorable="d"
                 d:DesignHeight="300"
                 d:DesignWidth="300">
        <FrameworkElement.Resources>
            <Style TargetType="{x:Type TextBox}">
                <Setter Property="Validation.ErrorTemplate">
                    <Setter.Value>
                        <ControlTemplate x:Name="TextErrorTemplate">
                            <DockPanel LastChildFill="True">
                                <AdornedElementPlaceholder>
                                    <Border BorderBrush="Red"
                                            BorderThickness="2" />
                                </AdornedElementPlaceholder>
                                <TextBlock FontSize="20"
                                           Foreground="Red">*?*</TextBlock>
                            </DockPanel>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
                <Style.Triggers>
                    <Trigger Property="Validation.HasError"
                             Value="True">
                        <Setter Property="ToolTip"
                                Value="{Binding RelativeSource=
                {x:Static RelativeSource.Self},
                Path=(Validation.Errors)[0].ErrorContent}"></Setter>
                    </Trigger>
                </Style.Triggers>
            </Style>
            <!--<Style TargetType="{x:Type TextBox}">
                <Style.Triggers>
                    <Trigger Property="Validation.HasError"
                             Value="true">
                        <Setter Property="ToolTip"
                                Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                            Path=(Validation.Errors)[0].ErrorContent}" />
                    </Trigger>
                </Style.Triggers>
            </Style>-->
        </FrameworkElement.Resources>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="5" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="5" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="5" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <TextBlock Text="Feed Name"
                       ToolTip="Display" />
            <TextBox Text="{Binding MainFeedViewModel.Title,UpdateSourceTrigger=PropertyChanged,ValidatesOnNotifyDataErrors=True,ValidatesOnDataErrors=True}"
                     Grid.Column="2" />
    
            <TextBlock Text="Feed Url"
                       Grid.Row="2" />
            <TextBox Text="{Binding MainFeedViewModel.Url,UpdateSourceTrigger=PropertyChanged,ValidatesOnNotifyDataErrors=True,ValidatesOnDataErrors=True}"
                     Grid.Column="2"
                     Grid.Row="2" />
        </Grid>
    </UserControl>
    

    <强> IDataErrorInfo的

    查看型号:

    public class OperationViewModel : ViewModelBase, IDataErrorInfo
    {
        private const int ConstCodeMinValue = 1;
        private readonly IEventAggregator _eventAggregator;
        private OperationInfoDefinition _operation;
        private readonly IEntityFilterer _contextFilterer;
        private OperationDescriptionViewModel _description;
    
        public long Code
        {
            get { return _operation.Code; }
            set
            {
                if (SetProperty(value, _operation.Code, o => _operation.Code = o))
                {
                    UpdateDescription();
                }
            }
        }
    
        public string Description
        {
            get { return _operation.Description; }
            set
            {
                if (SetProperty(value, _operation.Description, o => _operation.Description = o))
                {
                    UpdateDescription();
                }
            }
        }
    
        public string FriendlyName
        {
            get { return _operation.FriendlyName; }
            set
            {
                if (SetProperty(value, _operation.FriendlyName, o => _operation.FriendlyName = o))
                {
                    UpdateDescription();
                }
            }
        }
    
        public int Timeout
        {
            get { return _operation.Timeout; }
            set
            {
                if (SetProperty(value, _operation.Timeout, o => _operation.Timeout = o))
                {
                    UpdateDescription();
                }
            }
        }
    
        public string Category
        {
            get { return _operation.Category; }
            set
            {
                if (SetProperty(value, _operation.Category, o => _operation.Category = o))
                {
                    UpdateDescription();
                }
            }
        }
    
        public bool IsManual
        {
            get { return _operation.IsManual; }
            set
            {
                if (SetProperty(value, _operation.IsManual, o => _operation.IsManual = o))
                {
                    UpdateDescription();
                }
            }
        }
    
    
        void UpdateDescription()
        {
            //some code
        }
    
    
    
    
        #region Validation
    
    
    
    
        #region IDataErrorInfo
    
        public ValidationResult Validate()
        {
            return ValidationService.Instance.ValidateNumber(Code, ConstCodeMinValue, long.MaxValue);
        }
    
        public string this[string columnName]
        {
            get
            {
                var validation = ValidationService.Instance.ValidateNumber(Code, ConstCodeMinValue, long.MaxValue);
    
                return validation.IsValid ? null : validation.ErrorContent.ToString();
            }
        }
    
        public string Error
        {
            get
            {
                var result = Validate();
                return result.IsValid ? null : result.ErrorContent.ToString();
            }
        }
        #endregion
    
        #endregion
    }
    

    XAML:

    <controls:NewDefinitionControl x:Class="DiagnosticsDashboard.EntityData.Operations.Views.NewOperationView"
                                   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                                   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                                   xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                                   xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                                   xmlns:views="clr-namespace:DiagnosticsDashboard.EntityData.Operations.Views"
                                   xmlns:controls="clr-namespace:DiagnosticsDashboard.Core.Controls;assembly=DiagnosticsDashboard.Core"
                                   xmlns:c="clr-namespace:DiagnosticsDashboard.Core.Validation;assembly=DiagnosticsDashboard.Core"
                                   mc:Ignorable="d">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="40" />
                <RowDefinition Height="40" />
                <RowDefinition Height="40" />
                <RowDefinition Height="40" />
                <RowDefinition Height="40" />
                <RowDefinition Height="40" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Label Grid.Column="0"
                   Grid.Row="0"
                   Margin="5">Code:</Label>
            <Label Grid.Column="0"
                   Grid.Row="1"
                   Margin="5">Description:</Label>
            <Label Grid.Column="0"
                   Grid.Row="2"
                   Margin="5">Category:</Label>
            <Label Grid.Column="0"
                   Grid.Row="3"
                   Margin="5">Friendly Name:</Label>
            <Label Grid.Column="0"
                   Grid.Row="4"
                   Margin="5">Timeout:</Label>
            <Label Grid.Column="0"
                   Grid.Row="5"
                   Margin="5">Is Manual:</Label>
            <TextBox Grid.Column="1"
                     Text="{Binding Code,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}"
                     Grid.Row="0"
                     Margin="5"/>
            <TextBox Grid.Column="1"
                     Grid.Row="1"
                     Margin="5"
                     Text="{Binding Description}" />
            <TextBox Grid.Column="1"
                     Grid.Row="2"
                     Margin="5"
                     Text="{Binding Category}" />
            <TextBox Grid.Column="1"
                     Grid.Row="3"
                     Margin="5"
                     Text="{Binding FriendlyName}" />
            <TextBox Grid.Column="1"
                     Grid.Row="4"
                     Margin="5"
                     Text="{Binding Timeout}" />
            <CheckBox Grid.Column="1"
                      Grid.Row="5"
                      Margin="5"
                      IsChecked="{Binding IsManual}"
                      VerticalAlignment="Center" />
    
    
        </Grid>
    </controls:NewDefinitionControl>
    

答案 1 :(得分:26)

您还可以在视图模型中按如下方式实现IDataErrorInfo。如果你实现IDataErrorInfo,你可以在那里进行验证,而不是特定属性的setter,然后每当出现错误时,返回一条错误消息,以便有错误的文本框得到一个红色框它,表示错误。

class ViewModel : INotifyPropertyChanged, IDataErrorInfo
{
    private string m_Name = "Type Here";
    public ViewModel()
    {
    }

    public string Name
    {
        get
        {
            return m_Name;
        }
        set
        {
            if (m_Name != value)
            {
                m_Name = value;
                OnPropertyChanged("Name");
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public string Error
    {
        get { return "...."; }
    }

    /// <summary>
    /// Will be called for each and every property when ever its value is changed
    /// </summary>
    /// <param name="columnName">Name of the property whose value is changed</param>
    /// <returns></returns>
    public string this[string columnName]
    {
        get 
        {
            return Validate(columnName);
        }
    }

    private string Validate(string propertyName)
    {
        // Return error message if there is error on else return empty or null string
        string validationMessage = string.Empty;
        switch (propertyName)
        {
            case "Name": // property name
                // TODO: Check validiation condition
                validationMessage = "Error";
                break;
        }

        return validationMessage;
    }
}

您必须在XAML中设置ValidatesOnDataErrors=True才能调用IDataErrorInfo的方法,如下所示:

<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />

答案 2 :(得分:11)

要仅使用XAML完成此操作,您需要为各个属性添加验证规则。但我会建议你采用代码背后的方法。 在您的代码中,在属性设置器中定义您的规范,并在不符合您的规范时抛出异常。 并使用错误模板在UI中向用户显示错误。 您的XAML将如下所示

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <Style x:Key="CustomTextBoxTextStyle" TargetType="TextBox">
        <Setter Property="Foreground" Value="Green" />
        <Setter Property="MaxLength" Value="40" />
        <Setter Property="Width" Value="392" />
        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="True">
                <Trigger.Setters>
                    <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self},Path=(Validation.Errors)[0].ErrorContent}"/>
                    <Setter Property="Background" Value="Red"/>
                </Trigger.Setters>
            </Trigger>
        </Style.Triggers>
    </Style>
</Window.Resources>
<Grid>
    <TextBox Name="tb2" Height="30" Width="400"
             Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True}" 
             Style="{StaticResource CustomTextBoxTextStyle}"/>
</Grid>

代码背后:

public partial class MainWindow : Window
{
    private ExampleViewModel m_ViewModel;
    public MainWindow()
    {
        InitializeComponent();
        m_ViewModel = new ExampleViewModel();
        DataContext = m_ViewModel;
    }
}


public class ExampleViewModel : INotifyPropertyChanged
{
    private string m_Name = "Type Here";
    public ExampleViewModel()
    {

    }

    public string Name
    {
        get
        {
            return m_Name;
        }
        set
        {
            if (String.IsNullOrEmpty(value))
            {
                throw new Exception("Name can not be empty.");
            }
            if (value.Length > 12)
            {
                throw new Exception("name can not be longer than 12 charectors");
            }
            if (m_Name != value)
            {
                m_Name = value;
                OnPropertyChanged("Name");
            }
        }
    }


    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

    }
}

答案 3 :(得分:3)

我已经实现了这个验证。但你会被用在后面的代码。这是太容易和简单的方式。

<强> XAML: 对于名称Validtion,仅输入A-Z和a-z中的字符。

<TextBox x:Name="first_name_texbox" PreviewTextInput="first_name_texbox_PreviewTextInput" >  </TextBox>

代码背后。

private void first_name_texbox_PreviewTextInput ( object sender, TextCompositionEventArgs e )
{
    Regex regex = new Regex ( "[^a-zA-Z]+" );
    if ( regex.IsMatch ( first_name_texbox.Text ) )
    {
        MessageBox.Show("Invalid Input !");
    }
}

对于Salary和ID验证,请使用[0-9]+替换正则表达式构造函数传递的值。这意味着您只能输入从1到无限的数字。

您还可以使用[0-9]{1,4}定义长度。这意味着您只能输入小于或等于4位数的数字。这个baracket意味着{至少,多少个}。通过这样做,您可以在文本框中定义数字范围。

可能对别人有所帮助。

<强> XAML:       

代码背后。

private void salary_texbox_PreviewTextInput ( object sender, TextCompositionEventArgs e )
{
    Regex regex = new Regex ( "[^0-9]+" );
    if ( regex.IsMatch ( salary_texbox.Text ) )
    {
        MessageBox.Show("Invalid Input !");
    }
}

答案 4 :(得分:2)

谈到Muhammad Mehdi的回答,最好这样做:

private void salary_texbox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
     Regex regex = new Regex ( "[^0-9]+" );
     if(regex.IsMatch(e.Text))
     {
         MessageBox.Show("Error");
     }
}

因为与TextCompositionEventArgs进行比较时,它也会获得最后一个字符,而使用textbox.Text则不会。使用文本框时,错误将在下一个插入的字符后显示。

答案 5 :(得分:0)

当谈到DiSaSteR的答案时,我注意到了另一种行为。 textBox.Text显示用户输入新字符之前的文本框中的文本,而e.Text显示用户刚刚输入的单个字符。一个挑战是,该字符可能不会附加到末尾,但会插入到carret位置:

private void salary_texbox_PreviewTextInput(object sender, TextCompositionEventArgs e){
  Regex regex = new Regex ( "[^0-9]+" );

  string text;
  if (textBox.CaretIndex==textBox.Text.Length) {
    text = textBox.Text + e.Text;
  } else {
    text = textBox.Text.Substring(0, textBox.CaretIndex)  + e.Text + textBox.Text.Substring(textBox.CaretIndex);
  }

  if(regex.IsMatch(text)){
      MessageBox.Show("Error");
  }
}

答案 6 :(得分:0)

当我需要执行此操作时,我按照Microsoft的示例使用Binding.ValidationRules,并且它第一次起作用。

https://docs.microsoft.com/en-us/dotnet/desktop/wpf/data/how-to-implement-binding-validation?view=netframeworkdesktop-4.8