十年河东,十年河西,莫欺少年穷
学无止境,精益求精
参考: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(); }
}
}
View Code
原理就是使用索引器获取页面上的错误信息添加到一个字典集合中,公开一个是否验证成功的属性,方便外部验证数据是否正确,这里直接使用字典的一个扩展方法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; }
}
}
View Code
因其微软早已对验证进行其封装为注解(特性),相信做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>
View Code
这里直接定义为一个资源字典了,在要引用的窗体或者自定义用户控件中引用即可
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>
View Code
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("新生注册成功!");
}
}
}
View Code
7、总结
本文只针对TExtBox做了样式封装,其他诸如下拉框等需要自行构造样式~在资源文件中增加即可
@陈大六