mvc源码解读(19)-mvc客户端+服务端验证
发现很久没有更新过博客了,写这些博客一方面是为了重温之前所学的知识,另一方面是为了锻炼自己的语言组织能力,大家都应该知道程序员都有一个通病,语言表达能力差,他可以连绵不绝的写出几千行的代码,但是如果要让他讲出来,估计讲不出10句话。好吧,就权当锻炼一下自己的语言组织能力吧。
好了,废话不多说,我们这片文章就讲解一下mvc的关于利用标注特性做后台的数据验证吧。mvc中提供一个非常人性化数据验证方式--特性,即通过自定义的或是系统内置的特性标注在Model的属性上,而我们知道在ModelBinder的时候,在将参数绑定到具体Model之前回去解析应用在属性上的特性,正式利用这一特点,达到数据验证的目的。mvc下所有验证特性都要直接继承或是间接继承自类ValidationAttribute,我们来看一下实际开发中我们经常要用到的ValidationAttribute主要成员如下:
public string ErrorMessage { get; set; } public virtual string FormatErrorMessage(string name);
public virtual bool IsValid(object value);
protected virtual ValidationResult IsValid(object value, ValidationContext validationContext);
其中ErrorMessage属性用于指定验证发生错误时显示给用户的消息,在实际的项目开发中,出于对验证错误消息的单独维护和本地化的特性,我们会将错误消息作为资源文件保存起来,如下图所示:
大家看到这其中有个类似于string.Format方法中用到的占位符{0}字样,那这些错误的提示消息是如何与资源文件的这些数据格式关联起来呢?答案就是虚方法FormatErrorMessage的作用,该方法内部实际上就是一个string.Format方法,其中参数name就是ModelMetadata类的DisplayName属性,当然参数name除了来自ModelMetadata类的DisplayName之外,还有可能是在服务端验证失败的情况下ValidationContext对象的DisplayName属性,当然前提是ValidationContext不为null。这里面有两个IsValid方法,第一个返回bool值得没得说,就是根据传入要验证的值进行判断,第二个重载的IsValid返回的是一个ValidationResult对象,ValidationResult是包含一组错误消息和成员名称的列表。这主要体现在ValidationResult的ErrorMessage属性和MemberNames集合中:
public string ErrorMessage { get; set; } public IEnumerable<string> MemberNames { get; }
当然如果只要实现服务端的验证,这样重写ValidationAttribute里面的任意一个IsValid方法即可,但是如何集合客户端的验证来实现类似于Ajax效果的客户端+服务端的验证呢?
回答这个问题之前,我们先来看看系统内置的一些ValidationAttribute标注特性。
•RequiredAttribute:用于验证必需数据字段。 •RangeAttribute:用于验证数值字段的值是否在指定的范围之内。 •StringLengthAttribute:用于验证目标字段的字符串长度是否在指定的范围之内。 •MaxLengthAttribute/MinLengthAttribute:用于验证字符/数组字典的长度是否小于/大于指定的上/下限。 •RegularExpressionAttribute:用于验证字符串字段的格式是否与指定的正则表达式相匹配。 •CompareAttribute:用于验证目标字段的值是否与另一个字段值一致
•CustomValidationAttribute:指定一个用于验证目标成员的验证类型和验证方法。
我们拿其中的几个例子来做一个简单的demo。自定义一个EmailModel实体,在其Email属性上标上系统内置的Required,StringLength,RegularExpression特性,
public class EmailModel { [Required] [StringLength(10, MinimumLength = 3)] [RegularExpression(@"/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/;")] public string Email { get; set; } }
在Controller中创建相应的View,通过html.EditorFor辅助方法得到的,代码如下:
<script src="~/Scripts/jquery-1.7.1.min.js"></script> <script src="~/Scripts/jquery.validate.min.js"></script> <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script> @using (Html.BeginForm()) { @Html.ValidationSummary(true) <fieldset> <legend>EmailModel</legend> <div class="editor-label"> @Html.LabelFor(model => model.Email) </div> <div class="editor-field"> @Html.EditorFor(model => model.Email) @Html.ValidationMessageFor(model => model.Email) </div> <p> <input type="submit" value="Create" /> </p> </fieldset> }
大家可以注意到我引用的三个js文件,第一个就是jquery文件,没什么好说的,第二个是一个js验证库,第三个是微软在js验证库上封装的一个无介入式的js库,就是俗称的Unobtrusive JavaScript,简单地来说,就是一种代码分离的思想,把行为层和表现层分离开。在mvc3及其以后的版本中,我们如何开启Unobtrusive JavaScript来达到客户端的验证呢?我们可以发现在主项目的配置文件appSettings节点中有如下设置:
<add key="ClientValidationEnabled" value="true" /> <add key="UnobtrusiveJavaScriptEnabled" value="true" />
这样就表示开启了Unobtrusive JavaScript的客户端验证,请记住,我们现在都没有做,但是看下运行的结果后的源文件:
<div class="editor-field"> <input class="text-box single-line" data-val="true" data-val-length="字段 Email 必须是一个字符串,其最小长度为 3,最大长度为 10。"
data-val-length-max="10" data-val-length-min="3" data-val-regex="字段 Email 必须与正则表达式“/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/;”匹配。"
data-val-regex-pattern="/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/;" data-val-required="Email 字段是必需的。" id="Email" name="Email" type="text" value="" /> <span class="field-validation-valid" data-valmsg-for="Email" data-valmsg-replace="true"></span> </div>
请记住我们没有在客户端写任何的js验证逻辑,然而它却给我们生成了貌似逻辑的验证内嵌到了标签属性中,而且是一大推我们并不是很熟悉的属性。好吧,我们来一一解释一下。生成的是一些html5属性,我们可以看到生成的属性都是data-val-开头的。首先添加的是data-val:应用验证规则的名字。例如,当我们在Model的属性上添加了Required后,生成的就是data-val-required属性,跟属性关联的是值就是规则的错误信息。如data-val-length,显示是与字符串长度有关的信息,data-val-regex则表示应用在Model属性上相关的正则规则。
等等,如果我们在配置文件中将Unobtrusive JavaScript关闭,如下所示:
<add key="ClientValidationEnabled" value="false" /> <add key="UnobtrusiveJavaScriptEnabled" value="false" />
再次打开页面源文件:效果如下
<div class="editor-label"> <label for="Email">Email</label> </div> <div class="editor-field"> <input class="text-box single-line" id="Email" name="Email" type="text" value="" /> </div>
好了,我们现在暂时知道了Unobtrusive JavaScript方式无非就是将相关的验证规则内嵌到了要验证的元素的属性中,从而达到一种无介入式的代码分离效果。我们现在关心的是他是如何是生成的客户端的验证规则的呢?我们来做一个自定义的Model验证。
我们前面已经说过了,要实现一个自定义的服务端验证继承自ValidationAttribute,并重写里面的任意一个IsValid方法,要同时达到客户端的验证,还要实现IClientValidatable接口,该接口里面有唯一的方法:
IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context);
顾名思义就是获取客户端的验证规则,返回的是一个ModelClientValidationRule对象集合,该对象成员如下:
public string ErrorMessage { get; set; } public IDictionary<string, object> ValidationParameters { get; } public string ValidationType { get; set; }
分别对应着设置的验证的错误信息,验证的参数列表和验证的类型,mvc系统中有默认的几个ModelClientValidationRule,如下所示:
public class ModelClientValidationStringLengthRule;
public class ModelClientValidationRegexRule;
public class ModelClientValidationRequiredRule;
public class ModelClientValidationRangeRule;
我们自定义一个QQ特性用于专门的验证QQ号码的格式,具体如下:
/// <summary> /// 作为所有特性的基类 /// </summary> [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public class QQAttribute : ValidationAttribute, IClientValidatable { /// <summary> /// 获取客户端的验证规则。 /// </summary> /// <param name="metadata"></param> /// <param name="context"></param> /// <returns></returns> public override IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { if (metadata == null) { throw new ArgumentNullException("metadata"); } var rules = new List<ModelClientValidationRule>(); if (this.MaxLength > 0) { rules.Add(new ModelClientValidationStringLengthRule(string.Format(CultureInfo.CurrentCulture, Resources.StringAttribute_ValidationError_MaxLength, metadata.GetDisplayName(), this.MaxLength), 0, this.MaxLength)); } if (this.DangerousValidationEnabled) { rules.Add(new ModelClientValidationDangerousRule()); } if (!string.IsNullOrEmpty(this.RegexPattern) && this.CustomRegexPatternSet) { rules.Add(new ModelClientValidationRegexRule(this.FormatErrorMessage(metadata.DisplayName), this.RegexPattern)); rules.Add(new ModelClientValidationRule { ErrorMessage = FormatErrorMessage(metadata.DisplayName), ValidationType = this.GetClientValidationType() }); } return rules; } /// <summary> /// 服务端的验证 /// </summary> /// <param name="value"></param> /// <param name="validationContext"></param> /// <returns></returns> protected override ValidationResult IsValid(object value, ValidationContext validationContext) { if (value == null || validationContext == null) { return ValidationResult.Success; } string valuesAsString = value.ToString().Trim(); if (string.IsNullOrEmpty(valuesAsString)) { return ValidationResult.Success; } if (this.MaxLength > 0 && valuesAsString.Length > this.MaxLength) { return new ValidationResult(string.Format(CultureInfo.CurrentCulture, Resources.QQAttribute_ValidationError_MaxLength, this.MaxLength)); } if (!string.IsNullOrEmpty(this.RegexPattern) && !IsMatched(valuesAsString)) { return new ValidationResult(FormatErrorMessage(validationContext.DisplayName)); } return ValidationResult.Success; } }
IsMatched方法如下:
/// <summary> /// 验证字符串是否完全匹配正则表达式 /// </summary> /// <param name="valueAsString">待验证字符串</param> /// <returns>如果匹配,则为 true;否则,为 false</returns> private bool IsMatched(string valueAsString) { Regex regex = this.RegexIgnoreCase ? new Regex(this.RegexPattern, RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase) : new Regex(this.RegexPattern); Match match = regex.Match(valueAsString); if (match.Index == 0 && match.Length == valueAsString.Length) { return true; } return false; }
GetClientValidationType方法定义如下:
/// <summary> /// 获取客户端验证类型名 /// </summary> /// <returns>TemplateHint值</returns> private string GetClientValidationType() { return string.IsNullOrEmpty(this.ClientValidationType) ? this.GetType().Name.Replace("Attribute", string.Empty).ToLowerInvariant() : this.ClientValidationType; }
相关的QQ特性资源文件如下:
创建一个QQModel,如下:
public class QQModel { [QQ] public string QQ1 { get; set; } }
相关的QQAttribute的构造函数在此省略。创建相关的Controller和在View中引入刚才的相关js文件,查看源文件,代码如下:
<div class="editor-field"> <input class="input-medium" data-jqui-ime="inactive" data-val="true" data-val-length="QQ号码 不允许超过 12 个字符"
data-val-length-max="12" data-val-qq="QQ号码必须是正确的QQ号码格式" data-val-regex="QQ号码必须是正确的QQ号码格式"
data-val-regex-pattern="[1-9][0-9]{4,11}" id="QQ1" maxlength="12" name="QQ1" placeholder="" type="text" value="" /> <span class="field-validation-valid" data-valmsg-for="QQ1" data-valmsg-replace="true"></span> </div>
这样便实现了mvc提供的无介入式客户端+服务端的验证,当然我们还有另外一种稍微复杂的通过注册客户端验证的方式达到我们的目的,我们将在下一篇文章中讲解。