大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。
当前系列: ASP.NET 修改讲义

复习:JQuery Validation

后端永远不要相信前端。


所以ASP.NET内置了Model验证(Validation)模块,验证用户的输入是否符合要求。

三部曲

以“必填(Required)”为例,演示:Name不能为空

添加特性

首先在属性上添加ASP.NET内置的特性(Annotation)

[Required(ErrorMessage = "* 必须填写")]
public string UserName { get; set; }

RequiredAttribute和我们之前使用的DataType、Display等属于同一个Assembly:System.ComponentModel.Annotations.dll

[Display(Name = "自我介绍")]  //为label填充文本
public string SelfDescription { get; set; }

注意:ErrorMessage是错误提示信息,不填写的话用默认的英语提示。

检查验证

在Post的Action方法中检查ModelState:
if (!ModelState.IsValid)
{
    return View(student);
}

ModelState中包含两种错误信息:

  • 绑定时的异常,比如类型转换错误
  • Validation失败

错误提示

razor view上添加错误提示的HtmlHelper

@Html.ValidationMessageFor(m => m.Name)

不要忘了这个ValidationMessageFor标签,否则错误信息(ErrorMessage)不会呈现。


其他常用验证

字符串长度

[MaxLength(8, ErrorMessage = "* 最大长度不能超过8")]
[MinLength(4, ErrorMessage = "* 最小长度不能少于4")]
有时候我们需要同时限制最大最小,可以使用:
[StringLength(8, MinimumLength = 4)]

或者限定为一个固定长度:

[StringLength(4, MinimumLength = 4)]

数值范围

[Range(1950, 2020)]
同时限制了非数字的输入,即所有非数字输入都会因此而报错。

格式

[Url]
[EmailAddress]
[RegularExpression("[0-9]*")]

不同验证特性可以累加,但只要一个没通过,后面的就不再进一步验证。

compare

使用DataAnnotations(System.Web.Mvc)中的CompareAttribute:

using CompareAttribute = System.ComponentModel.DataAnnotations.CompareAttribute;
必须指定要和哪一个属性进行比较:
//[Compare("Name")]
[Compare(nameof(Name))]   //更优雅的写法
public string ConfirmName { get; set; }

AllowEmptyStrings 

在Required中有一个属性:AllowEmptyStrings,默认为false,即空格输入也被视为没有输入。

可以将其改为true,这样用户的纯空格输入就会视为有输入内容

[Required(ErrorMessage = "* 姓名必填", AllowEmptyStrings = true)]

PS:早期的一些ASP.NET版本,当AllowEmptyStrings = true 时还需要配合 

[DisplayFormat(ConvertEmptyStringToNull = false)]

ErrorMessage

除了可以按属性一条一条的显示错误消息,还可以利用

@Html.ValidationSummary()

把所有的ErrorMessages集中显示。

演示:要求radio也被选中

[Required(ErrorMessage = "* 至少选一个")]
public bool? IsMale { get; set; }    //注意这个“可空”
    <label>
        @Html.RadioButtonFor(m => m.IsMale, true) 男
    </label>
    <label>
        @Html.RadioButtonFor(m => m.IsMale, false) 女
    </label>

再引入:

@Html.ValidationSummary()

样式

查看生产的span
<span class="field-validation-error" data-valmsg-for="NewUser.Name" data-valmsg-replace="true">* 用户名不能为空</span>
它带有一个field-validation-error,可以通过定义其CSS:
    .field-validation-error {
        color: red;
        font-size: 12px;
    }

有时候我们还有更多的想法,比如加一个图标。

注意不能在ErrorMessage中直接添加图标内容:

[Required(ErrorMessage = "<span class='fa fa-icon'></span> 姓名必填")]

ASP.NET会将HTML内容“编码”后输出,使其不会被按HTML解析:(复习HTML转义字符

&lt;span class=&#39;fa fa-icon&#39;>&lt;/span>
而是应该按照图标生成的CSS原理(字体和伪类,复习)添加样式:
    .field-validation-error {
        font-family: "Glyphicons Halflings";
    }

        .field-validation-error::before {
            content: "\e107";
        }
演示:先生成一个图标,再复制其CSS样式


AddModelError()

有时候我们的验证规则不能由内置的Attribute完成。比如:用户名是否重复,需要查询数据库之后才能确认。

这时候就需要使用ModelState.AddModelError()方法:

if (true)//通过数据库的查询,发现student.Name重复了
{
    //ModelState.AddModelError("Name", "* 用户名重复");
    //ModelState.AddModelError(nameof(student.Name), "* 用户名重复");
    
    return View(student);
}

注意AddModelError()的第二个参数为ErrorMessage,

第一个参数可以是:

  • Model的属性名,这时候该错误就和该属性“关联/绑定”,类似于在该属性上添加特性实现验证,被称之为属性级(Property-Level)错误
  • 空,这时候该错误就属于Model级(Model-Level)错误:
    ModelState.AddModelError("", "* 用户名重复");
    只有在Summary中显示。ValidationSummary还可以排除掉属性级错误(excludePropertyErrors):
    @Html.ValidationSummary(true)


另外,如果Model中使用了“嵌套”属性(比如:InvitedBy.Username),AddModelError()的第一个参数也应该是相应的由小数点分割的:InvitedBy.Username

因为这样才能对应上最后呈现的原生HTML页面上Form表单控件的name属性:

<input type="text" data-val="true" data-val-required="* 邀请人必须填写" id="InvitedBy_Username" name="InvitedBy.Username" value="" />



unobtrusive.js

观察由ASP.NET HtmlHelper生成的表单元素:

<input type="text" data-val="true" data-val-required="* 必须填写" id="Register_UserName" name="Register.UserName" value=""> 

以及他们相应的错误消息元素:

<span class="field-validation-valid" data-valmsg-for="Register.UserName" data-valmsg-replace="true"></span>

为什么会有这些以 data-val 开头的属性呢?

实现机制

为了集成服务器/客户端验证,微软基于jquery validation复习开发出jquery.validate.unobtrusive.js插件(以下简称unobtrusive)。

生成的form表单元素中data-开头的属性,都是为unobtrusive准备的。unobtrusive利用:

  • data-val="true" :知道要对该元素进行验证(val是validate的简写)
  • data-val-required="* 必须填写":
    • required:对应validate中的method name
    • "* 必须填写":对应validate中的message

还可以有验证参数,比如使用[MinLength(4)]之后,会有属性:

  • data-val-minlength-min="4",minlength对应method name,min="4"对应参数名(min)和值(4)

data-valmsg-for="inputname"等也是类似的作用。

然后,unobtrusive根据以上属性自动设定rule并进行验证。

演示:在HTML页面再引入unobtrusive(一样要注意引入顺序,必须在validate之后),不需要任何额外的代码,就能实现验证效果。

自定义

因为unobtrusive.js是基于jquery.validate.js的,所以validate上的那些操作(比如:rules()/addMethod()……仍然有效)

但是,unobtrusive在页面加载时就已经进行了一次激活($('form').validate()),解析所有form表单元素,并生成rules。所以

可以直接使用rules(),比如:

$(document).ready(function () {
    $('#UserName').rules('add', {
        lessThan: '#threshold'   //lessThan是自定义method

而且,MVC生成的data-val*属性同于validate.js中的属性标记,他们都已经被解析(parse),所以可以直接remove:

$('#UserName').rules('remove')

但是如果页面是Ajax加载的,就不会有这么一个parse的过程。

演示:

  1. 把文本框放入_UserName的partial view中再load(),Model验证不会生效
    $("#ajaxContainer").load("/Register/_UserName");
  2. 但在F12控制台中执行以下代码之后就OK了:
    $.validator.unobtrusive.parse($('form'));

@想一想@:为什么?该怎么办呢?


自定义

通过

继承

已有的验证Attribute实现(简单改动),性价比高!

最常利用的就是RegularExpressionAttribute,比如:自定义的整数验证(正负皆可)

public class IntegerAttribute : RegularExpressionAttribute

然后,构造函数中传入自定义的正则表达式:

public IntegerAttribute() : base(@"^-?\d+$")

还可以自定义其error message:

public override string FormatErrorMessage(string name)
{
    //name为[Display(Name)](如果声明的话),否则为属性名
    return $"{name}不是整数";

演示:

[Integer]
public string Name { get; set; }

注意:观察生成的HTML标签,此时还没有客户端的JavaScript实现。

还需要最后一步,在Global.asax的Application_Start()中:

DataAnnotationsModelValidatorProvider.RegisterAdapter(
    typeof(IntegerAttribute),
    typeof(RegularExpressionAttributeAdapter));
另外一种方式是

完全独立(难,性价比低,现阶段不推荐)

的,通过继承ValidationAttribute和实现IClientValidatable

public class AtLeastOneRequired : ValidationAttribute, IClientValidatable

让生成的input标签中添加validation相关的属性()……

protected override ValidationResult IsValid(
    object value,
    ValidationContext validationContext)
{
    bool atLeastOneNotNull =

        //all properties 
        validationContext.ObjectType.GetProperties()

        //with Group attributes
        .Where(p => p.IsDefined(typeof(AtLeastOneRequired), true))

        //only in current group
        .Where(p => p.GetCustomAttributesData().Any(
            a => a.ConstructorArguments.Count > 0 &&
                    a.ConstructorArguments.Any(aca => aca.Value.ToString() == _group))
        )

        //at least one is not null
        .Any(p => p.GetValue(validationContext.ObjectInstance, null) != null)

        ;

    return atLeastOneNotNull ?
        null :
        new ValidationResult(base.ErrorMessage);
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
    ModelMetadata metadata,
    ControllerContext context)
{
    ModelClientValidationRule rules = new ModelClientValidationRule
    {
        ValidationType = "atleastonerequire",
        ErrorMessage = FormatErrorMessage(metadata.DisplayName)
    };
    rules.ValidationParameters["group"] = _group;
    yield return rules;
}

然后,在前台还要将自定义的method和rule“适配”(adapter)起来(略)……

$.validator.unobtrusive.adapters.add("atleastonerequire", ["group"], function (options) {
    options.rules["atleastonerequire"] = options.params.group;
    options.messages["atleastonerequire"] = options.message;
});

因为缺乏对unobtrusive的全面认识,从逻辑上看有些难以理解,

  • adapter:适配器,将unobtrusive和validate“适配”起来,或者讲unobtrusive转化成validate
  • atleastonerequire:method name,该rule使用哪一个method(adapter还可以理解成:设立一个规则)
  • ["group"]:必须是一个数组,可选,指定哪些参数被采用
  • options指定rule和message……


PRG模式

演示:一个带有DropdownList的页面,options由GET Action填充,Post之后抛出NullReference异常

为什么会报错?

@想一想@:为什么呢?因为:

  1. 第一次(通过GET Action)加载页面,,是一次HTTP请求
  2. 第二次(通过POST Action)点击提交,,又是一次HTTP请求
  3. HTTP协议无状态复习

DropdownList的生成需要数据(Options集合),这个数据是在GET Action里被赋值的。但它不会自动被“自动记录”,POST之后页面需要重新生成,这时候POST Action中没有为Options赋值的过程,自然就报错了。

如何解决这个问题呢?当然可以在POST的Action里再一次给Options赋值,但是更常见的办法是重定向(Redirect)一次。

这种对所有的Post请求(即使是回到当前页面,也要使用Redirect),都按照Post-Redirect-Get进行处理的模式被称之为PRG,目前被广泛使用。它除了能解决上述问题,还可以:

  • 减少重复代码
  • 解决浏览器重复Post的问题。很多浏览器,刷新Post之后返回的页面,会自动(或提示你)再次提交(Post)而不是我们期待的Get。演示
  • 避开其他一系列的问题(比如ModelState干扰)……

可以使用以下

Redirect

相关方法:

  • 传入Action名(站内最重用)
    return RedirectToAction(nameof(Index));
    还可以有Controller名、routeData等:
    //HomeController的Index() Action,还附带一个route data: id=7 
    return RedirectToAction("Index", "Home", new { id = 7 });
  • 传入url(站位)
    return Redirect("http://163.com/");
  • 传入route名称(罕见)
    return RedirectToRoute("");
  • 以上方法都可以添加Permanent实现301永久重定向,还可以额外指定route data
    return RedirectToActionPermanent("Index","Home", new { id = 10, name= "fg"});

但是,这样做似乎仍然有问题:验证错误提示呢?

TempData

然而,当Model验证失败时,重定向会丢失ModelState的Error信息,导致验证错误信息无法显示。(演示)

怎么办?我们可以:

  1. 在验证失败的时候,使用一个“数据容器”TempData,把ModelState装起来
    TempData[KEY_OF_ERROR] = ModelState;
  2. 在Get Action的时候,取出传递过来的ModelState,和当前ModelState进行融合(Merge)
    ModelState.Merge(TempData[KEY_OF_ERROR] as ModelStateDictionary);

注意

  1. TempData是<string, object>的键值对集合,
  2. ModelState本身是只读的,所以只能使用Merge()方法(#理解#:merge和直接赋值的不同)
  3. Merge()接受的参数类型必须是ModelStateDictionary的,所以需要as转换(不要用()强转,以免null异常)

TempData几乎是为PRG量身定做的:

  • 它一旦读取,就会被销毁。换言之,存储之后只能读取一次。(获取而不销毁,使用Peek()方法
  • 底层使用session(关于session,后文详述)


作业

  1. 使用PRG模式,对照一起帮,为之前的表单标签添加验证(客户端+服务器端,客户端最好能和unobtrusive融合)。注意不要遗漏:
    1. 联系方式:QQ只能由数字组成
    2. 注册:确认密码和密码必须一致
    3. 求督促:开始执行时间必须大于当前时间(日期),结束时间必须大于开始时间
    4. 求助发布:设置的帮帮币悬赏数量不能高于现有的数量(hidden input传回)
  2. 补充完成注册页面邀请人功能:
    1. 检查邀请人是否存在,邀请码是否正确并予以提示
    2. 将邀请人信息一并存入数据库
  3. 继续完善之前的文章单页:如果id值(比如25)在数据库中找不到,重定向到错误页(/error),提示:没有id为25的文章……
学习笔记
源栈学历
今天学习不努力,明天努力找工作

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 ASP.NET 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码