十年河东,十年河西,莫欺少年穷
学无止境,精益求精
参考: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做了样式封装,其他诸如下拉框等需要自行构造样式~在资源文件中增加即可
@陈大六