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
英文地址,請注明出處:http://en.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的長度&…