WPF使用FluentValidation进行表单验证
WPF使用FluentValidation进行表单验证
使用.net6.0
使用的NuGet包
FluentValidation:11.6.0
MaterialDesignThemes:4.9.0
Prism.DryIoc:8.1.97
在WPF里验证表单使用的是INotifyDataErrorInfo
接口,这个接口长这样
public interface INotifyDataErrorInfo
{
bool HasErrors { get; }
event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
IEnumerable GetErrors(string propertyName);
}
HasErrors
:用于判断是否有错误ErrorsChanged
:用于通知View刷新界面GetErrors
:用于获取属性的错误信息
实现INotifyDataErrorInfo
定义一个抽象类ValidatableBindableBase
,继承BindableBase
,并实现INotifyDataErrorInfo
,有表单验证的viewmodel继承这个类就好了,在viewmodel中实现ValidateAllProperty
函数
public abstract class ValidatableBindableBase : BindableBase, INotifyDataErrorInfo
{
/// <summary>
/// 错误字典中有数据则为true
/// </summary>
public bool HasErrors
{
get
{
return this._errorDic.Any(x => null != x.Value && x.Value.Count > 0);
}
}
/// <summary>
/// 错误字典变化时触发
/// </summary>
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged = delegate { return; };
/// <summary>
/// 通过属性获取错误集合
/// </summary>
/// <param name="propertyName"></param>
/// <returns></returns>
public IEnumerable GetErrors(string propertyName)
{
if (true == string.IsNullOrWhiteSpace(propertyName) || false == this._errorDic.ContainsKey(propertyName))
{
return null;
}
return this._errorDic[propertyName];
}
/// <summary>
/// 错误字典
/// </summary>
private readonly Dictionary<string, List<string>> _errorDic = new Dictionary<string, List<string>>();
/// <summary>
/// 将错误添加到错误字典中
/// 通知UI刷新
/// </summary>
/// <param name="propertyName"></param>
/// <param name="errorMessage"></param>
public void SetError(string propertyName, string errorMessage)
{
if (false == this._errorDic.ContainsKey(propertyName))
{
this._errorDic.Add(propertyName, new List<string>() { errorMessage });
}
else
{
//其实这步多余,不需要额外的错误信息
this._errorDic[propertyName].Add(errorMessage);
}
this.RaiseErrorChanged(propertyName);
}
/// <summary>
/// 从错误字典中移除错误
/// 通知UI刷新
/// </summary>
/// <param name="propertyName"></param>
protected void ClearError(string propertyName)
{
if (true == this._errorDic.ContainsKey(propertyName))
{
this._errorDic.Remove(propertyName);
}
this.RaiseErrorChanged(propertyName);
}
/// <summary>
/// 从错误字典移除所有错误
/// </summary>
protected void ClearAllError()
{
var propertyList = this._errorDic.Select(x => x.Key).ToList();
foreach (var property in propertyList)
{
this.ClearError(property);
}
}
/// <summary>
/// 通知UI刷新
/// </summary>
/// <param name="propertyName"></param>
public void RaiseErrorChanged(string propertyName)
{
this.ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
/// <summary>
/// 验证所有属性
/// 由于验证用的validator需要子类创建,所以该函数需要子类实现
/// </summary>
public abstract void ValidateAllProperty();
/// <summary>
/// 验证属性
/// 由于验证用的validator需要子类创建,所以该函数需要子类实现
/// </summary>
/// <param name="propertyName"></param>
public abstract void ValidateProperty(string propertyName);
}
案例
稍微写的全面一点,那就写个一般属性、复杂属性、集合属性验证
先定义一个类作为复杂属性吧
public class UserModel
{
public string Username { get; set; }
public string Nickname { get; set; }
}
因为要验证这个复杂属性,所以要再定义一个Validator
public class UserModelValidator : AbstractValidator<UserModel>
{
public UserModelValidator()
{
this.RuleFor(x => x.Username)
.NotNull()
.NotEmpty()
.WithMessage("用户名不能为空");
this.RuleFor(x => x.Nickname)
.NotNull()
.WithMessage("昵称不能为NULL")
.NotEmpty()
.WithMessage("昵称不能为空");
}
}
然后定义一个ViewModel类,这个就是表单
public class FormViewModel : ValidatableBindableBase
{
private string _firstName;
public string FirstName
{
get { return _firstName; }
set
{
_firstName = value;
this.RaisePropertyChanged(nameof(FirstName));
}
}
private string _lastName;
public string LastName
{
get { return _lastName; }
set
{
_lastName = value;
this.RaisePropertyChanged(nameof(LastName));
}
}
private List<string> _nameList;
public List<string> NameList
{
get { return _nameList; }
set
{
_nameList = value;
this.RaisePropertyChanged(nameof(NameList));
}
}
private UserModel _userModel;
public UserModel UserModel
{
get { return _userModel; }
set
{
_userModel = value;
this.RaisePropertyChanged(nameof(UserModel));
}
}
private List<UserModel> _userList;
public List<UserModel> UserList
{
get { return _userList; }
set
{
_userList = value;
this.RaisePropertyChanged(nameof(UserList));
}
}
public FormViewModel()
{
this.FirstName = string.Empty;
this.LastName = string.Empty;
this.NameList = new List<string>();
this.UserModel = new UserModel();
this.UserList = new List<UserModel>();
}
}
再定义ViewModel的Validator
public class FormViewModelValidator : AbstractValidator<FormViewModel>
{
public FormViewModelValidator()
{
//验证一般属性
this.RuleFor(x => x.FirstName)
.NotNull()
.NotEmpty()
.WithMessage("FirstName不能为空");
this.RuleFor(x => x.LastName)
.NotNull()
.WithMessage("LastName不能为NULL")
.NotEmpty()
.WithMessage("LastName不能为空");
//验证集合属性,对集合中每个元素进行验证
this.RuleForEach(x => x.NameList)
.NotNull()
.NotEmpty()
.WithMessage("NameList不能为空");
//验证复杂属性
this.RuleFor(x => x.UserModel)
.SetValidator(new UserModelValidator());
//验证复杂集合属性
this.RuleForEach(x => x.UserList)
.SetValidator(new UserModelValidator());
}
}
Validator
Validator要继承AbstractValidator<T>
,在构造函数中定义验证规则,本地化也可以在这里使用
一般属性
这个比较简单,直接加规则就可以,可以链式调用
此时_errorDic
的key为属性名称
注意,如果使用链式调用,错误消息可以是共享的,也可以是单独的
public FormViewModelValidator()
{
this.RuleFor(x => x.FirstName)
.NotNull()
.NotEmpty()
.WithMessage("FirstName不能为空");
this.RuleFor(x => x.LastName)
.NotNull()
.WithMessage("LastName不能为NULL")
.NotEmpty()
.WithMessage("LastName不能为空");
}
也可以分开写,下面两个写法是一样的
public FormViewModelValidator()
{
this.RuleFor(x => x.LastName)
.NotNull()
.WithMessage("LastName不能为NULL")
.NotEmpty()
.WithMessage("LastName不能为空");
this.RuleFor(x => x.LastName)
.NotNull()
.WithMessage("LastName不能为NULL");
this.RuleFor(x => x.LastName)
.NotEmpty()
.WithMessage("LastName不能为空");
}
复杂属性
其实复杂属性不应该在MVVM中存在,这不符合设计,不过来都来了,就顺便写了
此时_errorDic
的key为复杂属性名称.复杂属性的属性名称
public FormViewModelValidator()
{
this.RuleFor(x => x.UserModel)
.SetValidator(new UserModelValidator());
}
集合属性
此时_errorDic
的key为复杂集合属性名称[索引]
public FormViewModelValidator()
{
this.RuleForEach(x => x.NameList)
.NotNull()
.NotEmpty()
.WithMessage("NameList不能为空");
}
复杂集合属性
类似复杂属性
此时_errorDic
的key为复杂集合属性名称[索引].复杂属性的属性名称
public FormViewModelValidator()
{
this.RuleForEach(x => x.UserList)
.SetValidator(new UserModelValidator());
}
旧版写法,SetCollectionValidator
已弃用
public FormViewModelValidator()
{
this.RuleFor(x => x.UserList)
.SetCollectionValidator(new UserModelValidator());
}
规则集
如果不指定使用规则集校验,则不会使用
此时_errorDic
的key也和上面的一样,因为规则集只是规则的集合,并不影响校验结果的key
public FormViewModelValidator()
{
this.RuleSet("TestRuleSet", () =>
{
this.RuleFor(x => x.FirstName)
.NotNull()
.NotEmpty()
.WithMessage("FirstName不能为空");
this.RuleFor(x => x.LastName)
.NotNull()
.WithMessage("LastName不能为NULL")
.NotEmpty()
.WithMessage("LastName不能为空");
});
}
验证方法
在ViewModel中添加Validator并实现ValidateAllProperty
方法,验证结果判断HasErrors
就可以了
{
private readonly FormViewModelValidator _validator;
public FormViewModel()
{
this._validator = new FormViewModelValidator();
//初始化移除所有错误
this.ClearAllError();
}
public override void ValidateAllProperty()
{
//先移除所有错误
this.ClearAllError();
var result = this._validator.Validate(this);
//添加错误
foreach (var error in result.Errors)
{
this.SetError(error.PropertyName, error.ErrorMessage);
}
}
public override void ValidateProperty(string propertyName)
{
//先移除错误
this.ClearError(propertyName);
var result = this._validator.Validate(this, (option) =>
{
option.IncludeProperties(propertyName);
});
//添加错误
foreach (var error in result.Errors)
{
this.SetError(error.PropertyName, error.ErrorMessage);
}
}
}
验证所有规则
public override void ValidateAllProperty()
{
this._validator.Validate(this);
}
验证指定属性
public override void ValidateProperty()
{
this._validator.Validate(this, (option) =>
{
option.IncludeProperties(x => x.FirstName);
option.IncludeProperties(x => x.LastName);
});
}
验证规则集
这是params
参数
this._validator.Validate(this, (option) =>
{
option.IncludeRuleSets("TestRuleSet1", "TestRuleSet2");
});
也可以使用IncludeAllRuleSets
,这个会包括所有规则和规则集
验证规则
这些是FluentValidation自带的验证规则
Null
NotNull
Empty
NotEmpty
Length
MaximumLength
MinimumLength
Matches
EmailAddress
NotEqual
Equal
LessThan
LessThanOrEqualTo
GreaterThan
GreaterThanOrEqualTo
InclusiveBetween
ExclusiveBetween
CreditCard
IsInEnum
IsEnumName
ScalePrecision
PrecisionScale
Must
Custom
Must
和Custom
可以自定义规则,Must
自定义的是表达式,Custom
是自定义规则+错误提示
界面
注意:要启用错误消息,需要在绑定数据时添加ValidatesOnDataErrors=True
MainView
,主窗口,加一个区域
<Window
x:Class="BlankApp1.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:prism="http://prismlibrary.com/"
xmlns:viewmodels="clr-namespace:BlankApp1.ViewModels"
Title="{Binding Title}"
Width="500"
Height="600"
prism:ViewModelLocator.AutoWireViewModel="True">
<Grid>
<ContentControl prism:RegionManager.RegionName="{Binding FormRegionName}" />
</Grid>
</Window>
MainViewModel
,主窗口ViewModel,导航
public class MainViewModel : BindableBase
{
private readonly IRegionManager _regionManager;
private string _title;
public string Title
{
get { return _title; }
set { SetProperty(ref _title, value); }
}
private string _formRegionName;
public string FormRegionName
{
get { return _formRegionName; }
set
{
_formRegionName = value;
this.RaisePropertyChanged(nameof(FormRegionName));
}
}
public MainViewModel(IRegionManager regionManager)
{
this._regionManager = regionManager;
this.Title = "测试";
this.FormRegionName = "FormRegion";
this._regionManager.RegisterViewWithRegion(this.FormRegionName, typeof(FormView));
}
}
FormView
,表单控件,绑定数据记得添加ValidatesOnDataErrors=True
,这里我就验证简单的数据,只演示验证结果,不想搞太复杂的东西
<UserControl
x:Class="BlankApp1.Views.FormView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:BlankApp1.Views"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
d:DesignHeight="600"
d:DesignWidth="500"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="0,0,10,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="FirstName" />
<TextBox
Grid.Row="0"
Grid.Column="1"
Width="200"
Margin="10,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
materialDesign:TextFieldAssist.HasClearButton="True"
Style="{StaticResource MaterialDesignFilledTextBox}"
Text="{Binding FirstName, Mode=TwoWay, ValidatesOnDataErrors=True}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="0,0,10,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="LastName" />
<TextBox
Grid.Row="1"
Grid.Column="1"
Width="200"
Margin="10,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
materialDesign:TextFieldAssist.HasClearButton="True"
Style="{StaticResource MaterialDesignFilledTextBox}"
Text="{Binding LastName, Mode=TwoWay, ValidatesOnDataErrors=True}" />
<Button
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
Width="80"
Command="{Binding ValidateCommand}"
Content="验证" />
</Grid>
</UserControl>
FormViewModel
,这里验证表单
public class FormViewModel : ValidatableBindableBase
{
private readonly FormViewModelValidator _validator;
private string _firstName;
public string FirstName
{
get { return _firstName; }
set
{
_firstName = value;
this.ValidateProperty(nameof(FirstName));
this.RaisePropertyChanged(nameof(FirstName));
}
}
private string _lastName;
public string LastName
{
get { return _lastName; }
set
{
_lastName = value;
this.ValidateProperty(nameof(LastName));
this.RaisePropertyChanged(nameof(LastName));
}
}
public DelegateCommand ValidateCommand { get; set; }
public FormViewModel()
{
this._validator = new FormViewModelValidator();
this.FirstName = string.Empty;
this.LastName = string.Empty;
this.ValidateCommand = new DelegateCommand(this.ValidateCommandExecute);
//初始化移除所有错误
this.ClearAllError();
}
public void ValidateCommandExecute()
{
this.ValidateAllProperty();
if (true == this.HasErrors)
{
return;
}
}
public override void ValidateAllProperty()
{
//先移除所有错误
this.ClearAllError();
var result = this._validator.Validate(this);
//添加错误
foreach (var error in result.Errors)
{
this.SetError(error.PropertyName, error.ErrorMessage);
}
}
public override void ValidateProperty(string propertyName)
{
//先移除错误
this.ClearError(propertyName);
var result = this._validator.Validate(this, (option) =>
{
option.IncludeProperties(propertyName);
});
//添加错误
foreach (var error in result.Errors)
{
this.SetError(error.PropertyName, error.ErrorMessage);
}
}
}
FormViewModelValidator
,表单验证规则
public class FormViewModelValidator : AbstractValidator<FormViewModel>
{
public FormViewModelValidator()
{
this.RuleFor(x => x.FirstName)
.NotNull()
.NotEmpty()
.WithMessage("FirstName不能为空")
.NotEqual("123")
.WithMessage("不能等于123");
this.RuleFor(x => x.LastName)
.NotNull()
.WithMessage("LastName不能为NULL")
.NotEmpty()
.WithMessage("LastName不能为空");
}
}
效果
WPF使用FluentValidation进行表单验证 结束
有个小问题,绑定数据要到控件失去焦点才会改动ViewModel的数据
还有就是要注意控件的一键清除功能,比如material design的控件HasClearButton
,有null
和空字符串问题