快速入门系列--MVC--04模型

时间:2022-03-20 06:39:30
  • model元数据

闲来继续学习蒋金楠大师的ASP.NET MVC框架揭秘一书,当前主要阅读的内容是Model元数据的解析,即使是阅读完的现在,仍然有不少细节不是特别明白。好在这部分内容主要是关于Razor引擎的呈现的,通过注解的方式对Model进行自定的修饰,最终使得页面在渲染时(即从cshtml文件转化为html时),相关的数据能够按照指定的形式转化并显示。由于接下来的项目中不再打算使用Razor引擎,该引擎虽然很不错,但也有一些问题,例如存在HTML5代码与HtmlHelper的混写,使得UI层很难与业务代码层完全的分离。所以不太利于程序开发的分工,在当前的互联网需求迅速变化的场景下变得不是也别适合。而且相关工作人员的招聘等问题上也比较难得处理,不可能要求每一个工程师都具有全栈能力,企业也可能负担的其相关成本,自己对微软2012年推出的blend开发模式不太了解,不知道是不是只是支持WPF,WinForm等,可能有些理解上问题,请给予指正。个人观点上,更倾向于直接使用HTML5开发,框架主要负责路由,过滤器等功能,当前携程等网站也主要采用这种方式。因此,该章节算是选学了,主要介绍一些主要的概念。

首先要提及的是元数据,一说元数据,大家第一反应可能都是一样的,就是中间语言IL中对类的描述信息。一般我们可以通过自定义特性的方式对其进行扩充,这儿的Model元数据只要用于控制Model对象(ViewModel对象),在View上的呈现形式。其使用System.Web.Mvc.ModelMetadata来表示Model元数据,并且ModelMetadata是一种迭代,支持自包含的结构,有点像组合模式。如果ModelMetadata是一个复杂类型,可以通过一个相应的XXXTypeConverter辅助类将其转化为简单类型。

接下来,简单介绍一下与该框架相关数据注解特性,框架就是依靠这些注解特性和相应的模板方法来控制Model数据的显示,如下表所示。

特性名称
UIHintAttribute  
HiddenInputAttribute ScaffoldColumnAttribute
DisplayTypeAttribute DisplayFormatAttribute
EditableAttribute ReadOnlyAttribute
DisplayAttribute DisplayNameAttribute
RequiredAttribute  
AllowHtmlAttribute  
RadioButtonListAttribute CheckBoxListAttribute
DropdownListAttribute ListBoxAttribute

相关特性大部分都是顾名思义,比较容易理解,也与EF中的注解特性相似,就不一一介绍了。需要注意的是,如果想自定义一个特性,那么就要实现IMetadataAware接口,与它同名的接口也经常出现在.NET相关数据绑定中,代码如下所示。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
public class DisplayTextAttribute : Attribute, IMetadataAware
{
private static Type staticResourceType;
public string DisplayName { get; set; }
public Type ResourceType { get; set; }
public DisplayTextAttribute()
{
this.ResourceType = staticResourceType;
} public void OnMetadataCreated(ModelMetadata metadata)
{
this.DisplayName = this.DisplayName ?? (metadata.PropertyName ?? metadata.ModelType.Name);
if (null == this.ResourceType)
{
metadata.DisplayName = this.DisplayName;
return;
}
PropertyInfo property = this.ResourceType.GetProperty(this.DisplayName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
metadata.DisplayName = property.GetValue(null, null).ToString();
} public static void SetResourceType(Type resourceType)
{
staticResourceType = resourceType;
}
}

Tip:注意在项目中定义相关资源文件,将英文的属性名转化为中文。

常见模板方法为:

模板方法
HtmlHelper<TModel> Display DisplayFor
  Editor EditorFor
  DisplayForModel EditForModel
  Label LabelFor
  DisplayText DisplayTextFor
  DropdownList DropdownListFor
  ListBox ListBoxFor

以上可以很清楚的看到显示模式和编辑模式两种不同的显示形式,由于框架是根据元数据对象中的数据类型属性值去寻找对应模板的,因此将需要将模板的View定义放在EditorTemplates目录下。接下来通过一个表格介绍一些框架预定义的模板。

预定义方法
EmailAddress HiddenInput Html
Text&String Url MultilineText
Password Decimal Boolean
Collection Object  

最后用一个简图介绍下与ModelMetadata相关的类加强理解与记忆。

快速入门系列--MVC--04模型

可以看到,ModelMetadata是会进行缓存的,并且通过原型模式进行创建,在.NET中经常可以看到Provider,这儿主要起一个提供者的作用,但与工厂类等其他创建型模式有什么区别仍然不是很清楚,还需要加强理解。

  • Model绑定

数据绑定对于长期进行.NET相关开发的技术人员来说,非常的熟悉。无论是最开始的WinForm, WebForm, 还是现在的WPF,ASP.NET MVC,只要是与前台页面数据源相关的内容,都离不开这个概念,该概念的实现极大的简化了相关的开发工作。即使不使用Razor视图引擎,直接使用静态的HTML5页面,该模块仍然不可或缺。现在简单的介绍一下ASP.NET MVC中相关的Model绑定。

在ASP.NET MVC框架中,Model绑定本质上就是为目标Action方法生成参数列表的过程,这些参数列表的来源可能是请求的URL,可能是HTTP的请求头或请求体中,通过参数的元数据信息可以得到相关内容。主要有ControllerDescriptor,ActionDescriptor,ParameterDescriptor三个抽象类,他们均实现了System.Reflection.ICustomAttributeProvider接口,其实主要就是对类元数据信息的包装类,方便元数据信息的使用,其具体实现类与关系如下图所示。

快速入门系列--MVC--04模型

通过上图可以看到,所有Reflected作为前缀的类都是实现类,同时可以看到Controller、Action描述类的异步版本,比较特殊的是TaskAsyncActionDescriptor,它可以在普通的(非异步)的Controller中使用,在自己试图搭建框架时可以模仿该方式,抽象类,同步/异步版本,接下来通过一个表格简要介绍和比较以上三个描述类。

类型 简介
ControllerDescriptor 比较特殊是GetFilterAttributes方法,用于获取该控制器上的所有过滤器特性,ActionMethodSelectorAttribute特性包含GET, POST, PUT, DELETE, Head, Options, Patch等七个Http方法。
ActionDescriptor GetFilters方法返回FilterInfo类型,包含ActionFilter,AuthorizationFilter,ExceptionFilter,ResultFilter等四种类型的筛选器,与J2EE类似。
ParameterDescriptor 其中属性ParameterBindingInfo最为关键,实际包含ModelBinder对象,该对象是整个绑定模块的核心,同时Include,Exclude集合用于显示设置参与/不参与绑定的属性,Prefix属性主要用于复杂的类型的绑定。

由于数据绑定的来源各不相同,框架通过接口IValueProvider类来统一提供数据。首先介绍NameValueCollectionValueProvider,属于key/Value的形式,需要加强理解的是,在之前介绍的描述一个复杂数据类型的ModelMetadata具有树型层次化结构,而NameValueColletion对象却是一个"扁平"的结构,两者的匹配通过前缀来完成。"扁平化"这个概念在现在数据呈现中出现的非常多,无论是这儿MVC中的ViewModel, 还是WPF中MVVM框架下的ViewModel。该类型的Provider主要包括FormValueProvider和QueryStringProvider,顾名思义,关联HttpRequest中的QueryString属性。

接下来介绍DictionaryValueProvider,与以前数据提供器的主要区别是其数据值不仅支持字符串,还支持任意对象,可以使用泛型约束。主要包括RouteDataValueProvider,HttpFileCollectionProvider,ChildActionValueProvider。这里想重点提及的是ChildActionProvider,它主要用于子Action中,由于子Action不同独立用于响应客户端的请求,只是用于生成部分的HTML。框架中使用ValueProviderFactory工厂类用于创建一系列的值提供器,同时使用ValueProviderFactories这个静态类通过注册的方式管理以上工厂。

之前有提过这部分最重要的类型就是ModelBinder,有了之前的基础,现在是时候介绍它了。借用蒋大师的原话,"Model的绑定体现在从当前请求提取相应的数据并生成相应的对象作为调用目标Action方法的参数列表"。不知道大家还记不记得,之前在ActionDescriptor中提到的ParameterBindingInfo类型的对象,其中就有ModelBinder,为了便于理解仍然使用表格进行介绍。

类型 简介
IModelBinder 包含BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)方法。
ModelBinderAttribute 用于自己定义模型的绑定器,之后会介绍默认绑定器,在未指定绑定时使用,也是最常见的。
ModelBinders 静态类用于注册模型绑定器,可以在Application_Start方法中为指定类型设置绑定器
ModelBinderProvider GetBinder(Type modelType)方法用于根据指定的数据类型获取相应的ModelBinder对象。
ModelState *Model绑定除了设置参数列表以外,还将数据通过ModelState的形式存储于Controller的ViewData中。sw
ModelBindingContext 简述Model绑定的过程,首先Action调用ActionInvoker执行Action,此时获得ActionDescriptor对象,然后遍历ActionDescriptor参数列表,根据ParameterDescriptor等对象创建ModelBindingContext。然后获得指定的ModelBinder,调用GetModel得到由ValueProvider提供的相应参数值,最后以ModelState的形式保存。

最后,介绍默认的Model绑定DefaultModelBinder类,这块内容比较多,但由于是框架默认提供的,了解即可,主要思路如以下代码所示。

 public class DefaultModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
return this.GetModel(controllerContext, bindingContext.ModelType, bindingContext.ValueProvider, bindingContext.ModelName);
} public object GetModel(ControllerContext controllerContext, Type modeType, IValueProvider valueProvider, string key)
{
if (!valueProvider.ContainsPrefix(key))
return null;
return valueProvider.GetValue(key).ConvertTo(modeType);
}
}

以上可以比较明确的看到类型的绑定过程体现在GetModel方法中,所用参数均来自于绑定上下文对象,通过模型名称获得值并转化为指定模型类型,在简单类型的处理上,已完全满足。接下来是复杂类型,通过GetComplexModel方法获得复杂对象,其实也比较好理解,绑定的过程是一个递归的过程,它通过反射根据数据类型创建对象,并将相关值赋到其属性上。每一次的递归都是将属性名称作为前缀附加到现有前缀作为下一次递归的前缀。之后还有数组,集合,字典等类型的绑定,其中都包含一个深复刻的过程,有部分会涉及泛型方法的反射应用,都比较相似就不一一介绍了。

  • Model验证

蒋大师的MVC框架解析确实是越学越有趣,即使是跟着学写些示例代码也是收获良多,尤其是关于类型、反射和委托等方面,平时在应用开发中确实很少会有机会写这样的代码。今天学习的ASP.NET MVC中的Model的验证,刚开时会以为这一章会比较简单,因为之前已经学习过了Model元数据的解析、Model绑定,Model的验证可能就只是DataAnnotation相关类的介绍。但实际学习的过程中,尤其是自定义用于修饰Action的验证特性让我到现在仍然感觉是比较萌萌哒,毕竟这一块对于框架的扩展基本上涉及到了验证相关的所有类型。除此之外,昨晚也是我第一次从https://aspnetwebstack.codeplex.com/上用git下载到了到MVC的源码,本以为会比较艰难,但实际却非常的方便,怒赞下。之后在VS2012打开Nuget会自动下载依赖组件,就可以编译通过了。记得今年一直听到各种关于微软的开源计划,自己接触的知识领域还是比较低端,也不太清楚到底有些什么源代码可以看,当时首先想到的就是到目前为止仍然掌握很弱的WCF,然后查查居然也有源码了,顿时觉得压力山大,因为以后再做不好.NET就不能和妈妈说我看不到源码了。原来一直关于.NET的彷徨,至少在这一刻得到很好的坚定,虽然由于市场的原因.NET在国内的发展比较飘忽,但从自身技术发展的角度,有了源码,只要努力,我就可以生活大师的身边,知道什么是对的了,这个一直困惑我多年。不知道大家有没有这样的感受,即使对自己的代码风格、设计理念非常认同,但重来没有说应该这样做的底气。见笑了,言归正传,回到Model的验证,内容比较对,篇幅很能比较长,望见谅。

首先介绍最为核心的ModelValidator抽象类,该类的主要的成员方法包括:GetClientValidationRules(),返回值为客户端验证规则,最终由HtmlHelper的模板方法渲染为html语句,由于未来项目中并不打算使用Razor引擎,这部分会略过一些内容,但之后有一部分关于JQuery-validate组件的扩展还是很有价值的;Validate(object container),返回值为ModelValidationResult集合,需要注意的是该方法的参数container说明验证过程是包含类型本身和其所辖的属性成员的。接下来用图表简要介绍几个MVC中的Model验证解决方案:

验证解决方案 简介
DataAnnotationsModelValidator 最主要的验证方案,包括常见的验证特性:RequiredAttribute,RangeAttribute等
ClientModelValidator 客户端验证。
DataErrorInfoModelValidator 实现IDataErrorInfo接口,包括:DataErrorInfoClassModelValidator,DataErrorInfoPropertyModelValidator
ValidatableObjectAdapter 实现IValidatableObject接口,也称为"自我验证",比较少使用。

这儿仍然使用Provider模式来提供相应的组件,ModelValidatorProvider类具有GetValidators(ModelMetadata metadata, ControllerContext context)

方法,前一个参数描述被验证类型或熟悉的元数据对象,另一个为当前的ControllerContext。同时,具体的Provider与之前介绍的验证解决方案的中类型相对应,在此就不一一介绍,需要注意的是在验证一个类型时,是先验证它的属性,然后才验证它自身,因此会出现验证的短路现象,即属性出错,就不会继续验证和反馈容器类型的错误了。与之前一样,这儿也会使用注册表模式来管理Provider,使用上ModelValidatorProviders来进行注册,框架默认会加载DataAnnotationXXX,ClientXXX,DataErrorInfoPropertyXXX,也可以把自定义的Provider加入其中。在框架中真正负责验证工作的是一个CompositeModelValidator私有类,查看源码确定是ModelValidator中的一个内部类,但为什么这样使用还有一些困惑,为什么这样需要完全隐藏掉该类?

接下来,介绍Model绑定与验证的关系,在前文"Model的绑定"的介绍中提到Controller对象的ViewData包含ModelState集合,用于表示Model的状态,其中既包括ValueProvider提供的值,也包括Errors验证结果。验证结果的呈现通过ValidationMessage,ValidationMessageFor扩展方法对单个属性进行验证,输出html形式为(class="field-validation-error" data-valmsg-for="xxx",data-valmsg-replace="true"),ValidationSummary呈现容器整体的验证结果,可以设置excludePropertyErrors参数。同时注意可以通过ModelState的AddModelError方法添加错误信息,EditorForModel扩展方法在使用时会默认的显示验证错误时的信息。

Model绑定中的验证解释起来比较拗口,但简单说来就是DefaultModelBinder在递归的绑定复杂对象的过程中对绑定后的对象实施验证,如下图所示。

快速入门系列--MVC--04模型

为了更加了解Model绑定和验证的关联,自己跟着蒋大师的源码基本原样敲了一遍,主要里面有一些用法自己还是不够熟悉,多练练了,大家可以无视。

 public class CompositeModelValidator : ModelValidator
{
public CompositeModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
: base(metadata, controllerContext)
{
} public override IEnumerable<ModelValidationResult> Validate(object container)
{
bool isPropertiesValid = true;
//验证属性
foreach (var propertyMetadata in Metadata.Properties)
{
foreach (var validator in propertyMetadata.GetValidators(this.ControllerContext))
{
var results = validator.Validate(propertyMetadata.Model);
if (results.Any())
{
isPropertiesValid = false;
}
foreach (var result in results)
{
yield return new ModelValidationResult
{
MemberName = DefaultModelBinder.CreateSubPropertyName(propertyMetadata.PropertyName, result.MemberName),
Message = result.Message
};
}
}
} //验证容器类
if (isPropertiesValid)
{
foreach (var validator in Metadata.GetValidators(this.ControllerContext))
{
var results = validator.Validate(Metadata.Model);
foreach (var result in results)
{
yield return result;
}
}
}
}
}
 public class DefaultModelBinder : IModelBinder
{
internal static string CreateSubPropertyName(string prefix, string propertyName)
{
prefix = prefix ?? "";
propertyName = propertyName ?? "";
return (prefix + "." + propertyName).Trim('.');
} protected virtual object GetComplexModel(ControllerContext controllerContext, Type modelType, IValueProvider valueProvider, string prefix) {
object model = CreateModel(modelType);
foreach(PropertyDescriptor property in TypeDescriptor.GetProperties(modelType)){
if (property.IsReadOnly) {
continue;
} string key = string.IsNullOrEmpty(prefix) ? property.Name : prefix + "." + property.Name;
property.SetValue(model, GetModel(controllerContext, property.PropertyType, valueProvider, key));
} //Model验证
var metadata = ModelMetadataProviders.Current.GetMetadataForType(()=>model, modelType);
var validator = new CompositeModelValidator(metadata, controllerContext);
foreach(var result in validator.Validate(model)){
string key = CreateSubPropertyName(prefix, result.MemberName);
controllerContext.Controller.ViewData.ModelState.AddModelError(key, result.Message);
}
return model;
}
}

继续Model验证的学习,顺道提及一下,今天面试的时候又遇到了几个新的问题,包括Code Smith的代码生成工具(类似T4模板,但更方便)、Dapper.net的轻量级ORM框架、服务的幂等性等,将慢慢的学习和分享。

首先介绍最重要的基于验证特性的声明式Model验证,ValidationAttribute是所有验证特性的抽象基类,主要内容如下表所示:

成员变量或函数 简介
ErrorMessageResourceName 错误消息所在资源项的名称
ErrorMessageResourceType 错误消息所在资源项的类型
IsValid() 在验证失败时返一个ValidationResult对象
GetValidationResult() 实际调用受保护的IsValid()
TypeId 在需要多次使用同一验证特性时需要重写该属性,使得每一次的TypdId不相同。情形:假设需要控制不同级别人员用于不同工资范围,定岗定薪。

继承ValidationAttribute的验证特性类如下所示:

特性名称
RequiredAttribute
RangeAttribute
StringLengthAttribute
Max/MinLengthAttribute
RegularExpressionAttribute
CompareAttribute
CustomValidationAttribute
 

在验证过程中起作用的是DataAnnotaionsModelValidator对象的Validate方法,首先根据容器对象创建出表示验证上下文的ValidationContext对象,并采用ModelMetadata的DisplayName来做为上下文的名称,然后调用Attribute属性的GetValidatioResult方法进行最终的验证。同时MVC还定义了一个System.Web.Mvc.DataAnnotationsModelValidator<TAttribute>的泛型类,我们常用的RequiredAttribute均继承于该类。

数据特性验证的提供器其包含一个静态的验证工厂集合ValidatableFactories,是一个以类型Type为key,指定委托DataAnnotationsValidatableObjectAdapterFactory为value的字典,比较少见的方式。该提供器的静态构造方法中已将常见特性的验证提供器加入,并提供静态注册方法注册新的验证提供器。之后的内容蒋大师分享了两种扩展,一个是将ValidationAttribute应用在Action的参数上,和J2EE中Spring MVC的方式一致,以及实现同一个Model类型实现多种方式等,就不一一介绍了。

最后,简要介绍客户端验证,在不用Razor引擎的前提下,这部分的主要价值就体现在关于JQuery插件的扩展,关于javascript,提到最多的概念就是PE(Progressive Enhancement)渐进性增强和非入侵式Unobtrusive,最主要对的意思就是页面可以在不支持JS的情况下显示基本内容,再浏览器允许的情况增强显示效果。这里用到了最常见的前端验证框架文件jquery.validate.js,可以通过设置class的内联方式来完成验证,也可以直接通过validate方法来设置验证,代码如下:

 $(document).ready(function () {
$("form").validate({
rules: {
name: { required: true },
birthday: { required: true, date: true },
blogAddress: { required: true, url: true },
emailAddress: { required: true, email: true },
},
message: {
name: { required: "请输入姓名" },
birthday: { required: "请输入出生日期", date: "请输入一个合法的日期" },
blogAddress: { required: "请输入姓名", url: "请输入一个合法的URL" },
emailAddress: { required: "请输入Email地址", email: "请输入一个合法的Email地址" },
}
});
});

在框架中,基于JQuery的Model验证其实就是根据数据的验证特性生成相应的js代码,指定的html元素具有"data-val"属性和一系列的以"data-val-"为前缀的属性。并在之后一个<span>元素,该元素的CSS样式为"field-validation-valid",当验证失败时替换为"field-validation-error"。在表示客户端验证的ModelClientValidationRule中,有一个ValidationParameters来表示验证参数名和参数值。之后蒋大师又介绍了一个自定义验证的例子,我只节选出js作为自己学习JQuery插件的练习。

 jQuery.validator.addMethod("agerange", function (value, element, params) {
value = value.replace(/(^\s*)(\s*$)/g, "");
if (!value) {
return true;
}
var minAge = params.minage;
var maxAge = params.maxage;
var birthDateArray = value.split("-");
var birthDate = new Date(birthDateArray[0], birthDateArray[1], birthDateArray[2]);
var currentDate = new Date();
var age = currentDate.getFullYear() - birthDate.getFullYear();
return age >= minAge && age <= maxAge;
}); jQuery.validator.unobtrusive.adapters.add("agerange", ["minage", "maxage"], function (options) {
options.rules["agerange"] = {
minage: options.params.minage,
maxage:options.params.maxage
};
options.messages["agerange"] = options.message;
});

注:本文主要供自己学习,不妥之处望见谅。

参考资料:

[1]蒋金楠. ASP.NET MVC4框架揭秘[M]. 上海:电子工业出版社, 2012.