欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

《Dotnet9》系列-FluentValidation在C# WPF中的应用

程序员文章站 2022-04-10 14:01:29
时间如流水,只能流去不流回! 点赞再看,养成习惯,这是您给我创作的动力! 本文 Dotnet9 https://dotnet9.com 已收录,站长乐于分享dotnet相关技术,比如Winform、WPF、ASP.NET Core等,亦有C++桌面相关的Qt Quick和Qt Widgets等,只分 ......

时间如流水,只能流去不流回!

点赞再看,养成习惯,这是您给我创作的动力!

本文 dotnet9 https://dotnet9.com 已收录,站长乐于分享dotnet相关技术,比如winform、wpf、asp.net core等,亦有c++桌面相关的qt quick和qt widgets等,只分享自己熟悉的、自己会的。

一、简介

介绍fluentvalidation的文章不少,的介绍我引用下:fluentvalidation 是一个基于 .net 开发的验证框架,开源免费,而且优雅,支持链式操作,易于理解,功能完善,还是可与 mvc5、webapi2 和 asp.net core 深度集成,组件内提供十几种常用验证器,可扩展性好,支持自定义验证器,支持本地化多语言。

其实它也可以用于wpf属性验证,本文主要也是讲解该组件在wpf中的使用,fluentvalidation官网是:  。

二、本文需要实现的功能

提供wpf界面输入验证,采用mvvm方式,需要以下功能:

  1. 能验证viewmodel中定义的简单属性;
  2. 能验证viewmodel中定义的复杂属性,比如对象属性的子属性,如vm有个学生属性student,需要验证他的姓名、年龄等;
  3. 能简单提供两种验证样式;
  4. 没有了,就是前面3点…

先看实现效果图:

《Dotnet9》系列-FluentValidation在C# WPF中的应用

三、调研中遇到的问题

简单属性:验证viewmodel的普通属性比较简单,可以参考fluentvalidation官网: ,或者国外holymoo大神的代码: uservalidator.cs 。

复杂属性:我遇到的问题是,怎么验证viewmodel中对象属性的子属性?见第二个功能描述,fluentvalidation的官网有complex properties的例子,但是我试了没效果,贴上官方源码截图:

《Dotnet9》系列-FluentValidation在C# WPF中的应用

最后我google到这篇文章,根据该代码,viewmodel和子属性都实现idataerrorinfo接口,即可实现复杂属性验证,文章中没有具体实现,但灵感是从这来的,就不具体说该链接代码了,有兴趣的读者可以点击链接阅读,下面说说博主自己的研发步骤(主要就是贴代码啦,您可以直接拉到文章末尾,那里赋有源码下载链接)。

四、开发步骤

4.1、创建工程、引入库

创建.net core wpf模板解决方案(.net framework模板也行):wpffluentvalidation,引入nuget包fluentvalidation(8.5.1)。

4.2、创建测试实体类学生:student.cs

此类用作viewmodel中的复杂属性使用,学生类包含3个属性:名字、年龄、邮政编码。此实体需要继承idataerrorinfo接口,这是触发fluentvalidation验证的关键接口实现。

using system.componentmodel;
using system.linq;
using wpffluentvalidation.validators;

namespace wpffluentvalidation.models
{
    /// <summary>
    /// 学生实体
    /// 继承baseclasss,即继承属性变化接口inotifypropertychanged
    /// 实现idataerrorinfo接口,用于fluentvalidation验证,必须实现此接口
    /// </summary>
    public class student : baseclass, idataerrorinfo
    {
        private string name;
        public string name
        {
            get { return name; }
            set
            {
                if (value != name)
                {
                    name = value;
                    onpropertychanged(nameof(name));
                }
            }
        }
        private int age;
        public int age
        {
            get { return age; }
            set
            {
                if (value != age)
                {
                    age = value;
                    onpropertychanged(nameof(age));
                }
            }
        }
        private string zip;
        public string zip
        {
            get { return zip; }
            set
            {
                if (value != zip)
                {
                    zip = value;
                    onpropertychanged(nameof(zip));
                }
            }
        }

        public string error { get; set; }

        public string this[string columnname]
        {
            get
            {
                if (validator == null)
                {
                    validator = new studentvalidator();
                }
                var firstordefault = validator.validate(this)
                    .errors.firstordefault(lol => lol.propertyname == columnname);
                return firstordefault?.errormessage;
            }
        }

        private studentvalidator validator { get; set; }
    }
}

4.3、创建学生验证器:studentvalidator.cs

验证属性的写法有两种:

  1. 可以在实体属性上方添加特性(本文不作特别说明,百度文章介绍很多);
  2. 通过代码的形式添加,如下方,创建一个验证器类,继承自abstractvalidator,在此验证器构造函数中写规则验证属性,方便管理。

本文使用第二种,见下方学生验证器代码:

using fluentvalidation;
using system.text.regularexpressions;
using wpffluentvalidation.models;

namespace wpffluentvalidation.validators
{
    public class studentvalidator : abstractvalidator<student>
    {
        public studentvalidator()
        {
            rulefor(vm => vm.name)
                    .notempty()
                    .withmessage("请输入学生姓名!")
                .length(5, 30)
                .withmessage("学生姓名长度限制在5到30个字符之间!");

            rulefor(vm => vm.age)
                .greaterthanorequalto(0)
                .withmessage("学生年龄为整数!")
                .exclusivebetween(10, 150)
                .withmessage($"请正确输入学生年龄(10-150)");

            rulefor(vm => vm.zip)
                .notempty()
                .withmessage("邮政编码不能为空!")
                .must(beavalidzip)
                .withmessage("邮政编码由六位数字组成。");
        }

        private static bool beavalidzip(string zip)
        {
            if (!string.isnullorempty(zip))
            {
                var regex = new regex(@"\d{6}");
                return regex.ismatch(zip);
            }
            return false;
        }
    }
}

4.4、 创建viewmodel类:studentviewmodel.cs

studentviewmodel与student实体类结构类似,都需要实现idataerrorinfo接口,该类由一个简单的string属性(title)和一个复杂的student对象属性(currentstudent)组成,代码如下:

using system;
using system.componentmodel;
using system.linq;
using wpffluentvalidation.models;
using wpffluentvalidation.validators;

namespace wpffluentvalidation.viewmodels
{
    /// <summary>
    /// 视图viewmodel
    /// 继承baseclasss,即继承属性变化接口inotifypropertychanged
    /// 实现idataerrorinfo接口,用于fluentvalidation验证,必须实现此接口
    /// </summary>
    public class studentviewmodel : baseclass, idataerrorinfo
    {
        private string title;
        public string title
        {
            get { return title; }
            set
            {
                if (value != title)
                {
                    title = value;
                    onpropertychanged(nameof(title));
                }
            }
        }

        private student currentstudent;
        public student currentstudent
        {
            get { return currentstudent; }
            set
            {
                if (value != currentstudent)
                {
                    currentstudent = value;
                    onpropertychanged(nameof(currentstudent));
                }
            }
        }

        public studentviewmodel()
        {
            currentstudent = new student()
            {
                name = "李刚的儿",
                age = 23
            };
        }


        public string this[string columnname]
        {
            get
            {
                if (validator == null)
                {
                    validator = new viewmodelvalidator();
                }
                var firstordefault = validator.validate(this)
                    .errors.firstordefault(lol => lol.propertyname == columnname);
                return firstordefault?.errormessage;
            }
        }
        public string error
        {
            get
            {
                var results = validator.validate(this);
                if (results != null && results.errors.any())
                {
                    var errors = string.join(environment.newline, results.errors.select(x => x.errormessage).toarray());
                    return errors;
                }

                return string.empty;
            }
        }

        private viewmodelvalidator validator;
    }
}

仔细看上方代码,对比student.cs,重写自idataerrorinfo接口定义的error属性与定义有所不同。student.cs对error基本未做修改,而studentviewmodel.cs有变化,get器中验证属性(简单属性title和复杂属性currentstudent),返回错误提示字符串,诶,currentstudent的验证器怎么生效的?有兴趣的读者可以研究fluentvalidation库源码一探究竟,博主表示研究源码其乐无穷,一时研究一时爽,一直研究一直爽。

4.5 studentviewmodel的验证器viewmodelvalidator.cs

viewmodel的验证器,相比student的验证器studentvalidator,就简单的多了,因为只需要编写验证一个简单属性title的代码。而复杂属性currentstudent的验证器studentvalidator,将被wpf属性系统自动调用,即在studentviewmodel的索引器this[string columnname]和error属性中调用,界面触发规则时自动调用。

using fluentvalidation;
using wpffluentvalidation.viewmodels;

namespace wpffluentvalidation.validators
{
    public class viewmodelvalidator:abstractvalidator<studentviewmodel>
    {
        public viewmodelvalidator()
        {
            rulefor(vm => vm.title)
                .notempty()
                .withmessage("标题长度不能为空!")
                .length(5, 30)
                .withmessage("标题长度限制在5到30个字符之间!");
        }
    }
}

4.6 辅助类baseclass.cs

简单封装inotifypropertychanged接口

using system.componentmodel;

namespace wpffluentvalidation
{
    public class baseclass : inotifypropertychanged
    {
        public event propertychangedeventhandler propertychanged;
        protected virtual void onpropertychanged(string propertyname)
        {
            propertychangedeventhandler handler = propertychanged;
            if (handler != null)
            {
                handler(this, new propertychangedeventargs(propertyname));
            }
        }
    }
}

4.7 、视图studentview.xaml

用户直接接触的视图文件来了,比较简单,提供简单属性标题(title)、复杂属性学生姓名(currentstudent.name)、学生年龄( currentstudent .age)、学生邮政编码( currentstudent .zip)验证,xaml代码如下:

<usercontrol x:class="wpffluentvalidation.views.studentview"
             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:wpffluentvalidation.views"
             xmlns:vm="clr-namespace:wpffluentvalidation.viewmodels"
             mc:ignorable="d" 
             d:designheight="450" d:designwidth="800">
    <usercontrol.datacontext>
        <vm:studentviewmodel/>
    </usercontrol.datacontext>
    <grid>
        <grid.rowdefinitions>
            <rowdefinition height="auto"/>
            <rowdefinition height="auto"/>
            <rowdefinition height="*"/>
        </grid.rowdefinitions>

        <groupbox header="viewmodel直接属性验证">
            <stackpanel orientation="horizontal">
                <label content="标题:"/>
                <textbox text="{binding title, updatesourcetrigger=propertychanged,validatesondataerrors=true}"
                         style="{staticresource errorstyle1}"/>
            </stackpanel>
        </groupbox>
        <groupbox header="viewmodel对象属性currentstudent的属性验证" grid.row="1">
            <stackpanel>
                <stackpanel orientation="horizontal">
                    <label content="姓名:"/>
                    <textbox text="{binding currentstudent.name, updatesourcetrigger=propertychanged,validatesondataerrors=true}"
                         style="{staticresource errorstyle2}"/>
                </stackpanel>
                <stackpanel orientation="horizontal">
                    <label content="年龄:"/>
                    <textbox text="{binding currentstudent.age, updatesourcetrigger=propertychanged,validatesondataerrors=true}"
                         style="{staticresource errorstyle2}"/>
                </stackpanel>
                <stackpanel orientation="horizontal">
                    <label content="邮编:" />
                    <textbox text="{binding currentstudent.zip, updatesourcetrigger=propertychanged,validatesondataerrors=true}"
                         style="{staticresource errorstyle2}"/>
                </stackpanel>
            </stackpanel>
        </groupbox>
    </grid>
</usercontrol>

4.8 、错误提示样式

本文提供了两种样式,具体效果见前面的截图,代码如下:

<application x:class="wpffluentvalidation.app"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:wpffluentvalidation"
             startupuri="mainwindow.xaml">
    <application.resources>
        <style targettype="stackpanel">
            <setter property="margin" value="0 5"/>
        </style>
        <!--第一种错误样式,红色边框-->
        <style targettype="{x:type textbox}" x:key="errorstyle1">
            <setter property="width" value="200"/>
            <setter property="validation.errortemplate">
                <setter.value>
                    <controltemplate>
                        <dockpanel>
                            <grid dockpanel.dock="right" width="16" height="16"
                            verticalalignment="center" margin="3 0 0 0">
                                <ellipse width="16" height="16" fill="red"/>
                                <ellipse width="3" height="8" 
                                verticalalignment="top" horizontalalignment="center" 
                                margin="0 2 0 0" fill="white"/>
                                <ellipse width="2" height="2" verticalalignment="bottom" 
                                horizontalalignment="center" margin="0 0 0 2" 
                                fill="white"/>
                            </grid>
                            <border borderbrush="red" borderthickness="2" cornerradius="2">
                                <adornedelementplaceholder/>
                            </border>
                        </dockpanel>
                    </controltemplate>
                </setter.value>
            </setter>
            <style.triggers>
                <trigger property="validation.haserror" value="true">
                    <setter property="tooltip" value="{binding relativesource=
                            {x:static relativesource.self}, 
                            path=(validation.errors)[0].errorcontent}"/>
                </trigger>
            </style.triggers>
        </style>

        <!--第二种错误样式,右键文字提示-->
        <style targettype="{x:type textbox}" x:key="errorstyle2">
            <setter property="width" value="200"/>
            <setter property="validation.errortemplate">
                <setter.value>
                    <controltemplate>
                        <stackpanel orientation="horizontal">
                            <adornedelementplaceholder x:name="textbox"/>
                            <textblock margin="10" text="{binding [0].errorcontent}" foreground="red"/>
                        </stackpanel>
                    </controltemplate>
                </setter.value>
            </setter>
            <style.triggers>
                <trigger property="validation.haserror" value="true">
                    <setter property="tooltip" value="{binding relativesource=
                    {x:static relativesource.self}, 
                    path=(validation.errors)[0].errorcontent}"/>
                </trigger>
            </style.triggers>
        </style>
    </application.resources>
</application>

五、 介绍完毕

码农就是这样,文章基本靠贴代码,哈哈。

6、源码同步

本文代码已同步gitee: https://gitee.com/lsq6/fluentvalidationforwpf

github: https://github.com/dotnet9/fluentvalidationforwpf

csdn: https://download.csdn.net/download/henrymoore/11984265