WPF使用FluentValidation进行表单验证

发布时间 2023-08-01 17:32:37作者: .NET好耶

WPF使用FluentValidation进行表单验证

.net版本:6.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>,在构造函数中定义验证规则,本地化也可以在这里使用

一般属性

这个比较简单,直接加规则就可以,可以链式调用
注意,如果使用链式调用,错误消息可以是共享的,也可以是单独的

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中存在,这不符合设计,不过来都来了,就顺便写了

public FormViewModelValidator()
{
    this.RuleFor(x => x.UserModel)
        .SetValidator(new UserModelValidator());
}

集合属性

public FormViewModelValidator()
{
    this.RuleForEach(x => x.NameList)
        .NotNull()
        .NotEmpty()
        .WithMessage("NameList不能为空");
}

复杂集合属性

类似复杂属性

public FormViewModelValidator()
{
    this.RuleForEach(x => x.UserList)
        .SetValidator(new UserModelValidator());
}

旧版写法,SetCollectionValidator已弃用

public FormViewModelValidator()
{
    this.RuleFor(x => x.UserList)
        .SetCollectionValidator(new UserModelValidator());
}

规则集

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 FormViewModelValidator _validator { get; set; } = new FormViewModelValidator();

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");
});

验证规则

这些是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

MustCustom可以自定义规则,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"
            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"
            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 FormViewModelValidator _validator { get; set; } = new FormViewModelValidator();

    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.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和空字符串问题