利刃 MVVMLight 5:綁定在表單驗證上的應用


   表單驗證是MVVM體系中的重要一塊。而綁定除了推動 Model-View-ViewModel (MVVM) 模式松散耦合 邏輯、數據 和 UI定義 的關系之外,還為業務數據驗證方案提供強大而靈活的支持。

WPF 中的數據綁定機制包括多個選項,可用於在創建可編輯視圖時校驗輸入數據的有效性。

常見的表單驗證機制有如下幾種:

驗證類型 說明
Exception 驗證 通過在某個 Binding 對象上設置 ValidatesOnExceptions 屬性,如果源對象屬性設置已修改的值的過程中引發異常,則拋出錯誤並為該 Binding 設置驗證錯誤。
ValidationRule 驗證

Binding 類具有一個用於提供 ValidationRule 派生類實例的集合的屬性。這些 ValidationRules 需要覆蓋某個 Validate 方法,該方法由 Binding 在每次綁定控件中的數據發生更改時進行調用。

如果 Validate 方法返回無效的 ValidationResult 對象,則將為該 Binding 設置驗證錯誤。

IDataErrorInfo 驗證

通過在綁定數據源對象上實現 IDataErrorInfo 接口並在 Binding 對象上設置 ValidatesOnDataErrors 屬性,Binding 將調用從綁定數據源對象公開的 IDataErrorInfo API。

如果從這些屬性調用返回非 null 或非空字符串,則將為該 Binding 設置驗證錯誤。

 

 

 

 

 

 

 

 

驗證交互的關系模式如圖:

       

   我們在使用 WPF 中的數據綁定來呈現業務數據時,通常會使用 Binding 對象在目標控件的單個屬性與數據源對象屬性之間提供數據管道。

如果要使得綁定驗證有效,首先需要進行 TwoWay 數據綁定。這表明,除了從源屬性流向目標屬性以進行顯示的數據之外,編輯過的數據也會從目標流向源。

這就是偉大的雙向數據綁定的精髓,所以在MVVM中做數據校驗,會容易的多。

當 TwoWay 數據綁定中輸入或修改數據時,將啟動以下工作流:

1、  用戶通過鍵盤、鼠標、手寫板或者其他輸入設備來輸入或修改數據,從而改變綁定的目標信息
2、 設置源屬性值。
3、 觸發 Binding.SourceUpdated 事件。
4、 如果數據源屬性上的 setter 引發異常,則異常會由 Binding 捕獲,並可用於指示驗證錯誤。
5、 如果實現了 IDataErrorInfo 接口,則會對數據源對象調用該接口的方法獲得該屬性的錯誤信息。
6、 向用戶呈現驗證錯誤指示,並觸發 Validation.Error 附加事件。

 

 







綁定目標向綁定源發送數據更新的請求,而綁定源則對數據進行驗證,並根據不同的驗證機制進行反饋。 

 

    下面我們用實例來對比下這幾種驗證機制,在此之前,我們先做一個事情,就是寫一個錯誤觸發的樣式,來保證錯誤觸發的時候直接清晰的向用戶反饋出去。

我們新建一個資源字典文件,命名為TextBox.xaml,下面這個是資源字典文件的內容,目標類型是TextBoxBase基礎的控件,如TextBox和RichTextBox.

代碼比較簡單,注意標紅的內容,設計一個紅底白字的提示框,當源屬性觸發錯誤驗證的時候,把驗證對象集合中的錯誤內容顯示出來。

  1 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  2                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  3 
  4     <Style x:Key="{x:Type TextBoxBase}" TargetType="{x:Type TextBoxBase}" BasedOn="{x:Null}">
  5         <Setter Property="BorderThickness" Value="1"/>
  6         <Setter Property="Padding" Value="2,1,1,1"/>
  7         <Setter Property="AllowDrop" Value="true"/>
  8         <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
  9         <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
 10         <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
 11         <Setter Property="SelectionBrush" Value="{DynamicResource Accent}" />
 12         <Setter Property="Validation.ErrorTemplate">
 13             <Setter.Value>
 14                 <ControlTemplate>
 15                     <StackPanel Orientation="Horizontal">
 16                         <Border BorderThickness="1" BorderBrush="#FFdc000c" VerticalAlignment="Top">
 17                             <Grid>
 18                                 <AdornedElementPlaceholder x:Name="adorner" Margin="-1"/>
 19                             </Grid>
 20                         </Border>
 21                         <Border x:Name="errorBorder" Background="#FFdc000c" Margin="8,0,0,0"
 22                                 Opacity="0" CornerRadius="0"
 23                                 IsHitTestVisible="False"
 24                                 MinHeight="24" >
 25                             <TextBlock Text="{Binding ElementName=adorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"
 26                                        Foreground="White" Margin="8,2,8,3" TextWrapping="Wrap" VerticalAlignment="Center"/>
 27                         </Border>
 28                     </StackPanel>
 29                     <ControlTemplate.Triggers>
 30                         <DataTrigger Value="True">
 31                             <DataTrigger.Binding>
 32                                 <Binding ElementName="adorner" Path="AdornedElement.IsKeyboardFocused" />
 33                             </DataTrigger.Binding>
 34                             <DataTrigger.EnterActions>
 35                                 <BeginStoryboard x:Name="fadeInStoryboard">
 36                                     <Storyboard>
 37                                         <DoubleAnimation Duration="00:00:00.15"
 38                                                          Storyboard.TargetName="errorBorder"
 39                                                          Storyboard.TargetProperty="Opacity"
 40                                                          To="1"/>
 41                                     </Storyboard>
 42                                 </BeginStoryboard>
 43                             </DataTrigger.EnterActions>
 44                             <DataTrigger.ExitActions>
 45                                 <StopStoryboard BeginStoryboardName="fadeInStoryboard"/>
 46                                 <BeginStoryboard x:Name="fadeOutStoryBoard">
 47                                     <Storyboard>
 48                                         <DoubleAnimation Duration="00:00:00"
 49                                                          Storyboard.TargetName="errorBorder"
 50                                                          Storyboard.TargetProperty="Opacity"
 51                                                          To="0"/>
 52                                     </Storyboard>
 53                                 </BeginStoryboard>
 54                             </DataTrigger.ExitActions>
 55                         </DataTrigger>
 56                     </ControlTemplate.Triggers>
 57                 </ControlTemplate>
 58             </Setter.Value>
 59         </Setter>
 60         <Setter Property="Template">
 61             <Setter.Value>
 62                 <ControlTemplate TargetType="{x:Type TextBoxBase}">
 63                     <Border x:Name="Bd"
 64                             BorderThickness="{TemplateBinding BorderThickness}"
 65                             BorderBrush="{TemplateBinding BorderBrush}"
 66                             Background="{TemplateBinding Background}"
 67                             Padding="{TemplateBinding Padding}"
 68                             SnapsToDevicePixels="true">
 69                         <ScrollViewer x:Name="PART_ContentHost" RenderOptions.ClearTypeHint="Enabled"
 70                                       SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
 71                     </Border>
 72                     <ControlTemplate.Triggers>
 73                         <Trigger Property="IsEnabled" Value="false">
 74                             <Setter Property="Foreground" Value="{DynamicResource InputTextDisabled}"/>
 75                         </Trigger>
 76                         <Trigger Property="IsReadOnly" Value="true">
 77                             <Setter Property="Foreground" Value="{DynamicResource InputTextDisabled}"/>
 78                         </Trigger>
 79                         <Trigger Property="IsFocused" Value="true">
 80                             <Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Accent}" />
 81                         </Trigger>
 82                         <MultiTrigger>
 83                             <MultiTrigger.Conditions>
 84                                 <Condition Property="IsReadOnly" Value="False"/>
 85                                 <Condition Property="IsEnabled" Value="True"/>
 86                                 <Condition Property="IsMouseOver" Value="True"/>
 87                             </MultiTrigger.Conditions>
 88                             <Setter Property="Background" Value="{DynamicResource InputBackgroundHover}"/>
 89                             <Setter Property="BorderBrush" Value="{DynamicResource InputBorderHover}"/>
 90                             <Setter Property="Foreground" Value="{DynamicResource InputTextHover}"/>
 91                         </MultiTrigger>
 92                     </ControlTemplate.Triggers>
 93                 </ControlTemplate>
 94             </Setter.Value>
 95         </Setter>
 96     </Style>
 97     <Style BasedOn="{StaticResource {x:Type TextBoxBase}}" TargetType="{x:Type TextBox}">
 98     </Style>
 99     <Style BasedOn="{StaticResource {x:Type TextBoxBase}}" TargetType="{x:Type RichTextBox}">
100     </Style>
101     
102 </ResourceDictionary>

  然后在App.Xaml中全局注冊到整個應用中。

 1 <Application x:Class="MVVMLightDemo.App" 
 2              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
 3              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
 4              StartupUri="View/BindingFormView.xaml" 
 5              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
 6              d1p1:Ignorable="d" 
 7              xmlns:d1p1="http://schemas.openxmlformats.org/markup-compatibility/2006"
 8              xmlns:vm="clr-namespace:MVVMLightDemo.ViewModel"
 9              xmlns:Common="clr-namespace:MVVMLightDemo.Common">
10   <Application.Resources>
11     <ResourceDictionary>
12             <ResourceDictionary.MergedDictionaries>
13                 <ResourceDictionary Source="/MVVMLightDemo;component/Assets/TextBox.xaml" />
14             </ResourceDictionary.MergedDictionaries>
15             <vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
16             <Common:IntegerToSex x:Key="IntegerToSex" d:IsDataSource="True" />
17         </ResourceDictionary>
18   </Application.Resources>
19 </Application>

 達到的效果如下:

 

下面詳細描述下這三種驗證模式  

1、Exception 驗證:

正如說明中描述的那樣,在具有綁定關系的源字段模型上做驗證異常的引發並拋出,在View中的Xaml對象上設置 ExceptionValidationRule 屬性,響應捕獲異常並顯示。

View代碼:

 1                 <GroupBox Header="Exception 驗證" Margin="10 10 10 10" DataContext="{Binding Source={StaticResource Locator},Path=ValidateException}" >
 2                     <StackPanel x:Name="ExceptionPanel" Orientation="Vertical" Margin="0,10,0,0" >
 3                         <StackPanel>
 4                             <Label Content="用戶名" Target="{Binding ElementName=UserNameEx}"/>
 5                             <TextBox x:Name="UserNameEx" Width="150">
 6                                 <TextBox.Text>
 7                                     <Binding Path="UserNameEx" UpdateSourceTrigger="PropertyChanged">
 8                                         <Binding.ValidationRules>
 9                                             <ExceptionValidationRule></ExceptionValidationRule>
10                                         </Binding.ValidationRules>
11                                     </Binding>
12                                 </TextBox.Text>
13                             </TextBox>
14                         </StackPanel>
15                     </StackPanel>
16                 </GroupBox>

  ViewModel代碼:

 1     /// <summary>
 2     /// Exception 驗證
 3     /// </summary>
 4     public class ValidateExceptionViewModel:ViewModelBase
 5     {
 6         public ValidateExceptionViewModel()
 7         {
 8 
 9         }
10 
11         private String userNameEx;
12         /// <summary>
13         /// 用戶名稱(不為空)
14         /// </summary>
15         public string UserNameEx
16         {
17             get
18             {
19                 return userNameEx;
20             }
21             set
22             {
23                 userNameEx = value;
24                 RaisePropertyChanged(() => UserNameEx);
25                 if (string.IsNullOrEmpty(value))
26                 {
27                     throw new ApplicationException("該字段不能為空!");
28                 }
29             }
30         }
31     }

  結果如圖:

 

將驗證失敗的信息直接拋出來,這無疑是最簡單粗暴的,實現也很簡單,但是只是針對單一源屬性進行驗證, 復用性不高。

而且在組合驗證(比如同時需要驗證非空和其他規則)情況下,會導致Model中寫過重過臃腫的代碼。

 

2、ValidationRule 驗證:

通過繼承ValidationRule 抽象類,並重寫他的Validate方法來擴展編寫我們需要的驗證類。該驗證類可以直接使用在我們需要驗證的屬性。

View代碼:

 1                 <GroupBox Header="ValidationRule 驗證"  Margin="10 20 10 10" DataContext="{Binding Source={StaticResource Locator},Path=ValidationRule}" >
 2                     <StackPanel x:Name="ValidationRulePanel" Orientation="Vertical" Margin="0,20,0,0">
 3                         <StackPanel>
 4                             <Label Content="用戶名" Target="{Binding ElementName=UserName}"/>
 5                             <TextBox Width="150" >
 6                                 <TextBox.Text>
 7                                     <Binding Path="UserName" UpdateSourceTrigger="PropertyChanged">
 8                                     <Binding.ValidationRules>
 9                                         <app:RequiredRule />
10                                     </Binding.ValidationRules>
11                                 </Binding>
12                                 </TextBox.Text>
13                             </TextBox>
14                         </StackPanel>
15 
16                         <StackPanel>
17                             <Label Content="用戶郵箱" Target="{Binding ElementName=UserEmail}"/>
18                             <TextBox Width="150">
19                                 <TextBox.Text>
20                                     <Binding Path="UserEmail" UpdateSourceTrigger="PropertyChanged">
21                                         <Binding.ValidationRules>
22                                             <app:EmailRule />
23                                         </Binding.ValidationRules>
24                                     </Binding>
25                                 </TextBox.Text>
26                             </TextBox>
27                         </StackPanel>
28                     </StackPanel>
29                 </GroupBox>

 重寫兩個ValidationRule,代碼如下:

 1  public class RequiredRule : ValidationRule
 2     {
 3         public override ValidationResult Validate(object value, CultureInfo cultureInfo)
 4         {
 5             if (value == null)
 6                 return new ValidationResult(false, "該字段不能為空值!");
 7             if (string.IsNullOrEmpty(value.ToString()))
 8                 return new ValidationResult(false, "該字段不能為空字符串!");
 9             return new ValidationResult(true, null);
10         }
11     }
12 
13     public class EmailRule : ValidationRule
14     {
15         public override ValidationResult Validate(object value, CultureInfo cultureInfo)
16         {
17             Regex emailReg = new Regex("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$");
18 
19             if (!String.IsNullOrEmpty(value.ToString()))
20             {
21                 if (!emailReg.IsMatch(value.ToString()))
22                 {
23                     return new ValidationResult(false, "郵箱地址不准確!");
24                 }
25             }
26             return new ValidationResult(true, null);
27         }
28     }

 創建了兩個類,一個用於驗證是否為空,一個用於驗證是否符合郵箱地址標准格式。 

ViewModel代碼:

 1   public class ValidationRuleViewModel:ViewModelBase
 2     {
 3         public ValidationRuleViewModel()
 4         {
 5 
 6         }
 7 
 8         #region 屬性
 9 
10         private String userName;
11         /// <summary>
12         /// 用戶名
13         /// </summary>
14         public String UserName
15         {
16             get { return userName; }
17             set { userName = value; RaisePropertyChanged(()=>UserName); }
18         }
19 
20 
21 
22         private String userEmail;
23         /// <summary>
24         /// 用戶郵件
25         /// </summary>
26         public String UserEmail
27         {
28             get { return userEmail; }
29             set { userEmail = value;RaisePropertyChanged(()=>UserName);  }
30         }
31 
32         #endregion

 結果如下:

 說明:相對來說,這種方式是比較不錯的,獨立性、復用性都很好,從松散耦合角度來說也是比較恰當的。

可以預先寫好一系列的驗證規則類,視圖編碼人員可以根據需求直接使用這些驗證規則,服務端無需額外的處理。

但是仍然有缺點,擴展性差,如果需要個性化反饋消息也需要額外擴展。不符合日益豐富的前端驗證需求。

 

3、IDataErrorInfo 驗證:

3.1、在綁定數據源對象上實現 IDataErrorInfo 接口

3.2、在 Binding 對象上設置 ValidatesOnDataErrors 屬性

Binding 將調用從綁定數據源對象公開的 IDataErrorInfo API。如果從這些屬性調用返回非 null 或非空字符串,則將為該 Binding 設置驗證錯誤。

View代碼:

 1                 <GroupBox Header="IDataErrorInfo 驗證" Margin="10 20 10 10" DataContext="{Binding Source={StaticResource Locator},Path=BindingForm}" >
 2                     <StackPanel x:Name="Form" Orientation="Vertical" Margin="0,20,0,0">
 3                         <StackPanel>
 4                             <Label Content="用戶名" Target="{Binding ElementName=UserName}"/>
 5                             <TextBox Width="150" 
 6                                  Text="{Binding UserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" >
 7                             </TextBox>
 8                         </StackPanel>
 9 
10                         <StackPanel>
11                             <Label Content="性別" Target="{Binding ElementName=RadioGendeMale}"/>
12                             <RadioButton Content="" />
13                             <RadioButton Content="" Margin="8,0,0,0" />
14                         </StackPanel>
15                         <StackPanel>
16                             <Label Content="生日" Target="{Binding ElementName=DateBirth}" />
17                             <DatePicker x:Name="DateBirth" />
18                         </StackPanel>
19                         <StackPanel>
20                             <Label Content="用戶郵箱" Target="{Binding ElementName=UserEmail}"/>
21                             <TextBox Width="150" Text="{Binding UserEmail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
22                         </StackPanel>
23                         <StackPanel>
24                             <Label Content="用戶電話" Target="{Binding ElementName=UserPhone}"/>
25                             <TextBox Width="150" Text="{Binding UserPhone, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
26                         </StackPanel>
27                     </StackPanel>
28                 </GroupBox>

 ViewModel代碼:

 1  public class BindingFormViewModel :ViewModelBase, IDataErrorInfo  2     {
 3         public BindingFormViewModel()
 4         {
 5 
 6         }
 7 
 8         #region 屬性
 9              
10         private String userName;
11         /// <summary>
12         /// 用戶名
13         /// </summary>
14         public String UserName
15         {
16             get { return userName; }
17             set { userName = value; }
18         }
19 
20 
21 
22         private String userPhone;
23         /// <summary>
24         /// 用戶電話
25         /// </summary>
26         public String UserPhone
27         {
28             get { return userPhone; }
29             set { userPhone = value; }
30         }
31 
32 
33 
34         private String userEmail;
35         /// <summary>
36         /// 用戶郵件
37         /// </summary>
38         public String UserEmail
39         {
40             get { return userEmail; }
41             set { userEmail = value; }
42         }
43         #endregion
44 
45         public String Error
46         {
47             get { return null; }
48         }
49                 
50         public String this[string columnName]
51         {
52             get
53             {
54                 Regex digitalReg = new Regex(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$");
55                 Regex emailReg = new Regex("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$");
56 
57 
58                 if (columnName == "UserName" && String.IsNullOrEmpty(this.UserName))
59                 {
60                     return "用戶名不能為空";
61                 }
62                 
63                 if (columnName == "UserPhone" && !String.IsNullOrEmpty(this.UserPhone))
64                 {
65                     if (!digitalReg.IsMatch(this.UserPhone.ToString()))
66                     {
67                         return "用戶電話必須為8-11位的數值!";
68                     }
69                 }
70                 
71                 if (columnName == "UserEmail" && !String.IsNullOrEmpty(this.UserEmail))
72                 {
73                     if (!emailReg.IsMatch(this.UserEmail.ToString()))
74                     {
75                         return "用戶郵箱地址不正確!";
76                     }
77                 }
78 
79                 return null;
80             }
81         }
82 
83     }

繼承IDataErrorInfo接口后,實現方法兩個屬性:Error 屬性用於指示整個對象的錯誤,而索引器用於指示單個屬性級別的錯誤。

每次的屬性值發生變化,則索引器進行一次檢查,看是否有驗證錯誤的信息返回。

兩者的工作原理相同:如果返回非 null 或非空字符串,則表示存在驗證錯誤。否則,返回的字符串用於向用戶顯示錯誤。 

結果如圖:

 

 利用 IDataErrorInfo 的好處是它可用於輕松地處理交叉耦合屬性。但也具有一個很大的弊端:
索引器的實現通常會導致較大的 switch-case 語句(對象中的每個屬性名稱都對應於一種情況),
必須基於字符串進行切換和匹配,並返回指示錯誤的字符串。而且,在對象上設置屬性值之前,不會調用 IDataErrorInfo 的實現。

為了避免出現大量的 switch-case,並且將校驗邏輯進行分離提高代碼復用,將驗證規則和驗證信息獨立化於於每個模型對象中, 使用DataAnnotations 無疑是最好的的方案 。

所以我們進行改良一下:

View代碼,跟上面那個一樣:

 1 <GroupBox Header="IDataErrorInfo+ 驗證" Margin="10 20 10 10" DataContext="{Binding Source={StaticResource Locator},Path=BindDataAnnotations}" >
 2                     <StackPanel Orientation="Vertical" Margin="0,20,0,0">
 3                         <StackPanel>
 4                             <Label Content="用戶名" Target="{Binding ElementName=UserName}"/>
 5                             <TextBox Width="150" 
 6                                  Text="{Binding UserName,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}" >
 7                             </TextBox>
 8                         </StackPanel>
 9 
10                         <StackPanel>
11                             <Label Content="性別" Target="{Binding ElementName=RadioGendeMale}"/>
12                             <RadioButton Content="" />
13                             <RadioButton Content="" Margin="8,0,0,0" />
14                         </StackPanel>
15                         <StackPanel>
16                             <Label Content="生日" Target="{Binding ElementName=DateBirth}" />
17                             <DatePicker />
18                         </StackPanel>
19                         <StackPanel>
20                             <Label Content="用戶郵箱" Target="{Binding ElementName=UserEmail}"/>
21                             <TextBox Width="150" Text="{Binding UserEmail, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
22                         </StackPanel>
23                         <StackPanel>
24                             <Label Content="用戶電話" Target="{Binding ElementName=UserPhone}"/>
25                             <TextBox Width="150" Text="{Binding UserPhone,UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
26                         </StackPanel>
27 
28                         <Button Content="提交" Margin="100,16,0,0" HorizontalAlignment="Left" Command="{Binding ValidFormCommand}" />
29                     </StackPanel>
30 
31                 </GroupBox>

 VideModel代碼:

  1 using GalaSoft.MvvmLight;
  2 using System;
  3 using System.Collections.Generic;
  4 using System.Linq;
  5 using System.ComponentModel;
  6 using System.ComponentModel.DataAnnotations;
  7 using GalaSoft.MvvmLight.Command;
  8 using System.Windows;
  9 
 10 namespace MVVMLightDemo.ViewModel
 11 {
 12     [MetadataType(typeof(BindDataAnnotationsViewModel))]
 13     public class BindDataAnnotationsViewModel : ViewModelBase, IDataErrorInfo
 14     {
 15 
 16         public BindDataAnnotationsViewModel()
 17         {    
 18 
 19         }
 20 
 21         #region 屬性 
 22         /// <summary>
 23         /// 表單驗證錯誤集合
 24         /// </summary>
 25         private Dictionary<String, String> dataErrors = new Dictionary<String, String>();
 26 
 27 
 28         private String userName;
 29         /// <summary>
 30         /// 用戶名
 31         /// </summary>
 32         [Required]
 33         public String UserName
 34         {
 35             get { return userName; }
 36             set { userName = value; }
 37         }
 38 
 39 
 40 
 41         private String userPhone;
 42         /// <summary>
 43         /// 用戶電話
 44         /// </summary>
 45         [Required]
 46         [RegularExpression(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$", ErrorMessage = "用戶電話必須為8-11位的數值.")]
 47         public String UserPhone
 48         {
 49             get { return userPhone; }
 50             set { userPhone = value; }
 51         }
 52 
 53 
 54 
 55         private String userEmail;
 56         /// <summary>
 57         /// 用戶郵件
 58         /// </summary>
 59         [Required]
 60         [StringLength(100,MinimumLength=2)]
 61         [RegularExpression("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$", ErrorMessage = "請填寫正確的郵箱地址.")]
 62         public String UserEmail
 63         {
 64             get { return userEmail; }
 65             set { userEmail = value; }
 66         }
 67         #endregion
 68 
 69 
 70         #region 命令
 71 
 72         private RelayCommand validFormCommand;
 73         /// <summary>
 74         /// 驗證表單
 75         /// </summary>
 76         public RelayCommand ValidFormCommand
 77         {
 78             get
 79             {
 80                 if (validFormCommand == null)
 81                     return new RelayCommand(() => ExcuteValidForm());
 82                 return validFormCommand;
 83             }
 84             set { validFormCommand = value; }
 85         }
 86         /// <summary>
 87         /// 驗證表單
 88         /// </summary>
 89         private void ExcuteValidForm()
 90         {
 91             if (dataErrors.Count == 0) MessageBox.Show("驗證通過!");
 92             else MessageBox.Show("驗證失敗!");
 93         }
 94 
 95         #endregion
 96 
 97 
 98         public string this[string columnName]
 99         {
100             get
101             {
102                 ValidationContext vc = new ValidationContext(this, null, null);
103                 vc.MemberName = columnName;
104                 var res = new List<ValidationResult>();
105                 var result = Validator.TryValidateProperty(this.GetType().GetProperty(columnName).GetValue(this, null), vc, res);
106                 if (res.Count > 0)
107                 {
108                     AddDic(dataErrors,vc.MemberName);
109                     return string.Join(Environment.NewLine, res.Select(r => r.ErrorMessage).ToArray());
110                 }
111                 RemoveDic(dataErrors,vc.MemberName);
112                 return null;
113             }
114         }
115 
116         public string Error
117         {
118             get
119             {
120                 return null;
121             }
122         }
123 
124 
125         #region 附屬方法
126 
127         /// <summary>
128         /// 移除字典
129         /// </summary>
130         /// <param name="dics"></param>
131         /// <param name="dicKey"></param>
132         private void RemoveDic(Dictionary<String, String> dics, String dicKey)
133         {
134             dics.Remove(dicKey);
135         }
136 
137         /// <summary>
138         /// 添加字典
139         /// </summary>
140         /// <param name="dics"></param>
141         /// <param name="dicKey"></param>
142         private void AddDic(Dictionary<String, String> dics, String dicKey)
143         {
144             if (!dics.ContainsKey(dicKey)) dics.Add(dicKey, "");
145         }
146         #endregion
147 
148     }
149 }

  DataAnnotations相信很多人很熟悉,可以使用數據批注來自定義用戶的模型數據,記得引用 System.ComponentModel.DataAnnotations。

他包含如下幾個驗證類型: 

驗證屬性 說明 
CustomValidationAttribute 使用自定義方法進行驗證。
DataTypeAttribute 指定特定類型的數據,如電子郵件地址或電話號碼。
EnumDataTypeAttribute 確保值存在於枚舉中。
RangeAttribute 指定最小和最大約束。
RegularExpressionAttribute 使用正則表達式來確定有效的值。
RequiredAttribute 指定必須提供一個值。
StringLengthAttribute 指定最大和最小字符數。
ValidationAttribute 用作驗證屬性的基類。

 

 

 

 

 

 

 

 

 

    這邊我們使用到了RequiredAttribute、StringLengthAttribute、RegularExpressionAttribute 三項,如果有需要進一步了解 DataAnnotations 的可以參考微軟官網:

https://msdn.microsoft.com/en-us/library/dd901590(VS.95).aspx

用 DataAnnotions 后,Model 的更加簡潔,校驗也更加靈活。可以疊加組合驗證 , 面對復雜驗證模式的時候,可以自由的使用正則來驗證。

默認情況下,框架會提供相應需要反饋的消息內容,當然也可以自定義錯誤消息內容:ErrorMessage 。

這邊我們還加了個全局的錯誤集合收集器 :dataErrors,在提交判斷時候判斷是否驗證通過。

這邊我們進一步封裝索引器,並且通過反射技術讀取當前字段下的屬性進行驗證。

 結果如下:

 

=====================================================================================================================================

===================================================================================================================================== 

封裝ValidateModelBase類:

上面的驗證比較合理了,不過相對於開發人員還是太累贅了,開發人員關心的是Model的DataAnnotations的配置,而不是關心在這個ViewModel要如何做驗證處理,所以我們進一步抽象。

編寫一個ValidateModelBase,把需要處理的工作都放在里面。需要驗證屬性的Model去繼承這個基類。如下:

 

ValidateModelBase 類,請注意標紅部分:

 1  public class ValidateModelBase : ObservableObject, IDataErrorInfo
 2     {
 3         public ValidateModelBase()
 4         {
 5               
 6         }
 7 
 8         #region 屬性 
 9         /// <summary>
10         /// 表當驗證錯誤集合
11         /// </summary>
12         private Dictionary<String, String> dataErrors = new Dictionary<String, String>();
13 
14         /// <summary>
15         /// 是否驗證通過
16         /// </summary>
17         public Boolean IsValidated
18         {
19             get
20             {
21                 if (dataErrors != null && dataErrors.Count > 0)
22                 {
23                     return false;
24                 }
25                 return true;
26             }
27         }
28         #endregion
29 
30         public string this[string columnName]
31         {
32             get
33             {
34                 ValidationContext vc = new ValidationContext(this, null, null);
35                 vc.MemberName = columnName;
36                 var res = new List<ValidationResult>();
37                 var result = Validator.TryValidateProperty(this.GetType().GetProperty(columnName).GetValue(this, null), vc, res);
38                 if (res.Count > 0)
39                 {
40                     AddDic(dataErrors, vc.MemberName);
41                     return string.Join(Environment.NewLine, res.Select(r => r.ErrorMessage).ToArray());
42                 }
43                 RemoveDic(dataErrors, vc.MemberName);
44                 return null;
45             }
46         }
47 
48         public string Error
49         {
50             get
51             {
52                 return null;
53             }
54         }
55 
56 
57         #region 附屬方法
58 
59         /// <summary>
60         /// 移除字典
61         /// </summary>
62         /// <param name="dics"></param>
63         /// <param name="dicKey"></param>
64         private void RemoveDic(Dictionary<String, String> dics, String dicKey)
65         {
66             dics.Remove(dicKey);
67         }
68 
69         /// <summary>
70         /// 添加字典
71         /// </summary>
72         /// <param name="dics"></param>
73         /// <param name="dicKey"></param>
74         private void AddDic(Dictionary<String, String> dics, String dicKey)
75         {
76             if (!dics.ContainsKey(dicKey)) dics.Add(dicKey, "");
77         }
78         #endregion
79     }

 驗證的模型類:繼承 ValidateModelBase

 1 [MetadataType(typeof(BindDataAnnotationsViewModel))]
 2     public class ValidateUserInfo : ValidateModelBase
 3     {
 4         #region 屬性 
 5         private String userName;
 6         /// <summary>
 7         /// 用戶名
 8         /// </summary>
 9         [Required]
10         public String UserName
11         {
12             get { return userName; }
13             set { userName = value; RaisePropertyChanged(() => UserName); }
14         }
15 
16 
17 
18         private String userPhone;
19         /// <summary>
20         /// 用戶電話
21         /// </summary>
22         [Required]
23         [RegularExpression(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$", ErrorMessage = "用戶電話必須為8-11位的數值.")]
24         public String UserPhone
25         {
26             get { return userPhone; }
27             set { userPhone = value; RaisePropertyChanged(() => UserPhone); }
28         }
29 
30 
31 
32         private String userEmail;
33         /// <summary>
34         /// 用戶郵件
35         /// </summary>
36         [Required]
37         [StringLength(100, MinimumLength = 2)]
38         [RegularExpression("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$", ErrorMessage = "請填寫正確的郵箱地址.")]
39         public String UserEmail
40         {
41             get { return userEmail; }
42             set { userEmail = value; RaisePropertyChanged(() => UserEmail);  }
43         }
44         #endregion
45     }

  ViewModel代碼如下:

 1   public class PackagedValidateViewModel:ViewModelBase
 2     {
 3         public PackagedValidateViewModel()
 4         {
 5             ValidateUI = new Model.ValidateUserInfo();
 6         }
 7 
 8         #region 全局屬性
 9         private ValidateUserInfo validateUI;
10         /// <summary>
11         /// 用戶信息
12         /// </summary>
13         public ValidateUserInfo ValidateUI
14         {
15             get
16             {
17                 return validateUI;
18             }
19 
20             set
21             {
22                 validateUI = value;
23                 RaisePropertyChanged(()=>ValidateUI);
24             }
25         }             
26         #endregion
27 
28         #region 全局命令
29         private RelayCommand submitCmd;
30         public RelayCommand SubmitCmd
31         {
32             get
33             {
34                 if(submitCmd == null) return new RelayCommand(() => ExcuteValidForm());
35                 return submitCmd;
36             }
37 
38             set
39             {
40                 submitCmd = value;
41             }
42         }
43         #endregion
44 
45         #region 附屬方法
46         /// <summary>
47         /// 驗證表單
48         /// </summary>
49         private void ExcuteValidForm()
50         {
51             if (ValidateUI.IsValidated) MessageBox.Show("驗證通過!");
52             else MessageBox.Show("驗證失敗!");
53         }
54         #endregion
55     }

 結果如下:

 

 示例代碼下載

 MVVMLightDemo_Valid0.2.rar

 

轉載請標明出處,謝謝


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM