
首先声明,这篇博文是完善.ASP.NET全栈开发之在MVC中使用服务端验证 的,所以重复内容,我就不过多的阐述,很多问题都是在实践中去发现,然后再去完善,这篇博文也一样,建立在已阅 “.ASP.NET全栈开发之在MVC中使用服务端验证” 的基础上。 在上一篇中,虽然我们完成了服务端验证,但我们还是需要在Action里调用验证器来进行验证,像这样。 [HttpPost] public ActionResult ValidatorTest(Person model) { var result = this.ValidatorHub.PersonValidator.Validate(model); if (result.IsValid) { return Redirect("https://www.baidu.com"); } else { this.ValidatorErrorHandler(result); } return View(); } 很可恶,如果我需要验证,我需要在每一个Action 里像这样写,一次实验也就罢了,如果真要在每个Action里像这样干,我想到时候你一定会很讨厌这些代码的。至少我是这样认为。所以我很讨厌我之前的写法。 现在我想干嘛呢?我们知道其实MVC内置了一个数据校验。这里不过多介绍它,(偶尔适当的照照轮子,也有许多好处的)。这里简单描述下它的用法。 [HttpPost] public ActionResult ValidatorTest(Person model) { if (ModelState.IsValid) { /// ok } return View(); } 和咱们之前那样写比起来是精简了许多,但我还是觉得吧,他还是要在每个Action 里调用ModelState.IsValid,虽然只有一个if,但这不是我想要的,我希望它能像这样 [HttpPost] public ActionResult ValidatorTest(Person model) { // // 一大堆代码 // return Redirect("https://www.baidu.com"); } 不要影响我正常的编程,而我也不去做哪些重复的事。 换句话说,其实就是在执行我Action之前就去把数据给校验了。 于是我们想到了MVC给我们提供的Filter,OnActionExecuting,打开我们的ControllerEx,在里面重写OnActionExecuting,他有一个参数ActionExecutingContext,通过名字我们大致了解了,这个参数是个Action相关的上下文,那他一定装了Action相关的数据 我就不墨迹了,先直接上代码,其实这些代码也只是我刚刚才写出来的而已,我对这个参数也不是很了解,通过一个一个去尝试,慢慢得就试出来了。 protected override void OnActionExecuting(ActionExecutingContext filterContext) { var existError = false; foreach (var value in filterContext.ActionParameters.Values) { var modelValidatorPropertyInfo = this.ValidatorHub.GetType().GetProperty(value.GetType().Name + "Validator"); if (modelValidatorPropertyInfo != null) { var modelValidator = modelValidatorPropertyInfo.GetValue(this.ValidatorHub) as IValidator; var validateResult = modelValidator.Validate(value); if (!validateResult.IsValid) { this.ValidatorErrorHandler(validateResult); existError = true; } } } if (existError) { ViewData["Error"] = DicError; filterContext.Result = View(); } base.OnActionExecuting(filterContext); } 在 OnActionExecuting 里,我们首先定义了一个existError,用来判断是否验证失败的,然后我们遍历了 filterContext.ActionParameters.Values 在filterContext 里,我们看到ActionParameters 是关于Action的参数的,通过调试我发现,他是一个集合,其键是参数名,比如拿我们这个Person来讲。 [HttpPost] public ActionResult ValidatorTest(Person model) { // // 一大堆代码 // return Redirect("https://www.baidu.com"); } filterContext.ActionParameters 集合里就有一个数据,其键是"model" 值呢 model 所以呢我们通过遍历filterContext.ActionParameters.Value 就能取出每一个Action的所有参数了,而每一个参数通过.getType().Name 则能取出他的类型名,比如这里model类型是Person 所以filterContext.ActionParameters["model"].GetType().Name 就是“Person”了。 知道了实体是什么类型,如何获取具体验证器呢?想想我们的验证器配置 Person = PersonValidator 那太简单了,这不是一对一的关系嘛,但总不可能通过一个switch 去工厂返回吧,那这样还需要维护一个工厂方法。当然不是咯,这就要用到咱.NET 提供的强大反射技术 有时候我们有一个匿名对象,是一个object的时候,又不知道它具体是什么类型,如何取它的属性呢? 我这有一个方法,他能解决这个问题。 public static class ReflectHelper { public static object GetPropertyByAnonymousObject(string propertyName, object obj) { var property = obj.GetType().GetProperties().Where(p => p.Name == propertyName).FirstOrDefault(); if (property == null) { throw new Exception(string.Format("{0}对象未定义{1}属性", nameof(obj), nameof(propertyName))); } return property.GetValue(obj); } } 从使用上,传递一个属性名和对象进来,返回一个object的属性。 我们看看内部都做了些什么。 首先获取类型,然后获取执行的属性,诶,这个属性可不是真的属性哦,这个是PropertyInfo类型,是反射里的数据类型,它不是真正的属性值,但我们如果想要获取真正的属性值怎么办呢?其实只需要调用他的GetValue就行了,他有一个参数,这个参数是指获取那个对象上的属性,于是把object传进去就行。 有了这个基础,反观我们的目的,知道了Person,有一个对象叫ValidatotHub 里面有个属性PersonValidator ,所以我们只需要获取一个叫ValidatorHub对象里的PersonValidator属性就行了。(Person是可替换的,是根据参数类型来的,前面已经解释过了,这里以Person举例) 现在有个问题了,我们取到的PersonValidator 是一个object类型的,object类型我可不好使用啊,我们又不能显示的转换为具体类型,因为谁知道具体类型是啥呢。如果写死了就凉了。那肯定也不能用个switch来维护啊,那样不又增加工作量了吗。 我们慢慢发现PersonValidator继承自AbstractValidator<Person> 很显然它的基类也需要一个具体类型,不行,继续往上走,诶,发现了AbstractValidator<T>继承自IValidator,并且IValidator定义了Validate方法。这不就好了吗,我as 为IValidator类型,就可以用了。这里使用了(里氏转换原则)。我尽量写得通俗易懂,也将许多基础东西提一下,但不肯能面面俱到,所以还是建立在一部分基础之上的。(当然更重要的一点是,通过这次遇到的问题让我以后在设计泛型类结构的时候,都要去继承一个非泛型的接口,如果FluentValidator没有继承自IValidator 而只是继承自IValidator<T>其实从简单使用上来讲,并没有什么影响啊,但到了我们刚刚这里,问题就出来了,所以这也是给我们狠狠地上了一课啊) 现在我就可以在这里进行验证了,我们知道value 就是那个model 所以直接对他进行验证,验证会返回一个ValidationResult类型接下来的事我就不解释了,相信上一章已经讲得很清楚了。最后根据是否存在错误在进行提前处理,如果有错误的话就直接返回视图呈现错误了,连咱们的Action都不执行了。好了,到这里咱们昨天讲得OnActionExecuted 可以直接Delete拉 。 我们现在把ValidatorTest里的验证代码都去掉来测试一下。 [HttpPost] public ActionResult ValidatorTest(Person model) { // // 一大堆代码 // return Redirect("https://www.baidu.com"); } 在 ValidatorTest 里打上断点,然后什么都不填,直接提交。 断点没触发,但错误消息已呈现。多试几次~. 同样没触发。 那我们来一次正确的验证。 断点触发了。并且值都通过了校验 F5放行,最终我们的页面跳转到了 www.baidu.com。 好了,小伙伴们还不快去改改代码造就幸福生活。
前面分享了两篇关于.NET的服务端校验的文章,在系统里光有服务端校验虽然能够勉强使用,但会出现许多不愉快的体验,如上一章我在最后提交的时候填写了3个表单,在仅有最后一个表单出现了错误的时候,虽然达到了校验功能,表明了错误,但我前面三个表单的数据都被干掉了啊。再则比如注册的时候我填了那么多东西,我提交后你却告诉我已经被注册了,如果不是真爱,大概会毫不留情的Alt+F4 再也不见啦。 为了解决这个问题,我们必须在系统中采用双校验,前端校验那么多,咱们ASP.NET MVC创建时默认会引入jquery-validate,但在这里我就不过多介绍jquery-validate了,原因是...其实我也没怎么用过都是默认生成的顶多测试时用用,哈哈,这并不是说jquery-validate不好,只是我们现在所要搭建的这个系统是基于Vue的前后端分离项目,而Vue则是以数据为驱动的,所以咱们这暂时就不用讨论jquery-validate了。 如果不熟悉Vue的同学呢,可以先去Vue的官网看看,文档很详细,作者中国人,所以文档的易读性也很高,现在Vue已经是非常火的前端MVVM框架了,各种资料、教程也多,我就不过多重复了(其实我也才接触Vue不到一月,实在不敢瞎扯)。 在Vue的官网上看到表单验证的介绍,大致是通过给form绑定@submit事件,在事件里通过对字段的验证决定要不要阻止提交。 <form id="app" @submit="checkForm" action="https://vuejs.org/" method="post"> <p v-if="errors.length"> <b>Please correct the following error(s):</b> <ul> <li v-for="error in errors">{{ error }}</li> </ul> </p> <p> <label for="name">Name</label> <input type="text" name="name" id="name" v-model="name"> </p> <p> <label for="age">Age</label> <input type="number" name="age" id="age" v-model="age" min="0"> </p> <p> <label for="movie">Favorite Movie</label> <select name="movie" id="movie" v-model="movie"> <option>Star Wars</option> <option>Vanilla Sky</option> <option>Atomic Blonde</option> </select> </p> <p> <input type="submit" value="Submit"> </p> </form> const app = new Vue({ el:'#app', data:{ errors:[], name:null, age:null, movie:null }, methods:{ checkForm:function(e) { if(this.name && this.age) return true; this.errors = []; if(!this.name) this.errors.push("Name required."); if(!this.age) this.errors.push("Age required."); e.preventDefault(); } } }) Vue官网上的验证,是的,它非常短小精悍的完成了校验。 但说真的,这不是我想要的,我还是需要去写逻辑,我就想像前两章后台校验那样通过函数链式配置完成。 怎么办呢,Vue自己又没有,用别的框架?我咋知道有啥框架可用,我才刚认识它(Vue),对它的六大姑七大婆还不熟呢。 既然使用方式已定,又找不到别的办法,那就只有自己下地狱了。 本章节采用了ES6的语法,如看官还不熟悉ES6请先了解一下ES6基础后再来。 假设我有个实体Student class Student { constructor() { this.name = undefined; this.age = undefined; this.sex = undefined; } } 首先我确定了我的使用方式像这样 this.ruleFor("name") .NotEmpty() .WithMessage("名称必填") .MinimumLength(5) .WithMessage("最短长度为5"); this.ruleFor("age") .NotEmpty() .WithMessage("年龄必须") .Number(0, 100) .WithMessage("年龄必须在0-100岁之间"); this.ruleFor("sex") .NotEmpty() .WithMessage("性别必填") .Must(m => !m.sex) .WithMessage("gxq is a man"); 从FluentValidation认识哪里开始,FluentValidation的使用需要一个验证器,没关系,那我也创建一个验证器名字叫StudentValidator的验证器 class StudentValidator{ constructor(){ } } 我们知道要想向ruleFor()、NotEmpty()等这样使用 首先我得具有ruleFor这些方法才行啊,反观FluentValidation,我们发现他必须要继承自AbstractValidator<T>,其中T代表验证器要验证的类型。 javascript没有泛型,但我们也可以继承自Validator啊,不支持泛型,我还不能用参数表单我要验证那个实体了么。╭(╯^╰)╮ 于是乎 class Validator { constructor(model) { this.model =new model(); } ruleFor(field) { //此处表达式返回一个指定字段哈 } validation(targetElement) { } } Validator就这么出现了,首先他有一个参数,参数是传递一个构造函数进去的,实体名就是构造函数,比如前面的Student。 内部定义了一个ruleFor方法,有一个参数,表明为指定属性配置验证规则。为了降低使用成本,尽量和前面使用的后台验证框架用法一致。 还定义了一个validation方法有一个参数触发对象,这个对象实际上是sumbit那个按钮,后面在做解释。 现在我们让StudentValidator继承自Validator,这样我们的StudentValidator就变为了 class StudentValidator extends Validator { constructor(typeName) { super(typeName); this.ruleFor("name");this.ruleFor("age"); this.ruleFor("sex"); } } 可以指定字段了,但不能配置这像什么话,二话不说继续改。 从哪里开始改呢?已经调用了ruleFor方法,要想实现接着.NotEmpty()的话那必须从ruleFor下手了。 方向已经明确了,但总的有具体实施方案啊,于是啊苦想半天,我认为我们的ruleFor既然是为了给目标字段定义验证,并且是链式的可以为目标字段配置多个验证规则如 this.ruleFor("age") .NotEmpty() .WithMessage("年龄必须") .Number(0, 100) .WithMessage("年龄必须在0-100岁之间"); 那么我们就应该在ruleFor的时候返回一个以字段名为唯一标识的对象,幸好啊,在ES6中提供了一种数据类型叫Map,他有点类似于咱们C#的Dictionary,现在数据类型有了,键有了,那我这个键对应的值该是什么呢? 继续分析,这个值里必须具备NotEmpty、Number等这些函数我才能够调用啊,既然这样,那毫无疑问它又是一个对象了。 于是我创建了RuleBuilderInitial对象,基础结构如下 class RuleBuilderInitial { constructor() { this.length = undefined; this.must = undefined; this.maximumLength = undefined; this.minimumLength = undefined; this.number = undefined; } /* * 非空 */ NotEmpty() { } Length(min, max) { } Must(expression) { } EmailAddress() { } MaximumLength(max) { } MinimumLength(min) { } Number(min, max) { } } 有了RuleBuilderInitial后我们来改造下Validator class Validator { constructor(model) { this.model =new model(); this.fieldValidationSet = new Map(); } ruleFor(field) { //此处表达式返回一个指定字段哈 // 验证配置以字段作为唯一单位,每个字段对应一个初始化器 this.fieldValidationSet.set(field, new RuleBuilderInitial()); return this.fieldValidationSet.get(field); } validation(targetElement) { } } 在Validator里我们新增了fieldValidationSet 他是一个Map数据结构,前面说到了Map就是一个键值对,只不过他比较唯一,他不允许键重复,如果键值对里已存在Key,则新的Value不会替换掉之前的Value。 我们用这个fieldValidationSet来保存以字段名为单位的验证配置。因为Map的结构原因,ruleFor确保了每个字段的配置项唯一的工作。 在ruleFor时,我们将new 一个RuleBuilderInitial对象,并将它和字段一起添加到Map去,最后将这个RuleBuilderInitial对象从Map里取出并返回。 我们知道在RuleBuilderInitial对象里定义了许多方法如NotEmpty、Number等,所以现在我们可以这样写了。 class StudentValidator extends Validator { constructor(typeName) { super(typeName); this.ruleFor("name") .NotEmpty() this.ruleFor("age") .NotEmpty() this.ruleFor("sex") .NotEmpty() } } typeName是类型名,略略略,上面忘了解释了,在Validator的构造函数里我们不是有一个model吗,拿到model后我们new model了,所以typeName应该为我们要验证的实体如Student。 首先这显然不是我们想要的,我们希望在NotEmpty后能够接着调用WithMessage()方法,汲取了上面ruleFor的经验,肯定是在NotEmpty方法里返回一个新对象,并且这个新对象具备WithMessage方法。这没问题,太简单了,也许我们在创建一个叫 RuleBuilderOptions的对象就一切OK了,在NotEmpty()中只需要返回这个 RuleBuilderOptions对象就行了,可事实上我们希望的是在返回RuleBuilderOptions对象后调用WithMessage方法后并继续调用Number等其他方法。 梳理一下结构,我们在Validator种以字段作为key创建了RuleBuilderInitial,也就是说每个字段只有一个唯一的RuleBuilderInitial,在RuleBuilderInitial中如果享有多个验证的配置,同样我想也需要一个以验证名为key,RuleBuilderOptions为实例的针对不同验证信息的存储对象。于是我先创建了RuleBuilderOptions class RuleBuilderOptions { constructor() { this.errorMessage = ''; } WithMessage(errorMessage) { this.errorMessage = errorMessage; } } 紧接着在修改了 RuleBuilderInitial class RuleBuilderInitial { constructor() { // T:验证的目标类型 TProperty:验证的属性类型 // 以具体的验证方式作为唯一单位 this.validationSet = new Map(); this.length = undefined; this.must = undefined; this.maximumLength = undefined; this.minimumLength = undefined; this.number = undefined; } /* * 非空 */ NotEmpty() { this.validationSet.set("NotEmpty", new RuleBuilderOptions()); return this.validationSet.get('NotEmpty'); } Length(min, max) { this.validationSet.set("Length", new RuleBuilderOptions()); this.length = { min: min, max: max }; return this.validationSet.get("Length"); } Must(expression) { this.validationSet.set("Must", new RuleBuilderOptions()); this.must = expression; return this.validationSet.get("Must"); } EmailAddress() { this.validationSet.set("EmailAddress", new RuleBuilderOptions()); return this.validationSet.get('EmailAddress'); } MaximumLength(max) { this.validationSet.set("MaximumLength", new RuleBuilderOptions()); this.maximumLength = max; return this.validationSet.get('MaximumLength'); } MinimumLength(min) { this.validationSet.set("MinimumLength", new RuleBuilderOptions()); this.minimumLength = min; return this.validationSet.get('MinimumLength'); } Number(min, max) { this.validationSet.set("Number", new RuleBuilderOptions()); this.number = { min: min, max: max }; return this.validationSet.get("Number"); } } 在调用NotEmpty()之后先将我对该字段非空校验的要求保存到 validationSet 里去,具体措施就是以验证名为key,以一个RuleBuilderOptions实例为value 存储到 validationSet 然后返回这个 RuleBuilderOptions,因为它能让我接着调用WithMessage("") 遗憾的是这样做了之后我发现我调用WithMessage("")后不能再接着调用其他的验证方法了。噢,我想到了,我可以给 RuleBuilderOptions 的构造函数添加一个参数,然后在new RuleBuilderOptions 的时候将this传进去 而new RuleBuilderOptions的地方只有 RuleBuilderInitial,这个this 也就自然而然的成了当前所要验证的那个字段的唯一 RuleBuilderInitial 实例,只要在WithMessage之后将这个this 返回回来一切似乎就大功告成了。 于是美滋滋的把代码改成了这样。 class RuleBuilderOptions { constructor(initial) { this.errorMessage = ''; this.ruleBuilderInitial = initial; } WithMessage(errorMessage) { this.errorMessage = errorMessage; return this.ruleBuilderInitial; } } class RuleBuilderInitial { constructor() { // T:验证的目标类型 TProperty:验证的属性类型 // 以具体的验证方式作为唯一单位 this.validationSet = new Map(); this.length = undefined; this.must = undefined; this.maximumLength = undefined; this.minimumLength = undefined; this.number = undefined; } /* * 非空 */ NotEmpty() { this.validationSet.set("NotEmpty", new RuleBuilderOptions(this)); return this.validationSet.get('NotEmpty'); } Length(min, max) { this.validationSet.set("Length", new RuleBuilderOptions(this)); this.length = { min: min, max: max }; return this.validationSet.get("Length"); } Must(expression) { this.validationSet.set("Must", new RuleBuilderOptions(this)); this.must = expression; return this.validationSet.get("Must"); } EmailAddress() { this.validationSet.set("EmailAddress", new RuleBuilderOptions(this)); return this.validationSet.get('EmailAddress'); } MaximumLength(max) { this.validationSet.set("MaximumLength", new RuleBuilderOptions(this)); this.maximumLength = max; return this.validationSet.get('MaximumLength'); } MinimumLength(min) { this.validationSet.set("MinimumLength", new RuleBuilderOptions(this)); this.minimumLength = min; return this.validationSet.get('MinimumLength'); } Number(min, max) { this.validationSet.set("Number", new RuleBuilderOptions(this)); this.number = { min: min, max: max }; return this.validationSet.get("Number"); } } 至此,我们已经可以这样配置了。 class StudentValidator extends Validator { constructor(typeName) { super(typeName); this.ruleFor("name") .NotEmpty() .WithMessage("名称必填") .MinimumLength(5) .WithMessage("最短长度为5"); this.ruleFor("age") .NotEmpty() .WithMessage("年龄必须") .Number(0, 100) .WithMessage("年龄必须在0-100岁之间"); this.ruleFor("sex") .NotEmpty() .WithMessage("性别必填") .Must(m => !m.sex) .WithMessage("gxq is a man"); } } 以为可以用了?还早呢,我先去吃个饭,晚上回来接着写,(#^.^#)。 由于内容比较多,吃了个饭补充了点能量回来继续撸(* ̄︶ ̄) 上面的内容我们定义了三个对象,Validator、RuleBuilderOptions、RuleBuilderInitial 在使用方面,我们需要定义一个验证器去继承自Validator,在验证器的构造函数里进行配置验证规则。 而RuleBuilderInitial则是字段规则的生成器,换句话说,他里面能配置各种验证规则。 在验证规则生成器对象里又有一个大的容器啊,他能干满呢?他能装关于这个字段的多个验证规则,而每一个验证规则又对应着一个具体的规则选项 RuleBuilderOptions,说直白点就是你这个验证规则如果触发了,如果没通过,则我总要做点相应措施吧,都放到这个RuleBuilderOptions里面的,而我们现在最简单的就放着一个WithMessage方法嘛。 好了,结构已经再次梳理了一遍,但我们还没有编写验证功能啊,接下来我们要完成Validator类里的Validation方法。 在准备编写验证代码的时候,我又想到了,要是验证错了,我该怎么办呢?把错误信息放在那里呢? 因为最初想法是基于Vue的,而Vue是基于数据的,所以最好的办法就是在实体里定义一个error对象,在这里你是否有疑惑?这算什么好的办法,难道我每次使用的时候还要定义一个error属性?哈哈,当然不用,如果真是那样,我就不用写这么多了,因为没必要写,这样用起来简直太蠢了。幸好Javascript的易扩展性使我们轻易完成了这件事,还记得我们的Validator什么样子吗? class Validator { constructor(model) { this.model =new model(); this.fieldValidationSet = new Map(); } ruleFor(field) { //此处表达式返回一个指定字段哈 // 验证配置以字段作为唯一单位,每个字段对应一个初始化器 this.fieldValidationSet.set(field, new RuleBuilderInitial()); return this.fieldValidationSet.get(field); } validation(targetElement) { } } 没错,里面有个this.model,这个model是啥呢?为什么要new 他呢? 首先model是我们要验证的对象,如Student,用前端专业一点的描述就是一个构造函数,那为什么要new 他呢?因为Vue是基于数据的,你验证Student没关系,你只需要 var test = new StudentValidator(Student); 咱们这test.model就是一个Student的实例,用起来就简单多了。 而javascript的扩展性让我们可以轻松的为对象扩展属性,我们为Student添加一个error属性,想想他该存储些什么,首先我认为既然是error,那他一定要存储错误信息。对,没错,在error对象里首先得有这个对象的所有属性,除了error以外的,这是最基本的。 还有他必须具备一个清空验证信息的操作,因为在验证失败的时候我们会在error里记录验证的结果,若第一次name字段验证失败了,error["name"]记录了name的错误消息,第二次name验证成功了,却没更改error["name"]那将是个大bug啊。所以我们需要一个 clearValue方法 其次既然我们在验证,那我们肯定需要知道我们的error对象里面是否存在错误,所以我们还需要一个 exist方法 还有啊,我提前保留了一个code字段,我想将来一定会用上的,这里大家可以猜测哦。想好了就开始行动,经过一通修改, class Validator { constructor(model) { this.model = model.constructor.name === "Function" ? new model() : model; this.model.error = { code: '', clearValue: function () { for (const key in this) { if (this.hasOwnProperty(key) && key != 'clearValue' && key != 'exist') { this[key] = ''; } } }, exist: function () { let exist = false; for (const key in this) { if (this.hasOwnProperty(key) && key != 'clearValue' && key != 'exist') { if (this[key] != '') { exist = true; break; } } } return exist; } }; for (const key in this.model) { if (this.model.hasOwnProperty(key) && key != 'error') { this.model.error[key] = ''; } } this.fieldValidationSet = new Map(); } ruleFor(field) { //此处表达式返回一个指定字段哈 // 验证配置以字段作为唯一单位,每个字段对应一个初始化器 this.fieldValidationSet.set(field, new RuleBuilderInitial()); return this.fieldValidationSet.get(field); } validation(targetElement) { } } 哈哈,正如我之前所分析那样,为this.model定义一个error对象,并添加clearValue 和 exist方法,这里为什么 key != 'clearValue' && key != 'exist'相信已经不用我解释了,因为除了他两其他的字段才算有效字段啊。为什么是key!=‘’ 我认为在错误也是需要引用的,而''在显示中实际上什么也看不见,那就当它没失败咯。所以我们的clearValue也只是简单的将所有字段当然除了上述两位以外全部变为''即可。 现在错误也有了,真的就只差验证功能了。 想一下实现思路,我们之前将验证规则都存起来了,现在实际上我们只需要取出来就行啦。 在验证中首先我们要清空所有错误,以保证干净。 然后在Validator内部有一个名字为 fieldValidationSet 的键值对对象,他存储了所有属性的配置规则生成器,遍历这个生成器我们将能取出每个属性的验证规则。 然后对这个字段的验证规则进行筛选咯。如果验证失败的话,将这个验证规则从集合里取出来,他的Value就应该是一个 RuleBuilderOptions 然后取出errorMessage就是它验证失败后的显示消息,赋给谁呢?当然赋给this.model.error["字段名"]了。 思路有了接下来就照着去做了呗。于是Validator成了这个样子。 class Validator { constructor(model) { this.model = new model(); this.model.error = { code: '', clearValue: function () { for (const key in this) { if (this.hasOwnProperty(key) && key !== 'clearValue' && key !== 'exist') { this[key] = ''; } } }, exist: function () { let exist = false; for (const key in this) { if (this.hasOwnProperty(key) && key !== 'clearValue' && key !== 'exist') { if (this[key] !== '') { exist = true; break; } } } return exist; } }; for (const key in this.model) { if (this.model.hasOwnProperty(key) && key !== 'error') { this.model.error[key] = ''; } } this.fieldValidationSet = new Map(); } ruleFor(field) { //此处表达式返回一个指定字段哈 // 验证配置以字段作为唯一单位,每个字段对应一个初始化器 this.fieldValidationSet.set(field, new RuleBuilderInitial()); return this.fieldValidationSet.get(field); } validation(targetElement) { this.model.error.clearValue(); for (const [field, initial] of this.fieldValidationSet) { const property = this.model[field]; for (const [config, options] of initial.validationSet) { switch (config) { case "NotEmpty": if (!property) { this.model.error[field] = initial.validationSet.get("NotEmpty").errorMessage; } break; case "MinimumLength": if (property && initial.minimumLength) { if (property.length < initial.minimumLength) { this.model.error[field] = initial.validationSet.get("MinimumLength").errorMessage; } } break; case "MaximumLength": if (property && initial.maximumLength) { if (property.length > initial.minimumLength) { this.model.error[field] = initial.validationSet.get("MaximumLength").errorMessage; } } break; case "Length": if (property && initial.length) { if (property.length > initial.length["max"] || property.length < initial.length["min"]) { this.model.error[field] = initial.validationSet.get("Length").errorMessage; } } break; case "Must": if (property && initial.must && this.model) { if (initial.must(this.model)) { console.log(field); this.model.error[field] = initial.validationSet.get("Must").errorMessage; } } break; case "Number": let propertyNum = Number(property); if (property && !isNaN(propertyNum) && initial.number) { if (propertyNum > initial.number.max || propertyNum < initial.number.min) { this.model.error[field] = initial.validationSet.get("Number").errorMessage; } } else { this.model.error[field] = "必须是Number类型"; } break; } } } if (!this.model.error.exist()) { //没有错误 this.model.error.code = -2; //通过验证 targetElement.click(); // 重新触发submit } } passValidation() { return this.model.error.code === -2 ? true : false; } } 相信大家已经看到了那个code的作用了对吧。是的,他变成了一个内置的标识。等会大家就知道他的用途啦。 到这里验证器算是写完了。 现在大家来一起感受一下我们自己做的这个验证器吧。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <script type="text/javascript" src="vue.js"></script> <script src="validator.js"></script> </head> <body> <div id="box"> <form action="https://www.baidu.com"> <input type="text" v-model="model.name"> <span>{{model.error.name}}</span> <input type="text" v-model="model.age"> <span>{{model.error.age}}</span> <input type="checkbox" v-model="model.sex"> <span>{{model.error.sex}}</span> <input type="submit" value="提交" @click="submit({ev:$event})"> </form> </div> <script> class Student { constructor() { this.name = undefined; this.age = undefined; this.sex = undefined; } } // 自定义验证 class StudentValidator extends Validator { constructor(typeName) { super(typeName); this.ruleFor("name") .NotEmpty() .WithMessage("名称必填") .MinimumLength(5) .WithMessage("最短长度为5"); this.ruleFor("age") .NotEmpty() .WithMessage("年龄必须") .Number(0, 100) .WithMessage("年龄必须在0-100岁之间"); this.ruleFor("sex") .NotEmpty() .WithMessage("性别必填") .Must(m => !m.sex) .WithMessage("gxq is a man"); } } let vm = new Vue({ el: '#box', data: { validator: new StudentValidator(Student), }, methods: { submit: function ({ ev }) { if (this.validator.passValidation()) {return; } ev.preventDefault(); this.validator.validation(ev.target); } }, computed: { model: { get: function () { return this.validator.model; } } } }); </script> </body> </html> 在使用上其实就比较简单了,在Vue中添加一个计算属性model 返回return this.validator.model。实质上就是我们验证器内置的需要验证的对象。当然你可以不用添加计算属性,直接this.validator.model也行,只是我想这样太长了。 在submit的时候绑定click事件,里面有个事件对象,首先先调用validator.passValidation 判断是否通过验证,实际上啊,内部就是判断code是不是等于-2 为什么等于-2呢?因为我们在调用Validation的最后 会判断error里面是否exist,如果不存在值则表明验证通过,没有错误消息,这个时候就会将code改为-2。看官也许懵了,你一来就判断 那肯定不等于-2啊,确实如此啊。所以一来肯定会执行ev.preventDefault() 来阻止提交,既然阻止提交了,那我要是验证通过了怎么办?那不也不会提交了吗?关键参数就在这个validation的参数上,之前有解释参数是目标对象,实际上就是触发了submit的对象,很显然在这里就是那个<input type="submit" />的按钮,通过点击事件的ev.target就能获取到这个对象,我将它传给了validation 在validation内部,如果验证通过了,既error对象里不存在有值的话不仅会将code改为-2 还会调用targetElement的click方法 来再一次触发这个按钮得点击事件,所以在第二次触发的时候,他就通过并提及啦。 if (!this.model.error.exist()) { //没有错误 this.model.error.code = -2; //通过验证 targetElement.click(); // 重新触发submit } 前面在Input上绑定的model.name model.error["name"]我就不过多做解释了哈,原因全在上面代码里,(#^.^#)。接下来我们让他跑起来。 接下来来一张正确的时候 将111改为11,提交。 去百度了,因为我们在from的action设置的https://www.baidu.com。 到这基本算告一段落啦。但是聪明的你肯定发现了问题,这用起来确实方便了,也和之前后端验证有点模子了,但这毕竟是javascript啊 难道我从还得先定义一个Student 才能使用,万一服务端给我返回json数据呢?那怎么办,没关系我们来改改就是啦。看我们new StudentValidator 传递的是一个Student的构造函数进去,实际上我们知道在内部我们将new 了这个参数,创建了这个对象赋值给this.model,那我们直接传递个对象给他不就行了?干嘛要用这个Student啊,说改就改, 于是我通过判断传进来的model参数的构造函数名是否是“Function” 来判断model是构造函数还是一个对象,如果是对象的话直接赋值给this.model 如果是构造函数的话依旧new 出来。 这里model.constructor指向的是model的对象名。 比如var stu = new Student(); stu.constructor 指向的就是Student这个构造函数 甚至我们 var stu1 = new stu.constructor() 跟 var stu1 = new Student()效果是一模一样的。 而任何对象都默认继承自Function,啥意思呢就是我们的Student 其实是 Function对象的实例罢了,不仅他是就连我们Javascript中的所有内置对象都是,比如NUmber等。这里如果前端基础不太好的同学就不必太纠结啦。 总之经过改良我们现在在使用上已经可以直接传递一个Json对象啦。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <script type="text/javascript" src="vue.js"></script> <script src="validator.js"></script> </head> <body> <div id="box"> <form action="https://www.baidu.com"> <div> <input type="text" v-model="model.name"> <span>{{model.error.name}}</span> </div> <div> <input type="text" v-model="model.age"> <span>{{model.error.age}}</span> </div> <div> <input type="checkbox" v-model="model.sex"> <span>{{model.error.sex}}</span> </div> <input type="submit" value="提交" @click="submit({ev:$event})"> </form> </div> <script> // 自定义验证 class StudentValidator extends Validator { constructor(typeName) { super(typeName); this.ruleFor("name") .NotEmpty() .WithMessage("名称必填") .MinimumLength(5) .WithMessage("最短长度为5"); this.ruleFor("age") .NotEmpty() .WithMessage("年龄必须") .Number(0, 100) .WithMessage("年龄必须在0-100岁之间"); this.ruleFor("sex") .NotEmpty() .WithMessage("性别必填") .Must(m => !m.sex) .WithMessage("gxq is a man"); } } let vm = new Vue({ el: '#box', data: { validator: new StudentValidator({ name: undefined, age: undefined, sex: undefined }), }, methods: { submit: function ({ ev }) { if (this.validator.passValidation()) { return; } ev.preventDefault(); this.validator.validation(ev.target); } }, computed: { model: { get: function () { return this.validator.model; } } } }); </script> </body> </html> 像这样我们的需要验证的对象可以是一个json了,使用起来,是不是方便多啦? 但有的同学发现了,你这个使用起来还需要定义一个StudentValidator,这多麻烦啊,或者我对class不太熟悉,我想直接使用行不行? 既然有这样的需求,那我们就来分析实现的可行性和思路。 首先我们看到StudentValidator 继承自 Validator,在构造方法里,他实际上仅做了配置的事,配置工作调用的还是Validator的方法。 所以,其实我们要不要StudentValidator都没关系啊,我们可以直接new 一个Validator 然后在去调用他的 ruleFor 等一系列方法来完成嘛。 但这样的话又需要var vali = new Validator了,而且还要一个一个去调用,vali.ruleFor("name").NotEmpty().WithMessage(""); vall.ruleFor("age").Number().WithMessage("")。这也太麻烦了。 其实啊,我们可以传一个回调函数进去,在这个回调函数里进行配置,但回调函数里无法调用Validator的方法啊,那我们就定义一个参数,我们希望通过this.ruleFor,但this 是关键字无法作为参数名,那我们就叫than吧。 经过改造,我们把Validator的constructor改成了这样 constructor({ model, rule = undefined }) { this.fieldValidationSet = new Map(); if (rule.constructor.name === "Function") { rule(this); } this.model = model.constructor.name === "Function" ? new model() : model; this.model.error = { code: '', clearValue: function () { for (const key in this) { if (this.hasOwnProperty(key) && key !== 'clearValue' && key !== 'exist') { this[key] = ''; } } }, exist: function () { let exist = false; for (const key in this) { if (this.hasOwnProperty(key) && key !== 'clearValue' && key !== 'exist') { if (this[key] !== '') { exist = true; break; } } } return exist; } }; for (const key in this.model) { if (this.model.hasOwnProperty(key) && key !== 'error') { this.model.error[key] = ''; } } } 很简单,在Validator里把当前this 作为参数传给回调函数,这样,我们在配置的时候就能在回调函数中使用这个"this"了。经过改进我们的页面变成什么样了呢? <script> let vm = new Vue({ el: '#box', data: { validator: new Validator({ model: { name: undefined, age: undefined, sex: undefined }, rule: function (than) { than.ruleFor("name") .NotEmpty() .WithMessage("名称必填") .MinimumLength(5) .WithMessage("最短长度为5"); than.ruleFor("age") .NotEmpty() .WithMessage("年龄必须") .Number(0, 100) .WithMessage("必须在0-100岁之间"); } }), }, methods: { submit: function ({ ev }) { if (this.validator.passValidation()) { return; } ev.preventDefault(); this.validator.validation(ev.target); } }, computed: { model: { get: function () { return this.validator.model; } } } }); </script> 这样在一看是不是简单了许多啊?
上一章我们在控制台中基本的了解了FluentValidation是如何简洁,优雅的完成了对实体的验证工作,今天我们将在实战项目中去应用它。 首先我们创建一个ASP.NET MVC项目,本人环境是VS2017, 创建成功后通过在Nuget中使用 Install-Package FluentValidation -Version 7.6.104 安装FluentValidation 在Model文件夹中添加两个实体Address 和 Person public class Address { public string Home { get; set; } public string Phone { get; set; } } public class Person { /// <summary> /// 姓名 /// </summary> public string Name { get; set; } /// <summary> /// 年龄 /// </summary> public int Age { get; set; } /// <summary> /// 性别 /// </summary> public bool Sex { get; set; } /// <summary> /// 地址 /// </summary> public Address Address { get; set; } } 紧接着创建实体的验证器 public class AddressValidator : AbstractValidator<Address> { public AddressValidator() { this.RuleFor(m => m.Home) .NotEmpty() .WithMessage("家庭住址不能为空"); this.RuleFor(m => m.Phone) .NotEmpty() .WithMessage("手机号码不能为空"); } } public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { this.RuleFor(p => p.Name) .NotEmpty() .WithMessage("姓名不能为空"); this.RuleFor(p => p.Age) .NotEmpty() .WithMessage("年龄不能为空"); this.RuleFor(p => p.Address) .SetValidator(new AddressValidator()); } } 为了更好的管理验证器,我建议将使用一个Manager者来管理所有验证器的实例。如ValidatorHub public class ValidatorHub { public AddressValidator AddressValidator { get; set; } = new AddressValidator(); public PersonValidator PersonValidator { get; set; } = new PersonValidator(); } 现在我们需要创建一个页面,在默认的HomeController 控制器下添加2个Action:ValidatorTest,他们一个用于展示页面,另一个则用于提交。 [HttpGet] public ActionResult ValidatorTest() { return View(); } [HttpPost] public ActionResult ValidatorTest(Person model) { return View(); } 为 ValidatorTest 添加视图,选择Create模板,实体为Person 将默认的@Html全部删掉,因为在我们本次介绍中不需要,我们的目标是搭建一个前后端分离的项目,而不要过多的依赖于MVC。 最终我们将表单改写成了 @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div class="form-horizontal"> <h4>Person</h4> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div class="form-group"> <label for="Name" class="control-label col-md-2">姓名</label> <div class="col-md-10"> <input type="text" name="Name" class="form-control" /> </div> </div> <div class="form-group"> <label for="Age" class="control-label col-md-2">年龄</label> <div class="col-md-10"> <input type="text" name="Age" class="form-control" /> </div> </div> <div class="form-group"> <label for="Home" class="control-label col-md-2">住址</label> <div class="col-md-10"> <input type="text" name="Address.Home" class="form-control" /> </div> </div> <div class="form-group"> <label for="Phone" class="control-label col-md-2">电话</label> <div class="col-md-10"> <input type="text" name="Address.Phone" class="form-control" /> </div> </div> <div class="form-group"> <label for="Sex" class="control-label col-md-2">性别</label> <div class="col-md-10"> <div class="checkbox"> <input type="checkbox" name="Sex" /> </div> </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <input type="submit" value="Create" class="btn btn-default" /> </div> </div> </div> } 注意,由于我们的实体Person中存在复杂类型Address,我们都知道,表单提交默认是Key:Value形式,而在传统表单的key:value中,我们无法实现让key为Address的情况下Value为一个复杂对象,因为input一次只能承载一个值,且必须是字符串。实际上MVC中存在模型绑定,此处不作过多介绍(因为我也忘记了-_-||)。 简单的说就是他能根据你所需要类型帮我们自动尽可能的转换,我们目前只要知道如何正确使用,在Address中存在Home属性和Phone属性,我们可以将表单的name设置为Address.Home,MVC的模型绑定会将Address.Home解析到对象Address上的Home属性去。 简单的校验方式我也不过多介绍了。再上一章我们已经了解到通过创建一个实体的验证器来对实体进行验证,然后通过IsValid属性判断是否验证成功。对,没错,对于大家来说这太简单了。但我们每次校验都创建一个验证器是否显得有点麻烦呢?不要忘了我们刚刚创建了一个ValidatorHub,我们知道控制器默认继承自Controller,如果我们想为控制器扩展一些能力呢?现在我要创建一个ControllerEx了,并继承自Controller。 public class ControllerEx : Controller { protected Dictionary<string, string> DicError { get; set; } = new Dictionary<string, string>(); protected ValidatorHub ValidatorHub { get; set; } = new ValidatorHub(); protected override void OnActionExecuted(ActionExecutedContext filterContext) { base.OnActionExecuted(filterContext); ViewData["Error"] = DicError; } protected void ValidatorErrorHandler(ValidationResult result) { foreach (var failure in result.Errors) { if (!this.DicError.ContainsKey(failure.PropertyName)) { this.DicError.Add(failure.PropertyName, failure.ErrorMessage); } } } } 在ControllerEx里我创建了一个ValidatorHub属性,正如其名,他内部存放着各种验证器实体呢。有了它,我们可以在需要验证的Action中通过this.ValidatorHub.具体验证器就能完成具体验证工作了,而不需要再去每次new 一个验证器。 同样我定义了一个DicError的键值对集合,他的键和值类型都是string。key是验证失败的属性名,而value则是验证失败后的错误消息,它是用来存在验证的结果的。 在这里我还定义了一个ValidatorErrorHandler的方法,他有一个参数是验证结果,通过名称我们大致已经猜到功能了,验证错误的处理,对验证结果的错误信息进行遍历,并将错误信息添加至DicError集合。 最终我需要将这个DicError传递给View,简单的办法是ViewData["Error"] 但我不想在每个页面都去这么干,因为这使我要重复多次写这行代码,我会厌倦它的。很棒的是MVC框架为我们提供了Filter(有的地方也称函数钩子,切面编程,过滤器),能够方便我们在生命周期的不同阶段进行控制,很显然,我的需求是在每次执行完Action后要在末尾添加一句ViewData["Error"]=DicError。于是我重写了OnActionExecuted方法,仅添加了 ViewData["Error"] = DicError; 现在我只需要将HomeController继承自ControllerEx即可享受以上所有功能了。 现在基本工作基本都完成了,但我们还忽略了一个问题,我错误是存在了ViewData["Error"]里传递给View,只不过难道我们在验证错误的时候在页面显示一个错误列表?像li一样?这显然不是我们想要的。我们还需要一个帮助我们合理的显示错误信息的函数。在Razor里我们可以对HtmlHelper进行扩展。于是我为HtmlHelper扩展了一个方法ValidatorMessageFor public static class ValidatorHelper { public static MvcHtmlString ValidatorMessageFor(this HtmlHelper htmlHelper, string property, object error) { var dicError = error as Dictionary<string, string>; if (dicError == null) //没有错误 { // 不会等于空 } else { if (dicError.ContainsKey(property)) { return new MvcHtmlString(string.Format("<p>{0}</p>", dicError[property])); } } return new MvcHtmlString(""); } } 在ValidatorMessaegFor里需要2个参数property 和 error 前者是需要显示的错误属性名,后者则是错误对象即ViewData["Error"],功能很简单,在发现错误对象里存在key为错误属性名的时候将value用一个p标签包裹起来返回,value即为错误属性所对应的错误提示消息。 现在我们还需要在View每一个input下添加一句如: @Html.ValidatorMessageFor("Name", ViewData["Error"])即可。 @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div class="form-horizontal"> <h4>Person</h4> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div class="form-group"> <label for="Name" class="control-label col-md-2">姓名</label> <div class="col-md-10"> <input type="text" name="Name" class="form-control" /> @Html.ValidatorMessageFor("Name", ViewData["Error"]) </div> </div> <div class="form-group"> <label for="Age" class="control-label col-md-2">年龄</label> <div class="col-md-10"> <input type="text" name="Age" class="form-control" /> @Html.ValidatorMessageFor("Name", ViewData["Error"]) </div> </div> <div class="form-group"> <label for="Home" class="control-label col-md-2">住址</label> <div class="col-md-10"> <input type="text" name="Address.Home" class="form-control" /> @Html.ValidatorMessageFor("Address.Home", ViewData["Error"]) </div> </div> <div class="form-group"> <label for="Phone" class="control-label col-md-2">电话</label> <div class="col-md-10"> <input type="text" name="Address.Phone" class="form-control" /> @Html.ValidatorMessageFor("Address.Phone", ViewData["Error"]) </div> </div> <div class="form-group"> <label for="Sex" class="control-label col-md-2">性别</label> <div class="col-md-10"> <div class="checkbox"> <input type="checkbox" name="Sex" /> </div> </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <input type="submit" value="Create" class="btn btn-default" /> </div> </div> </div> } 到此我们的所有基本工作都已完成 [HttpPost] public ActionResult ValidatorTest(Person model) { var result = this.ValidatorHub.PersonValidator.Validate(model); if (result.IsValid) { return Redirect("https://www.baidu.com"); } else { this.ValidatorErrorHandler(result); } return View(); } 通过我们在ControllerEx种的ValidatorHub来对实体Person进行校验,如果校验成功了....这里没啥可干的就当跳转一下表示咯,否则的话调用Ex中的ValidatorErrorHandler 将错误消息绑定到ViewData["Error"]中去,这样就能在前端View渲染的时候将错误消息显示出来了。 接下来我们将程序跑起来。 正如大家所看到的,当我点击提交的时候 虽然只有电话没输入但其他三个表单被清空了,也许我们会觉得不爽,当然如果你需要那相信你在看完上述的错误信息绑定后一定也能解决这个问题的,但事实上,我们并不需要它,\(^o^)/~ 为什么呢?因为我们还要前端验证啊,当前端验证没通过的时候根本无法发送到后端来,所以不用担心用户在一部分验证失败时已填写的表单数据被清空掉。 这里提到在表单提交时需要前端校验,既然有前端校验了为何还要我们做后台校验呢?不是脱了裤子放屁吗?事实上,前端校验的作用在于优化用户体验,减轻服务器压力,也可以防住君子,但绝不能防止小人,由于Web客户端的不确定性,任何东西都可以模拟的。如果不做服务端验证,假如你的系统涉及金钱,也许那天你醒来就发现自己破产了。 来一个通过验证的。
int? 这种类型实际上是Nullable<int>类型的实例,这里不过多介绍Nullable,只说明一点它在int的基础上可存储了null值,有时候在数据库操作时,我们会创建一个用于封装所需参数的类Model,若数据库中某个Int类型的字段可为空,为了保证与数据库同步,我们会在Model里给该字段定义为int?类型,但在查询取出来的时候就出现问题了,如果数据库中是空,reader["xxx"] 返回的是object类型,而我们要转为int类型只能Convert.ToInt32(reader["xxx"]); 但这时候reader["xxx"]的值为{} Dbnull.Value 空的意思。空是无法转换成Int32的 会抛出异常的 但是有没有方法能狗Convert.ToInt32?,使用int32?强转同样会抛异常,这时怎么办呢?? ?: 都会告诉你两侧类型不一致导致无法使用。 1 public static object ChanageType(this object value, Type convertsionType) 2 { 3 //判断convertsionType类型是否为泛型,因为nullable是泛型类, 4 if (convertsionType.IsGenericType && 5 //判断convertsionType是否为nullable泛型类 6 convertsionType.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) 7 { 8 if (value == null || value.ToString().Length == 0) 9 { 10 return null; 11 } 12 13 //如果convertsionType为nullable类,声明一个NullableConverter类,该类提供从Nullable类到基础基元类型的转换 14 NullableConverter nullableConverter = new NullableConverter(convertsionType); 15 //将convertsionType转换为nullable对的基础基元类型 16 convertsionType = nullableConverter.UnderlyingType; 17 } 18 return Convert.ChangeType(value, convertsionType); 19 }
都属于用户控件,Action是直接连接到Action,并且会执行业务逻辑,通过源代码分析可以看出Action最终转换为HTML字符串输出了。并且通过断点调试可以发现Action和RenderAction可进行业务逻辑处理。其中由于RenderAction是写入到流里不返回,所以需要使用@{}方式调用。 1 public static MvcHtmlString Partial(this HtmlHelper htmlHelper, string partialViewName, object model, ViewDataDictionary viewData) 2 { 3 using (StringWriter writer = new StringWriter(CultureInfo.CurrentCulture)) 4 { 5 htmlHelper.RenderPartialInternal(partialViewName, viewData, model, writer, ViewEngines.Engines); 6 return MvcHtmlString.Create(writer.ToString()); 7 } 8 }而Partial、RenderPartial和Action、RenderAction极其相似,不同的是Partial和RenderPartial是直接显示分布页面信息,不进行业务逻辑处理。即不会像Action那样命中断点。
javascript 元素对象拥有client家族主要属性: clientHeight: (可见区域高度:height+padding) clientWidth: (可见区域宽度:width+padding) clientLeft (资料显示是当前元素的offsetLeft距离当前窗口左边的距离,但通过我的测试发现值永远等于border-left,并且在w3c的DOM Element上找不到clientLeft相关信息) clientTop (资料显示是当前元素的offsetLeft距离当前窗口左边的距离,但通过我的测试发现值永远等于border-top,并且在w3c的DOM Element上找不到clientTop相关信息) client可通过元素对象的属性clientWidth和clientHeight找到它。 但整个浏览器的滚动条即document的滚动条元素归属存在兼容性问题。 IE678以及其他浏览器认为整个文档对象属于document.documentElement元素的即html。 未声明DTD <!DOCTYPE html> 的浏览器认为文档对象属于document.body元素的。 所以只要找正确元素即可实现浏览器兼容。if(document.CompatMode==CSS1Compat)表示支持document.documentElement元素,else 表示支持document.body元素。 最新浏览器IE9+及其他浏览器都认为整个文档属于Window对象的,可通过Window.innerWidth和Window.innerHeight获得 注:参考网址http://www.w3school.com.cn/jsref/dom_obj_all.asp
javascript 元素对象拥有scroll家族主要属性: ScrollTop: (被滚动条卷去的头部高度) ScrollLeft: (被滚动条卷曲的左侧距离) ScrollWidth (内容实际宽度:width+padding+超出盒子的宽度) ScorllHeight (内容实际高度:height+padding+超出盒子的高度) Scroll可通过元素对象的属性ScrollTop和ScrollLeft找到它。 但整个浏览器的滚动条即document的滚动条元素归属存在兼容性问题。 IE678以及其他浏览器认为整个文档对象属于document.documentElement元素的即html。 未声明DTD <!DOCTYPE html> 的浏览器认为文档对象属于document.body元素的。 所以只要找正确元素即可实现浏览器兼容。if(document.CompatMode==CSS1Compat)表示支持document.documentElement元素,else 表示支持document.body元素。 最新浏览器IE9+及其他浏览器都认为整个文档属于Window对象的,可通过Window.pageXOffset和Window.pageYOffset获得,也可以通过Window.scrollTo(x,y)将浏览器滚动条滑动到指定x,y 封装 scroll函数 1 function scroll(element, vals) { 2 if (element == null) { 3 return; 4 } 5 if (vals == null) { 6 7 return { 8 left: element.scrollLeft, 9 top: element.scrollTop 10 } 11 12 } else { 13 element.scrollTop = vals.top; 14 element.scrollLeft = vals.left; 15 } 16 } scroll
event是javascript 元素对象的事件处理程序function(event){}所拥有的事件对象其中最主要(用的最多)的属性是 event对象存在兼容性。IE678的event对象为Window.event,其他浏览器为event,兼容写法为var event = event || window.event; clientX (距离浏览器左侧的距离) clientY (距离浏览器顶部的距离) pageX (距离文档——document左侧的距离) pageY (距离文档——document顶部的距离) screenX (距离客户端屏幕左侧的距离) screenY (距离客户端屏幕顶部的距离) 当然event还有一些其他属性如altkey(Alt键是否被按下)等属性,详情请查看http://www.w3school.com.cn/jsref/dom_obj_event.asp
javascript 元素对象拥有offset家族5大属性(offset家族和position紧密相连) offsetWidth:"元素内容的宽度" (border+padding+width) offsetHeight:"元素内容的高度" (border+padding+height) offsetLeft:"元素与浏览器客户端左侧的距离" (与其父级层级最近的定位元素左侧距离) offsetTop:"元素与浏览器客户端顶侧的距离" (与其父级层级最近的定位元素顶侧距离) offsetParent:"元素父级元素" (得到其父级层级最近的定位元素对象) 运行下列代码尝试一下 1 <!DOCTYPE html> 2 <html lang="en"> 3 4 <head> 5 <meta charset="UTF-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <meta http-equiv="X-UA-Compatible" content="ie=edge"> 8 <title>offset</title> 9 <style> 10 * { 11 padding: 0; 12 margin: 0; 13 } 14 15 .box { 16 border: 2px solid #caa; 17 padding: 1px; 18 width: 200px; 19 height: 200px; 20 background-color: yellow; 21 } 22 23 .bigbox { 24 margin: 100px; 25 width: 200px; 26 height: 200px; 27 position: relative; 28 padding: 10px; 29 border: 1px solid red; 30 } 31 32 .mask { 33 position: fixed; 34 right: 0; 35 bottom: 0; 36 width: 200px; 37 height: 200px; 38 background-color: red; 39 } 40 </style> 41 </head> 42 43 <body> 44 <div id="j_mask" class="mask"></div> 45 <div id="j_bigbox" class="bigbox"> 46 <div id="j_box" class="box"></div> 47 </div> 48 <button id="j_btnoffset">offset</button> 49 <button id="j_btnoffsetParent">offsetParent</button> 50 </body> 51 52 </html> 53 <script> 54 (function () { 55 var j_btnoffset = document.getElementById("j_btnoffset"), 56 j_box = document.getElementById("j_box"), 57 j_bigbox = document.getElementById("j_bigbox"), 58 j_mask = document.getElementById("j_mask"), 59 j_btnoffsetParent = document.getElementById("j_btnoffsetParent"); 60 j_btnoffset.onclick = function () { 61 var strHtml = "<p>"; 62 63 strHtml += "offsetWidth : " + j_box.offsetWidth + ""; 64 strHtml += "</p>" 65 strHtml += "<p>"; 66 strHtml += "offsetHeight : " + j_box.offsetHeight + ""; 67 strHtml += "</p>"; 68 strHtml += "<p>"; 69 strHtml += "offsetLeft : " + j_box.offsetLeft + ""; 70 strHtml += "</p>"; 71 strHtml += "<p>"; 72 strHtml += "offsetTop : " + j_box.offsetTop + ""; 73 strHtml += "</p>"; 74 75 j_mask.innerHTML = strHtml; 76 }; 77 j_btnoffsetParent.onclick = function () { 78 console.log(j_box.offsetParent); 79 } 80 }()); 81 82 </script> 注:当父级到body时,则以body为参照物
JSON(JavaScript Object Notation)是一中轻量级得JavaScript的对象表示法 使用JSON的方法 var json = { name:"张三", age:"18", sex"男" }; console.log(json.name) console.log(json.age) console.log(json.sex) 通过 var json = { key : value };可定义jsonjson.key;可拿到该json对象key对象的value值。json主要用于数据交换、配置表单等
曾经我一直认为Web服务器的Api使用ashx或ASP.NET MVC中返回JsonResult来实现的。 当我第一次接触WCF的时候,有同学告诉我目前比较流行WebApi和WebSocket了,于是我还担心着我怎么总在学不咋用了的技术哟。 今天第一次使用WebApi 具体步骤: 1、首先我创建了一个ASP.NET的空项目 2、在根目录创建了Controllers和Models文件夹 3、在Models文件夹下创建仓储(Storages.cs)、人(Person.cs)、学生(Student)、教师(Teacher)类并模拟了Student数据 如下: public static class Storages { public static IEnumerable<Student> Students { get; set; } public static IEnumerable<Teacher> Teachers { get; set; } static Storages() { Students = new List<Student>() { new Student(){ Id=1, Age=18, Gender=false, Name="Gxq" }, new Student(){ Id=2, Age=18, Gender=false, Name="Gxq2" }, new Student(){ Id=3, Age=18, Gender=false, Name="Gxq3" }, new Student(){ Id=4, Age=18, Gender=false, Name="Gxq4" } }; Teachers = new List<Teacher>(); } } public class Person { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public bool Gender { get; set; } } public class Student : Person { } public class Teacher : Person { } 4、在Controllers文件夹下创建StudentsController.cs(学生)和TeachersController.cs(教师)控制器 public class StudentsController : ApiController { public IEnumerable<Student> Get() { return Storages.Students; } public Student Get(string item) { return Storages.Students.FirstOrDefault(u => u.Name == item); } public void Post(Student entity) { IList<Student> list = Storages.Students as IList<Student>; entity.Id = list.Max(s => s.Id) + 1; list.Add(entity); } public void Put([FromUri]string item, [FromBody] Student entity) { Delete(item); Post(entity); } public void Delete([FromUri]string item) { var entity = getAdmin(item); IList<Student> list = Storages.Students as IList<Student>; list.Remove(entity); } } public class TeachersController : ApiController { } 5、新建Global.asax文件配置WebApi路由 1 public class Global : System.Web.HttpApplication 2 { 3 4 protected void Application_Start(object sender, EventArgs e) 5 { 6 GlobalConfiguration.Configuration.Routes.MapHttpRoute( 7 "default_api", 8 "{controller}/{item}", 9 new 10 { 11 item = RouteParameter.Optional 12 }); 13 } 14 } 6、现在就完成了WebApi的CRUD,但似乎遇到了些问题: 1、我不知道怎样去判断调用那个具体的方法,通过请求方式与方法名对应的匹配吗? 2、我在路由规则改为{Controller}/{Action}/{Item}后将Get方法名改为Admin后显示的调用Admin它提示我没有提供Get的方法,但我将Get改为GetAdmin后、显示的输入GetAdmin即可找到对应方法,难道必须带有Get标识吗 3、其他问题正在学习研究中...以上学习的代码有参考传智播客的.NET视频