MVVMLight項(xiàng)目之綁定在表單驗(yàn)證上的應(yīng)用示例分析
表單驗(yàn)證是MVVM體系中的重要一塊。而綁定除了推動 Model-View-ViewModel (MVVM) 模式松散耦合 邏輯、數(shù)據(jù) 和 UI定義 的關(guān)系之外,還為業(yè)務(wù)數(shù)據(jù)驗(yàn)證方案提供強(qiáng)大而靈活的支持。
WPF 中的數(shù)據(jù)綁定機(jī)制包括多個(gè)選項(xiàng),可用于在創(chuàng)建可編輯視圖時(shí)校驗(yàn)輸入數(shù)據(jù)的有效性。
常見的表單驗(yàn)證機(jī)制有如下幾種:
| 驗(yàn)證類型 | 說明 |
| Exception 驗(yàn)證 | 通過在某個(gè) Binding 對象上設(shè)置 ValidatesOnExceptions 屬性,如果源對象屬性設(shè)置已修改的值的過程中引發(fā)異常,則拋出錯(cuò)誤并為該 Binding 設(shè)置驗(yàn)證錯(cuò)誤。 |
| ValidationRule 驗(yàn)證 | Binding 類具有一個(gè)用于提供 ValidationRule 派生類實(shí)例的集合的屬性。這些 ValidationRules 需要覆蓋某個(gè) Validate 方法,該方法由 Binding 在每次綁定控件中的數(shù)據(jù)發(fā)生更改時(shí)進(jìn)行調(diào)用。 如果 Validate 方法返回?zé)o效的 ValidationResult 對象,則將為該 Binding 設(shè)置驗(yàn)證錯(cuò)誤。 |
| IDataErrorInfo 驗(yàn)證 | 通過在綁定數(shù)據(jù)源對象上實(shí)現(xiàn) IDataErrorInfo 接口并在 Binding 對象上設(shè)置 ValidatesOnDataErrors 屬性,Binding 將調(diào)用從綁定數(shù)據(jù)源對象公開的 IDataErrorInfo API。 如果從這些屬性調(diào)用返回非 null 或非空字符串,則將為該 Binding 設(shè)置驗(yàn)證錯(cuò)誤。 |
驗(yàn)證交互的關(guān)系模式如圖:

我們在使用 WPF 中的數(shù)據(jù)綁定來呈現(xiàn)業(yè)務(wù)數(shù)據(jù)時(shí),通常會使用 Binding 對象在目標(biāo)控件的單個(gè)屬性與數(shù)據(jù)源對象屬性之間提供數(shù)據(jù)管道。
如果要使得綁定驗(yàn)證有效,首先需要進(jìn)行 TwoWay 數(shù)據(jù)綁定。這表明,除了從源屬性流向目標(biāo)屬性以進(jìn)行顯示的數(shù)據(jù)之外,編輯過的數(shù)據(jù)也會從目標(biāo)流向源。
這就是偉大的雙向數(shù)據(jù)綁定的精髓,所以在MVVM中做數(shù)據(jù)校驗(yàn),會容易的多。
當(dāng) TwoWay 數(shù)據(jù)綁定中輸入或修改數(shù)據(jù)時(shí),將啟動以下工作流:
| 1、 | 用戶通過鍵盤、鼠標(biāo)、手寫板或者其他輸入設(shè)備來輸入或修改數(shù)據(jù),從而改變綁定的目標(biāo)信息 |
| 2、 | 設(shè)置源屬性值。 |
| 3、 | 觸發(fā) Binding.SourceUpdated 事件。 |
| 4、 | 如果數(shù)據(jù)源屬性上的 setter 引發(fā)異常,則異常會由 Binding 捕獲,并可用于指示驗(yàn)證錯(cuò)誤。 |
| 5、 | 如果實(shí)現(xiàn)了 IDataErrorInfo 接口,則會對數(shù)據(jù)源對象調(diào)用該接口的方法獲得該屬性的錯(cuò)誤信息。 |
| 6、 | 向用戶呈現(xiàn)驗(yàn)證錯(cuò)誤指示,并觸發(fā) Validation.Error 附加事件。 |
綁定目標(biāo)向綁定源發(fā)送數(shù)據(jù)更新的請求,而綁定源則對數(shù)據(jù)進(jìn)行驗(yàn)證,并根據(jù)不同的驗(yàn)證機(jī)制進(jìn)行反饋。
下面我們用實(shí)例來對比下這幾種驗(yàn)證機(jī)制,在此之前,我們先做一個(gè)事情,就是寫一個(gè)錯(cuò)誤觸發(fā)的樣式,來保證錯(cuò)誤觸發(fā)的時(shí)候直接清晰的向用戶反饋出去。
我們新建一個(gè)資源字典文件,命名為TextBox.xaml,下面這個(gè)是資源字典文件的內(nèi)容,目標(biāo)類型是TextBoxBase基礎(chǔ)的控件,如TextBox和RichTextBox.
代碼比較簡單,注意標(biāo)紅的內(nèi)容,設(shè)計(jì)一個(gè)紅底白字的提示框,當(dāng)源屬性觸發(fā)錯(cuò)誤驗(yàn)證的時(shí)候,把驗(yàn)證對象集合中的錯(cuò)誤內(nèi)容顯示出來。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="{x:Type TextBoxBase}" TargetType="{x:Type TextBoxBase}" BasedOn="{x:Null}">
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="2,1,1,1"/>
<Setter Property="AllowDrop" Value="true"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
<Setter Property="SelectionBrush" Value="{DynamicResource Accent}" />
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<Border BorderThickness="1" BorderBrush="#FFdc000c" VerticalAlignment="Top">
<Grid>
<AdornedElementPlaceholder x:Name="adorner" Margin="-1"/>
</Grid>
</Border>
<Border x:Name="errorBorder" Background="#FFdc000c" Margin="8,0,0,0"
Opacity="0" CornerRadius="0"
IsHitTestVisible="False"
MinHeight="24" >
<TextBlock Text="{Binding ElementName=adorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"
Foreground="White" Margin="8,2,8,3" TextWrapping="Wrap" VerticalAlignment="Center"/>
</Border>
</StackPanel>
<ControlTemplate.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<Binding ElementName="adorner" Path="AdornedElement.IsKeyboardFocused" />
</DataTrigger.Binding>
<DataTrigger.EnterActions>
<BeginStoryboard x:Name="fadeInStoryboard">
<Storyboard>
<DoubleAnimation Duration="00:00:00.15"
Storyboard.TargetName="errorBorder"
Storyboard.TargetProperty="Opacity"
To="1"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="fadeInStoryboard"/>
<BeginStoryboard x:Name="fadeOutStoryBoard">
<Storyboard>
<DoubleAnimation Duration="00:00:00"
Storyboard.TargetName="errorBorder"
Storyboard.TargetProperty="Opacity"
To="0"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBoxBase}">
<Border x:Name="Bd"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="true">
<ScrollViewer x:Name="PART_ContentHost" RenderOptions.ClearTypeHint="Enabled"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource InputTextDisabled}"/>
</Trigger>
<Trigger Property="IsReadOnly" Value="true">
<Setter Property="Foreground" Value="{DynamicResource InputTextDisabled}"/>
</Trigger>
<Trigger Property="IsFocused" Value="true">
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Accent}" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsReadOnly" Value="False"/>
<Condition Property="IsEnabled" Value="True"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" Value="{DynamicResource InputBackgroundHover}"/>
<Setter Property="BorderBrush" Value="{DynamicResource InputBorderHover}"/>
<Setter Property="Foreground" Value="{DynamicResource InputTextHover}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style BasedOn="{StaticResource {x:Type TextBoxBase}}" TargetType="{x:Type TextBox}">
</Style>
<Style BasedOn="{StaticResource {x:Type TextBoxBase}}" TargetType="{x:Type RichTextBox}">
</Style>
</ResourceDictionary>然后在App.Xaml中全局注冊到整個(gè)應(yīng)用中。
<Application x:Class="MVVMLightDemo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="View/BindingFormView.xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d1p1:Ignorable="d"
xmlns:d1p1="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:MVVMLightDemo.ViewModel"
xmlns:Common="clr-namespace:MVVMLightDemo.Common">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/MVVMLightDemo;component/Assets/TextBox.xaml" />
</ResourceDictionary.MergedDictionaries>
<vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
<Common:IntegerToSex x:Key="IntegerToSex" d:IsDataSource="True" />
</ResourceDictionary>
</Application.Resources>
</Application>達(dá)到的效果如下:

下面詳細(xì)描述下這三種驗(yàn)證模式
1、Exception 驗(yàn)證:
正如說明中描述的那樣,在具有綁定關(guān)系的源字段模型上做驗(yàn)證異常的引發(fā)并拋出,在View中的Xaml對象上設(shè)置 ExceptionValidationRule 屬性,響應(yīng)捕獲異常并顯示。
View代碼:
<GroupBox Header="Exception 驗(yàn)證" Margin="10 10 10 10" DataContext="{Binding Source={StaticResource Locator},Path=ValidateException}" >
<StackPanel x:Name="ExceptionPanel" Orientation="Vertical" Margin="0,10,0,0" >
<StackPanel>
<Label Content="用戶名" Target="{Binding ElementName=UserNameEx}"/>
<TextBox x:Name="UserNameEx" Width="150">
<TextBox.Text>
<Binding Path="UserNameEx" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule></ExceptionValidationRule>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
</StackPanel>
</GroupBox>ViewModel代碼:
/// <summary>
/// Exception 驗(yàn)證
/// </summary>
public class ValidateExceptionViewModel:ViewModelBase
{
public ValidateExceptionViewModel()
{
}
private String userNameEx;
/// <summary>
/// 用戶名稱(不為空)
/// </summary>
public string UserNameEx
{
get
{
return userNameEx;
}
set
{
userNameEx = value;
RaisePropertyChanged(() => UserNameEx);
if (string.IsNullOrEmpty(value))
{
throw new ApplicationException("該字段不能為空!");
}
}
}
結(jié)果如圖:

將驗(yàn)證失敗的信息直接拋出來,這無疑是最簡單粗暴的,實(shí)現(xiàn)也很簡單,但是只是針對單一源屬性進(jìn)行驗(yàn)證, 復(fù)用性不高。
而且在組合驗(yàn)證(比如同時(shí)需要驗(yàn)證非空和其他規(guī)則)情況下,會導(dǎo)致Model中寫過重過臃腫的代碼。
2、ValidationRule 驗(yàn)證:
通過繼承ValidationRule 抽象類,并重寫他的Validate方法來擴(kuò)展編寫我們需要的驗(yàn)證類。該驗(yàn)證類可以直接使用在我們需要驗(yàn)證的屬性。
View代碼:
<GroupBox Header="ValidationRule 驗(yàn)證" Margin="10 20 10 10" DataContext="{Binding Source={StaticResource Locator},Path=ValidationRule}" >
<StackPanel x:Name="ValidationRulePanel" Orientation="Vertical" Margin="0,20,0,0">
<StackPanel>
<Label Content="用戶名" Target="{Binding ElementName=UserName}"/>
<TextBox Width="150" >
<TextBox.Text>
<Binding Path="UserName" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<app:RequiredRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
<StackPanel>
<Label Content="用戶郵箱" Target="{Binding ElementName=UserEmail}"/>
<TextBox Width="150">
<TextBox.Text>
<Binding Path="UserEmail" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<app:EmailRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
</StackPanel>
</GroupBox>重寫兩個(gè)ValidationRule,代碼如下:
public class RequiredRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (value == null)
return new ValidationResult(false, "該字段不能為空值!");
if (string.IsNullOrEmpty(value.ToString()))
return new ValidationResult(false, "該字段不能為空字符串!");
return new ValidationResult(true, null);
}
}
public class EmailRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
Regex emailReg = new Regex("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$");
if (!String.IsNullOrEmpty(value.ToString()))
{
if (!emailReg.IsMatch(value.ToString()))
{
return new ValidationResult(false, "郵箱地址不準(zhǔn)確!");
}
}
return new ValidationResult(true, null);
}
創(chuàng)建了兩個(gè)類,一個(gè)用于驗(yàn)證是否為空,一個(gè)用于驗(yàn)證是否符合郵箱地址標(biāo)準(zhǔn)格式。
ViewModel代碼:
public class ValidationRuleViewModel:ViewModelBase
{
public ValidationRuleViewModel()
{
}
#region 屬性
private String userName;
/// <summary>
/// 用戶名
/// </summary>
public String UserName
{
get { return userName; }
set { userName = value; RaisePropertyChanged(()=>UserName); }
}
private String userEmail;
/// <summary>
/// 用戶郵件
/// </summary>
public String UserEmail
{
get { return userEmail; }
set { userEmail = value;RaisePropertyChanged(()=>UserName); }
}
#endregion結(jié)果如下:

說明:相對來說,這種方式是比較不錯(cuò)的,獨(dú)立性、復(fù)用性都很好,從松散耦合角度來說也是比較恰當(dāng)?shù)摹?/p>
可以預(yù)先寫好一系列的驗(yàn)證規(guī)則類,視圖編碼人員可以根據(jù)需求直接使用這些驗(yàn)證規(guī)則,服務(wù)端無需額外的處理。
但是仍然有缺點(diǎn),擴(kuò)展性差,如果需要個(gè)性化反饋消息也需要額外擴(kuò)展。不符合日益豐富的前端驗(yàn)證需求。
3、IDataErrorInfo 驗(yàn)證:
3.1、在綁定數(shù)據(jù)源對象上實(shí)現(xiàn) IDataErrorInfo 接口
3.2、在 Binding 對象上設(shè)置 ValidatesOnDataErrors 屬性
Binding 將調(diào)用從綁定數(shù)據(jù)源對象公開的 IDataErrorInfo API。如果從這些屬性調(diào)用返回非 null 或非空字符串,則將為該 Binding 設(shè)置驗(yàn)證錯(cuò)誤。
View代碼:
<GroupBox Header="IDataErrorInfo 驗(yàn)證" Margin="10 20 10 10" DataContext="{Binding Source={StaticResource Locator},Path=BindingForm}" >
<StackPanel x:Name="Form" Orientation="Vertical" Margin="0,20,0,0">
<StackPanel>
<Label Content="用戶名" Target="{Binding ElementName=UserName}"/>
<TextBox Width="150"
Text="{Binding UserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" >
</TextBox>
</StackPanel>
<StackPanel>
<Label Content="性別" Target="{Binding ElementName=RadioGendeMale}"/>
<RadioButton Content="男" />
<RadioButton Content="女" Margin="8,0,0,0" />
</StackPanel>
<StackPanel>
<Label Content="生日" Target="{Binding ElementName=DateBirth}" />
<DatePicker x:Name="DateBirth" />
</StackPanel>
<StackPanel>
<Label Content="用戶郵箱" Target="{Binding ElementName=UserEmail}"/>
<TextBox Width="150" Text="{Binding UserEmail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</StackPanel>
<StackPanel>
<Label Content="用戶電話" Target="{Binding ElementName=UserPhone}"/>
<TextBox Width="150" Text="{Binding UserPhone, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</StackPanel>
</StackPanel>
</GroupBox>ViewModel代碼:
public class BindingFormViewModel :ViewModelBase, IDataErrorInfo
{
public BindingFormViewModel()
{
}
#region 屬性
private String userName;
/// <summary>
/// 用戶名
/// </summary>
public String UserName
{
get { return userName; }
set { userName = value; }
}
private String userPhone;
/// <summary>
/// 用戶電話
/// </summary>
public String UserPhone
{
get { return userPhone; }
set { userPhone = value; }
}
private String userEmail;
/// <summary>
/// 用戶郵件
/// </summary>
public String UserEmail
{
get { return userEmail; }
set { userEmail = value; }
}
#endregion
public String Error
{
get { return null; }
}
public String this[string columnName]
{
get
{
Regex digitalReg = new Regex(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$");
Regex emailReg = new Regex("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$");
if (columnName == "UserName" && String.IsNullOrEmpty(this.UserName))
{
return "用戶名不能為空";
}
if (columnName == "UserPhone" && !String.IsNullOrEmpty(this.UserPhone))
{
if (!digitalReg.IsMatch(this.UserPhone.ToString()))
{
return "用戶電話必須為8-11位的數(shù)值!";
}
}
if (columnName == "UserEmail" && !String.IsNullOrEmpty(this.UserEmail))
{
if (!emailReg.IsMatch(this.UserEmail.ToString()))
{
return "用戶郵箱地址不正確!";
}
}
return null;
}
}
}繼承IDataErrorInfo接口后,實(shí)現(xiàn)方法兩個(gè)屬性:Error 屬性用于指示整個(gè)對象的錯(cuò)誤,而索引器用于指示單個(gè)屬性級別的錯(cuò)誤。
每次的屬性值發(fā)生變化,則索引器進(jìn)行一次檢查,看是否有驗(yàn)證錯(cuò)誤的信息返回。
兩者的工作原理相同:如果返回非 null 或非空字符串,則表示存在驗(yàn)證錯(cuò)誤。否則,返回的字符串用于向用戶顯示錯(cuò)誤。
結(jié)果如圖:

利用 IDataErrorInfo 的好處是它可用于輕松地處理交叉耦合屬性。但也具有一個(gè)很大的弊端:
索引器的實(shí)現(xiàn)通常會導(dǎo)致較大的 switch-case 語句(對象中的每個(gè)屬性名稱都對應(yīng)于一種情況),
必須基于字符串進(jìn)行切換和匹配,并返回指示錯(cuò)誤的字符串。而且,在對象上設(shè)置屬性值之前,不會調(diào)用 IDataErrorInfo 的實(shí)現(xiàn)。
為了避免出現(xiàn)大量的 switch-case,并且將校驗(yàn)邏輯進(jìn)行分離提高代碼復(fù)用,將驗(yàn)證規(guī)則和驗(yàn)證信息獨(dú)立化于于每個(gè)模型對象中, 使用DataAnnotations 無疑是最好的的方案 。
所以我們進(jìn)行改良一下:
View代碼,跟上面那個(gè)一樣:
<GroupBox Header="IDataErrorInfo+ 驗(yàn)證" Margin="10 20 10 10" DataContext="{Binding Source={StaticResource Locator},Path=BindDataAnnotations}" >
<StackPanel Orientation="Vertical" Margin="0,20,0,0">
<StackPanel>
<Label Content="用戶名" Target="{Binding ElementName=UserName}"/>
<TextBox Width="150"
Text="{Binding UserName,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}" >
</TextBox>
</StackPanel>
<StackPanel>
<Label Content="性別" Target="{Binding ElementName=RadioGendeMale}"/>
<RadioButton Content="男" />
<RadioButton Content="女" Margin="8,0,0,0" />
</StackPanel>
<StackPanel>
<Label Content="生日" Target="{Binding ElementName=DateBirth}" />
<DatePicker />
</StackPanel>
<StackPanel>
<Label Content="用戶郵箱" Target="{Binding ElementName=UserEmail}"/>
<TextBox Width="150" Text="{Binding UserEmail, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</StackPanel>
<StackPanel>
<Label Content="用戶電話" Target="{Binding ElementName=UserPhone}"/>
<TextBox Width="150" Text="{Binding UserPhone,UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</StackPanel>
<Button Content="提交" Margin="100,16,0,0" HorizontalAlignment="Left" Command="{Binding ValidFormCommand}" />
</StackPanel>
</GroupBox>VideModel代碼:
using GalaSoft.MvvmLight;
using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using GalaSoft.MvvmLight.Command;
using System.Windows;
namespace MVVMLightDemo.ViewModel
{
[MetadataType(typeof(BindDataAnnotationsViewModel))]
public class BindDataAnnotationsViewModel : ViewModelBase, IDataErrorInfo
{
public BindDataAnnotationsViewModel()
{
}
#region 屬性
/// <summary>
/// 表單驗(yàn)證錯(cuò)誤集合
/// </summary>
private Dictionary<String, String> dataErrors = new Dictionary<String, String>();
private String userName;
/// <summary>
/// 用戶名
/// </summary>
[Required]
public String UserName
{
get { return userName; }
set { userName = value; }
}
private String userPhone;
/// <summary>
/// 用戶電話
/// </summary>
[Required]
[RegularExpression(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$", ErrorMessage = "用戶電話必須為8-11位的數(shù)值.")]
public String UserPhone
{
get { return userPhone; }
set { userPhone = value; }
}
private String userEmail;
/// <summary>
/// 用戶郵件
/// </summary>
[Required]
[StringLength(100,MinimumLength=2)]
[RegularExpression("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$", ErrorMessage = "請?zhí)顚懻_的郵箱地址.")]
public String UserEmail
{
get { return userEmail; }
set { userEmail = value; }
}
#endregion
#region 命令
private RelayCommand validFormCommand;
/// <summary>
/// 驗(yàn)證表單
/// </summary>
public RelayCommand ValidFormCommand
{
get
{
if (validFormCommand == null)
return new RelayCommand(() => ExcuteValidForm());
return validFormCommand;
}
set { validFormCommand = value; }
}
/// <summary>
/// 驗(yàn)證表單
/// </summary>
private void ExcuteValidForm()
{
if (dataErrors.Count == 0) MessageBox.Show("驗(yàn)證通過!");
else MessageBox.Show("驗(yàn)證失敗!");
}
#endregion
public string this[string columnName]
{
get
{
ValidationContext vc = new ValidationContext(this, null, null);
vc.MemberName = columnName;
var res = new List<ValidationResult>();
var result = Validator.TryValidateProperty(this.GetType().GetProperty(columnName).GetValue(this, null), vc, res);
if (res.Count > 0)
{
AddDic(dataErrors,vc.MemberName);
return string.Join(Environment.NewLine, res.Select(r => r.ErrorMessage).ToArray());
}
RemoveDic(dataErrors,vc.MemberName);
return null;
}
}
public string Error
{
get
{
return null;
}
}
#region 附屬方法
/// <summary>
/// 移除字典
/// </summary>
/// <param name="dics"></param>
/// <param name="dicKey"></param>
private void RemoveDic(Dictionary<String, String> dics, String dicKey)
{
dics.Remove(dicKey);
}
/// <summary>
/// 添加字典
/// </summary>
/// <param name="dics"></param>
/// <param name="dicKey"></param>
private void AddDic(Dictionary<String, String> dics, String dicKey)
{
if (!dics.ContainsKey(dicKey)) dics.Add(dicKey, "");
}
#endregion
}
}DataAnnotations相信很多人很熟悉,可以使用數(shù)據(jù)批注來自定義用戶的模型數(shù)據(jù),記得引用 System.ComponentModel.DataAnnotations。
他包含如下幾個(gè)驗(yàn)證類型:
| 驗(yàn)證屬性 | 說明 |
| CustomValidationAttribute | 使用自定義方法進(jìn)行驗(yàn)證。 |
| DataTypeAttribute | 指定特定類型的數(shù)據(jù),如電子郵件地址或電話號碼。 |
| EnumDataTypeAttribute | 確保值存在于枚舉中。 |
| RangeAttribute | 指定最小和最大約束。 |
| RegularExpressionAttribute | 使用正則表達(dá)式來確定有效的值。 |
| RequiredAttribute | 指定必須提供一個(gè)值。 |
| StringLengthAttribute | 指定最大和最小字符數(shù)。 |
| ValidationAttribute | 用作驗(yàn)證屬性的基類。 |
這邊我們使用到了RequiredAttribute、StringLengthAttribute、RegularExpressionAttribute 三項(xiàng),如果有需要進(jìn)一步了解 DataAnnotations 的可以參考微軟官網(wǎng):
https://msdn.microsoft.com/en-us/library/dd901590(VS.95).aspx
用 DataAnnotions 后,Model 的更加簡潔,校驗(yàn)也更加靈活??梢辕B加組合驗(yàn)證 , 面對復(fù)雜驗(yàn)證模式的時(shí)候,可以自由的使用正則來驗(yàn)證。
默認(rèn)情況下,框架會提供相應(yīng)需要反饋的消息內(nèi)容,當(dāng)然也可以自定義錯(cuò)誤消息內(nèi)容:ErrorMessage 。
這邊我們還加了個(gè)全局的錯(cuò)誤集合收集器 :dataErrors,在提交判斷時(shí)候判斷是否驗(yàn)證通過。
這邊我們進(jìn)一步封裝索引器,并且通過反射技術(shù)讀取當(dāng)前字段下的屬性進(jìn)行驗(yàn)證。
結(jié)果如下:

封裝ValidateModelBase類:
上面的驗(yàn)證比較合理了,不過相對于開發(fā)人員還是太累贅了,開發(fā)人員關(guān)心的是Model的DataAnnotations的配置,而不是關(guān)心在這個(gè)ViewModel要如何做驗(yàn)證處理,所以我們進(jìn)一步抽象。
編寫一個(gè)ValidateModelBase,把需要處理的工作都放在里面。需要驗(yàn)證屬性的Model去繼承這個(gè)基類。如下:

ValidateModelBase 類,請注意標(biāo)紅部分:
public class ValidateModelBase : ObservableObject, IDataErrorInfo
{
public ValidateModelBase()
{
}
#region 屬性
/// <summary>
/// 表當(dāng)驗(yàn)證錯(cuò)誤集合
/// </summary>
private Dictionary<String, String> dataErrors = new Dictionary<String, String>();
/// <summary>
/// 是否驗(yàn)證通過
/// </summary>
public Boolean IsValidated
{
get
{
if (dataErrors != null && dataErrors.Count > 0)
{
return false;
}
return true;
}
}
#endregion
public string this[string columnName]
{
get
{
ValidationContext vc = new ValidationContext(this, null, null);
vc.MemberName = columnName;
var res = new List<ValidationResult>();
var result = Validator.TryValidateProperty(this.GetType().GetProperty(columnName).GetValue(this, null), vc, res);
if (res.Count > 0)
{
AddDic(dataErrors, vc.MemberName);
return string.Join(Environment.NewLine, res.Select(r => r.ErrorMessage).ToArray());
}
RemoveDic(dataErrors, vc.MemberName);
return null;
}
}
public string Error
{
get
{
return null;
}
}
#region 附屬方法
/// <summary>
/// 移除字典
/// </summary>
/// <param name="dics"></param>
/// <param name="dicKey"></param>
private void RemoveDic(Dictionary<String, String> dics, String dicKey)
{
dics.Remove(dicKey);
}
/// <summary>
/// 添加字典
/// </summary>
/// <param name="dics"></param>
/// <param name="dicKey"></param>
private void AddDic(Dictionary<String, String> dics, String dicKey)
{
if (!dics.ContainsKey(dicKey)) dics.Add(dicKey, "");
}
#endregion
}驗(yàn)證的模型類:繼承 ValidateModelBase
[MetadataType(typeof(BindDataAnnotationsViewModel))]
public class ValidateUserInfo : ValidateModelBase
{
#region 屬性
private String userName;
/// <summary>
/// 用戶名
/// </summary>
[Required]
public String UserName
{
get { return userName; }
set { userName = value; RaisePropertyChanged(() => UserName); }
}
private String userPhone;
/// <summary>
/// 用戶電話
/// </summary>
[Required]
[RegularExpression(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$", ErrorMessage = "用戶電話必須為8-11位的數(shù)值.")]
public String UserPhone
{
get { return userPhone; }
set { userPhone = value; RaisePropertyChanged(() => UserPhone); }
}
private String userEmail;
/// <summary>
/// 用戶郵件
/// </summary>
[Required]
[StringLength(100, MinimumLength = 2)]
[RegularExpression("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$", ErrorMessage = "請?zhí)顚懻_的郵箱地址.")]
public String UserEmail
{
get { return userEmail; }
set { userEmail = value; RaisePropertyChanged(() => UserEmail); }
}
#endregion
}ViewModel代碼如下:
public class PackagedValidateViewModel:ViewModelBase
{
public PackagedValidateViewModel()
{
ValidateUI = new Model.ValidateUserInfo();
}
#region 全局屬性
private ValidateUserInfo validateUI;
/// <summary>
/// 用戶信息
/// </summary>
public ValidateUserInfo ValidateUI
{
get
{
return validateUI;
}
set
{
validateUI = value;
RaisePropertyChanged(()=>ValidateUI);
}
}
#endregion
#region 全局命令
private RelayCommand submitCmd;
public RelayCommand SubmitCmd
{
get
{
if(submitCmd == null) return new RelayCommand(() => ExcuteValidForm());
return submitCmd;
}
set
{
submitCmd = value;
}
}
#endregion
#region 附屬方法
/// <summary>
/// 驗(yàn)證表單
/// </summary>
private void ExcuteValidForm()
{
if (ValidateUI.IsValidated) MessageBox.Show("驗(yàn)證通過!");
else MessageBox.Show("驗(yàn)證失?。?);
}
#endregion
}結(jié)果如下:

以上就是MVVMLight項(xiàng)目之綁定在表單驗(yàn)證上的應(yīng)用示例分析的詳細(xì)內(nèi)容,更多關(guān)于MVVMLight綁定在表單驗(yàn)證上的應(yīng)用的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android使用MediaPlayer和TextureView實(shí)現(xiàn)視頻無縫切換
這篇文章主要為大家詳細(xì)介紹了Android使用MediaPlayer和TextureView實(shí)現(xiàn)視頻無縫切換,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10
Android ActionBar制作時(shí)鐘實(shí)例解析
這篇文章主要為大家詳細(xì)介紹了Android ActionBar制作時(shí)鐘的實(shí)現(xiàn)代碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-05-05
Android 中ListView點(diǎn)擊Item無響應(yīng)問題的解決辦法
如果listitem里面包括button或者checkbox等控件,默認(rèn)情況下listitem會失去焦點(diǎn),導(dǎo)致無法響應(yīng)item的事件,怎么解決呢?下面小編給大家分享下listview點(diǎn)擊item無響應(yīng)的解決辦法2016-12-12
Android利用Paint自定義View實(shí)現(xiàn)進(jìn)度條控件方法示例
這篇文章主要給大家介紹了關(guān)于Android利用Paint自定義View實(shí)現(xiàn)進(jìn)度條控件的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11
Android開發(fā)MQTT協(xié)議的模型及通信淺析
這篇文章主要W為大家介紹了Android開發(fā)MQTT協(xié)議的模型及通信淺析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
Android中文件讀寫(輸入流和輸出流)操作小結(jié)
這篇文章主要介紹了Android中文件讀寫(輸入流和輸出流)操作小結(jié),本文總結(jié)了Android中文件讀寫的原理、字節(jié)流和字符流的區(qū)別、文件讀寫的步驟、輸入流和輸出流以及代碼實(shí)例等內(nèi)容,需要的朋友可以參考下2015-06-06

