WPF MVVM进阶系列教程(二、数据验证)

五一出去浪吹风着凉了,今天有点发烧😷

手头的工作放一放,更新一下博客吧。

什么是数据验证(Validation)

数据验证是指用于捕获非法数值并拒绝这些非法数值的逻辑。

大多数采用用户输入的应用都需要有验证逻辑,以确保用户已输入预期信息。 验证检查可以基于类型、范围、格式或其他特定于应用的要求。 

例如我有一个文本框,我限制该字段是必填项,当文本框内为空时,就会出现一个提示。

虽然我们可以通过编码来实现限制,但最佳实现方式是使用WPF的数据验证功能。

运行效果如下

WPF 提供了多种工具来帮助我们在应用程序中定义验证。可用的工具非常多,而且细节也非常丰富,本文主要介绍MVVM模式中数据验证功能的实现。

使用依赖属性进行验证

在前面的文章中,我们介绍了依赖属性

使用的是下面这种方式进行定义

1 public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata);

WPF还提供了一种重载,它增加了一个ValidateValueCallback回调。

1 public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback);

这个回调方法用于验证分配给它的值,返回 true(有效)或 false(无效)

ValidateValueCallback回调返回false 时,会引发 ArgumentException。

如果在绑定时,设置了ValidatesOnException属性为true,那么控件值将被设置为 “0”

并且控件将被设置为默认的错误模板(默认错误模板会用红色高亮显示绑定的控件)

1   <TextBox VerticalContentAlignment="Center" Text="{Binding PositiveNumber,RelativeSource={RelativeSource TemplatedParent},ValidatesOnExceptions=True}"></TextBox>

 默认错误模板

错误模板(ErrorTemplate)是数据绑定验证失败时的一种样式触发器,它提供视觉上的反馈,后面我们会详细介绍相关功能。

下面我们通过一个案例进行演示,我们新建一个只支持输入正数的自定义控件

PositiveValueTextBox.cs

使用NumberValidateValueCallback对值进行验证,只接受正数,不接受负数 

 1 public class MyTextBox : Control2 {3     static MyTextBox()4     {5         DefaultStyleKeyProperty.OverrideMetadata(typeof(MyTextBox), new FrameworkPropertyMetadata(typeof(MyTextBox)));6     }7 8     public static DependencyProperty PositiveNumberProperty = DependencyProperty.Register("PositiveNumber", typeof(int), typeof(MyTextBox), new PropertyMetadata(), NumberValidateValueCallback);9 
10     public int PositiveNumber
11     {
12         get => (int)GetValue(PositiveNumberProperty);
13         set => SetValue(PositiveNumberProperty, value);
14     }
15 
16     private static bool NumberValidateValueCallback(object value)
17     {
18         if ((int)value >= 0)
19             return true;
20 
21         return false;
22     }
23 }

定义控件模板

Generic.xaml

 1  <Style TargetType="{x:Type controls:PositiveValueTextBox}">2      <Setter Property="Template">3          <Setter.Value>4              <ControlTemplate TargetType="{x:Type controls:PositiveValueTextBox}">5                  <Border Background="{TemplateBinding Background}"6                          BorderBrush="{TemplateBinding BorderBrush}"7                          BorderThickness="{TemplateBinding BorderThickness}">8                      <TextBox VerticalContentAlignment="Center" Text="{Binding PositiveNumber,RelativeSource={RelativeSource TemplatedParent},ValidatesOnExceptions=True}"></TextBox>9                  </Border>
10              </ControlTemplate>
11          </Setter.Value>
12      </Setter>
13  </Style>

当我们输入负数时,可以看到TextBox会红色高亮显示

自定义错误模板(Error Templates)

在前面我们提到了错误模板这个概念,我们可以通过在视图中高亮显示验证失败的特定字段来通知用户出错情况。

默认情况下,该元素会以红色边框突出显示。我们可以通过重写错误模板自定义显示效果。

定义错误模板方法如下:

我们在窗口资源中定义一个控件模板

 1  <Window.Resources>2      <ControlTemplate x:Key="ValidationErrorTemplate">3          <DockPanel LastChildFill="True">4              <Border BorderBrush="Green" BorderThickness="2">5                  <AdornedElementPlaceholder></AdornedElementPlaceholder>6              </Border>7              <TextBlock DockPanel.Dock="Right" Foreground="Red" Margin="3,0,0,0" Text="*" FontSize="14" FontWeight="Bold" VerticalAlignment="Center"></TextBlock>8          </DockPanel>9      </ControlTemplate>
10  </Window.Resources>

使用方法如下:

1   <TextBox Text="{Binding DigitValue}" Width="600" Height="25" VerticalContentAlignment="Center" Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"></TextBox>

AdornedElementPlaceholder是支持这种技术的粘合剂。它代表控件自身,位于元素层中。

通过使用AdornedElementPlaceholder元素,能在文本框的背后安排自己的内容。

因此,在该例中,边框被直接放在文本框上,而不管文本框的尺寸是多少。

在这个示例中,星号放在右边。而且新的错误模板内容叠加在已存在的内容之上,从而不会在原始窗口的布局中触发任何改变(实际上,如果不小心在装饰层中包含了过多内容,最终会改变窗口的其他部分)。

运行效果如下:

自定义验证规则 

在前面的示例中,我们可以对绑定时验证失败的控件进行视觉效果上的显示。但是却无法看到具体的错误信息。

这个时候我们就需要自定义验证规则 。应用自定义验证规则的方法和应用自定义转换器的方法类似。

自定义验证规则方法如下:

1、创建一个类继承自ValidationRule(位于System.Windows.Controls名称空间)的类

2、重写Validation方法

3、自定义错误模板,显示验证失败消息

例如我想限制文本框输入0-100的值

创建RangeLimitRule.cs

 1 public class RangeLimitRule : ValidationRule2 {3     public override ValidationResult Validate(object value, CultureInfo cultureInfo)4     {5         if (int.TryParse(value.ToString(), out int number) == false)6         {7             return new ValidationResult(false, "请输入数字");8         }9         else
10         {
11             if (number >= 0 && number <= 100)
12                 return ValidationResult.ValidResult;
13 
14             return new ValidationResult(false, $"输入{value}格式错误,请输入0-100的数字");
15         }
16     }
17 }

放置一个TextBox并进行绑定,在绑定时,指定使用的验证规则。

MainWindow.xaml

 1 <Window x:Class="_3_ValidationRule.MainWindow"2         xmlns:validationRules="clr-namespace:_3_ValidationRule.CustomValidationRule"3         xmlns:local="clr-namespace:_3_ValidationRule"4         mc:Ignorable="d"5         Title="MainWindow" Height="450" Width="800">6   <StackPanel>7       <TextBox Height="30" Margin="20" VerticalContentAlignment="Center">8           <TextBox.Text>9               <Binding Path="RangeDigitValue">
10                   <Binding.ValidationRules>
11                       <validationRules:RangeLimitRule></validationRules:RangeLimitRule>
12                   </Binding.ValidationRules>
13               </Binding>
14           </TextBox.Text>
15       </TextBox>
16       <Button Content="确认" Width="88" Height="28" HorizontalAlignment="Center"></Button>
17   </StackPanel>
18 </Window>

然后再自定义错误模板,对验证失败的信息进行显示

 1  <Style TargetType="TextBox">2      <Setter Property="Validation.ErrorTemplate">3          <Setter.Value>4              <ControlTemplate>5                  <DockPanel LastChildFill="True">6                      <Border BorderBrush="Green" BorderThickness="2">7                          <AdornedElementPlaceholder Name="adornedElement"></AdornedElementPlaceholder>8                      </Border>9                      <TextBlock DockPanel.Dock="Right" Foreground="Red" Margin="3,0,0,0" Text="*" FontSize="14" FontWeight="Bold" VerticalAlignment="Center"></TextBlock>
10                  </DockPanel>
11              </ControlTemplate>
12          </Setter.Value>
13      </Setter>
14 
15      <Style.Triggers>
16          <Trigger Property="Validation.HasError" Value="True">
17              <Setter Property="ToolTip">
18                  <Setter.Value>
19                      <Binding RelativeSource="{x:Static RelativeSource.Self}" Path="(Validation.Errors)[0].ErrorContent"></Binding>
20                  </Setter.Value>
21              </Setter>
22          </Trigger>
23      </Style.Triggers>
24  </Style>

为检索实际错误,需要检查这个元素的 Validation.Error属性。

注意,需要用圆括号包围Validation.Errors属性,从而指示它是附加属性而不是TextBox类的属性。

最后,需要使用索引器从集合中检索第一个ValidationError 对象,然后提取该对象的ErrorContent属性:

1   <Binding RelativeSource="{x:Static RelativeSource.Self}" Path="(Validation.Errors)[0].ErrorContent"></Binding>

运行效果如下:

传递参数到ValidationRule

XAML 技术在向转换器/命令/自定义验证传递参数方面非常灵活。我们甚至可以将整个数据上下文作为参数传递。

自定义验证规则时,如果需要传递参数,可以直接在自定义验证规则中增加属性,并在XAML中使用它。

我们对前面的验证规则进行升级,增加最大最小值属性。

RangeLimitRuleWithParameter.cs

 1 public class RangeLimitRuleWithParameter : ValidationRule2 {3     public int MinValue { get; set; }4 5     public int MaxValue { get; set; }6 7 8     public override ValidationResult Validate(object value, CultureInfo cultureInfo)9     {
10         if (int.TryParse(value.ToString(), out int number) == false)
11         {
12             return new ValidationResult(false, "请输入数字");
13         }
14         else
15         {
16             if (number >= MinValue && number <= MaxValue)
17                 return ValidationResult.ValidResult;
18 
19             return new ValidationResult(false, $"输入{value}格式错误,请输入{MinValue}-{MaxValue}的数字");
20         }
21     }
22 }

使用方法如下:

1 <TextBox.Text>
2     <Binding Path="RangeDigitValue">
3         <Binding.ValidationRules>
4             <!--传递参数-->
5             <validationRules:RangeLimitRuleWithParameter MinValue="0" MaxValue="100"></validationRules:RangeLimitRuleWithParameter>
6         </Binding.ValidationRules>
7     </Binding>
8 </TextBox.Text>

自定义验证时使用转换器

假设我们需要在界面上输入一些数字,但是又要考虑多语言,比如我输入一(中文)/one(英文) 都要支持,那么应该如何去操作呢?

对于界面中的同一字段,我们可以同时定义转换器规则和验证规则。

可以通过ValidationRule的 ValidationStep属性用于控制规则的应用时间。

它是一个枚举类型,定义如下:

 1     //2     // 摘要:3     //     Specifies when a System.Windows.Controls.ValidationRule runs.4     public enum ValidationStep5     {6         //7         // 摘要:8         //    在进行任何转换之前运行 9         RawProposedValue = 0,
10         //
11         // 摘要:
12         //   在进行任何转换之后运行
13         ConvertedProposedValue = 1,
14         //
15         // 摘要:
16         //   在源更新以后运行
17         UpdatedValue = 2,
18         //
19         // 摘要:
20         //    在值提交到源后运行 
21         CommittedValue = 3
22     }

我们这里就可以使用ValidationStep.ConvertedProposedValue,在值进行转换以后运行。

我们先来看看如何使用:

我们需要在绑定时设置Converter,以及在ValidationRule里设置ValidationStep属性。

1  <TextBox.Text>
2      <Binding Path="RangeDigitValueWithConverter" Converter="{StaticResource MultiLangDigitConverter}">
3          <Binding.ValidationRules>
4              <validationRules:RangeLimitRuleWithParameter MinValue="1" MaxValue="3" ValidationStep="ConvertedProposedValue"></validationRules:RangeLimitRuleWithParameter>
5          </Binding.ValidationRules>
6      </Binding>
7  </TextBox.Text>

接下来我们演示一下详细的实现过程

在上个示例的基础上,我们增加一个字段RangeDigitValueWithConverter

MainWindowViewModel.cs

 1  public class MainWindowViewModel : INotifyPropertyChanged2  {3      public event PropertyChangedEventHandler? PropertyChanged;4 5      private int rangeDigitValueWithConverter = 1;6 7      public int RangeDigitValueWithConverter8      {9          get => rangeDigitValueWithConverter;
10          set
11          {
12              rangeDigitValueWithConverter = value;
13              PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("RangeDigitValueWithConverter"));
14          }
15      }
16  }

然后我们增加一个Converter

需要注意的是,这里的实现是写在ConvertBack函数下的。

MultiLangDigitConverter.cs

 1 public class MultiLangDigitConverter : IValueConverter2 {3     /// <summary>4     /// 从源到目标5     /// </summary>6     /// <param name="value"></param>7     /// <param name="targetType"></param>8     /// <param name="parameter"></param>9     /// <param name="culture"></param>
10     /// <returns></returns>
11     /// <exception cref="NotImplementedException"></exception>
12     public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
13     {
14         return value;
15     }
16 
17     /// <summary>
18     /// 从目标到源
19     /// </summary>
20     /// <param name="value"></param>
21     /// <param name="targetType"></param>
22     /// <param name="parameter"></param>
23     /// <param name="culture"></param>
24     /// <returns></returns>
25     /// <exception cref="NotImplementedException"></exception>
26     public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
27     {
28         var val = value.ToString();
29 
30         //直接输入了数字
31         if (int.TryParse(val, out int numValue))
32         {
33             return numValue;
34         }
35         else
36         {
37             var res = Application.Current.TryFindResource(val);
38 
39             if(res != null)
40             {
41                 return res;
42             }
43 
44             return value;
45         }
46     }
47 }

en-US.xaml

1 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
2                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3                     xmlns:sys="clr-namespace:System;assembly=mscorlib">
4     <!--仅供演示-->
5     <sys:Int32 x:Key="One">1</sys:Int32>
6     <sys:Int32 x:Key="Two">2</sys:Int32>
7     <sys:Int32 x:Key="Three">3</sys:Int32>
8 </ResourceDictionary>

zh-CN.xaml

1 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
2                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3                     xmlns:sys="clr-namespace:System;assembly=mscorlib">
4     <!--仅供演示-->
5     <sys:Int32 x:Key="一">1</sys:Int32>
6     <sys:Int32 x:Key="二">2</sys:Int32>
7     <sys:Int32 x:Key="三">3</sys:Int32>
8 </ResourceDictionary>

说明:

1、这里仅为演示自定义验证规则时使用Converter,不考虑实用性。

2、这里我们定义了多语言资源字典,从资源字典里去查找值。关于这里多语言切换是如何实现的,可以查看这篇文章里的第二种实现方法(使用.Net Core开发WPF App系列教程( 其他、实现多语言切换的几种方式) - zhaotianff - 博客园)

运行效果如下:

当切换为中文时,输入数字或中文一、二、三都能被验证成功。

当切换为英文时,输入数字或英文One、Two、Three都能被验证成功

使用IDataErrorInfo验证

当我们使用MVVM模式进行开发时,在ViewModel里就可以实现这个接口,对界面上的状态进行验证。

在前面的示例中,我们只是在界面上进行了视觉效果上的提醒,但是却没有去限制用户提交数据,因为我们无法在ViewModel层取到错误信息。

使用IDataErrorInfo就可以实现真正意义上的限制提交。

IDataErrorInfo的使用类似于前面介绍过的INotifyPropertyChanged.

它的定义如下:

 1     //2     // 摘要:3     //     提供了提供自定义错误信息的功能,用户界面 可绑定的自定义错误信息。4     [DefaultMember("Item")]5     public interface IDataErrorInfo6     {7         //8         // 摘要:9         //     获取指定属性的错误信息
10         //
11         // 参数:
12         //   columnName:
13         //     属性名
14         //
15         // 返回结果:
16         //     错误信息,默认为""
17         string this[string columnName] { get; }
18 
19         //
20         // 摘要:
21         //    获取当前对象的错误消息
22         //
23         // 返回结果:
24         //     当前对象的错误消息,默认为""
25         string Error { get; }
26     }

通过索引器方法,传入需要验证的属性名。当验证失败时,这个索引器就会返回错误消息。

Error属性也可以返回错误消息,但它是针对整个对象的,而不是某个具体的属性。

下面我们来进行演示一下

我们在界面上放置两个文本框,限制两个文本框为必填。再放置一个提交按钮。

首先我们定义一下错误模板

MainWindow.xaml

 1   <Window.Resources>2       <ControlTemplate x:Key="ValidationErrorTemplate">3           <DockPanel LastChildFill="True">4               <Border BorderBrush="Pink" BorderThickness="2">5                   <AdornedElementPlaceholder></AdornedElementPlaceholder>6               </Border>7               <TextBlock DockPanel.Dock="Right" Foreground="Red" Margin="3,0,0,0" Text="*" FontSize="14" FontWeight="Bold" VerticalAlignment="Center"></TextBlock>8           </DockPanel>9       </ControlTemplate>
10       <Style TargetType="TextBox">
11           <Style.Triggers>
12               <Trigger Property="Validation.HasError" Value="True">
13                   <Setter Property="ToolTip">
14                       <Setter.Value>
15                           <Binding RelativeSource="{RelativeSource Mode=Self}" Path="(Validation.Errors)[0].ErrorContent"></Binding>
16                       </Setter.Value>
17                   </Setter>
18               </Trigger>
19           </Style.Triggers>
20       </Style>
21   </Window.Resources>

然后定义一下界面

MainWindow.xaml

 1 <Window x:Class="_4_IDataErrorInfo.MainWindow"2         xmlns:local="clr-namespace:_4_IDataErrorInfo"3         mc:Ignorable="d"4         Title="MainWindow" Height="450" Width="800">5     <StackPanel>6         <Label Content="Id" Margin="10"></Label>7         <TextBox Margin="10" Text="{Binding Id,ValidatesOnDataErrors=True}" Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"></TextBox>8 9         <Label Content="Name" Margin="10"></Label>
10         <TextBox Margin="10" Text="{Binding Name,ValidatesOnDataErrors=True}" Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"></TextBox>
11 
12         <Button Content="提交" HorizontalAlignment="Center" Width="88" Height="28" Command="{Binding ConfirmCommand}"></Button>
13     </StackPanel>
14 </Window>

注意:这里我们在绑定时,使用了ValidatesOnDataErrors=True,它的作用就是使用系统提供的DataErrorValidationRule验证规则。

它等同于以下代码

1 <TextBox Margin="10" Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}">
2     <TextBox.Text>
3         <Binding Path="Name">
4             <Binding.ValidationRules>
5                 <DataErrorValidationRule></DataErrorValidationRule>
6             </Binding.ValidationRules>
7         </Binding>
8     </TextBox.Text>
9 </TextBox>

DataErrorValidationRule的作用是代表检查源对象的 System.ComponentModel.IDataErrorInfo实现引发的错误。

最后我们在ViewModel定义错误验证

MainWindowViewModel实现了IDataErrorInfo接口,它公开了一个Error属性和一个索引器方法

这个索引器方法会在运行时被调用,索引器会将属性名作为参数传递给验证逻辑,并获取任何验证错误信息。

当验证失败时,通过这个索引器方法返回错误信息,这里我们是通过反射判断了是否为空,它可以是其它的逻辑。

当验证成功时,通过这个索引器方法返回null。

当我们运行应用程序并在字段中输入数据时,每次在视图中改变焦点时,运行时都会调用索引器。

因为这里与常规验证规则一样,IDataErrorInfo依赖于绑定的 UpdateSourceTrigger属性。

MainWindowViewModel.cs

 1  public class MainWindowViewModel : INotifyPropertyChanged, IDataErrorInfo2  {3      public event PropertyChangedEventHandler? PropertyChanged;4 5      private string id;6      public string Id7      {8          get => id;9          set
10          {
11              id = value;
12              PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Id"));
13          }
14      }
15 
16 
17      private string name;
18      public string Name 
19      {
20          get => name;
21          set
22          {
23              name = value;
24              PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name"));
25          }
26      }
27 
28      public ICommand ConfirmCommand { get; private set; }
29 
30      public string Error => null;
31 
32      public string this[string columnName]
33      {
34          get
35          {
36              object propertyValue = this.GetType().GetProperty(columnName).GetValue(this, null);
37 
38              if (propertyValue == null || string.IsNullOrEmpty( propertyValue.ToString().Trim()))
39              {
40                  return $"{columnName}不能为空";
41              }
42 
43              return null;
44          }
45 
46      }
47 
48      public MainWindowViewModel()
49      {
50          ConfirmCommand = new RelayCommand(Confirm);
51      }
52 
53      private void Confirm()
54      {
55          if (this[nameof(Id)] != null || this[nameof(Name)] != null)
56              return;
57 
58          MessageBox.Show($"Id:{Id}\r\nName:{Name}");
59      }
60  }

注意:我们需要在提交时使用索引器方法对字段进行再次判断是否符合要求。

运行效果如下:

验证状态问题

在前面的示例中,我们可以发现一个明显的问题,就是程序运行后,所有的字段都进行了验证,都显示为验证失败模板。

这肯定 是不好的用户体验,那么如何去解决呢?

现在主流网站的验证规则 是只有当值更改后/点击提交后再进行验证,初次进入不会验证。

我们也可以通过增加一个枚举变量控制,来实现一样的功能。

当软件初次启动时,不需要验证,点击后/输入值更改后,才需要验证。

ValidationState.cs

1     public enum ValidationState
2     {
3         Initial,
4         Loaded,
5         Submit,
6     }

我们在ViewModel中增加一个枚举变量,并在点击提交按钮后,更新这个变量,并进行属性更改通知。

MainWindowViewModel.cs

 1   public class MainWindowViewModel : INotifyPropertyChanged, IDataErrorInfo2   {3       public Validations.ValidationState ValidationState { get; private set; } = Validations.ValidationState.Initial;4 5       public event PropertyChangedEventHandler? PropertyChanged;6 7       //字段省略8 9       public ICommand ConfirmCommand { get; private set; }
10 
11       public string Error => null;
12 
13       public string this[string columnName]
14       {
15           get
16           {
17               
18               object propertyValue = this.GetType().GetProperty(columnName).GetValue(this, null);
19 
20               if (propertyValue == null || string.IsNullOrEmpty( propertyValue.ToString().Trim()))
21               {
22                   return $"{columnName}不能为空";
23               }
24 
25               return null;
26           }
27 
28       }
29 
30       public MainWindowViewModel()
31       {
32           ConfirmCommand = new RelayCommand(Confirm);
33       }
34 
35       private void RaiseChanges()
36       {
37           PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Id"));
38           PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name"));
39       }
40 
41       private void Confirm()
42       {
43           ValidationState = Validations.ValidationState.Submit;
44           RaiseChanges();
45 
46           if (this[nameof(Id)] != null || this[nameof(Name)] != null)
47               return;
48 
49           MessageBox.Show($"Id:{Id}\r\nName:{Name}");
50       }
51   }

然后我们引入Microsoft.XAML.Behavior包,增加窗口Loaded事件的处理

MainWindow.xaml

 1   <Window x:Class="_5_IDataErrorInfoWithValidationState.MainWindow"2           xmlns:i="http://schemas.microsoft.com/xaml/behaviors"3           mc:Ignorable="d"4           Title="MainWindow" Height="450" Width="800">5       <i:Interaction.Triggers>6           <i:EventTrigger EventName="Loaded">7               <i:InvokeCommandAction Command="{Binding LoadedCommand}"></i:InvokeCommandAction>8           </i:EventTrigger>9       </i:Interaction.Triggers>
10   </Window>

MainWindowViewModel.cs

 public ICommand LoadedCommand { get; private set; }public MainWindowViewModel(){LoadedCommand = new RelayCommand(Loaded);}private void Loaded(){ValidationState = Validations.ValidationState.Loaded;}

最后我们在索引器方法中更新判断逻辑,当只有在界面加载后,再进行判断

 1 public string this[string columnName]2 {3     get4     {5         //判断当前验证状态6         if (ValidationState < Validations.ValidationState.Loaded)7             return null;8 9         。。。17     }
18 
19 }

运行效果如下:

汇总错误验证消息

在前面的示例中,当提交按钮点击时,我们使用索引器对各个字段再次进行了判断

1 object propertyValue = this.GetType().GetProperty(columnName).GetValue(this, null);
2 
3 if (propertyValue == null || string.IsNullOrEmpty( propertyValue.ToString().Trim()))
4 {
5     return $"{columnName}不能为空";
6 }

这种方法肯定是不够理想的,因为在字段较多时,需要写很长的判断逻辑。

此外,这种方法也无法对验证错误信息进行汇总。

理想的做法应该是使用一个集合将这些验证错误信息进行存储,然后在统一的地方进行判断或显示。

前面我们介绍过IDataErrorInfo.Error字段,目前我们还没有去使用它,到这里就可以派上用场了。

首先我们定义一下用于存储属性和错误信息的数据模型

ValidationErrorInfo.cs

1     public class ValidationErrorInfo
2     {
3         public string PropertyName { get; set; }
4 
5         public string ValidationError { get; set; }
6     }

然后我们定义一个存储错误信息的列表

MainWindowViewModel.cs

 1 private ObservableCollection<ValidationErrorInfo> validationErrorInfoList;2 3 public ObservableCollection<ValidationErrorInfo> ValidationErrorInfoList4 {5     get6     {7         if (validationErrorInfoList == null)8         {9             validationErrorInfoList = new ObservableCollection<ValidationErrorInfo>()
10             {
11                 new ValidationErrorInfo(){PropertyName = "Id" },
12                 new ValidationErrorInfo(){ PropertyName = "Name"}
13             };
14         }
15 
16         return validationErrorInfoList;
17     }
18 }

更新一下索引器方法,当有验证失败错误时,就更新到列表中

 1 public string this[string columnName]2 {3     get4     {5         if (ValidationState < Validations.ValidationState.Loaded)6             return null;7 8         string errorMsg = null;9 
10         object propertyValue = this.GetType().GetProperty(columnName).GetValue(this, null);
11 
12         if (propertyValue == null || string.IsNullOrEmpty(propertyValue.ToString().Trim()))
13         {
14             errorMsg =  $"{columnName}不能为空";
15         }
16 
17         ValidationErrorInfoList.FirstOrDefault(x => x.PropertyName == columnName).ValidationError = errorMsg;
18 
19         return errorMsg;
20     }
21 
22 }

此外,我们还可以进行更复杂 的判断,比如对值进行限制。

1  switch(columnName)
2  {
3      case nameof(Id):
4          Error = "xxxx";
5          break;
6      case nameof(Name):
7          break;
8  }

然后我们将IDataErrorInfo.Error字段也修改为可通知类型

 1  private string error = "";2 3  public string Error4  {5      get => error;6      set7      {8          error = value;9          PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Error"));
10      }
11  }

最后再更新一下提交按钮逻辑,将错误信息列表中的信息进行整合并进行判断

 1  private void Confirm()2  {3      ValidationState = Validations.ValidationState.Submit;4      RaiseChanges();5 6      Error = string.Join("\r\n", ValidationErrorInfoList.Select<ValidationErrorInfo, string>(e => e.ValidationError).ToArray<string>());7 8     if(!string.IsNullOrEmpty(Error))9      {
10          MessageBox.Show($"数据验证失败:\r\n{Error}");
11      }
12  }

同时验证多个字段

假设我们界面上有两个密码框,两次输入的密码要一样。

如何同时验证多个字段呢?

只需要在进行属性更改通知的时候,通知另外 一个属性即可

MainWindowViewModel.cs

 1  private string password;2  public string Password3  {4      get => password;5      set6      {7          password = value;8          PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Password"));9      }
10  }
11 
12 
13  private string confirmPassword;
14  public string ConfirmPassword
15  {
16      get => confirmPassword;
17      set
18      {
19          confirmPassword = value;
20          PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ConfirmPassword"));
21          PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Password"));
22      }
23  }

为了实现密码对比,只需要在索引器方法中,对两个值进行对比

 1                 switch(columnName)2                 {3                     case nameof(ConfirmPassword):4                         if(!string.IsNullOrEmpty(Password) && Password != ConfirmPassword)5                         {6                             errorMsg = "两次密码不一致.";7                         }    8 9                         break;
10                 }

INotifyDataErrorInfo

这个接口是在.NET Framework 4.5版本加入的。

定义如下:

 1 namespace System.ComponentModel2 {3     //4     // 摘要:定义了数据实体类可以实现的成员,以提供自定义同步和异步验证支持。5     public interface INotifyDataErrorInfo6     {7         //8         // 摘要:9         //     获取指示实体是否存在验证错误的值。
10         //
11         // 返回结果:
12         //     如果实体当前有验证错误,则为 true;否则为 false。
13         bool HasErrors { get; }
14 
15         //
16         // 摘要:
17         //     当某个属性或整个 实体的验证错误发生时。
18         event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
19 
20         //
21         // 摘要:
22         //     获取指定属性或整个实体的验证错误。
23         //
24         //
25         // 参数:
26         //   propertyName:
27         //    要检索验证错误的属性名称;或空或 System.String.Empty 来检索实体级错误
28         //
29         // 返回结果:
30         //     属性或实体的验证错误。
31         IEnumerable GetErrors(string? propertyName);
32     }
33 }

与 IDataErrorInfo相比,INotifyDataErrorInfo可以为一个属性返回多个错误信息。通过调用 GetErrors函数来 获取与作为参数传递的名称的属性相关的验证错误。

当属性值更新时,我们可以在后台线程中开始验证。如果验证失败,我们可以引发该属性的 ErrorsChanged事件。(跟属性更改通知一样的用法)

这种异步支持提高了应用程序的响应速度。

下面我们使用一个示例来进行演示

我们在界面上放置一个文本框,限制该文本框只能输入数字,且长度不能超过4

然后我们定义一下这个文本框的验证错误模板,这里我们使用了ItemsControl来展示多个验证错误信息。

MainWindow.xaml

 1 <Window x:Class="_8_INotifyDataErrorInfo.MainWindow"2         Title="MainWindow" Height="450" Width="800">3     <StackPanel>4         <Label Content="Id" Margin="10"></Label>5         <TextBox Text="{Binding Id, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" Margin="10">6             <Validation.ErrorTemplate>7                 <ControlTemplate>8                     <StackPanel>9                         <AdornedElementPlaceholder x:Name="textBox"/>
10                         <ItemsControl ItemsSource="{Binding}">
11                             <ItemsControl.ItemTemplate>
12                                 <DataTemplate>
13                                     <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
14                                 </DataTemplate>
15                             </ItemsControl.ItemTemplate>
16                         </ItemsControl>
17                     </StackPanel>
18                 </ControlTemplate>
19             </Validation.ErrorTemplate>
20         </TextBox>
21     </StackPanel>
22 </Window>

MainWindowViewModel.cs

定义属性

 1         private string id;2 3         public string Id4         {5             get => id;6             set7             {8                 id = value;9                 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Id"));11             }
12         }

定义错误信息容器

这里我们定义为字典类型,一个Key对应一个列表

 1 private Dictionary<string, ICollection<string>> validationErrorInfoList;2 3 public Dictionary<string, ICollection<string>> ValidationErrorInfoList4 {5     get6     {7         if (validationErrorInfoList == null)8             validationErrorInfoList = new Dictionary<string, ICollection<string>>();9 
10         return validationErrorInfoList;
11     }
12 }

然后我们封装一个用于内部验证逻辑的函数

说明:在后期正式项目时,这个函数可能来自于某个Service或单独的类

 1 private bool ValidatePropertyInternal(string propertyName, out ICollection<string> validationErrors)2 {3     validationErrors = new List<string>();4 5     if (string.IsNullOrEmpty(propertyName))6         return false;7 8     object propertyValue = this.GetType().GetProperty(propertyName).GetValue(this, null);9 
10 
11     if (propertyValue == null || propertyValue.ToString().Trim().Equals(string.Empty))
12     {
13         validationErrors.Add($"{propertyName}是必须的");
14     }
15 
16 
17     switch(propertyName)
18     {
19         case nameof(Id):
20             {
21                 if(int.TryParse(propertyValue.ToString(),out int nId) == false)
22                 {
23                     validationErrors.Add($"{propertyName}必须填入数字");
24                 }
25 
26                 if(propertyValue.ToString().Length > 4)
27                 {
28                     validationErrors.Add($"{propertyName}限制长度为4");
29                 }
30                 break;
31             }
32             
33     }
34 
35     return validationErrors.Count == 0;
36 
37 }

一切准备就绪后,我们就可以实现INotifyDataErrorInfo接口

在这里我们有一些封装

1、RaiseErrorsChanged

这个函数的功能类似于前面的RaisePropertyChanged,它的作用是用于通知某个属性的验证发生错误。

2、ValidatePropertyAsync

在这个函数里,我们调用ValidatePropertyInternal进行内部验证逻辑,并在验证后调用RaiseErrorsChanged引发错误验证通知。

然后系统会调用HasErrors判断是否有验证错误。如果有验证错误,并当需要获取错误信息时,系统会调用GetErrors函数来进行获取。

 1 public bool HasErrors2 {3     get { return ValidationErrorInfoList.Count > 0; }4 }  5 6 public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;7 8 private void RaiseErrorsChanged(string propertyName)9 {
10     if (ErrorsChanged != null)
11         ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
12 }
13 
14 public IEnumerable GetErrors(string propertyName)
15 {
16     if (string.IsNullOrEmpty(propertyName)|| !ValidationErrorInfoList.ContainsKey(propertyName))
17         return null;
18 
19     if(ValidationErrorInfoList.ContainsKey(propertyName))
20     {
21         return ValidationErrorInfoList[propertyName];
22     }
23 
24     return null;
25 }
26 
27 private async void ValidatePropertyAsync(string propertyName)
28 {
29     ICollection<string> validationErrors = null;
30 
31     //异步验证
32     bool isValid = await Task.Run(() =>
33     {
34         return ValidatePropertyInternal(propertyName, out validationErrors);
35     })
36     .ConfigureAwait(false);
37 
38     if (!isValid)
39     {
40         ValidationErrorInfoList[propertyName] = validationErrors;
41         RaiseErrorsChanged(propertyName);
42     }
43     else if (ValidationErrorInfoList.ContainsKey(propertyName))
44     {
45         ValidationErrorInfoList.Remove(propertyName);
46         RaiseErrorsChanged(propertyName);
47     }
48 }

最后,我们需要在属性值更改时,调用ValidatePropertyAsync函数进行验证

 1  public string Id2  {3      get => id;4      set5      {6          id = value;7          PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Id"));8          ValidatePropertyAsync("Id");
 9      }
10  }

运行效果如下:

示例代码

https://github.com/zhaotianff/WPF-MVVM-Beginner/tree/main/8_Validation

参考资料:

Data validation in WPF | Magnus Montin

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/90482.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/90482.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI 音频产品开发模板及流程(二)

AI 音频产品开发模板及流程&#xff08;一&#xff09; 6. 同声传译 实时翻译&#xff0c;发言与翻译几乎同步&#xff0c;极大提升沟通效率。支持多语言互译&#xff0c;适用于国际会议、商务洽谈等多场景。自动断句、转写和翻译&#xff0c;减少人工干预&#xff0c;提升准…

kafka4.0集群部署

kafka4.0是最新版kafka&#xff0c;可在kafka官网下载&#xff0c;依赖的jdk版本要求在jdk17及jdk17以上tar -xzf kafka_2.13-4.0.0.tgzmv kafka_2.13-4.0.0 kafkacd kafka# 随便一台节点运行生成随机uuid&#xff0c;后面每台节点都要使用此uuidbin/kafka-storage.sh random-u…

【News】同为科技亮相首届气象经济博览会

7月18日&#xff0c;由中国气象服务协会主办的国内首个以“气象经济”为核心的国家级博览会——首届气象经济博览会&#xff08;以下简称“博览会”&#xff09;在合肥滨湖国际会展中心开幕。北京同为科技有限公司&#xff08;TOWE&#xff09;作为雷电防护领域的技术领导企业&…

数据结构 堆(2)---堆的实现

上篇文章我们详细介绍了堆和树的基本概念以及它们之间的关系&#xff0c;还要知道一般实现堆的方式是使用顺序结构的数组进行存储数据及实现。下来我们看看利用顺序结构的数组如何实现对的内容:1.堆的实现关于堆的实现&#xff0c;也是三个文件&#xff0c;头文件&#xff0c;实…

Arraylist与LinkedList区别

&#x1f4da; 欢迎来到我的Java八股文专栏&#xff01; &#x1f389;各位程序员小伙伴们好呀~ &#x1f44b; 我是雪碧聊技术&#xff0c;很高兴能在CSDN与大家相遇&#xff01;✨&#x1f680; 专栏介绍这个专栏将专注于分享Java面试中的经典"八股文"知识点 &…

Java实战:基于Spring Cloud的电商微服务架构设计——从拆分到高可用的全流程解析

引言 2023年双十一大促期间,某传统电商平台的单体应用再次“爆雷”:凌晨1点订单量突破50万单/分钟时,用户服务因数据库连接池被订单模块占满,导致登录接口响应时间从200ms飙升至5秒,大量用户流失。技术团队紧急回滚后发现:这个运行了7年的单体应用,早已变成“代码泥潭”…

STL学习(二、vector容器)

1.vector构造函数函数原型vector<int> v // 默认构造&#xff0c;size为0vector(const_iterator beg, const_iterator end) // 将v的[begin, end) 元素拷贝过来vector(n, elem) // 构造函数将n个elem拷贝到本身vector(const vector & v) // 拷贝构造2.vect…

深度学习-算子

概念&#xff1a;标识数字图像中亮度变化明显的点处理步骤1.滤波处理算子通常被称为滤波器。2.增强确定各点sobel算子概念&#xff1a;主要用于获得数字图像的一阶梯度&#xff0c;本质是梯度运算。Scharr算子Scharr算子 是一种用于边缘检测的梯度算子&#xff0c;它是Sobel算子…

全国产8通道250M AD FMC子卡

4片8路ADS42LB69标准FMC采集子卡自研成品ADC采集子卡和定制化设计ADC采集子卡&#xff0c;实测采集指标均与手册标称值一致。该板卡有全国产化和进口两个版本&#xff0c;基于FMC标准设计&#xff0c;实现8路16bit/250MSPS ADC采集功能&#xff0c;遵循 VITA 57 标准&#xff0…

【牛客网C语言刷题合集】(三)

&#x1f31f;菜鸟主页&#xff1a;晨非辰的主页 &#x1f440;学习专栏&#xff1a;《C语言刷题集》 &#x1f4aa;学习阶段&#xff1a;C语言方向初学者 ⏳名言欣赏&#xff1a;"任何足够先进的bug都与魔法无异。" 前言&#xff1a;刷题博客主要记录在学习编程语言…

Python之--字典

定义字典&#xff08;dict&#xff09;是一种无序、可变且可哈希的数据结构&#xff0c;字典是根据一个信息来查找另一个信息&#xff0c;它表示索引用的键和对应的值构成的成对关系。特点&#xff08;1&#xff09;字典与列表一样&#xff0c;是Python里面的可变数据类型。&am…

【ARM】ARM微架构

1、 文档目标对 ARM 微架构的概念有初步的了解。2、 问题场景在和客户沟通和新同事交流时对于 ARM 架构和微架构二者有什么区别和联系&#xff0c;做一个简单的介绍。3、软硬件环境1、软件版本&#xff1a;不涉及2 、电脑环境&#xff1a;不涉及4、关于 ARM 架构和微架构架构不…

c++注意点(11)----设计模式(工厂方法)

创建型模式工厂方法模式是一种创建型设计模式&#xff0c; 其在父类中提供一个创建对象的方法&#xff0c; 允许子类决定实例化对象的类型。为什么需要工厂方法模式&#xff1f;看一个 “没有工厂模式” 的痛点场景&#xff1a;假设你在开发一个游戏&#xff0c;最初只有 “战士…

基于Kubernetes的微服务CI/CD:Jenkins Pipeline全流程实践

一、部署gitlab GitLab 是一个集代码托管、CI/CD、项目管理、安全扫描于一体的 DevOps 平台&#xff0c;提供从代码编写到部署的全生命周期管理。它支持 Git 版本控制&#xff0c;内置自动化流水线&#xff0c;可与 Kubernetes 集成&#xff0c;实现云原生应用的持续交付。同时…

Spring Bean初始化及@PostConstruc执行顺序

目录 1. Bean初始化执行顺序 2. 成员变量初始化顺序 2.1 普通Java类&#xff08;非Spring环境&#xff09; (1) 默认初始化(即初始分配内存) (2) 显式初始化 (3) 构造器初始化 (4)完整顺序 2.2 Spring管理的Bean&#xff08;依赖注入场景&#xff09; (1) 普通成员变量…

webRTC合并本地源码修改和官方更新

一、总体思路&#xff1a;基于 Git 分支管理改动origin/main 是官方 WebRTC 主干&#xff08;来自 webrtc.googlesource.com&#xff09;。my/webrtc 是你自己开发和修改的分支。每次 Google 更新 WebRTC&#xff0c;你从 origin/main 拉新代码&#xff0c;再把 my/webrtc 分支…

c++注意点(12)----设计模式(生成器)

创建型模式生成器模式&#xff08;Builder Pattern&#xff09;是一种创建型设计模式&#xff0c;它专注于将复杂对象的构建过程与表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。就像是做饭&#xff0c;你可以自己慢慢做&#xff0c;各个步骤自己选择。而使用生成…

[特殊字符] VLA 如何“绕过”手眼标定?—— 当机器人学会了“看一眼就动手”

&#x1f52e; VLA 如何“绕过”手眼标定&#xff1f;—— 当机器人学会了“看一眼就动手” 作者&#xff1a;石去皿 发布时间&#xff1a;2025年7月 在传统机器人系统中&#xff0c;“手眼标定”是每一个工程师都绕不开的课题。 你必须精确测量相机和机械臂之间的空间变换关系…

《Maven 核心基础笔记(第一天)》

1.说明maven软件依赖管理和项目构建功能maven是为Java项目工作的 功能体现&#xff1a;依赖管理&#xff0c;项目构建 依赖管理&#xff1a;我们只需要写配置文件(pom.xml)&#xff0c;maven就会帮我们下载依赖&#xff0c;并且也会下载依赖的依赖。 项目构建&#xff1a;项目源…

Yolo底层原理学习(V1~V3)(第一篇)

一&#xff0c;卷积后的特征图大小计算众所周知&#xff0c;提到深度学习&#xff0c;必不可少的会提及卷积&#xff0c;那么如何计算卷积之后的图片大小呢&#xff1f;下图呈现&#xff1a;如图&#xff0c; 我们令FH&#xff0c;FW为原图像的长度FH*FW。P为padding的长度&…