十年河東,十年河西,莫欺少年窮
學無止境,精益求精
參考:WPF表單驗證
摘要
WPF表單驗證是WPF重要基礎設施之一,依靠MVVM的數據綁定機制及微軟的有力封裝,使得我們在處理實體表單驗證等可以快捷高效的靈活處理。常見的表單驗證實現大概有Exception
、ValidationRule
、IDataErrorInfo
,而本文則是通過IDataErrorInfo
來實現表單驗證功能
1、要現實的效果 (本文資源只寫了針對textBox的樣式,其他控件用法類似)
2、封裝驗證模型
既然是直接對實體進行驗證,那首先肯定是從實體對象模型着手,爲了方便複用性,建議抽出一個公共的驗證模型
public class ValidateModelBase : BindableBase, IDataErrorInfo { public Dictionary<string, string> dataErrors = new Dictionary<string, string>(); //錯誤信息集合 public bool IsValidated { get { return !dataErrors.Any(); } } public string this[string columnName] { get { ValidationContext vc = new ValidationContext(this, null, null) { MemberName = columnName }; var res = new List<ValidationResult>(); var result = Validator.TryValidateProperty(this.GetType().GetProperty(columnName).GetValue(this, null), vc, res); if (res.Count > 0) { string errorInfo = string.Join(Environment.NewLine, res.Select(r => r.ErrorMessage).ToArray()); AddError(dataErrors, columnName, errorInfo); return errorInfo; } RemoveError(dataErrors, columnName); return null; } } /// <summary> /// 移除錯誤信息 /// </summary> /// <param name="dataErrors"></param> /// <param name="columnName"></param> private void RemoveError(Dictionary<string, string> dataErrors, string columnName) { dataErrors.Remove(columnName); } /// <summary> /// 添加錯誤信息 /// </summary> /// <param name="dataErrors"></param> /// <param name="columnName"></param> /// <param name="errorInfo"></param> private void AddError(Dictionary<string, string> dataErrors, string columnName, string errorInfo) { if (!dataErrors.ContainsKey(columnName)) { dataErrors.Add(columnName, errorInfo); } } public string Error { get; set; } private bool isFormValid; /// <summary> /// 是否全局驗證 /// </summary> public bool IsFormValid { get { return isFormValid; } set { isFormValid = value; RaisePropertyChanged(); } } }
原理就是使用索引器獲取頁面上的錯誤信息添加到一個字典集合中,公開一個是否驗證成功的屬性,方便外部驗證數據是否正確,這裏直接使用字典的一個擴展方法Enumerable.Any ,如果字典中包含元素則返回True,否則False
3、創建帶有數據註解的實體類
public class StudentModel : ValidateModelBase { private string _StudentName; /// <summary> /// 學生姓名 /// </summary> [Required(ErrorMessage = "學生姓名不允許爲空")] [MinLength(2, ErrorMessage = "學生姓名不能少於兩個字符")] public string StudentName { get { return _StudentName; } set { _StudentName = value; RaisePropertyChanged(); } } private int? _StudentAge; /// <summary> /// 學生年齡 /// </summary> [Required(ErrorMessage = "學生年齡不允許爲空")] [Range(18, 40, ErrorMessage = "學生年齡範圍需在18-40歲之間")] [RegularExpression(@"[1-9]\d*", ErrorMessage = "請輸入數字")] public int? StudentAge { get { return _StudentAge; } set { _StudentAge = value; RaisePropertyChanged(); } } private string _StudentEmail; /// <summary> /// 學生郵箱 /// </summary> [EmailAddress(ErrorMessage = "郵箱地址不合法")] public string StudentEmail { get { return _StudentEmail; } set { _StudentEmail = value; RaisePropertyChanged(); } } private string _StudentPhoneNumber; /// <summary> /// 學生手機號 /// </summary> [Required(ErrorMessage = "學生手機號不允許爲空")] [RegularExpression(@"^1[3-9]\d{9}$", ErrorMessage = "手機號不正確")] public string StudentPhoneNumber { get { return _StudentPhoneNumber; } set { _StudentPhoneNumber = value; } } }
因其微軟早已對驗證進行其封裝爲註解(特性),相信做Web的童鞋並不陌生
4、使用Adorned裝飾器對文本框進行錯誤模板重寫
項目中新建 Resource 文件夾,並創建資源文件 DataValidation.Xaml ,代碼如下
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Style x:Key="VaildationTextBoxStyle" TargetType="{x:Type TextBox}"> <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="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <StackPanel Orientation="Horizontal"> <Border x:Name="adornerborder" VerticalAlignment="Top" BorderThickness="1"> <Grid> <AdornedElementPlaceholder x:Name="adorner" Margin="-1" /> </Grid> </Border> <Border x:Name="errorBorder" MinHeight="24" Margin="8,0,0,0" Background="#FFdc000c" CornerRadius="0" IsHitTestVisible="False" Opacity="0"> <TextBlock Margin="8,2,8,3" VerticalAlignment="Center" Foreground="White" Text="{Binding ElementName=adorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" TextWrapping="Wrap" /> </Border> </StackPanel> <ControlTemplate.Triggers> <DataTrigger Value="True"> <DataTrigger.Binding> <Binding ElementName="adorner" Path="AdornedElement.Tag" /> </DataTrigger.Binding> <DataTrigger.EnterActions> <BeginStoryboard x:Name="fadeInStoryboard1"> <Storyboard> <DoubleAnimation Storyboard.TargetName="errorBorder" Storyboard.TargetProperty="Opacity" To="1" Duration="00:00:00.15" /> </Storyboard> </BeginStoryboard> </DataTrigger.EnterActions> <DataTrigger.Setters> <Setter TargetName="adornerborder" Property="BorderBrush" Value="#FFdc000c" /> </DataTrigger.Setters> </DataTrigger> <DataTrigger Value="True"> <DataTrigger.Binding> <Binding ElementName="adorner" Path="AdornedElement.IsKeyboardFocused" /> </DataTrigger.Binding> <DataTrigger.EnterActions> <BeginStoryboard x:Name="fadeInStoryboard"> <Storyboard> <DoubleAnimation Storyboard.TargetName="errorBorder" Storyboard.TargetProperty="Opacity" To="1" Duration="00:00:00.15" /> </Storyboard> </BeginStoryboard> </DataTrigger.EnterActions> <!-- 是否保留異常直到數據正常:如果不需要則放開下列驗證 --> <DataTrigger.ExitActions> <StopStoryboard BeginStoryboardName="fadeInStoryboard" /> <BeginStoryboard x:Name="fadeOutStoryBoard"> <Storyboard> <DoubleAnimation Storyboard.TargetName="errorBorder" Storyboard.TargetProperty="Opacity" To="0" Duration="00:00:00" /> </Storyboard> </BeginStoryboard> </DataTrigger.ExitActions> <DataTrigger.Setters> <Setter TargetName="adornerborder" Property="BorderBrush" Value="#FFdc000c" /> </DataTrigger.Setters> </DataTrigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style BasedOn="{StaticResource VaildationTextBoxStyle}" TargetType="{x:Type TextBox}" /> </ResourceDictionary>
這裏直接定義爲一個資源字典了,在要引用的窗體或者自定義用戶控件中引用即可
Tips:如果要全局引用,建議定義一個無Key的Style,並放置在App.xaml
中
5、在頁面/用戶控件中引入資源文件,並創建如下佈局
<UserControl x:Class="WpfApp.UserControls.SetingView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:WpfApp.UserControls" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <UserControl.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/Resource/DataValidation.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </UserControl.Resources> <Grid> <StackPanel Orientation="Vertical"> <StackPanel Orientation="Horizontal" Height="40" VerticalAlignment="Center" > <TextBlock Width="70" TextAlignment="Right" VerticalAlignment="Center"> <Run Foreground="Red" Text="*" /> <Run Text="學生姓名:" /> </TextBlock> <TextBox Width="240" Height="30" VerticalAlignment="Center" VerticalContentAlignment="Center" Tag="{Binding StudentInfo.IsFormValid, UpdateSourceTrigger=PropertyChanged}" Text="{Binding StudentInfo.StudentName, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" /> </StackPanel> <StackPanel Orientation="Horizontal" Height="40" VerticalAlignment="Center" > <TextBlock Width="70" TextAlignment="Right" VerticalAlignment="Center"> <Run Foreground="Red" Text="*" /> <Run Text="學生年齡:" /> </TextBlock> <TextBox Width="240" Height="30" VerticalAlignment="Center" VerticalContentAlignment="Center" Tag="{Binding StudentInfo.IsFormValid, UpdateSourceTrigger=PropertyChanged}" Text="{Binding StudentInfo.StudentAge, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" /> </StackPanel> <StackPanel Orientation="Horizontal" Height="40" VerticalAlignment="Center" > <TextBlock Width="70" TextAlignment="Right" VerticalAlignment="Center"> <Run Text="郵箱:" /> </TextBlock> <TextBox Width="240" Height="30" VerticalAlignment="Center" VerticalContentAlignment="Center" Tag="{Binding StudentInfo.IsFormValid, UpdateSourceTrigger=PropertyChanged}" Text="{Binding StudentInfo.StudentEmail, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" /> </StackPanel> <StackPanel Orientation="Horizontal" Height="40" VerticalAlignment="Center" > <TextBlock Width="70" TextAlignment="Right" VerticalAlignment="Center"> <Run Foreground="Red" Text="*" /> <Run Text="聯繫方式:" /> </TextBlock> <TextBox Width="240" Height="30" VerticalAlignment="Center" VerticalContentAlignment="Center" Tag="{Binding StudentInfo.IsFormValid, UpdateSourceTrigger=PropertyChanged}" Text="{Binding StudentInfo.StudentPhoneNumber, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" /> </StackPanel> <Button Width="150" Height="26" Margin="76,5" IsEnabled="{Binding IsValidated}" HorizontalAlignment="Left" Command="{Binding SubmitCommand}" Content="新生註冊" Cursor="Hand" /> </StackPanel> </Grid> </UserControl>
6、ViewModel 如下:
using Prism.Commands; using Prism.Mvvm; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Windows; using WpfApp.UserControlModels; namespace WpfApp.ViewModels { public class SetingViewModel : BindableBase { public DelegateCommand SubmitCommand { get; private set; } private StudentModel studentInfo; public StudentModel StudentInfo { get { return studentInfo; } set { studentInfo = value; RaisePropertyChanged(); } } public SetingViewModel() { StudentInfo = new StudentModel(); SubmitCommand = new DelegateCommand(Submit); } private void Submit() { if (!StudentInfo.IsValidated) { StudentInfo.IsFormValid = true; MessageBox.Show("新生註冊失敗,請檢查錄入的新生信息是否正確!"); return; } MessageBox.Show("新生註冊成功!"); } } }
7、總結
本文只針對TExtBox做了樣式封裝,其他諸如下拉框等需要自行構造樣式~在資源文件中增加即可
@陳大六