MVVMLight项目之绑定在表单验证上的应用示例分析
表单验证是mvvm体系中的重要一块。而绑定除了推动 model-view-viewmodel (mvvm) 模式松散耦合 逻辑、数据 和 ui定义 的关系之外,还为业务数据验证方案提供强大而灵活的支持。
wpf 中的数据绑定机制包括多个选项,可用于在创建可编辑视图时校验输入数据的有效性。
常见的表单验证机制有如下几种:
验证类型 | 说明 |
exception 验证 | 通过在某个 binding 对象上设置 validatesonexceptions 属性,如果源对象属性设置已修改的值的过程中引发异常,则抛出错误并为该 binding 设置验证错误。 |
validationrule 验证 |
binding 类具有一个用于提供 validationrule 派生类实例的集合的属性。这些 validationrules 需要覆盖某个 validate 方法,该方法由 binding 在每次绑定控件中的数据发生更改时进行调用。 如果 validate 方法返回无效的 validationresult 对象,则将为该 binding 设置验证错误。 |
idataerrorinfo 验证 |
通过在绑定数据源对象上实现 idataerrorinfo 接口并在 binding 对象上设置 validatesondataerrors 属性,binding 将调用从绑定数据源对象公开的 idataerrorinfo api。 如果从这些属性调用返回非 null 或非空字符串,则将为该 binding 设置验证错误。 |
验证交互的关系模式如图:
我们在使用 wpf 中的数据绑定来呈现业务数据时,通常会使用 binding 对象在目标控件的单个属性与数据源对象属性之间提供数据管道。
如果要使得绑定验证有效,首先需要进行 twoway 数据绑定。这表明,除了从源属性流向目标属性以进行显示的数据之外,编辑过的数据也会从目标流向源。
这就是伟大的双向数据绑定的精髓,所以在mvvm中做数据校验,会容易的多。
当 twoway 数据绑定中输入或修改数据时,将启动以下工作流:
1、 | 用户通过键盘、鼠标、手写板或者其他输入设备来输入或修改数据,从而改变绑定的目标信息 |
2、 | 设置源属性值。 |
3、 | 触发 binding.sourceupdated 事件。 |
4、 | 如果数据源属性上的 setter 引发异常,则异常会由 binding 捕获,并可用于指示验证错误。 |
5、 | 如果实现了 idataerrorinfo 接口,则会对数据源对象调用该接口的方法获得该属性的错误信息。 |
6、 | 向用户呈现验证错误指示,并触发 validation.error 附加事件。 |
绑定目标向绑定源发送数据更新的请求,而绑定源则对数据进行验证,并根据不同的验证机制进行反馈。
下面我们用实例来对比下这几种验证机制,在此之前,我们先做一个事情,就是写一个错误触发的样式,来保证错误触发的时候直接清晰的向用户反馈出去。
我们新建一个资源字典文件,命名为textbox.xaml,下面这个是资源字典文件的内容,目标类型是textboxbase基础的控件,如textbox和richtextbox.
代码比较简单,注意标红的内容,设计一个红底白字的提示框,当源属性触发错误验证的时候,把验证对象集合中的错误内容显示出来。
<resourcedictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <style x:key="{x:type textboxbase}" targettype="{x:type textboxbase}" basedon="{x:null}"> <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="selectionbrush" value="{dynamicresource accent}" /> <setter property="validation.errortemplate"> <setter.value> <controltemplate> <stackpanel orientation="horizontal"> <border borderthickness="1" borderbrush="#ffdc000c" verticalalignment="top"> <grid> <adornedelementplaceholder x:name="adorner" margin="-1"/> </grid> </border> <border x:name="errorborder" background="#ffdc000c" margin="8,0,0,0" opacity="0" cornerradius="0" ishittestvisible="false" minheight="24" > <textblock text="{binding elementname=adorner, path=adornedelement.(validation.errors)[0].errorcontent}" foreground="white" margin="8,2,8,3" textwrapping="wrap" verticalalignment="center"/> </border> </stackpanel> <controltemplate.triggers> <datatrigger value="true"> <datatrigger.binding> <binding elementname="adorner" path="adornedelement.iskeyboardfocused" /> </datatrigger.binding> <datatrigger.enteractions> <beginstoryboard x:name="fadeinstoryboard"> <storyboard> <doubleanimation duration="00:00:00.15" storyboard.targetname="errorborder" storyboard.targetproperty="opacity" to="1"/> </storyboard> </beginstoryboard> </datatrigger.enteractions> <datatrigger.exitactions> <stopstoryboard beginstoryboardname="fadeinstoryboard"/> <beginstoryboard x:name="fadeoutstoryboard"> <storyboard> <doubleanimation duration="00:00:00" storyboard.targetname="errorborder" storyboard.targetproperty="opacity" to="0"/> </storyboard> </beginstoryboard> </datatrigger.exitactions> </datatrigger> </controltemplate.triggers> </controltemplate> </setter.value> </setter> <setter property="template"> <setter.value> <controltemplate targettype="{x:type textboxbase}"> <border x:name="bd" borderthickness="{templatebinding borderthickness}" borderbrush="{templatebinding borderbrush}" background="{templatebinding background}" padding="{templatebinding padding}" snapstodevicepixels="true"> <scrollviewer x:name="part_contenthost" renderoptions.cleartypehint="enabled" snapstodevicepixels="{templatebinding snapstodevicepixels}"/> </border> <controltemplate.triggers> <trigger property="isenabled" value="false"> <setter property="foreground" value="{dynamicresource inputtextdisabled}"/> </trigger> <trigger property="isreadonly" value="true"> <setter property="foreground" value="{dynamicresource inputtextdisabled}"/> </trigger> <trigger property="isfocused" value="true"> <setter targetname="bd" property="borderbrush" value="{dynamicresource accent}" /> </trigger> <multitrigger> <multitrigger.conditions> <condition property="isreadonly" value="false"/> <condition property="isenabled" value="true"/> <condition property="ismouseover" value="true"/> </multitrigger.conditions> <setter property="background" value="{dynamicresource inputbackgroundhover}"/> <setter property="borderbrush" value="{dynamicresource inputborderhover}"/> <setter property="foreground" value="{dynamicresource inputtexthover}"/> </multitrigger> </controltemplate.triggers> </controltemplate> </setter.value> </setter> </style> <style basedon="{staticresource {x:type textboxbase}}" targettype="{x:type textbox}"> </style> <style basedon="{staticresource {x:type textboxbase}}" targettype="{x:type richtextbox}"> </style> </resourcedictionary>
然后在app.xaml中全局注册到整个应用中。
<application x:class="mvvmlightdemo.app" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" startupuri="view/bindingformview.xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" d1p1:ignorable="d" xmlns:d1p1="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:mvvmlightdemo.viewmodel" xmlns:common="clr-namespace:mvvmlightdemo.common"> <application.resources> <resourcedictionary> <resourcedictionary.mergeddictionaries> <resourcedictionary source="/mvvmlightdemo;component/assets/textbox.xaml" /> </resourcedictionary.mergeddictionaries> <vm:viewmodellocator x:key="locator" d:isdatasource="true" /> <common:integertosex x:key="integertosex" d:isdatasource="true" /> </resourcedictionary> </application.resources> </application>
达到的效果如下:
下面详细描述下这三种验证模式
1、exception 验证:
正如说明中描述的那样,在具有绑定关系的源字段模型上做验证异常的引发并抛出,在view中的xaml对象上设置 exceptionvalidationrule 属性,响应捕获异常并显示。
view代码:
<groupbox header="exception 验证" margin="10 10 10 10" datacontext="{binding source={staticresource locator},path=validateexception}" > <stackpanel x:name="exceptionpanel" orientation="vertical" margin="0,10,0,0" > <stackpanel> <label content="用户名" target="{binding elementname=usernameex}"/> <textbox x:name="usernameex" width="150"> <textbox.text> <binding path="usernameex" updatesourcetrigger="propertychanged"> <binding.validationrules> <exceptionvalidationrule></exceptionvalidationrule> </binding.validationrules> </binding> </textbox.text> </textbox> </stackpanel> </stackpanel> </groupbox>
viewmodel代码:
/// <summary> /// exception 验证 /// </summary> public class validateexceptionviewmodel:viewmodelbase { public validateexceptionviewmodel() { } private string usernameex; /// <summary> /// 用户名称(不为空) /// </summary> public string usernameex { get { return usernameex; } set { usernameex = value; raisepropertychanged(() => usernameex); if (string.isnullorempty(value)) { throw new applicationexception("该字段不能为空!"); } } }
结果如图:
将验证失败的信息直接抛出来,这无疑是最简单粗暴的,实现也很简单,但是只是针对单一源属性进行验证, 复用性不高。
而且在组合验证(比如同时需要验证非空和其他规则)情况下,会导致model中写过重过臃肿的代码。
2、validationrule 验证:
通过继承validationrule 抽象类,并重写他的validate方法来扩展编写我们需要的验证类。该验证类可以直接使用在我们需要验证的属性。
view代码:
<groupbox header="validationrule 验证" margin="10 20 10 10" datacontext="{binding source={staticresource locator},path=validationrule}" > <stackpanel x:name="validationrulepanel" orientation="vertical" margin="0,20,0,0"> <stackpanel> <label content="用户名" target="{binding elementname=username}"/> <textbox width="150" > <textbox.text> <binding path="username" updatesourcetrigger="propertychanged"> <binding.validationrules> <app:requiredrule /> </binding.validationrules> </binding> </textbox.text> </textbox> </stackpanel> <stackpanel> <label content="用户邮箱" target="{binding elementname=useremail}"/> <textbox width="150"> <textbox.text> <binding path="useremail" updatesourcetrigger="propertychanged"> <binding.validationrules> <app:emailrule /> </binding.validationrules> </binding> </textbox.text> </textbox> </stackpanel> </stackpanel> </groupbox>
重写两个validationrule,代码如下:
public class requiredrule : validationrule { public override validationresult validate(object value, cultureinfo cultureinfo) { if (value == null) return new validationresult(false, "该字段不能为空值!"); if (string.isnullorempty(value.tostring())) return new validationresult(false, "该字段不能为空字符串!"); return new validationresult(true, null); } } public class emailrule : validationrule { public override validationresult validate(object value, cultureinfo cultureinfo) { regex emailreg = new regex("^\\s*([a-za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$"); if (!string.isnullorempty(value.tostring())) { if (!emailreg.ismatch(value.tostring())) { return new validationresult(false, "邮箱地址不准确!"); } } return new validationresult(true, null); }
创建了两个类,一个用于验证是否为空,一个用于验证是否符合邮箱地址标准格式。
viewmodel代码:
public class validationruleviewmodel:viewmodelbase { public validationruleviewmodel() { } #region 属性 private string username; /// <summary> /// 用户名 /// </summary> public string username { get { return username; } set { username = value; raisepropertychanged(()=>username); } } private string useremail; /// <summary> /// 用户邮件 /// </summary> public string useremail { get { return useremail; } set { useremail = value;raisepropertychanged(()=>username); } } #endregion
结果如下:
说明:相对来说,这种方式是比较不错的,独立性、复用性都很好,从松散耦合角度来说也是比较恰当的。
可以预先写好一系列的验证规则类,视图编码人员可以根据需求直接使用这些验证规则,服务端无需额外的处理。
但是仍然有缺点,扩展性差,如果需要个性化反馈消息也需要额外扩展。不符合日益丰富的前端验证需求。
3、idataerrorinfo 验证:
3.1、在绑定数据源对象上实现 idataerrorinfo 接口
3.2、在 binding 对象上设置 validatesondataerrors 属性
binding 将调用从绑定数据源对象公开的 idataerrorinfo api。如果从这些属性调用返回非 null 或非空字符串,则将为该 binding 设置验证错误。
view代码:
<groupbox header="idataerrorinfo 验证" margin="10 20 10 10" datacontext="{binding source={staticresource locator},path=bindingform}" > <stackpanel x:name="form" orientation="vertical" margin="0,20,0,0"> <stackpanel> <label content="用户名" target="{binding elementname=username}"/> <textbox width="150" text="{binding username, mode=twoway, updatesourcetrigger=propertychanged, validatesondataerrors=true}" > </textbox> </stackpanel> <stackpanel> <label content="性别" target="{binding elementname=radiogendemale}"/> <radiobutton content="男" /> <radiobutton content="女" margin="8,0,0,0" /> </stackpanel> <stackpanel> <label content="生日" target="{binding elementname=datebirth}" /> <datepicker x:name="datebirth" /> </stackpanel> <stackpanel> <label content="用户邮箱" target="{binding elementname=useremail}"/> <textbox width="150" text="{binding useremail, mode=twoway, updatesourcetrigger=propertychanged, validatesondataerrors=true}" /> </stackpanel> <stackpanel> <label content="用户电话" target="{binding elementname=userphone}"/> <textbox width="150" text="{binding userphone, mode=twoway, updatesourcetrigger=propertychanged, validatesondataerrors=true}" /> </stackpanel> </stackpanel> </groupbox>
viewmodel代码:
public class bindingformviewmodel :viewmodelbase, idataerrorinfo { public bindingformviewmodel() { } #region 属性 private string username; /// <summary> /// 用户名 /// </summary> public string username { get { return username; } set { username = value; } } private string userphone; /// <summary> /// 用户电话 /// </summary> public string userphone { get { return userphone; } set { userphone = value; } } private string useremail; /// <summary> /// 用户邮件 /// </summary> public string useremail { get { return useremail; } set { useremail = value; } } #endregion public string error { get { return null; } } public string this[string columnname] { get { regex digitalreg = new regex(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$"); regex emailreg = new regex("^\\s*([a-za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$"); if (columnname == "username" && string.isnullorempty(this.username)) { return "用户名不能为空"; } if (columnname == "userphone" && !string.isnullorempty(this.userphone)) { if (!digitalreg.ismatch(this.userphone.tostring())) { return "用户电话必须为8-11位的数值!"; } } if (columnname == "useremail" && !string.isnullorempty(this.useremail)) { if (!emailreg.ismatch(this.useremail.tostring())) { return "用户邮箱地址不正确!"; } } return null; } } }
继承idataerrorinfo接口后,实现方法两个属性:error 属性用于指示整个对象的错误,而索引器用于指示单个属性级别的错误。
每次的属性值发生变化,则索引器进行一次检查,看是否有验证错误的信息返回。
两者的工作原理相同:如果返回非 null 或非空字符串,则表示存在验证错误。否则,返回的字符串用于向用户显示错误。
结果如图:
利用 idataerrorinfo 的好处是它可用于轻松地处理交叉耦合属性。但也具有一个很大的弊端:
索引器的实现通常会导致较大的 switch-case 语句(对象中的每个属性名称都对应于一种情况),
必须基于字符串进行切换和匹配,并返回指示错误的字符串。而且,在对象上设置属性值之前,不会调用 idataerrorinfo 的实现。
为了避免出现大量的 switch-case,并且将校验逻辑进行分离提高代码复用,将验证规则和验证信息独立化于于每个模型对象中, 使用dataannotations 无疑是最好的的方案 。
所以我们进行改良一下:
view代码,跟上面那个一样:
<groupbox header="idataerrorinfo+ 验证" margin="10 20 10 10" datacontext="{binding source={staticresource locator},path=binddataannotations}" > <stackpanel orientation="vertical" margin="0,20,0,0"> <stackpanel> <label content="用户名" target="{binding elementname=username}"/> <textbox width="150" text="{binding username,updatesourcetrigger=propertychanged,validatesondataerrors=true}" > </textbox> </stackpanel> <stackpanel> <label content="性别" target="{binding elementname=radiogendemale}"/> <radiobutton content="男" /> <radiobutton content="女" margin="8,0,0,0" /> </stackpanel> <stackpanel> <label content="生日" target="{binding elementname=datebirth}" /> <datepicker /> </stackpanel> <stackpanel> <label content="用户邮箱" target="{binding elementname=useremail}"/> <textbox width="150" text="{binding useremail, updatesourcetrigger=propertychanged, validatesondataerrors=true}" /> </stackpanel> <stackpanel> <label content="用户电话" target="{binding elementname=userphone}"/> <textbox width="150" text="{binding userphone,updatesourcetrigger=propertychanged, validatesondataerrors=true}" /> </stackpanel> <button content="提交" margin="100,16,0,0" horizontalalignment="left" command="{binding validformcommand}" /> </stackpanel> </groupbox>
videmodel代码:
using galasoft.mvvmlight; using system; using system.collections.generic; using system.linq; using system.componentmodel; using system.componentmodel.dataannotations; using galasoft.mvvmlight.command; using system.windows; namespace mvvmlightdemo.viewmodel { [metadatatype(typeof(binddataannotationsviewmodel))] public class binddataannotationsviewmodel : viewmodelbase, idataerrorinfo { public binddataannotationsviewmodel() { } #region 属性 /// <summary> /// 表单验证错误集合 /// </summary> private dictionary<string, string> dataerrors = new dictionary<string, string>(); private string username; /// <summary> /// 用户名 /// </summary> [required] public string username { get { return username; } set { username = value; } } private string userphone; /// <summary> /// 用户电话 /// </summary> [required] [regularexpression(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$", errormessage = "用户电话必须为8-11位的数值.")] public string userphone { get { return userphone; } set { userphone = value; } } private string useremail; /// <summary> /// 用户邮件 /// </summary> [required] [stringlength(100,minimumlength=2)] [regularexpression("^\\s*([a-za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$", errormessage = "请填写正确的邮箱地址.")] public string useremail { get { return useremail; } set { useremail = value; } } #endregion #region 命令 private relaycommand validformcommand; /// <summary> /// 验证表单 /// </summary> public relaycommand validformcommand { get { if (validformcommand == null) return new relaycommand(() => excutevalidform()); return validformcommand; } set { validformcommand = value; } } /// <summary> /// 验证表单 /// </summary> private void excutevalidform() { if (dataerrors.count == 0) messagebox.show("验证通过!"); else messagebox.show("验证失败!"); } #endregion public string this[string columnname] { get { validationcontext vc = new validationcontext(this, null, null); vc.membername = columnname; var res = new list<validationresult>(); var result = validator.tryvalidateproperty(this.gettype().getproperty(columnname).getvalue(this, null), vc, res); if (res.count > 0) { adddic(dataerrors,vc.membername); return string.join(environment.newline, res.select(r => r.errormessage).toarray()); } removedic(dataerrors,vc.membername); return null; } } public string error { get { return null; } } #region 附属方法 /// <summary> /// 移除字典 /// </summary> /// <param name="dics"></param> /// <param name="dickey"></param> private void removedic(dictionary<string, string> dics, string dickey) { dics.remove(dickey); } /// <summary> /// 添加字典 /// </summary> /// <param name="dics"></param> /// <param name="dickey"></param> private void adddic(dictionary<string, string> dics, string dickey) { if (!dics.containskey(dickey)) dics.add(dickey, ""); } #endregion } }
dataannotations相信很多人很熟悉,可以使用数据批注来自定义用户的模型数据,记得引用 system.componentmodel.dataannotations。
他包含如下几个验证类型:
验证属性 | 说明 |
customvalidationattribute | 使用自定义方法进行验证。 |
datatypeattribute | 指定特定类型的数据,如电子邮件地址或电话号码。 |
enumdatatypeattribute | 确保值存在于枚举中。 |
rangeattribute | 指定最小和最大约束。 |
regularexpressionattribute | 使用正则表达式来确定有效的值。 |
requiredattribute | 指定必须提供一个值。 |
stringlengthattribute | 指定最大和最小字符数。 |
validationattribute | 用作验证属性的基类。 |
这边我们使用到了requiredattribute、stringlengthattribute、regularexpressionattribute 三项,如果有需要进一步了解 dataannotations 的可以参考微软官网:
https://msdn.microsoft.com/en-us/library/dd901590(vs.95).aspx
用 dataannotions 后,model 的更加简洁,校验也更加灵活。可以叠加组合验证 , 面对复杂验证模式的时候,可以*的使用正则来验证。
默认情况下,框架会提供相应需要反馈的消息内容,当然也可以自定义错误消息内容:errormessage 。
这边我们还加了个全局的错误集合收集器 :dataerrors,在提交判断时候判断是否验证通过。
这边我们进一步封装索引器,并且通过反射技术读取当前字段下的属性进行验证。
结果如下:
封装validatemodelbase类:
上面的验证比较合理了,不过相对于开发人员还是太累赘了,开发人员关心的是model的dataannotations的配置,而不是关心在这个viewmodel要如何做验证处理,所以我们进一步抽象。
编写一个validatemodelbase,把需要处理的工作都放在里面。需要验证属性的model去继承这个基类。如下:
validatemodelbase 类,请注意标红部分:
public class validatemodelbase : observableobject, idataerrorinfo { public validatemodelbase() { } #region 属性 /// <summary> /// 表当验证错误集合 /// </summary> private dictionary<string, string> dataerrors = new dictionary<string, string>(); /// <summary> /// 是否验证通过 /// </summary> public boolean isvalidated { get { if (dataerrors != null && dataerrors.count > 0) { return false; } return true; } } #endregion public string this[string columnname] { get { validationcontext vc = new validationcontext(this, null, null); vc.membername = columnname; var res = new list<validationresult>(); var result = validator.tryvalidateproperty(this.gettype().getproperty(columnname).getvalue(this, null), vc, res); if (res.count > 0) { adddic(dataerrors, vc.membername); return string.join(environment.newline, res.select(r => r.errormessage).toarray()); } removedic(dataerrors, vc.membername); return null; } } public string error { get { return null; } } #region 附属方法 /// <summary> /// 移除字典 /// </summary> /// <param name="dics"></param> /// <param name="dickey"></param> private void removedic(dictionary<string, string> dics, string dickey) { dics.remove(dickey); } /// <summary> /// 添加字典 /// </summary> /// <param name="dics"></param> /// <param name="dickey"></param> private void adddic(dictionary<string, string> dics, string dickey) { if (!dics.containskey(dickey)) dics.add(dickey, ""); } #endregion }
验证的模型类:继承 validatemodelbase
[metadatatype(typeof(binddataannotationsviewmodel))] public class validateuserinfo : validatemodelbase { #region 属性 private string username; /// <summary> /// 用户名 /// </summary> [required] public string username { get { return username; } set { username = value; raisepropertychanged(() => username); } } private string userphone; /// <summary> /// 用户电话 /// </summary> [required] [regularexpression(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$", errormessage = "用户电话必须为8-11位的数值.")] public string userphone { get { return userphone; } set { userphone = value; raisepropertychanged(() => userphone); } } private string useremail; /// <summary> /// 用户邮件 /// </summary> [required] [stringlength(100, minimumlength = 2)] [regularexpression("^\\s*([a-za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$", errormessage = "请填写正确的邮箱地址.")] public string useremail { get { return useremail; } set { useremail = value; raisepropertychanged(() => useremail); } } #endregion }
viewmodel代码如下:
public class packagedvalidateviewmodel:viewmodelbase { public packagedvalidateviewmodel() { validateui = new model.validateuserinfo(); } #region 全局属性 private validateuserinfo validateui; /// <summary> /// 用户信息 /// </summary> public validateuserinfo validateui { get { return validateui; } set { validateui = value; raisepropertychanged(()=>validateui); } } #endregion #region 全局命令 private relaycommand submitcmd; public relaycommand submitcmd { get { if(submitcmd == null) return new relaycommand(() => excutevalidform()); return submitcmd; } set { submitcmd = value; } } #endregion #region 附属方法 /// <summary> /// 验证表单 /// </summary> private void excutevalidform() { if (validateui.isvalidated) messagebox.show("验证通过!"); else messagebox.show("验证失败!"); } #endregion }
结果如下:
以上就是mvvmlight项目之绑定在表单验证上的应用示例分析的详细内容,更多关于mvvmlight绑定在表单验证上的应用的资料请关注其它相关文章!