ASP.NET没有魔法——ASP.NET MVC 模型绑定解析(上篇)
前面文章介绍了ASP.NET MVC中的模型绑定和验证功能,本着ASP.NET MVC没有魔法的精神,本章内容将从代码的角度对ASP.NET MVC如何完成模型的绑定和验证进行分析,已了解其原理。
本文的主要内容有:
● ModelBinder
● ValuePrivoder
● ModelMetadata
● 简单模型与复杂模型
● 小结
ModelBinder
ModelBinder是ASP. NET MVC用于模型绑定的核心组件,所有的ModelBinder都实现了IModelBinder接口,如下图:
该接口只有一个方法,那就是根据控制器以及绑定上下文完成模型绑定。
在ASP.NET MVC中有不同的ModelBinder,它们分别用于绑定不同类型的数据,如普通的.Net对象、HTTP上传的文件等。
默认有以下5种ModelBinder:
● DefaultModelBinder:默认的模型绑定器,一般情况下从浏览器提交的请求都将使用默认处理器来绑定模型。
● HttpPostedFileBaseModelBinder:HTTP文件模型绑定
● ByteArrayModelBinder:绑定二进制数据。
● LinqBinaryModelBinder:将请求绑定到System.Data.Linq.Binary对象。参考: http://stephenwalther.com/archive/2009/02/25/asp-net-mvc-tip-49-use-the-linqbinarymodelbinder-in-your
● CancellationTokenModelBinder:提供了一个用于传播模型绑定操作取消的机制。
所有的ModelBinder都被一个名为ModelBinderDictionary的字典进行管理,而这个字典就存在于Controller类型的定义中,如下图所示,它是一个被保护的内部属性,用于Controller执行时完成模型绑定:
ModelBinderDictionary的定义:
从ModelBinderDictionary的定义中可以看到它实现了以Type为Key、IModelBinder类型为值的字典接口以及集合接口,可以动态的根据ModelBinder的类型增减ModelBinder,除此之外还有一个DefaultBinder,在一般情况下其运行的实例如下:
Controller中的ModelBinder字典包含了上面介绍的5个ModelBinder。更多关于自定义ModelBinder的内容可参考:https://www.cnblogs.com/Cwj-XFH/p/5977508.html
ValuePrivoder
在前面的文章中介绍了ASP.NET MVC的模型绑定可以从Form Data、Query String以及Route Data等数据源中获取数据,其原因是针对每一个数据源都有一个专门的数据提供器来获取数据源的数据,在ASP.NET MVC中存在一个定义值提供器的接口IValueProvider:
核心方法GetValue通过一个Key来获取值,ContainsPrefix则判断提供器所指的数据源中是否包含以指定字符串为前缀的Key。
直接实现该接口的类型有3个:
● NameValueCollectionValueProvider:通过名称和值共同存储数据的集合,可以通过名称查找到一个或多个值。
● DictionaryValueProvider<TValue>:通过键值对存储数据的泛型字典集合,字典的Key是唯一的,换句话说通过Key最多只能找到一个值。
● ValueProviderCollection:一个特殊的值提供器,它包含了所有相关的值提供器,在模型绑定中就是这个列表通过遍历的方式,调用相关值提供器的获取值方法来完成数据获取的。
之前说过针对不同的数据源都有一个特定值提供器,那么这些提供器是如何实现的呢?它们是根据特性分别继承NameValueCollectionValueProvider及DictionaryValueProvider<TValue>类型来实现的,其具体分类如下,一共有7种不同数据源的值提供器:
● NameValueCollectionValueProvider:
○ JQueryFormValueProvider:用于获取被Jquery格式化的Form值。
○ FormValueProvider:用于获取Form表单的值。
○ QueryStringValueProvider:用于获取查询字符串的值。
● DictionaryValueProvider<TValue>
○ ChildActionValueProvider:用于获取子Action方法的值。
○ JsonValueProvider:用于获取请求中以Json格式传输的值(注:没有该类型的值提供器,Json的值提供器直接由JsonValueProviderFactory创建一个DictionaryValueProvider<object>类型的字典值提供器)。
○ HttpFileCollectionValueProvider:用于从Http请求中的文件集合中获取文件数据。
○ RouteDataValueProvider:用于从Route Data中获取值
所有的值提供器都是由对应的工厂创建的,在默认情况下ASP.NET MVC中存在以下7种值提供器工厂,刚好对应上面的7种值提供器,其实现接口定义如下:
每一个工厂的GetValueProvider方法获取对应的值提供器。
所有的提供器工厂在MVC中被一个名为ValueProviderFactories的类型维护,该类型以硬编码的方式维护了一个静态、只读的提供器工厂列表:
运行时结果,一共有7个工厂:
下图是ASP.NET MVC在未特殊配置的情况下Controller的运行状态:
下图是发送Json格式Post请求的状态,ValueProvider中多了一个用来获取Json数据的字典值提供器:
发起请求的内容:
请求中的值提供器与之前的相比多了一个用于提供Json数据的DictionaryValueProvider<object>类型:
ModelMetadata
Metadata译为元数据,是一种描述数据的数据,而这里加上了Model,那么就是说描述Model数据的数据,下图为ASP.NET MVC中的一个Model定义:
从图中代码用语言可以这样描述:
● 该对象有3个属性。
● 其中UserName是String类型的,必填并且格式为Email格式,展示名称为用户名。
● Password以及ConfirmPassword都是String类型,且类型都为密码,它们除了展示名称不同外,ConfirmPassword还需要和Password相比较,如果不同则给出相应的错误提示。
而在MVC里面是通过ModelMatedata来对Model进行描述的,先看一下ASP.NET MVC中的ModelMetadata类型:
从中可以看到一些是否只读、是否必填、模型类型、属性(同样是ModelMetadata类型)、展示名称、是否是复杂类型等描述信息。换句话就是ASP.NET MVC通过ModelMetadata可以对模型的属性是否只读、是否必填、类型等相应信息进行描述,甚至还包含了模型验证器来完成合法性验证。
这里需要注意的是模型类型本身通过一个ModelMetadata来描述,而类型的属性同样被ModelMetadata描述,就是说ModelMetadata描述类型的结构是与对应类结构有相同的层次。
注:ModelMetadata涉及到View的渲染,关于View的内容会在后续文章中介绍。
简单模型与复杂模型
在ModelMetadata类型中有一个名为IsComplexType的属性,用于表示该类型是否为复杂类型,那么什么是复杂类型?相对应的什么是简单类型?
上图是IsComplexType的实现代码,其核心有两个点:
1. 通过当前模型类型来获取一个转换器(注:TypeDescriptor是一个用来获取类型相关信息的类型,如特性、属性、事件等,当然也包括类型转换器,下图是TypeDescriptor部分定义)。
2. 获取到类型转换器后,通过转换器判断该类型是否能够从字符串转换,如果能那么它就是简单类型否则为复杂类型。(下图为TypeConverter的部分定义)
知道了简单类型与复杂类型的区别,那么它们在MVC中有什么意义呢?
首先对于ASP.NET MVC来说,它接收到的Http请求无论Header、Body等它实质上都是字符串,那么根据Http协议从中取出来的数据也是字符串,但是对于MVC的Action方法来说,它接受的参数可能是字符串的,也可能是数字、时间等其它类型,所以这里需要一个从字符串到其它类型的转换过程,而简单类型的目的就在于MVC进行模型绑定时可以直接根据名称从ValueProvider中获取到值(一个字符串),然后通过类型转换器将该字符串转换成所需类型。
下面就介绍一些“理所当然”的类型转换器:
● 数字:下图是数字转换器的基类,它的CanConvertFrom方法的实现直接硬编码了能够从string类型转换数字类型(不同数字类型有具体的实现,这里不再介绍)。
● 时间:同样的时间类型也能从字符串转换。
● 布尔:布尔类型能够从字符串转换。
为什么说“理所当然”?因为在实际的开发中某action需要一个时间参数,那么在表单中填写或者通过一些js组件选择一个日期,然后提交到服务器这个填写的时间“就是”一个时间类型,填写的数字也就是数字类型,一切都是理所当然的。但是打着没有“魔法”的目的,需要知道的是,哪怕最简单的布尔类型实际上也进行了转换工作,下图就是Boolean转换器的转换代码:
在MVC中简单模型和复杂模型绑定的方式是不同的,也很容易理解简单模型仅需要一个字符串就可以完成转换,而复杂模型还需要更多的操作。
在ASP.NET MVC中还有一种用法,就是自定义将特殊格式的字符串转换成特定对象,常用的例子就是坐标(经纬度)的转换,下面将使用逗号分隔字符串的格式定义用户注册的RegisterViewModel:
1、创建一个RegisterViewModel的转换器,继承TypeConverter类型并重载CanConvertFrom及ConvertFrom方法即可:
2、使用TypeConverter特性在RegisterViewModel类型上使用该转换器:
3、通过Postman模拟请求:
注:model为action参数名称。
Action能够正确的获取到数据:
小结
本篇内容主要是介绍了ASP.NET MVC中模型绑定的主要组件与概念,ValueProvider提供数据、ModelMetadata描述数据、ModelBinder绑定数据,绑定数据过程中不可或缺的数据验证、转换功能分别对应了ModelValidation以及TypeConverter类型。下一篇文章将介绍ASP.NET MVC在Controller执行时如何结合这些组件实现模型绑定的逻辑。
PS.由于篇幅较长所以将模型绑定解析的内容分为两篇,下篇将尽快整理并发出。祝各位元宵快乐(*^_^*)
参考:
http://stephenwalther.com/archive/2009/02/25/asp-net-mvc-tip-49-use-the-linqbinarymodelbinder-in-your
https://www.cnblogs.com/Cwj-XFH/p/5977508.html