五一出去浪吹風著涼了,今天有點發燒😷
手頭的工作放一放,更新一下博客吧。
什么是數據驗證(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