ASP.NET MVC4 框架揭秘


AS P. NET 孔1VC4 。蒋金楠著 穹手;r.;幸也版社· Publishing House of Electronics Industry 北京 .BEIJING内容简介 针对最新版本的 ASP.NET MVC 4 ,深入剖析底层框架从请求接收到响应回复的整个处理流程(包 括 URL 路由、 Controller 的激活、 Model 元数据的解析、 Model 的绑定、 Model 的验证、 Action 的执行、 View 的呈现和 ASP.NET Web API 等),并在此基础上指导读者如何通过对 AS 丑陋 TMVC 框架本身的 扩展解决应用开发中的实际问题。 未经许可,不得以任何方式复制或抄袭本书之部分或全部内容。 版权所有,侵权必究。 图书在版编目 (CIP) 数据 ASP.NET MVC 4 框架揭秘/蒋金楠著.一北京:电子工业出版社, 2013.1 ISBN 978-7-121-19049-0 1.(A … II. ①蒋… II I.①网页制作工具一程序设计 N. ( TP393.092 中国版本图书馆 CIP 数据核宇 (2012) 第 281603 号 策划编辑:张春雨 责任编辑:葛娜 印 刷:北京东光印刷厂 装 订:三河市鹏成印业有限公司 出版发行:电子工业出版社 北京市海淀区万寿路 173 信箱 邮编 100036 开 本: 787X 1092 1/16 印张: 37 字数: 855 千字 印 次: 2013 年 1 月第 1 次印刷 印数: 3000 册定价: 89.00 元 凡所购买电子工业出版社图书有缺损问题,请向购买书店调换。若书店售缺,请与本社发行部联 系,联系及邮购电话: (010) 88254888 。 质量投诉请发邮件至 zlts@phei.com.cn ,盗版侵权举报请发邮件至 dbqq@phei.com.cno 服务热线: (010) 88258888 。~ 目 U C=J ASP.NETMVC 是一个建立在 ASP.NET 平台上基于 MVC 模式的 Web 开发框架,它提供 了一种与传统 Web Forms 完全不同的 Web 应用开发方式。 ASP.NET Web Forms 借鉴了 Windows F orms 基于控件和事件注册的编程模式,使 Web 应用的开发变得简单而快捷,但是 它却使开发人员与 Web 的本质渐行渐远。 ASP.NETMVC 是-种回归,它使开发人员可以真 正地面向 Web 进行编程,我们面对的不再是拖拉到 Web 页面的控件,而是整个 HTTP 请求 和响应的流程。 这不是一本 ASP.NETMVC 入门书籍 我个人觉得掌握 ASP.阳 T MVC 具有三个层次。了解基本的编程模式,掌握 Controller 和 View 的定义方式,知道路由如何注册,以及验证规则如何定义,此为第一层次。第二层 次要求我们对 AS 卫 NETMVC 框架本身从请求接收到响应回复的整个流程具有一个清晰的认 识,包括请求如何被路由、目标 Controller 如何被激活、 Model 元数据如何被解析、 Action 方法如何被执行、 View 如何呈现等。 ASP.NETMVC 本身是一个极具可扩展的开发框架,合 理利用其扩展性可以解决很多开发中的实际问题,而掌握 AS 丑陋 TMVC 的最高层次就是凭 着对框架本身的运行机制的了解准确地找到相应的扩展点,并创建相应的扩展来解决我们遇 到的问题。本书不是一本 AS 卫 NETMVC 入门书籍,而是让处于第一层次的读者快速进入第 二和第三层次的书。 这是一本讲述 ASP.NETMVC 框架本质的书 很多 .NET 开发人员都在抱怨微软开发技术过快的更新频率让他们无所适从。其实他们 看到的只是单纯的版本升级而己,一些本质的东西一直是"稳、定"的。微软推出.阳T 战略 已经十多年了, CLR 却只有四个版本而己。最新版本的 ASP.NET 虽然表面上已经看不到太 多最初的影子,但是整个请求处理的管道一直未曾改变。对于一项开发技术,只要我们了解 了它最根本的一些东西,就不应该惧怕其高频率的版本更替,而应该热烈拥抱它。本书力求 ASP. NET MVC 4 在架揭秘iv 醺前言 将关于 ASP.NETMVC 框架最根本的东西带给大家,而不是罗列一些简单的编程技巧。 这是一本实用的书 可能有人觉得这本剖析 ASP.NETMVC 框架运行原理的书没有什么"实际"的意义,因 为我们每天的日常工作就是编程,知道了 ASP.NETMVC 从请求接收到响应回复之间整个处 理流程并不会对我们的工作造成实质性的改变。其实这种想法是极端错误的,因为我们编写 的程序最终是在 AS丑陋TMVC 框架上运行的,程序的高效性决定于它是否能够最大限度地 "迎合"框架的运行机制,所以了解 ASP.NETMVC 框架的运行原理有利于我们写出高质量 的程序。 我个人将基于 ASP.NETMVC 的编程分为两类, ep 咽rtJ~务编程"和"面向框架编程"。 前者根据具体的业务逻辑定义 Controller 和设计 View ,这是大部分 Web 开发人员的主要工 作:后者则是为整个 Web 应用搭建一个框架,让最终的开发人员只需要关注具体的业务逻 辑,而让框架来完成所有与业务无关的部分。对于后者,我们可以充分利用 ASP.阳TMVC 的扩展性,通过自定义的扩展将非业务的功能自动"注入"到业务逻辑的处理流程中,这样 不仅可以提高开发效率,而且还能提高开发质量。本书在剖析 ASP.NETMVC 框架运行机制 过程中几乎列出了其所有的扩展点,并且通过实例演示的形式提供了很多实用的扩展。 可以将本书视为一本"架构设计"的书 在我的周围存在这样的一些人,他们以刚毕业一两年的毕业生为主,他们大都工作勤奋、 聪明好学,手中经常捧着 GoF 的《设计模式)) ,总是希望将书中的设计模式应用到具体项目 之中,或者希望通过项目的实践来印证他们在书本上的设计模式,但是理论和实践之间的距 离总让他们感到困惑。 要从真实的项目或者产品中学习"实用"的软件架构设计知识,先得确定目标项目或者 产品中采用的架构思想和设计模式是正确的,而我们参与的很多项目其实被"架构"得一塌 糊涂。对于像 ASP.NET 这样的产品,其基础架构能够在很长一段时间内保持不变,本身就 证明了应用在上面的架构设计的正确性,它们不正是我们学习架构设计最好的素材吗?本书 对 ASP.NETMVC 框架的运行机制进行了深入剖析,实际上是将 ASP.NETMVC 的整个设计 展示在读者面前,读者朋友们也许可以将本书作为一本"架构设计"的书来读。 本书的写作特点 我想本书的读者可能很多都读过我的 < 的模板方法过程中是如何控制最终生成的 H刊伍的。 本章的最后关注于以 ModelMetadataProvider 为核心的 Model 元数据提供机制,以及如何通 过自定义 ModelMetadataProvider 实现对 Model 元数据提供机制的定制。 第 5 章 Model 的绑定 ASP.NETMVC 的 Model 绑定旨在为目标 Action 方法提供参数列表。 ParameterDescriptor 为 Model 绑定提供了相关的元数据信息,本章以介绍 ParameterDescriptor 以及相关的 ControllerDescriptor 和 ActionD escI悄or 作为开篇。 Model 绑定所需的最终数据通过 ValueProvider 来提供,本章接下来会对实现在各种不同 ValueProvider 中的数据值提供机制, 以及以 ValueProviderFactory 为核心的 ValueProvider 提供机制进行全面而深入的介绍。本章 的最后部分着重介绍以 ModelB inder 为核心的 Model 绑定系统,以及实现在 DefaultModelB inder 中的默认 Model 绑定机制。 第 6 章 Model 的验证 Action 方法在执行之前需要通过 Model 验证机制确保提供参数的有效性。本章会着重讲 述以 ModelValidator 为核心的 Model 验证系统,以及通过 ModelValidatorProvider 实现的 ModelValidator 提供机制。 Model 验证是伴随着 Model 绑定进行的,具体执行流程的介绍也 ASP. NET MVC 4 在架揭翻前言黯 vii 包含在本章之中。 ASP.NETMVC 利用 ValidationAttribute 特性为 Model 验证提供了一种声明 式编程方式,其背后的实现机制是本章重要讲述的内容。 jQuery 验证框架被默认用于客户端 验证, jQuery 验证的编程方式,以及与 ASP.NETMVC 验证系统的协作方式会在本章的最后 一部分予以介绍。 第 7 章 Action 的执行 针对请求的处理最终体现在对目标 Action 方法的执行上面。 Action 方法可以以同步或 者异步的方式执行,所以本章以介绍两种不同的异步 Action 编程模式作为开篇:此外,同 步与异步的差异体现在整个请求的处理过程中, MvcHandler 、 Controller 、 ActionInvoker 、 ControllerDescriptor 和 ActionD escriptor 等都具有同步和异步的版本,本章会对它们作一个系 统的比较。 Action 的执行还伴随着筛选器的执行,在本章的最后对四种筛选器的作用和执行 流程进行单独介绍。 第 8 章 View 的呈现 ActionResult 作为执行 Action 返回的结果,实现了对请求的最终响应,本章介绍了所有 预定义的 ActionResult 分别是如何完成针对请求的响应的。作为最重要的 ActionResult , ViewResult 将整个预定义的 View 呈现出来,而它背后是一套完整的 View 引擎, View 引擎 的运行机制,以及与 ViewResult 的协作方式是本章介绍的一个重点。 ASP.NETMVC 默认提 供了 ASPX 和 Razor 这两种原生 View 引擎的支持,针对 Razor 引擎的深入剖析被放在本章 的最后一部分。 第 9 章 ASP.NET Web API ASP.NET Web API 使我们可以很容易地定义 REST 服务,本章会提供 WebAPI 基本编程 模式的介绍。 ASP.NET Web API 采用了与 ASP.NETMVC 独立但类似的执行管道,对整个管 道从请求接收到响应回复的整个流程的介绍是本章的重点,包括 H忧pController 的激活与执 行、 Action 的选择、 Model 元数据的解析、 Action 参数的绑定与执行等。 第 10 章案例实践 本章提供了一个名为 Video Mall (简称 VM) 的在线电子商务购物网站来模拟 AS卫NET MVC 在真实项目中的应用。 VM 以 SQL Server 作为数据存储,并采用 Entity Framework 作 为 ORM 框架进行数据存取。 VM 利用了在前面章节中定义的一系列扩展,同时还涉及了一 些架构思想和涉及模式,比如模块化设计、 IoC 、 AOP 以及 Repository 等。 ASP. NET MVC 4 框架揭秘viii 11 前言 关于作者 蒋金楠(网名 Artech) 现就职于某知名软件公司担任高级软件顾问。连续 5 届微软 MVP (最有价值专家),同时也是少数的双料 MVP C Solutions Architecture + Connected System) 之 一。国内较早接触 WCF 的人之一, 2007 年 2 月起在个人博客 Ch句 ://www.cnblogs.comJartech) 上发表超过两百篇深入介绍 WCF 的文章,成为了目前国内 WCF 在线资料的主要来源。 致谢 本书得以出版,需要感谢本书的编辑张春雨先生和葛娜小姐,你们的专业水准和责任心 是为本书提供的质量保证,期待着与你们第三度合作的机会。此外,最需要感谢的是我的老 婆徐如娇,只有我知道你在本书提交给出版社之前所作的校对工作有多么重要。 本书支持 本书针对最新版本的 ASP.阳TMVC ,同时涉及太多底层实现的内容,所以大部分内容 是找不到任何现成参考资料的,这些内容大都来自于作者对源码的分析和试验的证明。本书 的最初版本是根据 AS丑陋TMVC 4 Beta 撰写的,差不多快写完的时候微软发布了 AS丑陋T MVC 4 RC ,然后我根据 RC 对原来的内容作了不小的改动。在 ASP.NET MVC 4 正式推出 之后,我第一时间联系到了 Sco忧 Guthrie ,从他们团队得到了一份正式版与 RC 之间变化的 列表,并据此又作了一些修改。这些因素加上我本人能力的限制,都可能造成一些无法避免 的错误或者偏差,如果读者在阅读过程中发现了任何问题,希望能够反馈给我。如果读者遇 到任何 ASP.NETMVC 或者是 WCF 的问题,也欢迎与我通过以下的方式进行交流。 • 作者博客: http://www.cnblogs.comJartech • 作者微博: h忧p://www.weibo.comJa白ch • 电子邮箱: jiangjinnan@gmail.com 本书每一章节都会提供一系列实例演示,读者可以根据编号(比如 S101 、 S202 等)从 下载的源代码压缩包中找到对应的实例。本书的附录给出了所有源代码可供下载的所有的实 例演示的列表和相关描述。 • 源代码下载地址: http://files.cnblogs.comJartech/asp.net.mvc.4.samples.rar ASP. NET MVC 4 框架揭秘目录 第 1 章 ASP.NET + MVC...... .. 1. 1 传统 MVC 模式………………………………………………………………………………………………………… '2 1.1.1 自治视图…….......….......….........….........................…..........................….........................… "2 1.1 .2 什么是 MVC 模式…·…………………………………………………………………………………………… '3 1.2 MVC 的变体….......……………………………………………………………………………………………………… 4 1.2.1 MVP…………………………………………………………………………………………………………………… 4 1.2.2 Model 2 …….......….......…·…........…..................…........…................…..................….........… 12 1.2.3 ASP.NETMVC 与 Mode12 ……..............................................…...............… ......................13 1.3 IIS/ ASP .NET 管道…..…………………………………………………….......… ........................................14 1.3.1 IIS 5.x 与 ASP.NET …….......…................……………………………………………………………… "14 1.3.2 IIS 6.0 与 ASP.NET …·………………………………………………………………………………………… 15 1.3.3 IIS 7.0 与 ASP.NET …………………………………………………………………………………………… 17 1.3.4 ASP.NET 管道...………………………………………………………………………………………………… m 1.4 ASP.NET MVC 是如何运行的……………………………………………………………………………… "25 1.4.1 建立在"迷你版" ASP.NET MVC 上的 Web 应用.......…........…·……................… ........25 1.4.2 URL 路由……...............…....………………………………………………………………………………… '27 1.4.3 Controller 的激活…..…………………………………………………………………………………………..引 1.4.4 Action 的执行.................................................………………………………………………… ......35 本章小结.......…… ..................................................................................................................................39 第 2 章 URL 路由...............……………………………………………………………………………………………………… '41 2.1 ASP.NET 路由系统….......….......………………………………………………………………………………… c 2. 1. 1 请求 URL 与物理文件的分离…………………………………………………………………………… "42 2. 1.2 实例演示:通过 URL 路由实现请求地址与 .aspx 页面的映射 (S201) ....................43 2.1.3 Route 与 RouteTable …………………………………………………………………………………………… 46 2. 1.4 路由映射…………………………………………………………………………………………………………… 52 2.1.5 根据路由规则生成 URL …… ............................................................................................59 ASP. NET MVC 4 框架揭秘X 部目录 2.2 ASP.NET MVC 扩展 ..............................................................................................................61 2.2.1 路由映射…….......….........….........................…................….............................................… 61 2.2 .2 实例演示:注册路由映射与查看路由信息 (S208) ...................................................62 2.2.3 缺省 URL 参数...............….......….......…..……………………………………………………………… '65 2.2 .4 基于 Area 的路由映射……………………………………………………………………………………… "67 2.2.5 链接和 URL 的生成…..……………………………………………………………………………………… '71 2.3 动态 H即 Handler 映射…..………………………………………………………………………………………… 78 2 .3 .1 UrlRoutingModule ……………………………………………………………………………………………… 78 2 .3.2 PageRouteHandler 与 MvcRouteHandler" .......…................…… .......................................79 2.3 .3 ASP.NET 路由系统扩展.........…………………………………………………………………………… "80 本章小结…..………………………………………………………………………………………………………………………… 85 第 3 章 Controller 的激活 .....................................................................................................................86 3.1 总体设计.......……………………………………………………………………………………………………………..幻 3. 1. 1 Controller ..............… .........................................................................................................87 3.1.2 ControllerFactory......... ……………………………………………………………………………………… "92 3.1 .3 ControllerBuilder …........…..................…...........................… .............................................93 3. 1. 4 Controller 的激活与 URL 路由.........…………………………………………………………………… 99 3.2 默认实现………………………………………………………………………………………………………………… '101 3.2.1 Controller 类型的解析……………………………………………………………………………………… 102 3.2.2 Controller 类型的缓存……………………………………………………………………………………… 105 3.2.3 Controller 的释放和会话状态行为的控制…..……………………………………………………… 106 3 .3 IoC 的应用…..………………………………………………………………................................… ...............108 3 .3.1 从 Unity 来认识 IoC ……………………………………………………………..............… .................108 3.3 .2 Controller 与 Model 的分离…..………………………………………………………………………… "110 3.3.3 基于 IoC 的 ControllerFactory …………………………………………………………………………… 111 3 .3.4基于 IoC 的 ControllerActivator …….......….....................…........................................… '117 3.3 .5 基于 IoC 的 DependencyResolver" .......…..........................…… ......................................119 本章小结………………………………………………........................................……………………………………… "122 第 4 章 Model 元数据的解析….......……………………………………………………………………………………… m 4.1 Model 元数据及其定制…..…………………………………………………………………………………… "124 4.1.1 Model 元数据层次化结构…..……………………………………………........….......................… 124 4. 1. 2 基本 Model 元数据信息…………………………………………………………………………………… 125 4.1.3 Model 元数据的定制…..…………………………………………………………………………………… 128 4.1 .4 IMetadat aAware 接口……………………………………………………………………………………… "142 4.2 Model 元数据与 Model 模板….......……………………………………………………………………… "146 ASPNETMVC4 框架揭秘目录黯 xi 4.2.1 实例演示:通过模板将布尔值显示为 RadioButton (S409) ...... .........…...............… 147 4.2.2 预定义模板…..……........…………………………………………………………………………………… "148 4.2.3 DataTypeName 与模板名称….......….......….......…................…........… ............................157 4.2.4 模板的获取与执行….................………………………………………………………………………… '160 4.2.5 实例演示:通过定制 Model 元数据和自定义模板实现预定义列表的呈现 (S412) ................................….........….................................….....................................164 4.3 Model 元数据的提供机制….......….......….......… ..................................................................172 4.3.1 再谈 ModelMetadata ….....................................….......….......….......….......….......… .........172 4.3 .2 ModelMetadataProvider.......…..............................................................................:........176 4.3.3 Model 元数据提供系统的扩展..........………………………………………………………………… 180 本章小结...............….......................…....................…….......….....................….......................…........… 182 第 5 章 Model 的绑定…………………………………………………………………………………………………………… 183 5.1 ControllerDescriptor 、 ActionDescriptor 与 ParameterDescriptor … .........................184 5. 1. 1 ControllerDescriptor .. .......….................................................................….................…… 184 5.1 .2 ActionDescriptor ....... ..........…............….............................................…........…...............189 5.1.3 ParameterDescriptor........….........….............................…..............................................… 193 5.2 ValueProvider........................…..........................…...................…..............…...........................196 5.2.1 Name ValueCollection ValueProvider.......................…........….......…..................…...........197 5.2.2 DictionaryValueProvider.................……………………………………………………………………"203 5.2.3 ValueProviderFactory………………………………………………………………………………………… 211 5.2.4 ValueProviderFactories ........………………………………………………………………………………'211 5.3 ModelBinder........................………………………………………………………………………………………'215 5.3 .1 ModelB inder 与 Mode lB inderProvider………………………………………………………………… 215 5.3 .2 ModelState 与 Model 绑定…….......….........….........… .....................................................223 5.3.3 ModelB indingContext 的创建…….......….................................................….................… 227 5.4 Model 绑定的默认实现…...........................….......….................….......….......…..............…… 228 5.4.1 简单类型…..…………………………………………………………………………………………………… "229 5.4.2 复杂类型………………………………………………………………………………………………………… '232 5.4.3 数组...............…………………………………………………………………………………………………… 238 5.4.4 集合………………………………………………………………………………………………………………… 246 5.4.5 字典….......…..…………………………………………………………………………………………………… 248 本章小结………………………………………………………………………………………………………………………… '252 第 6 章 Model 的验 i.iE ...............……………………………………………………………………………………………… 254 6.1 ModelValidator 与 ModelValidatorProvider' ………………………………………………………… 255 6.1.1 ModelValidat时………………………………………………………………………………………………… '255 ASP. NET MVC 4 在架揭秘xii 跚目录 6.1.2 ModelValidatorProvider' ….........………………………………………………………………………… '258 6.1 .3 ModelValidatorProviders .... .....…………………………………………………………………………… 264 6.2 Model 绑定与验证………………………………………………………………………………………………… '269 6.2.1 ModelState …·……………………………………………………………………………………… ...............269 6.2.2 验证消息的呈现..................................................…...............…….....................…… ........272 6.2 .3 Model 绑定中的验证.................…........……………………………………………………………… '278 6.3 基于数据注解特性的 Model 验证………………………………………………………………………… 283 6 .3 .1 Validatio nA ttribute 特性…………………………………………………………………………………… '283 6.3.2 DataA nnotationsModelValidator" ……………………………………………………………………… "290 6 .3 .3 DataAnnotationsModelValidatorProvider ....... ..,…………………………………………………… '292 6.3 .4 将 Validatio nA ttribute 应用到参数上………………………………………………………………… '295 6 .3 .5 一种 Model 类型,多种验证规则.........….......…………………………………………………… "300 6.4 客户端验证…..………………………………………………………………………………………………………… '3 们 6 .4 .1 jQuery 验证…..………………………………………………………………………………………………… '307 6 .4 .2 基于 jQuery 的 Model 验证.................…....................................................................... 311 6 .4.3 自定义验证…................…………………………………………………………………………………… "315 本章小结….......….........….......…........…................…........…........…….........…........….........…·….......… 318 第 7 章 Action 的执行.......…...............….......................、........…………………………………………………… "320 7.1 异步 Action 的定义….......….............………………………………………………………… ...................321 7. 1. 1 基于线程池的请求处理机制.......……………………………………………………………………… '321 7.1 .2 两种异步 Action 方法的定义............................................................…..… ....................322 7.1 .3 AsyncManager ……..............……………………………………………………………………………… "324 7.2 Action 方法的执行...............…………………………………………………………………………………… '330 7.2.1 MvcHandler 对请求的处理……………………………………………………………………………… "330 7.2.2 Controller 的执行….,………………………………………………………………………………………… 330 7 .2.3 Actio nI nvoker 的执行….................................…............................................….........… "331 7.2 .4 ControllerDescriptor 的同步与异步...................…·……………………………………………… "336 7.2 .5 Actio nD escriptor 的执行…………………………………………………………………………………… 339 7.3 筛选器的执行………………………………………………………….........…………………………………… '345 7.3.1 Filter 及其提供机制………………………………………………………………………………………… '345 7 .3 .2 Authorizatio nF i1 ter" ….......………………………………………………………………………………… '355 7 .3.3 Actio nF ilter........ …·…………………………………………………………………………………………… 365 7.3.4 Exceptio nF ilter..........................ι................................................................................ … 371 7 .3 .5 实例演示:集成 EntLib 实现自动化异常处理 (S713 , S714 , S715) ........................373 7.3.6 ResultFilter" ….........….........…........................…...............… ............................................387 本章小结................................................….......…..….........…….......................................…….........…… 388 ASP. NET MVC 4 框架揭秘目录黯 xiii 第 8 章 View 的呈现…..………………………………………………………………………………………………………… 390 8.1 ActionResult ……....................................................................…........…....................................391 8.1.1 EmptyResult.................................………………………………………………………………………"391 8. 1.2 ContentResult ....................….................…................…..................….........…...................392 8.1.3 FileResult …...............................................….........….................................…...................398 8.1 .4 JavaScriptResult ....................…...........................…......................................................"'402 8. 1.5 JsonResult ………………………………………………………………………………………………………..405 8.1.6 HttpStatusCodeResult .................…..................…................................................….........408 8.1.7 RedirectResultIR.edirectToRouteResult.................……................................................…'409 8.2 ViewResult 与 ViewEngine …………………………………………………………………………………… '411 8.2.1 View 引擎中的 View" …........…………………………………………………………………………… "411 8.2.2 ViewEngine" …................…………………………………………………………………………………… 4 日 8.2.3 ViewResult 的执行.................…………………………………………………………………………… '415 8.3 Razor 引擎….................................…·…………………………………………………………………………… 423 8.3 .1 View 的编译原理…….......................….........…................……… .....................................423 8.3 .2 Web ViewPage 与 WebViewPage…………………………………………………………'427 8.3.3 RazorView.........………………………………………………………………………………………………..432 8.3 .4 RazorViewEngine ..........................……...........................................................................441 本章小结….......…................................................…………………………………………………………………… ..444 第 9 章 ASP.NETWebAPI …..…………………………………………………………………………………………… ..445 9.1 Web 、 REST 与 WebA凹……………………………………………………………………………………… '446 9. 1. 1 Web 如此简单….......….........…..……………………………………………………………………… "446 9. 1.2 REST 是什么…………………………………………………………………………………………………… 447 9. 1.3 ASP.NET Web A凹…………………………………………………………………………………………… '450 9.2 服务端管道……………………………………………………………………………………………………………… 458 9.2.1 ASP.NET Web API 管道式设计………………………………………………………………………… '459 9.2.2 HttpMessageHandler…..............................................................................................…"461 9.2.3 HttpServer…….......….........……..........................................…........................................…464 9.2.4 实例演示:自定义 HttpMessageHandler 实现 HTTP 方法重写 (S903) .................469 9.3 H仕pControllerDispatcher' …................……………………………………………………………………… '471 9.3.1 HttpController 的激活...............….......….......….......… ....................................................472 9.3.2 HttpController 的执行…..………………………………………………………………………………… "485 9.3 .3 Action 的选择………………………………………………………………………………………………… "486 9.3 .4 Model 元数据的解析...............………………………………………………………………………… "492 9.3.5 Action 参数绑定...............…............................………..............…….......…..… ...................495 9.3.6 Model 验证….......…………………………………………………………………………………………… "508 ASPNETMVC4 框架揭秘xiv 黯目录 9.3.7 Action 的执行与结果的响应…………………………………………………………………………….. 512 9.4 Web A凹的调用和自我寄宿………………………………………………………………………………… 516 9.4.1 HttpClient..….......................................….................................…...................…........……'516 9.4.2 HttpSelfHostServer …·……………………………………………………………………………………….. 521 本章小结….................………………………………………………………………………………………………………… '525 第 10 章案例实践….......……………………………………………………………………………………………………… "527 10.1 功能性简介….........………………………………………………………………………………………………… 528 10. 1.1 商品列表的呈现........……………………………………………………………………………………… 528 10. 1.2 定购商品........……………………………………………………………………………………………...... 530 10. 1.3 登录与错误页面….........…...............……………………………………………………….............. 531 10.2 设计概述………………………………………………………………………………… .................................532 10.2.1 Controller-Service-Repository ........……..............…………………………………………………'532 10.2.2 IoC 的应用.................…·………………………………………………………………………………… "536 10.2.3 AOP 的应用………………………………………………………………………………………………… "539 10.2.4 异常处理.................................................…..................................….........…..… .............545 10.3 编程实现...............….......….....................…………………………………………………………….......… 546 10.3 .1 数据表的创建…….......…........…..............…………………………………………………………… '546 10.3 .2 Repository …·…………………………………………………………………………………………………"548 10.3.3 Service…………………………………………………………………………………………………………… 552 10.3.4 路由注册和布局…............................…........…..................……..................................… 555 10.3 .5 ProductController'…..…................................................................................….........….. 558 10.3 .6 OrderController.......................……........…...............................…...................................565 10.3 .7 AccountController'…........................………………………………………………………………….. 571 本章小结…………………………………………………………………………………………………………………………… 574 附录 A 实例列表…………………………………………………………………………………………………………………… '575 ASP. NET MVC 4 框架揭秘第 1 章 ASP.NET + MVC AS 卫 NET MVC 是一个全新的 Web 应用框架。将术语 ASP.NET MVC 拆 分开来,即 ASP. 阳 T+MVC ,前者代表支撑该应用框架的技术平台,意味着 ASP.NETMVC 和传统的 WebFonns 应用框架一样都是建立在 ASP.NET 平台 之上;后者则表示该框架背后的设计思想,意味着 ASP.NETMVC 采用了 MVC 架构模式。 ASP. NET MVC 4 框架揭秘2 .第 1 章 AS P. NET + MVC 1.1 传统 MVC 模式 对于大部分面向最终用户的应用来说,它们都需要具有一个可视化的 UI 界面与用户进 行交互,我们将这个 UI 称为视图 (View) 。在早期,我们倾向于将所有与 UI 相关的操作揉 合在一起,这些操作包括 UI 界面的呈现、用于交互操作的捕捉与响应、业务流程的执行以 及对数据的存取,我们将这种设计模式称为自治视图 (Autonomous View , AV) 。 1.1.1 自治视国 说到自治视图,很多人会感到陌生,但是我们(尤其是 .NET 开发人员)可能经常在采 用这种模式来设计我们的应用。 Windows F orms 和 ASP.NET Web Forms 虽然分别属于 GUI 和 Web 开发框架,但是它们都采用了事件驱动的开发方式,所有与 UI 相关的逻辑部可以定 义在针对视图 (Windows F orms 或者 Web Forms) 的后台代码( Code Behind) 中,并最终注 册到视图本身或者视图元素(控件)的相应事件上。 一个典型的人机交互应用具有三个主要的关注点,即数据在可视化界面上的呈现、 UI 处理逻辑(用于处理用户交互式操作的逻辑)和业务逻辑。自治视图模式将三者混合在一起, 势必会带来如下一些问题: • 业务逻辑是与 UI 无关的,应该最大限度地被重用。由于业务逻辑定义在自治视图中, 相当于完全与视图本身绑定在一起,如果我们能够将 UI 的行为抽象出来,基于抽象化 u 的处理逻辑也是可以被共享的。但是定义在自治视图中的 UI 处理逻辑完全丧失了重 用的可能。 • 业务逻辑具有最强的稳定性, UI 处理逻辑次之,而可视化界面上的呈现最差(比如我 们经常会为了更好地呈现效果来调整 HTML) 。如果将具有不同稳定性的元素融为一 体,那么具有最差稳定性的元素决定了整体的稳定性,这是"短板理论"在软件设计 中的体现。 • 任何涉及 UI 的组件都不易测试。 UI 是呈现给人看的,并且用于与人进行交互,用机器 来模拟活生生的人来对组件实施自动化测试不是一件容易的事,自治视图严重损害了组 件的可测试性。 为了解决自治视图导致的这些问题,我们需要采用关注点分离( Seperation of Concems , SoC) 的方针将可视化界面呈现、 UI 处理逻辑和业务逻辑三者分离出来,并且采用合理的交 互方式将它们之间的依赖降到最低。将三者"分而治之",自然也使 UI 逻辑和业务逻辑变得 更容易测试,测试驱动设计与开发变成了可能。这里用于进行关注点分离的模式就是 MVC 。 ASP. NET MVC 4 框架揭秘1.1 传统 MVC 模式 • 3 1.1.2 什么是 MVC 模式 MVC 的创建者是 Trygve M. H. Reenskau ,他是挪威的计算机专家,同时也是奥斯陆大 学的名誉教授。 MVC 是他在 1979 年访问施乐帕克研究中心 (Xerox Palo Alto Research Center, XeroxPARC) 期间提出一种主要针对 GUI 应用的软件架构模式。 MVC 最初用于 SmallTalk , Trygve 最初对 MVC 的描述记录在 Applications Programming in Smallta/k-80 (TM) :How ω use Mode/- 阿ew-Controller (MV。这篇论文中,有兴趣的读者可以通过地址 http://st-www.cs .i llinois.eduJ users/ smarch/ st -docs/mvc.html 阅读这篇论文。 MVC 体现了关注点分离这一基本的设计方针,它将构成一个人机交互应用涉及的功能 分为 Model 、 Controller 和 View 三部分,它们各自具有相应的职责。 • Model 是对应用状态和业务功能的封装,我们可以将它理解为同时包含数据和行为的领 域模型 (Domain Model) 0 Model 接受 Controller 的请求并完成相应的业务处理,在状态 改变的时候向 View 发出相应的通知。 • View 实现可视化界面的呈现并捕捉最终用户的交互操作(比如鼠标和键盘操作)。 • View 捕获到用户交互操作后会直接转发给 Controller ,后者完成相应的 UI 逻辑。如果 需要涉及业务功能的调用, Controller 会直接调用 Model 。在完成 UI 处理之后, Controller 会根据需要控制原 View 或者创建新的 View 对用户交互操作予以响应。 图 1-1 揭示了 MVC 模式下 Model 、 View 和 Controller 之间的交互。对于传统的 MVC 模式,很多人认为 Controller 仅仅是 View 和 Model 之间的中介,实则不然, View 和 Model 存在直接的联系。 View 可以直接调用 Model 查询其状态信息。当 Model 状态发生改变的时 候,它也可以直接通知 View 。比如在一个提供股票实时价位的应用中,维护股价信息的 Model 在股价变化的情况下可以直接通知相关的 View 改变其显示信息。 State Notification S1ate Change State Querý View 图 1-1 Model-View-Controller 之间的交互 从消息交换模式的角度来讲, Model 针对 View 的状态通知和 View 针对 Controller 的用 户交互通知都是单向的,我们推荐采用事件机制来实现这两种类型的通知。从设计模式的角 度来讲就是采用观察者 (Observer) 模式通过注册/订阅的方式来实现它们,即 View 作为 Model 的观察者通过注册相应的事件来检测状态的改变,而 Controller 作为 View 的观察者通过注 ASP. NET MVC 4 框架揭秘4 源第 1 章 ASP.NET + MVC 册相应的事件来处理用户的交互操作。 我看到很多人将 MVC 和所谓的"三层架构"进行比较,其实两者并没有什么可比性, MVC 更不是分别对应着 UI 、业务逻辑和数据存取三个层次,不过两者也不能说完全没有关 系。 Tηrgve M. H. Reenskau 当时提出 MVC 的时候是将其作为构建整个 GUI 应用的架构模式, 这种情况下的 Model 实际上维护着整个应用的状态并实现了所有的业务逻辑,所以它更多地 体现为一个领域模型。而对于多层架构来说(比如我们经常提及的三层架构), MVC 是被当 成 UI 呈现层 C Presentation Layer) 的设计模式,而 Model 则更多地体现为访问业务层的入 口 CGateway) 。如果采用面向服务的设计,业务功能被定义成相应服务并通过接口(契约) 的形式暴露出来,这里的 Model 还可以表示成进行服务调用的代理。 1.2 MVC 的变体 通过采用 MVC 模式,我们可以将可视化 UI 元素的呈现、 UI 处理逻辑和业务逻辑分别 定义在 View 、 Controller 和 Model 中,但是对于三者之间的交互, MVC 并没有进行严格的 限制。最为典型的就是允许 View 和 Model 绕开 Controller 进行直接交互, View 可以通过调 用 Model 获取需要呈现给用户的数据, Model 也可以直接通知 View 让其感知到状态的变化。 当我们将 MVC 应用于具体的项目开发中,不论是基于 GUI 的桌面应用还是基于 WebUI 的 Web 应用,如果不对 Model 、 View 和 Controller 之间的交互进行更为严格的限制,我们编写 的程序可能比自治视图更加难以维护。 今天我们将 MVC 视为一种模式 CP硝ern) ,但是作为 MVC 最初提出者的 Trygve M. H. Reenskau 却将 MVC 视为一种范例 CP町adigm) ,这可以从它在 Applications Programming in Smalltαlk-80 σ附 :How to use Model- VÏew-Controller (MVC) 中对 MVC 的描述可以看出来 :In the MVC paradigm the user input, the modeljng o[ the 仰'rnal world, αnd the visuαlfi主edbα灿 the user are explicitly separated and handled by three types o[ object, each specialized [or its task. 模式和范例的区别在于前者可以直接应用到具体的应用上,而后者则仅仅提供一些基本 的指导方针。在我看来 MVC 是一个很宽泛的概念,任何基于 Model 、 View 和 Controller 对 UI 应用进行分解的设计都可以成为 MVC 。当我们采用 MVC 的思想来设计 UI 应用的时候, 应该根据开发框架(比如 Windows Forms 、 WPF 和 Web Forms) 的特点对 Model 、 View 和 Con位oller 的界限以及相互之间的交互设置一个更为严格的规则。 在软件设计的发展历程中出现了一些 MVC 的变体 C V:缸ation) ,它们遵循定义在 MVC 中的基本原则,我们现在来简单地讨论一些常用的 MVC 变体。 1.2.1 MVP MVP 是一种广泛使用的 UI 架构模式,适用于基于事件驱动的应用框架,比如 ASP.NET ASPNETMVC4 在架揭秘1.2 MVC 的变体 黯 5 Web Forms 和 Windows Forms 应用。 MVP 中的 M 和 V 分别对应于 MVC 的 Model 和 View , 而 P (Presenter )则自然代替了 MVC 中的 Controller 。但是 MVP 并非仅仅体现在从 Controller 到 Presenter 的转换,更多地体现在 Model 、 View 和 Presenter 之间的交互上。 MVC 模式中元素之间"混乱"的交互主要体现在允许 View 和 Model 绕开 Controller 进 行单独"交流",这在 MVP 模式中得到了彻底解决。如图 1-2 所示,能够与 Model 直接进行 交互的仅限于 Presenter , View 只能通过 Presenter 间接地调用 Modelo Model 的独立性在这 里得到了真正的体现,它不仅仅与可视化元素的呈现 (View) 无关,与 UI 处理逻辑 (Presenter) 也无关。使用 MVP 的应用是用户驱动的而非 Model 驱动的,所以 Model 不需要主动通知 View 以提醒状态发生了改变。 |ModelI : |仁__I 图 1-2 Model-View-Presenter 之间的交互 MVP 不仅仅避免了 View 和 Model 之间的稿合,更进一步地降低了 Presenter 对 View 的 依赖。如图 1-2 所示, Presenter 依赖的是一个抽象化的 View , ep View 实现的接口 IView , 这带来的最直接的好处就是使定义在 Presenter 中的 UI 处理逻辑变得易于测试。由于 Presenter 对 View 的依赖行为定义在接口 IView 中,我们只需要 Mock 一个实现了该接口的 View 就能 对 Presenter 进行测试。 构成 MVP 三要素之间的交互体现在两个方面,即 View lP resenter 和 PresenterIModel 。 Presenter 和 Model 之间的交互很清晰,仅仅体现在 Presenter 对 Model 的单向调用。而 View 和 Presenter 之间该采用怎样的交互方式是整个 MVP 的核心, MVP 针对关注点分离的初衷 能否体现在具体的应用中很大程度上取决于两者之间的交互方式是否正确。按照 View 和 Presenter 之间的交互方式以及 View 本身的职责范围, Martin Folwer 将 MVP 可分为 PV (Passive View) 和 SC (Supervising Controller) 两种模式。 PV 与 sc 解决 View 难以测试的最好的办法就是让它无需测试,如果 View 不需要测试,其先决 条件就是让它尽可能不涉及到 UI 处理逻辑,这就是 PV 模式目的所在。顾名思义, PV (Passive View) 是一个被动的 View ,包含其中的针对 UI 元素(比如控件)的操作不是由 View 自身 主动来控制,而被动地交给 Presenter 来操控。 ASP. NET MVC 4 在架揭秘6 随第 1 章 ASP.NET + MVC 如果我们纯粹地采用 PV 模式来设计 View ,意味着我们需要将 View 中的 U 元素通过 属性的形式暴露出来。具体来说,当我们在为 View 定义接口的时候,需要定义基于 UI 元素 的属性使 Presenter 可以对 View 进行细粒度操作,但这并不意味着我们直接将 View 上的控 件暴露出来。举个简单的例子,假设我们开发的 HR 系统中具有如图 1-3 所示的一个 Web 页 面,我们通过它可以获取某个部门的员工列表。 IO,07:19S2 21 咀9 ' 19Si 销售部 人事部 人事部 图 1-3 员工查询页面 现在通过 ASP.NET Web Forms 应用来设计这个页面,我们来讨论一下如果采用 PV 模式, View 的接口该如何定义。对于 Presenter 来说, View 供它操作的控件有两个,一个是包含所 有部门列表的 DropDownL ist ,另一个则是显示员工列表的 GridView 。在页面加载的时候, Presenter 将部门列表绑定在 DropDownL ist 上,与此同时包含所有员工的列表被绑定到 GridView 。当用户选择某个部门并点击"查询"按钮后, View 将包含筛选部门在内的查询 请求转发给 Presenter ,后者筛选出相应的员工列表之后将其绑定到 GridView 。 如果我们为该 View 定义一个接口 IEmployeeSearchView ,我们不能按照所示的代码将上 述这两个控件直接以属性的形式暴露出来。针对具体控件类型的数据绑定属于 View 的内部 细节(比如说针对部门列表的显示,我们可以选择 DropDownL ist 也可以选择 ListBox) ,不 能体现在表示用于抽象 View 的接口中。另外,理想情况下定义在 Presenter 中的 U 处理逻 辑应该是与具体的技术平台无关的,如果在接口中涉及控件类型,这无疑将 Presenter 也与 具体的技术平台绑定在了一起。 public interface IEmployeeSearchView ←」S ·工L nw we o-­DV pd o· 工 rr nu 户U Departments { get;} Employees { get; } 正确的接口和实现该接口的 View (一个 Web 页面)应该采用如下的定义方式。 Presenter 通过对属性 Departments 和 Employees 赋值进而实现对相应 DropDownL ist 和 GridView 的数 据绑定,通过属性 SelectedDepartment 得到用户选择的筛选部门。为了尽可能让接口只暴露 必需的信息,我们特意将对属性的读/写作了控制。 public interface IEmployeeSearchView ASP. NET MVC 4 框架揭翻1.2 MVC 的变体 黯 7 > e >e qy no ·工 1- rp& tm snL << ee --l bb aa rr eqJe mnm u--u nrn EtE T4ST·- Departrneηts { seti SelectedDepartrnent { geti } Ernployees { seti } public partial class ErnployeeSearchView: page, IErnployeeSearchView //其他成员 public IEnurnerable Departrnents set this.DropDownListDepartrnents.DataSource = valuei this.DropDownListDepartrneηts.DataBind()i public string SelectedDepartrnent get { return this.DropDownListDepartrnents.SelectedValuei} public IEnurnerable Ernployees set this.GridViewErnployees.DataSource = valuei this.GridViewErnployees.DataBind()i PV 模式将所有的 UI 处理逻辑全部定义在 Presenter 上,意味着所有的 UI 处理逻辑都可 以被测试,所以从可测试性的角度来这是一种不错的选择,但是它要求将 View 中可供操作 的 UI 元素定义在对应的接口中,对于一些复杂的富客户端(Rich Client) View 来说,接口 成员将会变得很多,这无疑会提升编程所需的代码量。从另一方面来看,由于 Presenter 需 要在控件级别对 View 进行细粒度的控制,这无疑会提供 Presenter 本身的复杂度,往往会使 原本简单的逻辑复杂化,在这种情况下我们往往采用 SC 模式。 在 SC 模式下,为了降低 Presenter 的复杂度,我们将诸如数据绑定和格式化这样简单的 U 处理逻辑转移到 View 中,这些处理逻辑会体现在 View 实现的接口中。尽管 View 从 Presenter 中接管了部分 UI 处理逻辑,但是 Presenter 依然是整个三角关系的驱动者, View 被 动的地位依然没有改变。对于用户作用在 View 上的交互操作, View 本身并不进行响应,而 是直接将交互请求转发给 Presenter ,后者在独立完成相应的处理流程(可能涉及针对 Model 的调用)之后会驱动 View 或者创建新的 View 作为对用户交互操作的响应。 View 和 Presenter 交豆的规则(针对 sc 模式) View 和 Presenter 之间的交互是整个 MVP 的核心,能否正确地应用 MVP 模式来架构我 们的应用主要取决于能否正确地处理 View 和 Presenter 两者之间的关系。在由 Model 、 View ASP. NET MVC 4 在架揭秘8 翻第 1 章 AS P. NET + MVC 和 Presenter 组成的三角关系中,核心不是 View 而是 Presenter , Presenter 不是 View 调用 Model 的中介,而是最终决定如何响应用户交互行为的决策者。 打个比方, View 是 Presenter 委派到前端的客户代理,而作为客户的自然就是最终的用 户。对于以鼠标/键盘操作体现的交互请求应该如何处理,作为代理的 View 并没有决策权, 所以它会将请求汇报给委托人 Presenter 0 View 向 Presenter 发送用户交互请求应该采用这样 的口吻: "我现在将用户交互请求发送给你,你看着办,需要我的时候我会协助你",而不应 该是这样: "我现在处理用户交互请求了,我知道该怎么办,但是我需要你的支持,因为实 现业务逻辑的 Model 只信任你"。 对于 Presenter 处理用户交互请求的流程,如果中间环节需要涉及到 Model ,它会直接发 起对 Model 的调用。如果需要 View 的参与(比如需要将 Model 最新的状态反应在 View 上), Presenter 会驱动 View 完成相应的工作。 对于绑定到 View 上的数据,不应该是 View 从 Presenter _ t" 拉"回来的,应该是 Presenter 主动"推"给 View 的。从消息流(或者消息交换模式)的角度来讲,不论是 View 向 Presenter 完成针对用户交互请求的通知,还是 Presenter 在进行交互请求处理过程中驱动 View 完成相 应的 UI 操作,都是单向 COne-Way) 的。反应在应用编程接口的定义上就意味着不论是定 义在 Presenter 中被 View 调用的方法,还是定义在 IView 接口中被 Presenter 调用的方法最好 都没有返回值。如果不采用方法调用的形式,我们也可以通过事件注册的方式实现 View 和 Presenter 的交互,事件机制体现的消息流无疑是单向的。 View 本身仅仅实现单纯的、独立的 UI 处理逻辑,它处理的数据应该是 Presenter 实时推 送给它的,所以 View 尽可能不维护数据状态。定义在 IView 的接口最好只包含方法,而避 免属性的定义, Present巳r 所需的关于 View 的状态应该在接收到 View 发送的用户交互请求的 时候一次得到,而不需要通过 View 的属性去获取。 实例演示: 8C 模式的应用 (8101 ) 为了让读者对 MVP 模式,尤其是该模式下的 View 和 Presenter 之间的交互方式有一个 深刻的认识,我们现在来做一个简单的实例演示。本实例采用上面提及的关于员工查询的场 景,并且采用 ASP.NET Web Forms 来建立这个简单的应用,最终呈现出来的效果如图 1-3 所示。前面我们已经演示了采用 PV 模式下的 IView 应该如何定义,现在我们来看看 sc 模 式下的 IView 有何不同。 先来看看表示员工信息的数据类型如何定义。我们通过具有如下定义的数据类型 Employee 来表示一个员工。简单起见,我们仅仅定义了表示员工基本信息(lD 、姓名、性 别、出生日期和部门)的 5 个属性。 e e V 」 o l p m PM S s a 14 C C .工14 ku u p{ public string Id { get; private set; } ASP. NET MVC 4 握架揭秘1.2 MVC 的变体 麟 9 e m qJqJ· 工 qJ nn 巾4n ·工 -le-­ rrtr ttat ssDS CCCC ·工·工 -1· 工 14l 、i1 bbbb uuuu p‘p&p ‘ P ‘ Name { geti private seti } Gender { geti private seti } BirthDate { geti private seti } Department { geti private seti } public Employee(string id, string name , string gender, DateTime birthDate, string departme 口 t) this.Id this.Name this.Gender this.BirthDate this.Department .,• L en te -Fam rDt ., e-nr edta Jmnrp-daeie iηGJbd ===== 作为包含应用状态和状态操作行为的 Model 通过如下一个简单的 EmployeeRepository 类 型来体现。如代码所示,表示所有员工列表的数据通过一个静态字段来维护,而 GetEmployees 返回指定部门的员工列表,如果没有指定筛选部门或者指定的部门字符为空,则直接返回所 有的员工列表。 public class EmployeeRepository private static IList employeesi static EmployeeRepository() employees = new List()i employees.Add(new Employee("OOl" , "张三","男", new DateTime(1981, 8 , 24) , "销售部") ) ; emp1oyees.Add(new Employee("002" , "李四","女", new DateTime(1982, 7 , 10) , "人事部") ) i employees.Add(new Employee("003" , "王五","男", new DateTime(1981, 9, 21) , "人事部") ) i public IEηumerable GetEmployees(string department = "") if (string.IsNullOrEmpty(department)) return employeesi return employees.Where(e => e.Department == department) .ToArraY()i 接下来我们来看作为 View 接口的 IEmployeeSearchView 的定义。如下面的代码片段所 示,该接口定义了 BindEmployees 和 BindDepartments 两个方法,分别用于绑定基于部门列 表的 DropDownL ist 和基于员工列表的 GridView 。除此之外, IEmployeeSearch View 接口还 定义了一个事件 DepartmentSelected ,该事件会在用户选择了筛选部门后点击"查询"按钮 时触发。 DepartmentSelected 事件参数类型为自定义的 DepartmentSelectedEventArgs ,属性 Department 表示用户选择的部门。 public interface IEmployeeSearchView ASP. NET MVC 4 框架揭秘10 醺第 1 章 ASP.NET + MVC void BindEmployees(IEnumerable employees)i void BindDepartments(IEnumerable departments)i eventEventHandler DepartmentSelectedi public class DepartmentSelectedEventArgs : EventArgs public string Department { geti private seti } public Departme 口 tSelectedEventArgs(string department) this.Department = department; 作为 MVP 三角关系核心的 Presenter 通过 EmployeeSe 缸 chP resenter 表示。如下面的代码 片段所示,表示 View 的只读属性类型为 IEmployeeSearchView 接口,而另一个只读属性 Repository 则表示作为 Model 的 Emplo 严 eRepository 对象,两个属性均在构造函数中初始化。 public class EmployeeSearchPresenter public IemployeeSearchView View { get; private seti } public EmployeeRepository Repository { get; private set; } public EmployeeSearchPrese 口 ter(IEmployeeSearchView view) this.View this.Repository this.View.DepartmentSelected = v 工 ew; = new EmployeeRepository(); += OnDepartme 口 tSelected; ) ( e z -1 14 a .1 ·℃ -l n T4 AU .l o v c -L 14 b u p{ IEnumerable employees = this.Repository.GetEmployees(); this.View.BindEmployees(employees); string[] departments = new string[] { "销售部","采购部","人事部", "IT 部" }; this.View.BindDepartments(departments); protected void OnDepartmentSelected(object sender , DepartmentSelectedEventArgs args) string department = args.Departme 口 t; var employees = th 工 s.Repository.GetEmployees(department); this.View.BindEmployees(employees); 在构造函数中我们注册了 View 的 DepartmentS elected 事件,作为事件处理器的 O nD epartmentSelected 方法通过调用 Repository (即 Model) 得到了用户选择部门下的员工列 表,返回的员工列表通过调用 View 的 BindEmployees 方法实现了在 View 上的数据绑定。在 Initialize 方法中,我们通过调用 Repository 获取所有员工的列表,并通过 View 的 Bin dE mployees 方法显示在界面上。作为筛选条件的部门列表通过调用 View 的 Bin dD epartments 方法绑定在 View 上。 AS P. NET MVC 4 框架揭秘1.2 MVC 的变体程 11 最后我们来看看作为 View 的 Web 页面如何定义。如下所示的是作为页面主体部分的 HTML ,核心部分是一个用于绑定筛选部门列表的 DropDow nL ist 和一个绑定员工列表的 GridView 。 员工管理
选择查询部门:
如下所示的是该 Web 页面的后台代码的定义,它实现了定义在 IEmployeeSearchView 接 口的两个方法 C Bin dE mployees 和 Bin dD epartments) 和一个事件 C DepartmentSelected) 。表 示 Presenter 的同名只读属性在构造函数中被初始化。在页面加载的时候 CPage_Load 方法) Presenter 的 Initialize 方法被调用,而在"查询"按钮被点击的时候 CButtonSearch Click) 事 件 Dep 缸 tmentSelected 被触发。 public partial class Default : Page , IEmployeeSearchView public EmployeeSearchPresenter Presenter { get; private set; } publiceventEventHandlerDepartmentSelected; public Default() this.Presenter = new EmployeeSearchPresenter(this); protected void Page_Load(object sender , EveηtArgs e) AS P. NET MVC 4 框架揭秘12 露第 1 章 ASP.NET + MVC if (!this. 工 sPostBack) { this.Presenter.Initialize()j protected void ButtonSearch_Click(object sender, EventArgs e) { string department = this.DropDownListDepartme口 ts.SelectedValuej DepartmentSelectedEven 仁 Args eventArgs = new DepartmentSelectedEventArgs(department)j if (null != DepartmentSelected) DepartmentSelected(this, eventArgs)j public void BindEmployees(IEnumerable employees) this.GridViewEmployees.DataSource = employeesj this.GridViewEmployees.DataBind()j public void BindDepartments( 工 Enumerable departments) { this.DropDownListDepartments.DataSource = departmentsj this.DropDownListDepartments.DataBind()j 1.2.2 Model 2 Trygve M. H. Reenskau 当初提出的 MVC 是作为基于 GUI 的桌面应用的架构模式并不太 适合 Web 本身的特性,虽然 MVCIMVP 也可以直接用于 ASP.NET Web Forms 应用,但这是 因为微软就是基于桌面应用的编程模式来设计基于 WebForms 的 ASP.NET 应用框架的。 Web 应用不同于 GUI 桌面应用的主要区别在于:用户是通过浏览器与应用进行交互,交互请求 和响应是通过 HTTP 请求和响应来完成的。 为了让 MVC 能够为 Web 应用提供原生的支持,另一个被称为 Mode12 的 MVC 变体被 提出来,这来源于基于 Java 的 Web 应用架构模式。 Java Web 应用具有两种基本的基于 MVC 的架构模式,分别被称为 Model1 和 Mode12o Mode1 1 类似于我们前面提及的自治试图模式, 它将数据的可视化呈现和用户交互操作的处理逻辑合并在一起。 Model1 使用于那些比较简 单的 Web 应用,对于相对复杂的应用应该采用 Mode12 。 为了让开发者采用相同的编程模式进行 GUI 桌面应用和 Web 应用的开发,微软通过 ViewState 和 Postback 对 HπP 请求和回复机制进行了封装,使我们能够像编写 Windows Forms 应用一样采用事件驱动的方式进行 ASP.NET Web Forms 应用的编程。而 Mode12 采用 完全不同的设计,它让开发者直接面向 Web ,让他们关注 HTTP 的请求和响应,所以 Mode12 提供对 Web 应用原生的支持。 ASP. NET MVC 4 框架揭秘13 对于 Web 应用来说,和用户直接交互的 UI 界面由浏览器来提供,用户交互请求通过浏 览器以 HTTP 请求的方式发送到 Web 服务器,服务器对请求进行相应的处理并最终返回一 个 HTTP 回复对请求予以响应。接下来我们详细讨论作为 MVC 的三要素是如何相互协作最 终完成对请求的响应的。图 1-4 所示的序列图体现了整个流程的全过程。 MVC 的变体1.2 |也| hEll-1 … 叫一··m • HTTP Response 等--一一一一- Model2 交互流程 Model 2 中一个 HTTP 请求的目标是 Controller 中的某个 Action ,后者体现为定义在 Controller 类型中的某个方法,所以对请求的处理最终体现在对目标 Controller 对象的激活和 对相应 Action 方法的执行。一般来说, Controller 的类型和 Action 方法的名称以及作为 Action 方法的部分参数(针对 HT 回国 GET) 可以直接通过请求的 URL 解析出来。 图 1-4 如图 1-4 所示,我们通过一个拦截器( Interceptor )对抵达 Web 服务器的 HTTP 请求 进行拦截。一般的 Web 应用框架都提供了这样的拦截机制,对于 ASP.NET 来说,我们可 以通过 HttpModule 的形式来定义这么一个拦截器。拦截器根据请求解析出目标 Controller 的类型和对应的 Action 方法的名称,随后目标 Con位oller 被激活,相应的 Action 方法被执行。 在激活 Controller 对象的目标 Action 方法被执行过程中,它可以调用 Model 获取相应的 数据或者改变其状态。在 Action 方法执行的最后阶段会选择相应的 View ,整个 View 被最终 转换成 HTML ,以 HTTP 响应的形式返回到客户端并呈现在浏览器中。绑定在 View 上的数 据来源于 Model 或者基于显示要求进行的简单逻辑计算,我们有时候将它们称为 VM (View Model) ,即基于 View 的 Model (这里的 ViewModel 与 MVVM 模式下的 VM 是完全不同的 两个概念,后者不仅包括呈现在 View 中的数据,也包括数据操作行为)。 ASP.NETMVC 与 Model2 ASP.NET MVC 就是根据 Model 2 模式设计的。对于 HTTP 请求的拦截以实现对目标 Controller 和 Action 的解析是通过一个自定义 H即 Module 来实现的,而对目标 Controller 的 激活则通过一个自定义 HttpHandler 来完成。在本章的最后我们会通过一个例子来模拟 ASP.NETMVC 的工作原理。 1.2.3 ASP. NET MVC 4 在架揭秘14 由第 1 章 ASP.NET + MVC 在上面我们多次强调 MVC 的 Model 是维持应用状态提供业务功能的领域模型,或者是 多层架构中进入业务层的入口或者业务服务的代理,但是 ASP.NETMVC 中的 Model 还是这 个 Model 吗?稍微了解 ASP.NETMVC 的读者都知道, ASP.NET MVC 的 Model 仅仅是绑定 到 View 上的数据而己,它和 MVC 模式中的 Model 并不是一回事。由于 ASP.NET MVC 中 的 Model 是基于 View 的,我们可以将其称为 ViewModel 。 由于 ASP.NETMVC 只有 ViewModel ,所以 ASP.NETMVC 应用框架本身仅仅关于 View 和 Controller ,真正的 Model 以及 Model 和 Controller 之间的交互体现在我们如何来设计 Con位oller 。我个人觉得将用于构建 ASP.NETMVC 的 MVC 模式成为 M CModel) -V CView) -VM C View Model) -C C Controller )也许更为准确。 1.3 IIS/ASP.NET 管道 前面我们对 MVC 模式及其变体作了详细的介绍,其目的在于让读者充分地了解 ASP.NETMVC 框架的设计思想,接下来我们来介绍支撑 ASP.NETMVC 的技术平台。顾名 思义, ASP.NET MVC 就是建立在 AS卫NET 平台上基于 MVC 模式建立的 Web 应用框架,深 刻理解 ASP.阳TMVC 的前提是对 ASP.阳T 管道式设计具有深刻的认识。由于 ASP.NETWeb 应用总是寄宿于 IIS 上,所以我们将两者结合起来介绍,力求让读者完整地了解请求在 IIS/ ASP.NET 管道中是如何流动的。由于不同版本的 IIS 的处理方式具有很大的差异,接下 来会介绍 3 个主要的 IIS 版本各自对 Web 请求的不同处理方式。 1.3.1 1185.x 与 A8P.NET 我们先来看看 IIS 5.x 是如何处理基于 ASP.NET 资源(比如呻x 、 .asmx 等)请求的。整 个过程基本上可以通过图 1-5 体现。 IIS 5.x 运行在进程 InetInfo.exe 中,该进程寄宿着一个名 为 World Wide Web Publishing Service C 简称 W3SVC) 的 Windows 服务。 W3SVC 的主要功能 包括 Hη? 请求的监听、工作进程和配置管理(通过从 Metabase 中加载相关配置信息)等。 l 『 R S-e •L qwva- < M ee F 们U 比队一 e 阳 w-- um 汗创 - m 归 mF 一阳 阳阳 -E川" Application NamedPipes 图 1-5 IIS 5.x 与 ASP.NET ASP. NET MVC 4 框架揭秘1.3 IIS/ASP.NET 管道 • 15 当检测到某个 HTTP 请求时,先根据扩展名判断请求的是否是静态资源(比 如 .html 、.img 、 .txt 、 .xml 等),如果是,则直接将文件内容以 HTTP 回复的形式返回:如果 是动态资源(比如 .aspx 、 .asp 、 .php 等) ,则通过扩展名从 IIS 的脚本映射 C Script Map) 中 找到相应的 ISAPI 动态连接库 CDynamic Link Library, DLL) 。 ISAPI C Intemet Server Application Programming Interface) 是一套本地的 CNative) Win32 API,是 IIS 和其他动态 Web 应用或平台之间的纽带。 ISAPI 定义在一个动态连接库 CDLL) 文件中, ASP.NET ISAPI 对应的 DLL 文件名称为 aspnet_isap i. dll ,我们可以在目录 "%windir% \Microsoft.NET\Framework\ {version no} \"中找到它。 ISAPI 支持 ISAPI 扩展 CISAPI Extension) 和 ISAPI 筛选 C ISAPI Filter) ,前者是真正处理 HTTP 请求的接口,后者则可以 在 HTTP 请求真正被处理之前查看、修改、转发或拒绝请求,比如 IIS 可以利用 ISAPI 筛选 进行请求的验证。 如果我们请求的是一个基于 AS丑陋T 的资源类型,比如 .aspx 、 .asmx 和 .svc 等, aspnet _ isapi. dll 会被加载,而 AS卫NETISAPI 扩展会创建 ASP.NET 的工作进程(如果该进程 尚未启动)。对于 IIS 5.x 来说,该工作进程为 aspnet. exeo IIS 进程与工作进程之间通过命名 管道 CNamed Pipes) 进行通信。 在工作进程初始化过程中, .NET 运行时 (CLR) 被加载进而构建了一个托管的环境。 对于某个 Web 应用的初次请求, CLR 会为其创建一个应用程序域 C Application Domain) 。 在应用程序域中, HTTP 运行时 C HTTP Runtime) 被加载并用以创建相应的应用。寄宿于 IIS 5.x 的所有 Web 应用都运行在同一个进程(工作进程 aspnet_wp.exe) 的不同应用程序 域中。 1.3.2 118 6.0 与 A8P.NET 通过上面的介绍,我们可以看出 IIS 5.x 至少存在着如下两个方面的不足。 • ISAPI 动态连接库被加载到 Inetlnfo.exe 进程中,它和工作进程之间是一种典型的跨进程 通信方式,尽管采用命名管道,但是仍然会带来性能的瓶颈。 • 所有的 ASP.NET 应用运行在相同进程 C aspnet_ wp.exe) 中的不同的应用程序域中,基 于应用程序域的隔离不能从根本上解决一个应用程序对另一个程序的影响。在更多的时 候,我们需要不同的 Web 应用运行在不同的进程中。 为了解决第一个问题, IIS 6.0 将 ISAPI 动态连接库直接加载到工作进程中:为了解决第 二个问题,引入了应用程序池 C Application Pool )的机制。我们可以为一个或多个 Web 应用 创建应用程序池,由于每一个应用程序池对应一个独立的工作进程,从而为运行在不同应用 程序池中的 Web 应用提供基于进程的隔离级别。 IIS 6.0 的工作进程名称为 w3wp.exe 。 ASP. NET MVC 4 在架揭秘16 团第 1 章 ASP.NET + MVC 除了上面两点改进之外, IIS 6.0 还有其他一些值得称道的地方。其中最重要的一点就是 创建了一个名为 HTTP.SYS 的 HTTP 监听器。 HTTP.SYS 以驱动程序的形式运行在 Windows 的内核模式 C Kemel Mode) 下,它是 Windows 2003 的 TCP/IP 网络子系统的一部分,从结 构上看它属于 TCP 之上的一个网络驱动程序。 严格地说, HTTP.SYS 已经不属于 IIS 的范畴了,所以 HTTP.SYS 的配置信息也没有保 存在 IIS 的元数据库 CMetabase) 中,而是定义在注册表中。 HTTP.SYS 的注册表项的路径 为 HKEY LOCAL MACHINE/SYSTEMlCurrentControlSetlServices/HTTP 0 HTTP.SYS 能够带 来如下的好处。 • 持续监昕:由于 HTTP.SYS 是一个网络驱动程序,始终处于运行状态,对于用户的 HTTP 请求能够及时作出反应。 • 更好的稳定性: HTTP.SYS 运行在操作系统内核模式下,并不执行任何用户代码,所以 其本身不会受到 Web 应用、工作进程和 IIS 进程的影响。 • 内核模式下数据缓存:如果某个资源被频繁请求, HTTP.SYS 会把响应的内容进行缓存, 缓存的内容可以直接响应后续的请求。由于这是基于内核模式的缓存,不存在内核模式 和用户模式的切换,响应速度将得到极大的改进。 图 1-6 体现了 IIS 的结构和处理 HTTP 请求的流程。与 IIS 5.x 不同, W3SVC 从 Ine tI nfo.exe 进程脱离出来(对于 IIS 6.0 来说, Ine tI nfo.exe 基本上可以看作单纯的 IIS 管 理进程) ,运行在另一个进程 SvcHos t. exe 中。不过 W3SVC 的基本功能并没有发生变化, 只是在功能的实现上作了相应的改进。与 IIS 5.x 一样,元数据库 CMetabase) 依然存在于 Inetlnfo.exe 进程中。 -HTTP. 回· KernelMode 图 1-6 IIS 6.0 与 AS P. NET 当 HTTP.SYS 监听到用户的 HTTP 请求时将其分发给 W3SVC , W3SVC 解析出请求的 URL ,并根据从 Metabase 获取的 URL 与 Web 应用之间的映射关系得到目标应用,并进一步 ASP. NET MVC 4 在架揭秘1.3 IIS/ASP.NET 管道磁 17 得到目标应用运行的应用程序池或工作进程。如果工作进程不存在(尚未创建或被回收) , 则为该请求创建新的工作进程。我们将工作进程的这种创建方式称为请求式创建。在工作进 程的初始化过程中,相应的 ISAPI 动态连接库被加载。对于 ASP.NET 应用来说,被加载的 ISAP I. dll 为 aspnet_isap i. dllo AS卫NETISAPI 再负责进行 CLR 的加载、应用程序域的创建和 Web 应用的初始化等操作。 1.3.3 118 7.0 与 A8P.NET IIS 7. 。在请求的监听和分发机制上又进行了革新性的改进,主要体现在对于 Windows 进程激活服务 C Windows Process Activation Service, WAS) 的引入,将原来 CIIS 6.0) W3SVC 承载的部分功能分流给了 WAS 。通过上面的介绍,我们知道对于 IIS6.0 来说 W3SVC 主要 承载着 3 大功能。 • HTTP 请求接收:接收 HTTP.SYS 监听到的 HTTP 请求。 • 配置管理:从元数据库 CMetabase) 中加载配置信息对相关组件进行配置。 • 进程管理:创建、回收、监控工作进程。 IIS 7.0 将后两组功能实现到了 WAS 中,接收 HTTP 请求的任务依然落在 W3SVC 头上。 WAS 的引入为 IIS 7.0 提供了对非 HTTP 协议的支持。 WAS 通过监听器适配器接口 CListener Adapter Interface) 抽象出不同协议监听器。具体来说,除了基于网络驱动的 HTTP.SYS 提供 HTTP 请求监听功能外还提供了 TCP 监听器、命名管道监听器和 MSMQ 监昕器以提供基于 TCP 、命名管道和 MSMQ 传输协议的监听支持。 与此 3 种监听器相对的是 3 种监听适配器,它们提供监昕器与 WAS 中的监听器适配 器接口之间的适配。从这个意义上讲, IIS 7.0 中的 W3SVC 更多地为 HTTP.SYS 起着监昕 适配器的作用。这 3 种非 HTTP 监听器和监昕适配器定义在程序集 SMHos t. exe 中,我们 可以在目录 %windir%\Microsoft.NET飞Framework\v3.0\Windows Communication Foundation \ 中找到它们。 WCF 提供的这 3 种监昕器和监昕适配器最终以 Windows 服务的形式体现。虽然它们定 义在一个程序集中,我们依然可以通过服务工作管理器对其进行单独的启动、终止和配置。 SMHos t. exe 提供了 4 个重要的 Windows Service 。 • NetTcpPortSharing: 为 WCF 提供 TCP 端口共享。关于端口共享在 WCF 中的应用,本 人拙著 < HttpModule ASP.NET 拥有一个具有高度可扩展性的引擎,并且能够处理对于不同资源类型的请求, 那么是什么成就了 ASP.NET 的高可扩展性呢? HtφModule 功不可没。 当请求转入 ASP.NET 管道时,最终负责处理该请求的是与请求资源类型相匹配的 HttpHandler 对象,但是在 Handler 正式工作之前, ASP.NET 会先加载并初始化所有配置的 HttpModule 对象。 HttpModule 在初始化的过程中,会将一些功能注册到 HttpApplication 相 应的事件中,在 HttpApplication 请求处理生命周期中的某个阶段,相应的事件会被触发,通 过 HttpModule 注册的事件处理程序也得以执行。 所有的 HttpModule 都实现了具有如下定义的 System. Web .IHttpModule 接口,其中 Init 方法用于实现 HttpModule 自身的初始化,该方法接受一个 H忧pApplication 对象,有了这个 对象,事件注册就很容易了。 public interface IHttpModule void Dispose(); void Init(HttpApplication context); AS卫NET 提供的很多基础功能都是通过相应的 H即Module 实现的,下面列出了一些典 型的 HttpModule 。 • OutputCacheModule: 实现了输出缓存( Output Caching) 的功能。 • SessionStateModule: 在无状态的 HTTP 协议上实现了基于会话 (Session) 的状态。 • WindowsAuthenticationModule+ F ormsAuthenticationModule+PassportAuthentication Module: 实现了 Windows 、 Forms 和 Passport 这 3 种典型的身份认证方式。 • UrlAuthorizationModule + FileAuthorizationModule: 实现了基于 URI 和文件 ACL(Access Control Li st) 的授权。 ASP. NET MVC 4 在牵强秘24 部第 1 章 ASP.NET + MVC 除了这些系统定义的 H即 Module 之外,我们还可以自定义 HttpModule ,通过 Web.config 可以很容易地将其注册到 Web 应用中。 HttpHandler 对于不同资源类型的请求, ASP.NET 会加载不同的 Handler 来处理,也就是说 .aspx 页 面与础 nxweb 服务对应的 Handler 是不同的。所有的 HttpHandler 都实现了具有如下定义的 接口 System. Web .I H忧pHandler ,方法 ProcessRequest 提供了处理请求的实现。 public interface IHttpHandler void ProcessRequest(HttpContext context); bool IsReusable { get; } 某些 H忱pHandler 具有一个与之相关的 HttpHandlerFactory ,它实现了具有如下定义的接 口 System. Web.IHttpHandlerFactory ,方法 Ge tH andler 用于创建新的 HttpHandler ,或者获取 已经存在的 H仕 pHandler 。 public interface IHttpHandlerFactory IHttpHandler GetHandler(HttpContext context , string requestType , string url , string pathTranslated); void ReleaseHandler(IHttpHandler handler); HttpHandler 和 HtφHandlerFactory 的类型都可以通过相同的方式配置到 Web.config 中。 下面一段配置包含对 .aspx 、 .asmx 和 .svc 这 3 种典型的资源类型的 H仕 pHandler 配置。 ASP. NET MVC 4 蓓架揭翻1.4 ASP.NET MVC 是如何运行的 陈 25 1.4 ASP.NET MVC 是如何运行的 ASP.NET 由于采用了管道式设计,具有很好的扩展性,而整个 AS卫NETMVC 应用框架 就是通过扩展 ASP.NET 实现的。通过上面对 AS卫NET 管道设计的介绍我们知道, AS丑陋T 的扩展点主要体现在 HttpModule 和 HttpHandler 这两个核心组件之上,实际上整个 AS卫NET MVC 框架就是通过自定义的 HttpModule 和 H口pHandler 建立起来的。 为了使读者能够从整体上把握 AS卫NETMVC 的工作机制,接下来按照其原理通过一些 自定义组件来模拟 ASP.NETMVC 的运行原理,也可以将此视为一个"迷你版"的 ASP.NET MVC 。值得一提的是,为了让读者根据该实例从真正的 ASP.NETMVC 中找到对应的组件, 本书完全采用了与 ASP.NETMVC 一致的类型命名方式。 1.4.1 建立在"迷仰版" ASP.NET MVC 上的 Web 应用 在正式介绍我们自己创建的"迷你版" AS丑陋TMVC 的实现原理之前,不妨来看看建 立在该框架之上的 Web 应用如何定义。通过 Visual Studio 创建一个空的 AS卫NET Web 应 用(注意不是 ASP.NETMVC 应用)并不会引用 System. Web.Mvc.dll 这个程序集,所以在接 下来的程序中看到的所谓 MVC 的组件都是我们自行定义的。 首先定义了如下一个 SimpleModel 类型,它表示最终需要绑定到 View 上的数据。为了 验证针对 Controller 和 Action 的解析机制, SimpleModel 定义的两个属性分别表示当前请求 的目标 Controller 和 Action 。 l e d O M e 14 p m .工qu s s a --C C .1 14 b u p{ public string Controller { get; set; } public string Action { get; set; } 与真正的 ASP .NETMVC 应用开发一样,我们需要定义 Controller 类。按照约定的命名 方式(以字符" Controller" 作为后缀),我们定义了如下一个 HomeController 0 HomeController 实现的抽象类型 ControllerBase 是我们自行定义的。以自定义的 ActionResult 作为返回类型 的 Index 方法表示 Controller 的 Action ,它接受一个 SimpleModel 类型的对象作为参数。该 Action 方法返回的 ActionResult 是一个 RawContextResult 对象,顾名思义, RawContextResult 就是将指定的内容进行原样返回。在这里我们将作为参数的 SimpleModel 对象的 Con位o l1er 和 Action 属性显示出来。 public class HomeController: ControllerBase public ActionResult Index(SimpleModel model) string content = string.Format("Controller: {O}
Actio口: {1} " , ASP. NET MVC 4 框架揭秘26 电第 1 章 ASP. NET + MVC model.Controller , model.Action)i return new RawContentResult(content)i AS 卫 NET MVC 根据请求地址来解析出用于处理该请求的 Controller 的类型和 Action 方 法名称。具体来说,我们预注册一些包含 Con位 oller 和 Action 名称作为占位符的〈相对)地 址模板,如果请求地址符合相应地址模板的模式, Controller 和 Action 名称就可以正确地解 析出来。和 ASP.NET MVC 应用类似,我们在 Globa l. asax 中注册了如下一个地址模板 ( { controller} / {action} )。我们还注册了一个用于创建 Controller 对象的工厂。 RouteTable 、 ControllerBuilder 和 DefaultControllerF actory 都是我们自定义的类型。 public class Global : System.Web.HttpApplication protected void Application Start(object sender , EventArgs e) RouteTable.Routes.Add("default" , new Route{Url = "{controller}/{action}"}) i ControllerBuilder.Current.SetControllerFactory( new DefaultControllerFactorY())i 正如上面所说的, ASP.NET MVC 是通过一个自定义的 HttpModule 实现的,在这个"迷 你版" ASP.NET MVC 框架中我们也将其起名为 UrIRoutin gM odule 。在运行 Web 应用之前, 我们需要通过配置对该自定义 H仕pModule 进行注册,下面是相关的配置。 到目前为止,所有的编程和配置工作己经完成,为了让定义在 HomeController 中的 Action 方法 Index 来处理针对该 Web 应用的访问请求,我们需要指定与之匹配的地址(符合定义在 注册地址模板的 URL 模式)。如图 1-12 所示,由于在浏览器中输入地址( http://. . .lH ome/lndex) 正好对应着 HomeCon位。 ller 的 Action 方法 Index ,所以对应的方法会被执行,而执行的结果 就是将当前请求的目标 Contro l1 er 和 Action 的名称显示出来。 (SI02) co曲。,IIe:r: Home Actiordndex 图 1-12 采用符合注册的路由地址模板的地址访问 Web 应用 ASP. NET MVC 4 在架揭秘1 .4 ASP.NET MVC 是如何运行的 黯 27 上面演示了如何在我们自己创建的"迷你版" ASP.NET MVC 框架中创建一个 Web 应用, 从中可以看到和创建一个真正的 ASP.NETMVC 应用别无二致。接下来我们就来逐步地分析 这个自定义的 AS 卫 NETMVC 框架是如何建立起来的,而它也代表了真正的 ASP.NETMVC 框架的工作原理。 1.4.2 URL 路由 对于一个 ASP.NET MVC 应用来说,针对 HTTP 请求的处理实现在某个 Controller 类型 的某个 Action 方法中,每个 HTTP 请求不再像 ASP .NET Web Forms 应用一样是对应着一个 物理文件,而是对应着某个 Controller 的某个 Action 。目标 Controller 和 Action 的名称包含在 HTIP 请求的 URL 中,而 ASP.NET MVC 的首要任务就是通过当前 Hπ? 请求的解析得到正 确的 Controller 和 Action 的名称,这个过程是通过 ASP.NETMVC 的 URL 路由机制来实现的。 RouteData ASP. 阳 T 定义了一个全局的路由表,路由表中的每个路由对象包含一个 URL 模板。目 标 Controller 和 Action 的名称可以通过路由变量以占位符〈比如" { controller} "和" { action} ") 定义在 URL 模板中,也可以作为路由对象的默认值。对于每一个抵达的 Hπ? 请求, ASP.NET MVC 会遍历路由表找到一个具有与当前请求 URL 模式相匹配的路由对象,并最终解析出以 Controller 和 Action 名称为核心的路由数据。在我们自定义的 AS 卫 NETMVC 框架中,路由 数据通过具有如下定义的 RouteData 类型表示。 a 卡」a nu e 』LU O R S s a 14 C C .1 14 .b u p{ public IDictionary public IDictionary public IRouteHandler public RouteBase Values { geti private seti } DataTokens { geti private seti } RouteHandler {geti seti } Route { geti seti } public RouteData() this.Values = new Dictionary()i this.DataTokens = new Dictionary()i this.DataTokens.Add("namespaces" , new List())i r e 14 14 o r 』Ln o c qd n .l r ←」S C ·-14 b u p{ get object controllerName = string.EmptYi this.Values.TryGetValue("controller" , out controllerName)i return controllerName.ToString()i public string ActionName ASPNETMVC4 在架揭翻28 如第 1 章 AS P. NET + MVC get object actionName = string.Empty; this.Values.TryGetValue("actioη" , out actionName); return actionName.ToString(); 如上面的代码片段所示, RouteData 定义了两个字典类型的属性 Values 和 DataTokens , 前者代表直接从请求地址解析出来的变量列表,后者代表具有其他来源的变量列表。表示 Con位 oller 和 Action 名称的同名属性直接从 Values 字典中提取,对应的 Key 分别为 controller 和 action 。 我们之前己经提到过 ASP.NETMVC 本质上是由两个自定义的 ASP. 阳 T 组件来实现的, 一个是自定义的 HttpModule ,另一个是自定义的 HttpHandler ,而后者从 RouteData 的 RouteHandler 属性获得。 RouteData 的 RouteHandler 属性类型为 IRouteHandler 接口,如下面 的代码片段所示,该接口具有一个唯一的 GetHttpHandler 用于返回真正用于处理 HTTP 请求 的 HttpHandler 对象。 public interface IRouteHandler IHttpHandler GetHttpHandler(RequestContext requestContext); IRouteHandler 接口的 GetHttpHandler 方法接受一个类型为 RequestContext 的参数,顾名 思义, RequestContext 表示当前 (HTTP) 请求的上下文,其核心就是对当前 HttpContext 和 RouteData 的封装,这可以通过如下的代码片段看出来。 public class RequestContext public virtual HttpContextBase public virtual RouteData Route 和 RouteTable HttpCo 口 text { get; set; } RouteData { get; set; } RouteData 具有一个类型为 RouteBase 的 Route 属性,表示生成路由数据对应的路由对 象。如下面的代码片段所示, RouteBase 是一个抽象类,它仅仅包含一个 GetRouteData 方法。 该方法判断是否与当前请求相匹配,并在匹配的情况下返回用于封装路由数据的 RouteData 对象。该方法接受一个表示当前 Hπ? 上下文的 HttpContextBase 对象,如果与当前请求不 匹配,则返回 Null 。 public abstract class RouteBase public abstract RouteData GetRouteData(HttpContextBase httpContext); AS P. NET MVC 4 框架揭秘1 .4 ASP.NET MVC 是如何运行的 嚣 29 ASP.NETMVC 提供的基于 URL 模板的路由机制是通过其子类 Route 实现的。如下面的 代码片段所示,它具有一个代表 URL 模板的字符串类型的 Url 属性。在实现的 GetRouteData 方法中,我们通过 HttpContextBase 获取当前请求的 URL ,如果它与 URL 模板的模式相匹配 则创建一个 RouteData 返回,否则返回 Null 。对于返回的 RouteData 对象,其 Values 属性表 示的字典对象包含直接通过地址解析出来的变量,而对于 DataTokens 字典和 RouteHandler 属性,则直接取自 Route 对象的同名属性。 public class Route : RouteBase public IRouteHandler public string public IDictionary RouteHandler { get; set; } Url { geti set; } DataTokens { geti seti } public Route() this.DataTokens this.RouteHandler = new Dictionary(); =ηew MvcRouteHandler(); public override RouteData GetRouteData(HttpContextBase httpContext) IDictionary variables; if (this.Match(httpContext.Request .AppRelativeCurrentExecutionFilePath.Substring(2) , out variables)) RouteData routeData = new RouteData()i foreach (var itern in variables) { routeData.Values.Add(itern.Key , itern.Value); foreach (var itern in DataTokens) routeData. DataTokens.Add (itern.Key , itern.Value); routeData.RouteHandler = this.RouteHandleri return routeData; return 口 ull; protected bool Match(string requestUrl , out IDictionary variables) variables = new Dictionary()i string[] strArrayl = requestUrl.Split('j'); string[] strArray2 = this.Url.Split('j')i if (strArrayl.Length != strArray2.Length) return false; for (int i = 0; i < strArray2.Length; i++) if(strArray2[i] .StartsWith("{") && strArray2[i] .EndsWith("}")) ASP. NET MVC 4 症架揭秘30 .第 1 章 ASP.NET + MVC variables.Add(strArray2[i] .Trim("{}".ToCharArray()) , strArrayl[i]); } return true; 由于同一个 Web 应用可以采用多种不同的 URL 模式,所以需要注册多个继承自 RouteBase 的路由对象,多个路由对象组成了一个路由表。在我们自定义 ASP.NET MVC 框 架中,路由表通过类型 RouteTable 表示。如下面的代码片段所示, RouteTable 仅仅具有一个 类型为 RouteDictionary 的 Routes 属性表示针对整个 Web 应用的全局路由表。 e l b a T e ←」u O R S s a --C C .-14 b u p{ public static RouteDictionary Routes { get; private set; } static RouteTable() Routes = new RouteDictionary(); RouteDictionary 表示一个具名的路由对象的列表,我们直接让它继承自泛型的字典类型 Dictionary ,其中的 Key 表示路由对象的注册名称。在 GetRouteData 方法 中,我们遍历集合找到与指定的 H即 ContextBase 对象匹配的路由对象,并得到对应的 RouteData 。 public class RouteDictionary: Dictionary public RouteData GetRouteData(HttpContextBase httpContext) foreach (var route in this.Values) RouteData routeData = route.GetRouteData(httpContext); if (null != routeData) return routeData; return null; 在 Globa l. asax 中我们创建了一个基于指定 URL 模板(" {controller} / {action} 勺的 Route 对象,并将其添加到通过 RouteTable 的静态只读属性 Routes 所表示的全局路由表中。 UrlRoutingModule 路由表的作用是对当前的 HTTP 请求的 URL 进行解析,从而获取一个以 Controller 和 Action 名称为核心的路由数据,即上面介绍的 RouteData 对象。整个解析工作是通过一个类 型为 UrIRoutin gM odule 的自定义 HttpModule 来完成的。如下面的代码片段所示,在实现了 接口 IHttpModule 的 UrIRoutin gM odule 类型的 Init 方法中,我们注册了 H忧 pApplicataion 的 ASP. NET MVC 4 框架揭秘1.4 ASP.NET MVC 是如何运行的 回 31 PostResolveRequestCache 事件。 public class UrlRoutingModule: IHttpModule e s o p s -工hυ d .工O V C ·工14 ·。ulJ PJ ‘ public void Init(HttpApplication context) context.PostResolveRequestCache += OnPostResolveRequestCache; protected virtual void OnPostResol veRequestCache (obj ect sender, EventArgs e) HttpContextWrapper httpCo 口 text = new HttpContextWrapper(HttpContext.Current); RouteData routeData = RouteTable.Routes.GetRouteData(httpContext); if (null == routeData) return; RequestContext requestContext = new RequestContext { RouteData = routeData, HttpContext = httpContext }; IHttpHandler handler = routeData.RouteHandler.GetHttpHandler(requestContext); httpContext.RemapHandler(handler); 当 PostResolveRequestCache 事件触发之后, Ur1Routin民.fodule 通过 RouteTable 的静态只 读属性 Routes 得到表示全局路由表的 RouteDictionary 对象,然后调用其 GetRouteData 方法 并传入用于封装当前 H即Context 的 HtφContextWrapper 对象( HttpContextWrapper 是 HttpContextB ase 的子类)最终得到一个封装路由数据的 RouteData 对象。如果得到的 RouteData 不为 Null,则根据该对象本身和之前得到的 HttpContextWrapper 对象创建一个表 示当前请求上下文的 RequestContext 对象,将其作为参数传入 RouteData 的 RouteHandler 的 GetHt甲Handler 方法得到一个 HttpHandler 对象。最后我们调用 H忧pContextWrapper 对象的 RemapHandler 方法将得到的 Ht甲Handl町进行映射使之用于对当前 HTTP 请求的处理。 1.4.3 Controller 的激活 ASP.NETMVC 的 URL 路由系统通过注册的路由表对 HTTP 请求进行解析从而得到一个 用于封装路由数据的 RouteData 对象,而这个过程是通过自定义的 UrlRoutinβ.f odule 对 HttpApplication 的 PostResolveRequestCache 事件进行注册实现的。 RouteData 中已经包含了 目标 Contro l1 er 的名称,我们需要根据该名称激活对应的 Contro l1er 对象。接下来进一步分 析真正的 Contro l1 er 对象是如何被激活的。 MvcRouteHandler 通过前面的介绍我们知道,继承自 RouteBase 的 Route 类型具有一个类型为 ASP. NET MVC 4 蓓架揭秘32 .第 1 章 AS P. NET + MVC IRouteHandler 接口的属性 RouteHandler ,它主要的用途就是用于根据指定的请求上下文(通 过一个 RequestContext 对象表示〉来获取一个 H即 Handler 对象。当 GetRouteData 方法被执 行后, Route 的 RouteHandler 属性值将反映在得到的 RouteData 的同名属性上。在默认的情 况下, Route 的 RouteHandler 属性是一个 MvcRouteHandler 对象,如下的代码片段反映了这 一占 public class Route : RouteBase //其他成员 public IRouteHandler RouteHandler { get; set; } public Route ( ) //其他操作 this.RouteHandler = new MvcRouteHandler(); 对于我们这个"迷你版"的 ASP.NETMVC 框架来说, MvcRouteHandler 是一个具有如 下定义的类型,在实现的 Ge但即 Handler 方法中,它会直接返回一个 MvcHandler 对象。 public class MvcRo~teHandler: IRouteHandler public IHttpHandler GetHttpHandler(RequestContext requestContext) return new MvcHandler(requestContext); MvcHandler 在前面的内容中已经提到整个 ASP.NET MVC 框架是通过自定义的 HtφModule 和 HttpHandler 对象 ASP.NET 进行扩展实现的。这个自定义 HttpModule 已经介绍过了,就是 UrlRoutinβ,f odule ,而这个自定义的 Ht甲 Handler 则是要重点介绍的 MvcHandler 。 UrlRoutingModule 在通过路由表解析 HTTP 请求得到一个用于封装路由数据的 RouteData 后,会调用其 RouteHandler 的 GetHtφHandler 方法得到 HtφHandler 对象并注册到 当前的 HTTP 上下文。由于 RouteData 的 RouteHandler 来源于对应 Route 对象的 RouteHandler , 而后者在默认的情况下是一个 MvcRouteHandler 对象,所以默认情况下用于处理 Hπ? 请求 的就是这么一个 MvcHandler 对象。 MvcHandler 实现了对 Controller 对象的激活和对相应 Action 方法的执行。 下面的代码片段体现了整个 MvcHandler 的定义,它具有一个类型为 RequestContext 的 属性,表示被处理的当前请求上下文,该属性在构造函数中指定。在实现的 ProcessRequest 中实现了对 Con位 o l1 er 对象的激活和执行。 public class MvcHandler: IHttpHandler public bool IsReusable ASP. NET MVC 4 在架揭秘1 .4 ASP.NET MVC 是如何运行的 回 33 get{return false;} public RequestCo 口 text RequestContext { get; private set; } public MvcHandler(RequestContext requestCo 口 text) this.RequestContext = requestContext; public void ProcessRequest(HttpContext context) string controllerName = this.RequestCo 口 text.RouteData.Controller; IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory(); IController controller = controllerFactory.CreateController( this.RequestContext , controllerName); controller.Execute(this.RequestContext); Controller 与 ContrllerFactory 我们为 Controller 定义了一个接口 Icontroller ,如下面的代码片段所示,该接口具有唯一 的方法 Execute 表示对 Controller 的执行。该方法在 MvcHandler 的 ProcessRequest 方法中被 执行,而传入该方法的参数是表示当前请求上下文的 RequestContext 对象。 public interface IController void Execute(RequestContext requestContext); 从 MvcHandler 的定义可以看到 Con位 oller 对象的激活是通过工厂模式实现的,我们为 Controller 工厂定义了一个具有如下定义的 IConωllerFacto l)'接口。 ICon位 ollerF actol)'通过 CreateCon位oller 方法根据传入的请求上下文和 Controller 的名称来激活相应的 Con位。 ller 对象。 public interface IControllerFactory IController CreateController(RequestContext requestContext , string controllerName)i 在 MvcHandler 的 ProcessRequest 方法中,它通过 ControllerBuilder 的静态属性 Current 得到当前的 ControllerBuilder 对象,并调用 GetControllerFacto l)'方法获得当前的 ControllerF acto l)'。然后通过从 RequestContext 中提取的 RouteData 获得 Controller 的名称, 最后将它连同 RequestContext 一起作为参数调用 ContollerFacto l)'的 CreateController 方法实 现对目标 Controller 对象的创建。 ControllerBuilder 的整个定义如下面的代码片段所示,表示当前 ControllerBuilder 的静态 只读属性的 Current 在静态构造函数中被创建。 SetControllerFactory 和 GetCon位 ollerFacto l)' 方法用于 ContorllerF acto l)'的注册和获取。 ASP. NET MVC 4 框架揭秘34 瓢第 1 章 ASP.NET + MVC public class ControllerBuilder private Func factoryThunk; public static ControllerBuilder Current { geti private seti } static ControllerBuilder() Current = new ControllerBuilder()i public IControllerFactory GetControllerFactory() return factoryThunk()i public void SetControllerFactory(IControllerFactory controllerFactory) factoryThunk = () => controllerFactorYi 再回头看看之前建立在自定义 ASP. 阻 T MVC 框架的 Web 应用,我们就是通过当前的 ControllerB uilder 来注册 ControllerFactory 。如下面的代码片段所示,注册的 ControllerFactory 的类型为 DefaultControllerFactory 。 public class Global : System.Web.HttpApplication protected voìd Applicatio 口 Start(object sender , EventArgs e) //其他操作 ControllerBuilder.Current.SetControllerFactory( new DefaultControllerFactorY())i 作为默认 ControllerFactory 的 DefaultControllerF actory 类型定义如下。激活 Controller 对 象的前提是能够正确解析出 Controller 的真实类型,作为 CreateController 方法输入参数的 controllerName 仅仅表示 Controller 的名称,我们需要加上 Controller 字符后缀作为类型名称。 在 DefaultControllerFactory 类型被加载的时候(静态构造函数被调用),通过 BuildManager 加载所有引用的程序集,并得到所有实现了接口 IController 的类型并将其缓存起来。在 CreateController 方法中根据 Controller 的名称和命名空间从保存的 Controller 类型列表中得 到对应的 Controller 类型,并通过反射的方式创建它。 public class DefaultControllerFactory :工 ControllerFactory private static List 、 controllerTypes = new List()i static DefaultControllerFactory() foreach (Assembly assembly in Buil dM anager. GetReference dAssemblies () ) AS P. NET MVC 4 框架揭磁1.4 ASP.NET MVC 是如何运行的 ω 醺 35 foreach (Type type in assernbly.GetTypes() .Where( type => typeof(IController) .IsAssignableFrorn(type))) controllerTypes.Add(type)i public IController CreateController(RequestContext requestContext , string controllerNarne) string typeNarne = controllerNarne + "Controller"i Type controllerType = controllerTypes.FirstOrDefault( c => string.Cornpare(typeNarne , c.Narne , true) == O)i if (null == controllerType) return nulli return (IController)Activator.Createlnsta 口 ce(controllerType); 上面我们详细地介绍了 Controller 的激活原理,现在将关注点返回到 Controller 自身。 通过实现 IController 接口我们为所有的 Controller 定义了一个具有如下定义的 ControllerBase 抽象基类,从中可以看到在实现的 Execute 方法中, ControllerBase 通过一个实现了接口 IActionlnvoker 的对象完成了针对 Action 方法的执行。 public abstract class ControllerBase: IController protected IActionlnvoker Actionlnvoker { geti seti } public ControllerBase() this.Actionlnvoker = new ControllerActionlnvoker(); public void Execute(RequestContext requestContext) ControllerContext context = new ControllerContext { RequestContext = requestContext , Controller = this }; string actionNarne = requestContext.RouteData.ActioηNarnei this.Actionlnvoker.lnvokeAction(context , actionNarne)i 1.4.4 Action 的执行 作为 Controller 基类 ControllerBase 的 Execute 方法的核心在于对 Action 方法本身的执行 和作为方法返回的 Actio nR esult 的执行,两者的执行是通过一个叫做 Actio nI nvoker 的组件来 完成的。 ASP. NET MVC 4 在架揭秘36 搬第 1 章 ASP.NET + MVC Action I nvoker 同样为 Actionlnvoker 定义了一个接口 lactio nI nvoker ,如下面的代码片段所示,该接口 定义了一个唯一的方法 InvokeAction 用于执行指定名称的 Action 方法,该方法的第一个参 数是一个表示基于当前 Controller 上下文的 ControllerContext 对象。 public interface IActionlnvoker void InvokeAction(ControllerContext controllerContext~ string actionName); ControllerContext 类型在真正的 ASP.NET MVC 框架中要复杂一些,在这里我们对它进 行了简化,仅仅将它表示成对当前 Controller 和请求上下文的封装,而这两个要素分别通过 如下所示的 Con位 oller 和 RequestContext 属性表示。 public class ControllerContext public ControllerBase Controller { get; set; } public RequestContext RequestContext { get; set; } Con忧。 llerBase 中表示 Actio nI nvoker 的同名属性在构造函数中被初始化。在 Execute 方 法中,通过作为方法参数的 RequestContext 对象创建 ControllerContext 对象,并通过包含在 RequestContext 中的 RouteData 得到目标 Action 的名称,然后将这两者作为参数调用 Actio nI nvoker 的 InvokeAction 方法。 从前面给出的关于 ControllerBase 的定义中可以看到在构造函数中默认创建的 Actio nI nvoker 是一个类型为 ControllerActio nI nvoker 的对象。如下所示的代码片段反映了整 个 ControllerActionlnvoker 的定义, InvokeAction 方法的目的在于实现针对 Action 方法的执 行。由于 Action 方法具有相应的参数,在执行 Action 方法之前必须进行参数的绑定。 ASP.NET MVC 将这个机制称为 Model 的绑定,而这又涉及另一个重要的组件 Mode lB inder 。 public class ControllerActionInvoker : IActionlnvoker public IModelBinder ModelBinder { geti private seti } public ControllerActionlnvoker() this.ModelBinder = new DefaultModelBinder()i public void InvokeAction(ControllerContext controllerContext , stringactionName) MethodInfo method = controllerContext.Controller.GetType() .GetMethods() .First(m =>string.Compare(actionName , m.Name , true) == O)i List parameters = new List()i foreach (Parameterlnfo parameter in method.GetParameters()) parameters.Add(this.ModelBinder.BindModel(controllerContext , parameter.Name , parameter.ParameterType))i ASP. NET MVC 4 在架揭秘1.4 ASP.NET MVC 是如何运行的 v 由 37 ActionResult actionResult = method.Invoke(controllerContext.Controller, parameters.ToArray()) as ActionResult; actionResult.ExecuteResult(controllerContext); Model8inder 我们为 ModelBinder 提供了一个简单的定义,这与在真正的 ASP.NET MVC 中的同名 接口的定义不尽相同。如下面的代码片段所示,该接口具有唯一的 BindModel 方法,根据 ControllerContext 和 Model 名称(在这里实际上是参数名称〉和类型得到一个作为参数的 对象。 public interface IModelBinder object BindModel(ControllerContext controllerContext, string modelName, Type modelType); 通过前面给出的关于 ControllerActionInvoker 的定义可以看到,在构造函数中默认创建 的 Modeffiinder 对象是一个 DefaultModeffiinder 对象,由于仅仅是对 ASP.NETMVC 的模拟, 定义在自定义的 DefaultModeffiinder 中的 Model 绑定逻辑比 ASP.NET MVC 的 DefaultModeffiinder 要简单得多,很多复杂的 Model 机制并未在我们自定义的 DefaultModeffiinder 体现出来。 如下面的代码片段所示,绑定到参数上的数据具有三个来源 :HηP-POST Form 、 RouteData 的 Values 和 DataTokens 属性,它们都是字典结构的数据集合。如果参数类型为字 符串或者简单的值类型,我们可以直接根据参数名称和 Key 进行匹配:对于复杂类型(比如 之前例子中定义的包含 Controller 和 Action 名称的数据类型 SimpleModel) ,则通过反射根据 类型创建新的对象,并根据属性名称与 Key 的匹配关系对相应的属性进行赋值。 public class DefaultModelBinder : IModelBinder public object BindModel(ControllerContext controllerContext, stringmodelName, Type modelType) if (modelType.IsValueType II typeof(string) == modelType) object instance; if (GetValueTypeInstance(controllerContext, modelName, modelType, out instance)) return instance; returnActivator.CreateInstance(modelType); objectmodelInstance = Activator.CreateInstance(modelType); foreach (PropertyInfo property in modelType.GetProperties()) ASP. NET MVC 4 框架揭翻38 部第 1 章 ASP.NET + MVC if (!property.CanWrite II (!property.PropertyType.IsValueType &&property.PropertyType!=typeof(string))) continue; objectpropertyValue; if (GetValueTypelnstance(controllerContext, property.Name, property.PropertyType, out propertyValue)) property.SetValue(modellnstance, propertyValue, null); returnmodellnstance; private boolGetValueTypelnstance(ControllerContext controllerContext, stringmodelName, Type modelType, out object value) var form = HttpContext.Current.Request.Form; string key; if (null != form) key = form.AllKeys.FirstOrDefault(k =>string.Compare(k, modelName, true) == 0); if (key != null) value = Convert.ChangeType(form[key] , modelType); return true; key = controllerContext.RequestContext.RouteData.Values .Where(item =>stri 口 g.Compare(item.Key , modelName, true) == 0) .Select(item =>item.Key) .FirstOrDefault(); if (null != key) value = Convert.ChangeType(controllerContext.RequestContext .RouteData.Values[key] , modelType); return true; key = controllerContext.RequestCo口 text.RouteData.DataTokens .Where(item =>string.Compare(item.Key, modelName, true) == 0) .Select(item =>item.Key) .FirstOrDefault(); if (null != key) } value = Convert.ChangeType(controllerContext.RequestContext .RouteData.DataTokens[key] , modelType); return true; value = nulli return falsei 在 Con位ollerActionInvoker 的 InvokeAction 方法中,我们直接将传入的 Action 名称作为 方法名从 Controller 类型中得到表示 Action 操作的 Methodlnfo 对象,然后遍历 Methodlnfo 的参数列表,对于每一个 ParameterInfo 对象,我们将它的 Name 和 ParameterType 属性表示 ASP. NET MVC 4 框架揭秘本章小结由 39 的参数名称和类型,连同创建的 ControllerContext 作为参数调用 Mode lB inder 的 BindModel 方法并得到对应的参数值,最后通过反射的方式传入参数列表并执行 MethodInfo 。 和真正的 ASP.NET MVC 一样,定义在 Controller 的 Action 方法返回一个 Actio nR esult 对象,我们通过执行它的 Execute 方法实现对请求的响应。 ActionResult 我们为具体的 Actio nR esult 定义了一个 Actio nResult 抽象基类,如下面的代码片段所示, 该抽象类具有一个参数类型为 ControllerContext 的抽象方法 ExecuteResult ,我们最终对请求 的响应就实现在该方法中。 public abstract class ActionResult public abstract void ExecuteResult(ControllerContext context); 在之前创建的例子中, Action 方法返回的是一个类型为 RawContentResult 的对象,顾名 思义, RawContentResult 将初始化时指定的内容(字符串)原封不动地写入针对当前请求的 HTTP 响应消息中,具体的实现如下所示。 public class RawContentResult: ActionResult public string RawData { get; private set; } publicRawConte 口 tResult(string rawData) RawData = rawData; public override void ExecuteResult(ControllerContext context) context.RequestContext.HttpContext.Response.Write(this.RawData); 本章小结 ASP.NETMVC 是在现有的 AS 卫 NET 平台上基于 MVC 架构模式创建的 Web 应用开发框 架。 MVC 体现了界面呈现、 u 处理逻辑和业务逻辑之间的分离,传统的 MVC 并没有对 Model 、 View 和 Controller 之间的交互进行严格的约束。在软件设计的发展历程中出现了一 些 MVC 的变体,它们遵循定义在 MVC 中的基本原则,并定义更加严格的交互规则,其中 MVP 和 Mode12 是两个典型的 MVC 变体,而 ASP.NETMVC 就是对 Mode12 的实现。 ASP.NET 采用极具扩展性的管道式设计, Ht甲 Application 是整个 ASP.NET 管道的核心, 它定义了一系列的事件,它们会在请求处理过程中相应的阶段被触发。 Ht甲 Module 是成就 ASP.NET 可扩展的"头号功臣",通过 H即 Model 注册 HtψApplication 相应的事件帮助我们 ASP. NET MVC 4 蓓架揭秘40 旬第 1 章 ASP.NET + MVC 在某个阶段参与到对请求处理的整个流程之中,而请求的最终处理者是注册的 HttpHandler 。 ASP.NETMVC 实际上是通过自定义的 HttpModule 和 HttpHandler 构建的。 为了让读者对 ASP.NETMVC 对从"接收请求"到咽复响应"的整个处理流程有一个 大致的了解,我们按照 ASP.NETMVC 本身的实现原理构建了一个模拟程序,该程序模拟了 URL 路由、 Contro l1 er 的激活、 Action 的执行和 View 的呈现,可以将此模拟程序看成是一个 "迷你版"的 ASP.NETMVC 。 ASP. NET MVC 4 框架揭秘第 2 章 URL 路由 Ht叩Module 和 HttpHandler 是 ASP.NET 管道的两个重要的纽件。请求的 最终处理通过 Handler 来完成, ASP.NET MVC 就是通过一个名为 MvcHandler 的自定义 HttpHandler 实现了对 Controller 的激活和 Action 的执行。但是在这 之前对 Controller 和 Action 名称的解析则是通过 ASP.NET 的 URL 路由系统 来完成的,而整个 URL 路由系统,是通过一个名为 UrIRoutingModule 的自定义 Ht甲Module 实现的。 ASP. NET MVC 4 在架揭秘42 醺第 2 章 URL 路由 2.1 ASP.NET 路由系统 AS卫NETMVC 对请求的处理最终体现在对激活的目标 Controller 对应的 Action 方法的 执行。一般来说,目标 Controller 和 Action 的名称由请求的 URL 决定, URL 路由系统通过 对请求的拦截和对请求 URL 的解析,得到以 Controller 和 Action 名称为核心的路由数据。 URL 路由系统并不是专属于 ASP.NETMVC 的,而是直接建立在 ASP.NET 上。 ASP.NET 通 过 URL 路由系统实现了请求地址与物理文件的分离。 2.1.1 请求 URL 与物理文件的分离 对于一个 ASP.NET Web Forms 应用来说,每一个有效的请求都对应着一个具体的物理 文件。部署在 Web 服务器上的物理文件可以是静态的(比如图片和静态 H刊也文件等),也 可以是动态的〈比如 .aspx 文件)。对于静态文件的请求, ASP.NET 直接返回文件的整个内容, 而针对动态文件的请求则会涉及到相关代码的执行。但是这种将 URL 与物理文件紧密绑定 在一起的方式并不是一种好的解决方案,它带来的局限性主要体现在如下几个方面。 • 灵活性:由于 URL 是对物理文件路径的反映,意味着如果物理文件的路径发生了改变 (比如改变了文件的目录结构或者文件名) ,原来基于该文件的链接将变得无效。 • 可读性:在很多情况下, U也不仅仅具各基本的可用性(能够访问正确的网络资源) , 还需要具有很好的可读性。好的 URL 设计应该让我们一眼就能看出针对它访问的目 标资源是什么。请求地址与物理文件紧密绑定会让我们完全失去了定义可读性 URL 的机会。 • SEO 优化:对于网站开发来说,为了迎合搜索引擎检索的规则,我们需要对 URL 进行 有效的设计使之能易于被主流的引擎检索收录,如果 URL 完全与物理地址关联,这无 异于失去了 SEO 优化的能力。 我们需要一种更加灵活的机制来实现请求地址与文件路径的分离。说到这里,可能很多 人会想到 URL 重写。为了使 Web 应用可以独立地设计用于访问应用资源的 URL ,微软为 IIS 7 编写了一个 URL 重写模块。这是一个基于规则的 URL 重写引擎,它在 URL 被 Web 服务 器处理之前根据定义的规则重定向某个物理文件。 URL 重写在 IIS 级别解决了 URL 与物理地址的分离,它通过一个基于本地 CNative) 代 码的模块注册到 IIS 管道上,所以可以应用于所有寄宿于 IIS 中的 Web 应用,而 URL 路由 系统则是 ASP.阳T 的一部分,是通过托管代码实现的。为了让读者对 ASP.阳T 的 URL 路 由具有一个感官的认识,我们来演示一个简单的实例。 ASP. NET MVC 4 框架揭秘2.1 ASP.NET 路由系统毡 43 2.1.2 实例演示:通过 URL 路由实现请求地址与 .aspx 页面的映射 (8201 ) 我们创建一个简单的 ASP.NET Web Forms 应用,并采用一个独立于 .aspx 文件路径的 URL 来访问对应的 Web 页面,两者之间的映射通过 URL 路由来实现。我们依然沿用第 1 章 关于员工管理的场景,可以创建一个页面来显示员工的列表和某个员工的详细信息,呈现效 果如图 2-1 所示。 图 2-1 员工列表和员工详细信息页面 我们将关注点放到如图 2-1 所示的两个页面的 URL 上,用于显示员工列表的页面地址 为 h即://...l employees ,当用户点击某个显示为姓名的链接后,用于显示所选员工详细信息的 页面被呈现出来,其页面地址的 URL 模式为 "h句://...l employees/ {姓名}/ {ID} "。对于后者, 最终用户一眼可以从 URL 中看出通过该地址获取的是哪个员工的信息。 有人可能会问,为什么我们要在 URL 中同时包含员工的姓名和 ID 呢?这是因为 ID( 本 例采用 GUID) 的可读性不如员工姓名,但是员工姓名不具有唯一性,在这里我们使用的 D 是为了逻辑处理的需要而提供的唯一标识,而姓名则是出于可读性的需要。 我们将员工的所有信息(l D 、姓名、性别、出生日期和所在部门)定义在如下所示的 Employee 类型中,它与我们在第 1 章" ASP.NET + MVC" 中演示 Model2 模式中的同名类型 具有一致的定义。我们照例定义了如下一个 EmployeeRepository 类型来维护员工列表的数 据。简单起见,员工列表通过静态字段 employees 表示。 EmployeeRep0 sitory 的 GetEmployees 方法根据指定的 ID 返回包含指定的员工,如果指定的 ID 为"*",则返回所有员工列表。 e e vd o 币 4p m E S s a 币 4C C .-14 ·p u p-- ‘ αdqJ nn -l ·工 rr tt ss ce ---l y414 bb uu pp Id { geti private seti } Name { geti private seti } ASP. NET MVC 4 框架揭秘44 由第 2 章 URL 路由 e m qiq n 巾 4 口 ·工 e-l r ←」 r tat sDS CCC ·工·工-ll114 bbb uuu ppp Gender { get; private set; } BirthDate { get; private set; } Department { geti private set; } public Employee (string id , string name , string gender , DateTime birthDate , string department) this.Id = id; this.Name =ηame; this.Gender = ge 口 der; this.BirthDate = birthDate; this.Department = department; public class EmployeeRepository private static IList employeesi static EmployeeRepository() employees = new List(); employees.Add(new Employee(Guid.NewGuid() .ToString() , "张三","男", new DateTime(1981 , 8 , 24) , "销售部") ) ; employees.Add(new Employee(Guid.NewGuid() .ToString() , "李四","女", new DateTime (1982 , 7 , 10) , "人事部") ) ; employees.Add(new Employee(Guid.NewGuid() .ToString() , "王五", "男", new DateTime(1981 , 9 , 21) , "人事部") ) ; public IEnumerable. GetEmployees (string id = "") return employees.Where(e => e.Id == id II stri 口 g. 工 sNullOrEmpty(id) II id=="*"); 对于如图 2-1 所示的两个页面实际上对应着同一个 .aspx 文件,即作为 Web 应用默认页 面的 Defaul t. aspx 。要通过一个独立于物理路径的 URL 来访问该 .aspx 页面,就需要采用 URL 路由机制来实现两者之间的映射。我们将实现映射的路由注册代码定义在 Globa l. asax 文件 中,如下面的代码片段所示,在 Application_ Start 方法中通过 System. Web.Routing.RouteTable 的 Routes 属性得到了表示路由对象列表的 System. Web.Routing.RouteCollection 对象,并调用 该列表对象的 MapPageRoute 方法将 Defaul t. aspx 页面( -lD efaul t. aspx) 与一个 URL 模板 (employees/ {name}/ {id}) 进行了映射。 public class Global : System.Web.HttpApplication protected void Application_Start(object sender , EventArgs e) var defaults = new RouteValueDictionary{{"name" ,"女"}, {"id" , "女" } } ; RouteTable.Routes.MapPageRoute("" , "employees/{name}/{id}" , "-/Default.aspx" , true , defaults); 作为 MapPageRoute 方法最后一个参数的 Route ValueDictionary 对象用于指定定义在路由 ASP. NET MVC 4 框架揭秘2.1 AS P. NET 路由系统 滋 45 模板中路由变量(" {name}" 和" {id} 勺的默认值。如果我们为定义 URL 模板中的路由变 量指定了默认值(指定默认值的变量不一定需要定义在 URL 模板中),在当前请求地址的后 续部分缺失的情况下,它会采用提供的默认值对该地址进行填充之后再进行模式匹配。在如 上所示的代码片段中,我们将 {name} 和 {id} 两变量的默认值均指定为"牢"。对于针对 URL 为" /employees" 的请求,我们注册的路由对象会将其格式成" /employees/串户",后者无疑是 与定义的 URL 模板模式相匹配的。 在 Default. aspx 页面中,我们分别采用 GridView 和 DetailsView 来显示所有员工列表和 某个列表的详细信息,下面的代码片段表示该页面主体部分的 HTML 0 GridView 模板中显 示为员工姓名的 HyperLinkF ield 的链接采用了上面我们定义在 URL 模板 (employees/ {name } / {id} )中的模式。
, ICollection> , IEnumerable> , IEnumerable //省咯成员 在某些路由场景中,我们要求路由对象针对当前请求进行解析得到的变量集合 CValues 属性)必须包含某些固定名称的变量值(比如 ASP.NETMVC 应用中表示 Controller 和 Action 名称的变量),而 Ge tRequiredS 位 ing 方法用于获取指定名称的变量值。对于该方法的调用,如 果指定名称的变量在 Values 属性中不存在,则直接抛出一个InvalidOperatio nE xception 异常。 RouteData 还具有另一个名称为 RouteHandler 的属性,其类型为具有如下定义的 System. Web.Routing .I RouteHandler 接口。 IRouteHandler 接口在整个 URL 路由系统中具有重 要的地位,其重要作用在于提供最终用于处理请求的 H即 Handler 对象(通过调用其 GetHttpHandler 方法获取)。我们可以在构造函数中对 RouteData 的 RouteHandler 属性进行 初始化,也可以直接对该属性进行赋值。 ASPNETMVC4 框架揭秘48 • 第 2 章 URL 路由 public interface IRouteHandler IHttpHandler GetHttpHandler(RequestContext requestContext)i 当请求被成功路由到某个 .aspx 页面后,通过匹配路由对象的 GetRouteData 方法得到的 RouteData 对象被直接附加到目标页面对应的 Page 对象上。如下面的代码片段所示, Page 类型具有一个类型为 RouteData 的同名只读属性,返回的正是这个 RouteData 对象。 public class Page : TemplateControl, IHttpHandler //其他成员 public RouteData RouteData { geti } 介绍完了作为 RouteBase 的 GetRouteData 方法的返回类型 RouteData 之后,我们接着介 绍作为 GetVirtualPath 方法返回类型的 VirtualPathData 。当 RouteBase 的 GetVirtualPath 方法 被执行的时候,如果定义在 URL 模板中的变量与指定变量列表相匹配,则将指定的路由变 量值替换 URL 模板中的变量占位符以生成一个虚拟路径。生成的虚拟路径与路由对象最终 被封装成一个 VirtualPathData 对象返回,它们对应着 VirtualPathD ata 对象的 VirtualPath 和 Route 属性。另一个 DataTokens 属性和 RouteData 的同名属性一样都是来源于附加到路由对 象的自定义变量集合。. public class VirtualPathData public VirtualPathData(RouteBase route, string virtualPath)i public RouteValueDictionary public RouteBase public string DataTokens {geti } Route { geti seti } VirtualPath {geti seti } RouteBase 的 GetVirtualPath 方法还涉及另一个 System. Web.Routing.RequestContext 类型。 RequestContext 在 U也路由系统和 ASP.NETMVC 路由体系中是一个频繁使用的类型,用于 表示当前的请求上下文。从如下的代码片段中不难看出它实际上是对 HTTP 上下文和 RouteData 的封装。 public class RequestContext public RequestContext()i public RequestContext(HttpContextBase httpContext, RouteData routeData)i Route public virtual HttpContextBase public virtual RouteData HttpContext { geti seti } RouteData { geti seti } System. Web.Routing.Route 是抽象类 RouteBase 唯一的直接子类,基于 URL 模板模式的 路由匹配规则就定义在 Route 中,在默认的情况下通过调用 RouteCollection 的 MapPageRoute ASP. NET MVC 4 框架揭秘2.1 AS P. NET 路由系统 霞 49 方法在全局路由表中添加的就是这么一个对象。如下面的代码片段所示, Route 具有一个字 符串类型的属性 Url.它代表绑定在该路由对象的 URL 模板。 public class Route : RouteBase public Route(string url, IRouteHandler routeHandler)i public Route(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)i public Route(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)i public Route(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler)i public override RouteData GetRouteData(HttpContextBase httpContext)i public override VirtualPathData GetVirtualPath (RequestContext requestContext, RouteValueDictio口 ary valueS)i public RouteValueDictionary public RouteValueDictionary public RouteValueDictionary public IRouteHandler public string Constraints { geti seti } Defaults { geti seti } DataTokens { geti seti } RouteHandler { geti seti } Url { geti seti } 在默认的情况下,针对请求 URL 的路由通过 Route 对象来完成,而某个 Route 对象是 否会被选择取决于请求的地址是否与对应的 URL 模板的模式相匹配。具体的匹配规则很简 单,可以通过→个简单的例子来说明,假设我们具有如下一个模板表示获取某个地区(通过 电话区号表示)未来 N 天的天气情况的 URL 。 /weather/{areacode}/{days} 对于上述这个 URL 模板来说,通过分隔符"/"对其进行拆分得到 4 个基本的字符串,我 们将它们称为"段 (Segment)"。对于组成某个段的内容,又可以分为"变量 (Variable) "和 "文本(Literal) ",前者通过采用花括号(" {} ")对变量名的封装来表示(比如表示电话区号 的" {areacode} "和天数的" {days} 勺,后者则代表单纯的文字(比如 "wea由er") 。值得一提 的是 URL 路由体系在字符比较时对于大小写是不敏感的,因为 URL 本来就不区分大小写。 对于一个试图进行匹配的地址来说,匹配成功需要有两个基本的条件,即该地址包含的 段的数量和 URL 模板相同,对应的文本段内容也一致。按照这个匹配规则,下面这个 URL 和上面我们定义的 URL 模板是相匹配的。 /weather/0512/2 除了用于表示 URL 模板的核心属性 Ur1之外, Route 还具有额外一些属性。属性 Constraints 为定义在 URL 模板中的变量以正则表达式的形式设定一些限制条件,该属性类 型为 RouteValueDictionary ,其 Key 和 Value 分别表示变量名和作为限制的正则表达式。比如 对于上面定义的这个 URL 模板来说,我们为两个变量指定相应的正则表达式使请求地址具 有合法的区号和作为整数的未来天数。如果我们通过该属性为 Route 对象定义了基于某些变 ASP. NET MVC 4 框架揭秘50 • 第 2 章 URL 路由 量的正则表达式,匹配成功的先决条件除了上述两个之外,被验证的 URL 中对应的段还必 须通过对应的正则表达式的验证。 Route 另一个类型为 Route ValueDictionary 的属性 Defaults 为变量定义默认一个默认值, 提供默认值的变量不要求一定要定义在 URL 模板中。当 Route 对给定 URL 进行匹配判断的 时候,如果 URL 只能匹配模板前面的部分,但是后边部分均为变量段并且具有对应的默认 值,这种情况下依然被视为成功匹配。还是以前面给出的 URL 模板为例,如果我们将 {areacode} 和 {days} 这两个变量的默认值分别设置为 "010" (北京)和 "2" (未来两天),如 下所示的 3 个 URL 都能和该 Route 成功匹配,并且它们是等效的。 /weather/Ol0/2 /weather/Ol0 /weather/ 关于定义在 URL 模板中的变量,我们并不要求它作为整个段的内容,换句话说,一个 段可以同时包含文本和变量。此外,我们可以采用" {*<>} "来匹配 URL 的最后 的部分〈可以包含多个段),而匹配的内容最终作为对应变量的值,姑且称之为"通配变量"。 /{filename}.{extension}/{*pathi 口 fo} 对于如上的这个 URL 模板,第一个段中包含两部分内容,即表示文件名称和扩展名的变 量{fi1 ename} 和{ extension } ,以及作为两者分隔符的文本内容".",后边紧跟一个通配文变量 {*pa恤削。这个 URL 模板与下面一个 U也是可以成功匹配的,匹配后定义在 URL 模板中 的三个变量( {fi1 ename} 、 {extension} 和 {pathinfo} )的值分别为 "default" 、 "aspx" 和 "abc/123" 。 /default.aspx/abc/123 Route 的 DataTokens 属性在之前已经有所提及,它用于存储一些额外变量,它们不会参 与针对请求地址的匹配工作。对于调用 Route 的 GetRouteData 和 Get VirtualPath 方法分别得 到的 RouteData 和 VirtualPat hD ata 对象,它们的 DataTokens 属性所包含的数据都来源于此。 RouteTable 对于一个 Web 应用来说,针对所有页面对应的 U也不可能采用相同的模式,与之匹配 的路由对象自然也不可能是唯一的。一个 Web 应用通过 RouteTable 的静态只读属性 Routes 维护一个全局的路由表,如下面的代码片段所示,该属性的类型为 System. Web.Routing. RouteCollection 。 e l b a m4 e ←」u O R s s a 14 C C .工14 b u p{ public static RouteCollection .Routes { get; } 顾名思义, RouteCollection 就是一个路由对象的集合,它提供了如下一些方法和属性。 定义在 RouteCollection 中的方法和属性是为最终的开发人员设计的,我们针对 URL 路由系 ASP. NET MVC 4 框架揭翻2.1 ASP.NET 路由系统白 51 统的编程所涉及的方法主要集中在这里。 public class RouteCollection : Collection //其他成员 public RouteData GetRouteData(HttpContextBase httpContext); public VirtualPathData GetVirtualPath(RequestContext requestContext , RouteValueDictionary values); public VirtualPathData GetVirtualPath(RequestCoηtext requestContext , string narne , RouteValueDictionary values); public void Ignore(string url); public void Ignore(string url , object constraints); public Route MapPageRoute(string routeNarne , string routeUrl , string physicalFile); public Route MapPageRoute(string routeNarne , string routeUrl , string physicalF 工 le , bool checkPhysicalUrlAccess); public Route MapPageRoute(string routeNarne , string routeUrl , string physicalFile , bool checkPhysicalUrlAccess , RouteValueDictionary defaults); public Route MapPageRoute(string routeName , string routeUrl , string physicalFile , bool checkPhysicalUrlAccess , RouteValueDictionary defaults , RouteValueDictionary constraints); public Route MapPageRoute(string routeName , string routeUrl , string physicalFile , bool checkPhysicalUrlAccess , RouteValueDictionary defaults , RouteValueDictionary constraints , RouteValueDictionary dataTokens); public bool AppendTrailingSlash { get; set; } public bool LowercaseUrls { get; set; } public bool RouteExistingFiles { get; set; } 当我们调用 RouteCollection 的 GetRouteData 和 Get VirtuaIPath 方法的时候,其内部会遍 历集合中的每一个路由对象,并传入给定的参数调用同名方法直到找到一个与指定的请求 URL 相匹配的路由对象(返回值不为 Nu l1) ,并返回相应的 RouteData 和 VirtualPathD ata 对 象。如果集合中的任何一个路由对象都不匹配,则最终的返回结果是 Null 。 RouteCollection 的 RouteExistingFiles 属性用于控制是否需要对存在的物理文件实施路 由,也就是说如果请求的 URL 与某个物理文件的路径一致的情况下是否还需要对其实施路 由。该属性默认值为 False ,即注册的路由不会影响针对物理文件的请求。 AppendTrailingSlash 和 LowercaseU r1 s 这两个布尔类型的属性与方法 GetVirtuaIPath 有关, 它们决定了对 URL 的正常化 (N onnalization) 。具体来说, AppendTrailingSlash 表示是否需要 在末尾添加"/" (如果没有),而 LowercaseUr1 s 则意味着是否需要将生成的 URL 转变成小写。 其实我们使用得较为频繁的还是 MapPageRoute 和 Ignore 方法,前者用于注册某个物理 文件(路径)与 URL 模板之间的映射,其本质就是在本集合中添加一个 Route 对象。后者 则与此相反,用于注册一个 URL 模板使路由系统可以忽略掉某些 URL 。 ASP. NET MVC 4 框架揭秘52 坦第 2 章 URL 路由 2.1.4 路由映射 总的来说,我们可以通过 RouteTable 的静态属性 Routes 得到一个基于整个应用的全局路由 表,通过上面的介绍我们知道这是一个类型为 RouteCollection 的集合对象,可以通过调用它的 MapPageRoute 进行路由映射,即注册 URL 模板与某个物理文件的匹配关系。路由注册的核心 就是在全局路由表中添加一个 Route 对象,该对象的绝大部分属性都可以通过 MapPageRoute 方 法的相关参数来指定。接下来我们通过实现演示的方式来说明路由注册的一些细节问题。 前面给出了一个获取天气预报信息的 URL 模板,现在在 AS 卫 NET Web 应用中创建一个 Weather.aspx 页面,不过我们并不打算在该页面中呈现任何天气信息,而是将基于该页面的路由 信息打印出来。该页面主体部分的 HTML 如下所示,不仅将基于当前页面的 RouteData 对象的 Route 和 RouteHandler 属性类型输出来,还将存储于 Values 和 DataTokens 字典的变量显示出来。
RouteData.Route.GetType() .FullNarne:"" 毛 >
Route: < 毛 =RouteData.Route != null?
RouteHandler: < 毛 =RouteData.RouteHandler !=口 ull? RouteData.RouteHandler.GetType() .FullName:"" 毛 >
Values:
    <毛 foreach (var variable in RouteData.Values) {毛>
  • <毛 =variable.Key 毛>=<毛 =variable.Value 毛 >
  • <宅}毛>
DataTokens:
    <毛 foreach (var variable in RouteData.DataTokens) {毛>
  • <毛 =variable.Key 宅>=<毛 =variable.Value 毛 >
  • <毛}毛>
ASPNETMVC4 在架揭秘2.1 ASP.NET 路由系统 l* 53 在添加的 Globa l. asax 文件中,我们将路由注册操作定义在 App lication _ Start 方法中,如 下面的代码片段所示,映射到 weather.aspx 页面的 U阻J 模板为"{缸 eacode } / { days } "。在调 用 MapPageRoute 方法的时候,我们还为定义在 URL 模板的两个变量定义了默认值以及正则 表达式。除此之外,我们还在注册的路由对象上附加了两个变量,表示对变量默认值的说明 ( defaultCity: BeiJing; defaul tD ays: 2 )。顺便说一下, MapPageRoute 方法中布尔类型的参 数 chec kP hysicalUrlAccess 表示是否需要对表示被路由的目标地址的 URL 实施授权(针对原 请求地址的 URL 授权总是会执行〉。 public class Global : System.Web.HttpApplication protected void Application Start(object sender , EventArgs e) 变量默认值 var defaults = new RouteValueDictionary { { "areacode" , "010" }, { "days" , 2 }}; var constaints = new RouteValueDictionary { { "areacode" ,自" 0 \ d { 2 , 3 }" }, { " da y s ", @" [1- 3 ]{ 1 }" } }; var dataTokens = new RouteValueDictionary { { "defaul tCi ty" , "BeiJing" }, { "defaul tDays" , 2 } }; RouteTable.Routes.MapPageRoute("default" , "{areacode}/{days}" , "-/weather.aspx" , false , defaults , constaints , dataTokens); 由于我们为定义在 URL 模板中表示区号和天数的变量定义了默认值(缸 eacode: 010; days: 2) ,如果希望返回北京地区未来两天的天气,可以直接访问应用根地址,也可以只指 定具体区号,或者同时指定区号和天数。如图 2-2 所示,当我们在浏览器地址栏中输入上述 三种不同的 URL 会得到相同的输出结果。 DataTokens: defaultCi ty.B~1 defaul tDays ~ 2 ~ RouteHandler: Sy山 m. Web.Rou ti n g. P ageRou tëHan dler 图 2-2 基于变量默认值的 URL 等效性 Vah居民 areacode=O 1 0 days=2 DataTokens: defaultCity=BeiJ ing defaultDaysa2 从图 2-2 所示的路由信息中可以看到,默认情况下 RouteData 的 Route 属性类型正是 ASP. NET MVC 4 框架揭秘54 、 第 2 章 URL 路由 约束 System. Web.Routing.Route ,而 RouteHandler 属性则是一个 System. Web.Routing.PageRouteHandler 对象,我们会在本章后续部分对 PageRouteHandler 进行详细介绍。通过地址解析出来的变量 被保存在 Values 属性中,而在进行路由注册过程为 Route 对象的 DataTokens 属性指定的变 量被转移到了 RouteData 的同名属性中。 (S202) 我们以电话区号代表对应的城市,为了确保用户在请求地址中提供有效的区号,通过正 则表达式(" 0\d{2,3} ")对其进行了约束。此外,假设只能提供未来 3 天以内的天气情况, 我们同样通过正则表达式(" [l-3]{ 1} ")对请求地址中表示天数的变量进行了约束。如果请 求地址中的内容不能符合相关变量段的约束条件,则意味着对应的路由对象与之不匹配。 对于本例来说,由于只注册了唯一的路由对象,如果请求地址不能满足我们定义的约束 条件,则意味着找不到一个具体目标文件,会返回 404 错误,如图 2-3 所示,由于在请求地 址中指定了不合法的区号 (01) 和天数 (4) ,我们直接在浏览器界面上得到一个 Hπ'p 404 错误。 (S202) The resource cannot be found. Description: tfπP 404. The resource you are 10。嗣 9 for (or one 0.1 i旨 depend回四"四 uld have b回n removed, had 幅 name changed, or is • 图 2-3 不满足正则表达式约束导致的 404 错误 对于约束,除了可以通过字符串的形式为某个变量定义相应的正则表达式之外,还可 以指定一个实现了 System. Web.Routing .IRouteConstraint 接口类型的对象对整个请求进行 约束。如下面的代码片段所示, IRouteConstraint 具有唯一的方法 Match 用于定义约束的逻 辑,该方法的 5 个参数分别表示: HTTP 上下文、当前路由对象、约束的名称(存储约束 对象在 RouteValueDictionary 中对应的 Key) 、解析被匹配 U也得到的变量集合以及表示路 由的方向。 public interface IRouteConstraint bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection); ASP. NET MVC 4 框架揭翻2.1 ASP.NET 路由系统 黯 55 public enum RouteDirection , •L s en uo qf 工 et Ra qdr ηe ·工 n me OG cl nr IU 所谓路由的方向表示是针对请求匹配(入校〉还是针对 URL 的生成(出校) ,分别通过 如上所示的枚举类型 System. Web.Routing.RouteDirection 的两个枚举值表示。具体来说,当 调用路由对象的 GetRouteData 和 GetVirtualPathD ata 方法时,分别采用枚举值 IncomingRequest 矛日 UrlGeneration 。 ASP.阳T 路由系统的应用编程接口中定义了如下一个实现了 IRouteConstraint 接口的 H即MethodConstraint 类型,顾名思义, H忧pMethodConstraint 提供针对 HTTP 方法 CGET 、 POST 、 PUT 、 DELETE 等〉的约束,可以通过 HttpMethodConstraint 为路由对象设置一个允 许的 HTTP 方法列表,只有在这个指定的列表中的 HTTP 方法名称的 HTTP 请求才允许被路 由。这个被允许路由的 HTTP 方法列表对应于 HtφMethodConstraint 的只读属性 AllowedMethods ,并在构造函数中初始化。 public class HttpMethodConstraint : IRouteConstraint public HttpMethodConstraint(params string[] allowedMethods); bool IRouteConstraint.Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection); public ICollection AllowedMethods { get; } 同样是针对前面演示的例子,我们在进行路由注册的时候通过如下的代码应用了一个类 型为 H即MethodConstraint 的约束,并将允许的 HTTP 方法设置为 POST ,意味着被注册的 Route 对象仅限于路由 POST 请求。 public class Global : System.Web.HttpApplication protected void Application Start(object sender, EventArgs e) var defaults = new RouteValueDictionary { { "areacode" , "010" }, { " da y s ", 2 } }; var constaints = new RouteValueDictionary { { "areacode" ,自 "0\d{2 , 3}" }, { "days" ,自 "[1-3]{1}" }, { "httpMethod" , new HttpMethodConstraint("POST") } }; var dataTokens =ηew RouteValueDictionary { { "defaultCity", "BeiJi 口g" }, { "defaul tDays", 2 } }; RouteTable.Routes.MapPageRoute("default", "{areacode}/{days}", "-/weather.aspx", false, defaults, constaints, dataTokens); 现在我们采用与注册的 URL 模板相匹配的地址 (/010/2) 来访问 Weather.aspx 页面,依 然会得到如图 2-4 所示的 HTTP404 错误。 CS203) ASP. NET MVC 4 框架揭秘56 电第 2 章 URl 路由 醒黠曹再唱E S … Erro时rro阳ro阳O旷r in '州川'/γ呻/' The fi陌esou旷rc臼e cannot b悦e 白旬'J und. Description: HTIP 404 . 节晴陪source you are 100 胁 g for (or cne 01 ts 曲 pend酬惜别∞ uld have been removed , had ts name changed , or is terrøorarily unava 阳ble . 问 8se review the 旬 IOwing URL and make sure thalit 幅SPeJIed corr由::tIy . 图 2 -4 不满足 HTTP 方法约束 (POST) 导致的 404 错误 对现青物理文件的路由 在成功注册路由的情况下,如果我们按照传统的方式访问一个物理文件〈比如 .aspx 、 .css 或者扣等),在请求地址满足某个路由的 URL 模板模式的情况下, ASP.NET 是否还是正常 实施路由呢?不妨通过实例来测试一下。为了让针对某个物理文件的访问地址也满足注册路 由对象的 U也模板模式,我们需要按照如下的方式在进行路由注册时将表示约束的参数设 置为 Null 。 public class Global : System.Web.HttpApplication protected void Application Start(object sender , EventArgs e) var defaults = new RouteValueDictionary { { "areacode" , "010" }, { "days" , 2 }}; var dataTokens = new RouteValueDictionary { { "defaultCity" , "BeiJing" }, { "defaultDays" , 2 } }; RouteTable.Routes.MapPageRoute("default" , "{areacodeJ/{days}" , "-/weather.asþx" , false , defaults , null , dataTokens); 当通过传统的方式来访问存放于根目录下的 weather.aspx 页面时会得到如图 2-5 所示的 结果,从界面上的输出结果不难看出,虽然请求地址完全满足我们注册路由对象的 URL 模 板模式,但是 ASP. 阳 T 并没有对请求地址实施路由。原因很简单,如果中间发生了路由, 基于页面的 RouteData 的各项属性都不可能为空。 (S204) Route: RouteHandler: Values: DataTokens: 图 2-5 直接请求现存的物理文件 (RouteExistingFiles = false) ASP. NET MVC 4 在架揭秘2 . 1 ASP. NET 路由系统 L. 57 如果请求地址对应着一个现存的物理文件, ASP.NET 会不会总是自动忽略路由呢?实则 不然,不对现有文件实施路由仅仅是默认采用的行为而己,是否对现有文件实施路由取决于 代表全局路由表的 RouteCollection 对象的 RouteExistin gFi1 es 属性,该属性默认情况下为 False ,可以将此属性设置为 True 使 ASP.NET 路由系统忽略现有物理文件的存在,总是按照 注册的路由表进行路由。为了演示这种情况,我们对 Globa l. asax 文件作了如下改动,在进 行路由注册之前将 RouteTable 的 Routes 属性代表的 RouteCollection 对象的 RouteExistin gF iles 属性设置为 True 。 public class Global : System.Web.HttpApplication protected void Application Start(object sender , EventArgs e) RouteTable.Routes.RouteExistinqFiles = true; var defaults = new RouteValueDictionary { { "areacode" , "010" }, { "days" , 2 } }; var dataTokens = new RouteValueDictionary { { "defaul tCi ty" , "BeiJing" }, { "defaul tDays" , 2 } }; RouteTable.Routes.MapPageRoute("default" , "{areacode}/{days}" , "-/weather . aspx" , false , defaults , null , dataTokens); 依旧是针对 weather.aspx 页面的访问,却得到了不一样的结果。从图 2-6 中可以看到, 针对页面的相对地址 weather.aspx 不再指向具体的 Web 页面,在这里就是一个表示获取的天 气信息对应的目标城市忧 eacode =w eather碑 x)o (S205) Route : System .Web .Routing .Route RouteHandler: System .\\' eb .Routing .PageRouteHandler Values : • areacode =w ea也 er.aspx • dav s= 2 DataT okens: • d e:也世tCit)=B eiJing • def挝tD ay s=.2 图 2-6 直接请求现存的物理文件 (RouteExistingFiles = true) 注册路由忽略地址 如果将代表全局路由表的 RouteTable 的静态属性 Routes 的 RouteExistin gF iles 属性设置 为 True ,意味着 ASP.NET 针对所有抵达的请求都按照注册的路由表进行注册,但这同样会 带来一些问题 。 不知道读者有没有发现图 2-5 和图 2-2/2-4 所示的页面具有不一样的样式, 这是因为我们可以在页面中按照如下的方式引用一个 .css 文件来定义样式。 ASP. NET MVC 4 在架渴秘58 电第 2 章 URL 路由 由于我们将全局路由表的 RouteExistingF i1 es 属性设置为 True ,意味着针对上面这个 Style.css 文件的访问也会被路由。根据我们注册的路由规则,针对这个文件的访问会被自动 导向 weather.aspx 这个页面, JS 脚本文件名被当成了路由变量 {areaCode} 的值。如图 2-7 所 示,我们直接在浏览器的地址栏中输入 Style.css 文件的地址,呈现出来还是我们所熟悉的界 面 (areacode= Style.css)0 (S205) I lj J X / ‘ 黯囱脚 八. ! 争 吵 σ tÎ l …阴阳市市副 1 61!Stvl跚黯圈~耐 ~、 Rot田皿eιSys筑t臼n Rou回teH缸啤er: Sys恒m . Web . R,α血ε ..P a驾ε:eRou回t eH恤缸!dI丑如er f V a1ues: • areacod e= S 恍.四 • days=2 DataTokens: • def泪趾C ity=B eiJ句 • def泣ùtD ays=2 图 2-7 直接请求现存的扣文件 (RouteExistingFiles = true) 这是一个不得不解决的问题,因为它使我们无法正常地在页面中引用 JavaScript 和 CSS 文件,可以通过调用 RouteCollection 的 Ignore 方法来注册一些需要让路由系统忽略的 URL 模板。从前面给出的关于 RouteCollection 的定义中可以看到它具有两个 Ignore 方法重载,除 了指定需要忽略的 URL 模板之外,还可以对相关的变量定义约束正则表达式。为了让路由 系统忽略掉针对 CSS 文件请求,我们可以按照如下的方式在 Globa l. asax 中调用 RouteTable 的 Routes 属性的 Ignore 方法。值得一提的是这样的方法调用应该放在路由注册之前,否则 起不到任何作用。 (S206) public class Global : System.Web.HttpApplication protected void Application_Start(object sender , EventArgs e) RouteTable.Routes.RouteExistingFiles = true; RouteTable.Routes.Ignore("{filename}.css/{*pa 世lInfo} ") ; //其他操作 直接添加路由对象 我们调用 RouteCollection 对象的 MapPageRoute 方法进行路由注册的本质就在路由字典 1 由于 URL 路由是实现在 ASP.NET 中,在采用 IIS 作为宿主的情况下,对于 IIS 6.0 之前的版本或者 Classic 模式下 的 ßS 7.0 ,针对静态 .css 文件的请求根本不会进入 ASP.NET 管道,所以对 .css 文件的请求是不会被路由的.我们 这个例子是寄宿在 Visual Studio Development Server 中. ASP. NET MVC 4 框架揭秘2.1 ASP.NET 路由系统 部 59 中添加 Route 对象,所以我们完全可以调用 Add 方法添加一个手工创建的 Route 对象,如下 所示的两种路由注册方式是完全等效的。如果需要添加一个继承自 RouteBase 的自定义路由 对象,我们不得不采用手工添加的方式。 public class Global : System.Web.HttpApplication protected void Application Start(object sender , EventArgs e) var defaults =口 ew RouteValueDictionary { { "areacode" , "010" }, { "days" , 2 }}i var constaints = new RouteValueDictionary { { "areacode" , @"0\d{2 , 3}" }, { "days" , @"[1-3]{1}" } }i var dataTokens = new RouteValueDictionary { { "defaultCity" , "Be 工 Jing" }, { "defaul tDays" , 2 } } i //路由注册方式 1 RouteTable.Routes.MapPageRoute("default" , "{areacode}/{days}" , '-/weather.aspx" , false , defaults , constaints , dataTokens)i //路由注册方式 2 Route route = new Route("{areacode}/{days}" , defaults , constaints , dataTokens , new PageRouteHandler("-/weather.aspx" , false))i RouteTable.Routes.Add("default" , route)i 2.1.5 根据路由规则生成 URL 前面我们已经提到过 ASP.NET 的路由系统主要有两个方面的应用,一个就是通过注册 URL 模板与物理文件路径的匹配实现请求地址和物理地址的分离,另一个则是通过注册的 路由规则生成一个相应的 URL ,后者通过调用 RouteCollection 对象的 GetVirtuaIPath 方法来 实现。 如下面的代码片段所示, GetVirtualPath 定义了两个 GetVirtualPath 方法重载,它们共同 的参数 requestContext 和 values 分别表示请求上下文 CRouteData 和 HTTP 上下文的封装)和' 用于替换定义在 URL 模板中的变量占位符的值。另一个 GetVirtualPath 方法具有一个额外的 字符串参数 name ,它表示集合中具体使用的路由对象的注册名称(调用 MapPageRoute 方法 时指定的第一个参数)。 public class RouteCollection : Collection //其他成员 public VirtualPathData GetVirtualPath(RequestContext requestContext , RouteValueDictionary values)i public VirtualPathData GetVirtualPath(RequestContext requestCo 口 text , string name , RouteValueDictionary values)i 如果调用 GetVirtualPath 方法时没有指定具体生成虚拟路径的路由对象,该方法会遍历 ASP. NET MVC 4 在架揭秘60 ''11 第 2 章 URL 路由 整个集合直到找到一个 URL 模板与指定的路由参数列表相匹配的路由对象,并返回它生成 的封装虚拟路径的 VirtualPathData 对象。具体来说就是依次调用集合中每个路由对象的 Get VirtualPath 方法,直到方法返回的 VirtualPathData 对象不为 Null ,而该 VirtualPathD ata 对象则作为整个方法调用的返回值。如果所有路由的 GetVirtualPath 方法返回值均为 Null , 那么整个方法的返回值也为 Null 。 我们在调用 GetVirtualPath 方法的时候可以传入 Null 作为第一个参数 C requestContext) , 在这种情况下会基于当前 HTTP 上下文〈对应于 HttpContext 的静态属性 Current) 创建一 个 RequestContext 对象作为调用路由对象 GetVirtualPath 方法的参数,该参数包含一个空 的 RouteData 对象。如果当前 HTTP 上下文不存在则直接抛出一个 InvalidOperationException 异常。 路由对象针对 GetVirtualPath 方法而进行的路由匹配只要求 URL 模板中定义的变量的值 都能被提供,而这些变量值具有三种来源,分别是路由对象定义的默认变量值、指定 RequestContext 的 RouteData 提供的变量值 CValues 属性)和手工提供的变量值(通过 values 参数指定的 RouteValueDictionary 对象),这三种变量值具有由低到高的选择优先级。 同样以之前定义的关于获取天气信息的 URL 模板为例,我们在 Weather.aspx 页面的后 台代码中通过如下的代码调用 RouteTable 和 Routes 属性的 GetVi阳 alPath 方法将生成三个具 体的 U虹。 public partial class Weather : page protected void Page_Load(object sender, EventArgs e) RouteData routeData = new RouteData(); routeData.Values.Add("areaCode" , "0512"); routeData.Values.Add("days", "l"); RequestContext requestContext = new RequestContext(); requestContext. HttpContext = new HttpCo口 textWrapper(HttpContext.Current); requestContext.RouteData = routeData; RouteValueDictionary values = new RouteValueDictionary(); values.Add("areaCode", "028"); values.Add("days" , "3"); Response.Write(RouteTable.Routes.GetVirtualPath(null,null) .VirtualPath + "
"); Response.Write(RouteTable.Routes.GetVirtualPath(requestContext, null) ;VirtualPath + "
"); Response.Write(RouteTable.Routes.GetVirtualPath(requestContext, values) .VirtualPath + "
"); 从上面的代码片段可以看到,第一次调用 GetVirtualPath 方法传输的 requestContext 和 values 参数均为 Null; 第二次则指定了一个手工创建的 RequestContext 对象,其 RouteData 的 Values 属性具有两个变量(缸eaCode=0512; days=l) ,而 values 参数依然为 Null; 第三次 ASPNETMVC4 在架揭秘2.2 ASP.NET MVC 扩展旬 61 我们同时为参数 requestContext 和 values 指定了具体的对象,而后者包含两个参数 (缸 eaCode=028; days=3) 。在浏览器上访问 Weather.aspx 页面会得到如图 2-8 所示的 3 个 URL ,这充分证实了上面提到的关于变量选择优先级的结论。 (S207) 图 2-8 GetVirtualPath 方法接受不同参数生成的 URL 2.2 ASP.NET MVC 扩展 ASP.NET 的路由系统旨在通过注册 URL 模板与物理文件之间的映射进而实现请求地址 与文件路径之间的分离,但是对于 ASP.NETMVC 应用来说,请求的目标不再是一个具体的 物理文件,而是定义在某个 Controller 类型中的 Action 方法。出于自身路由特点的需要, AS 卫 NETMVC 对 ASP.NET 的路由系统进行了相应的扩展。 2.2.1 路由映射 通过前面的介绍我们知道,基于某个物理文件的路由注册通过调用 RouteTable 的静态属 性 Routes (一个代表全局路由表的 RouteCollection 对象〉的 MapPageRoute 方法来完成。为 了实现针对目标 Con 位 o l1 er 和 Action 的路由, ASP.NET MVC 针对 RouteCollection 类型定义 了一系列的扩展方法以实现文件路径无关的路由映射,这些扩展方法定义在 System.Web.Mvc.RouteCollectio nE xtensions 类型中。 如下面的代码片段所示, RouteCollectio nE xtensions 定义了两组方法,方法 IgnoreRoute 用于注册不需要进行路由的 URL 模板,对应于 RouteCollectio nE xtensions 的 Ignore 方法:方 法 MapRoute 用于进行基于 URL 模板的路由注册,对应于 RouteCollectio nE xtensions 的 MapPageRoute 方法。 public static class RouteCollectionExtensions //其他成员 public static void IgnoreRoute(this RouteCollection routes , string url); public static void IgnoreRoute(this RouteCollection routes , string url , 。 bject constraints); public static Route MapRoute(this RouteCollection routes , string name , string url); public static Route MapRoute(this RouteCollection routes , string narne , string url , object defaults); AS P. NET MVC 4 框架揭秘62 旬第 2 章 URL 路由 public static Route MapRoute(this RouteCollection routes, string narne , string url, string[] 口 arnespaces); public static Route MapRoute(this RouteCollection routes, string narne , string url, object defaults, object constraints); public static Route MapRoute(this RouteCollection routeS, string narne , string url, object defaults, string[] narnespaces); public static Route MapRoute(this RouteCollection routes, string narne , string url, obj ect defaul ts, obj ect constraints, string [] narnespaces); 由于 ASP.NETMVC 的路由注册与具体的物理文件无关,所以 MapRoute 方法中并没有 一个表示文件路径的 physicalF i1e 参数。与直接定义在 RouteCollectionExtensions 中的 Ignore 和 M叩PageRoute 方法不同的是,表示默认变量的参数 defaults 和基于正则表达式的变量约 束的参数 constraints 都不再是一个 RouteValueDictionary 对象,而是一个普通的。均 ect 。这主 要是为了编程上的便利,可以使我们通过匿名类型对象的方式来指定这两个参数值。该方法 在内部会通过反射的方式得到指定对象的属性列表,并转换为 RouteValueDictionary 对象, 其属性名和属性值可以作为字典元素的 Key 和 Value 。 对于 ASP.NETMVC 来说, URL 路由系统对请求地址进行解析后生成的路由数据中必须 包含目标 Controller 的名称。由于 Controller 名称仅仅对应着类型的名称,但是激活 Con位oller 实例的前提是我们能够正确地解析出它的具体类型,所以在具有多个同名 Controller 类型时 可能需要用到命名空间。在调用 M叩Route 方法的时候可以通过字符串数组类型的参数 namespaces 来指定一个命名空间的列表。对于注册的命名空间,可以指定一个代表完整命名 空间的字符串,也可以使用 "μ 作为通配符表示对命名空间相应的部分不作任何约束。 添加的命名空间列表最终是被存储于 Route 对象的 DataTokens 属性中,对应的 Key 为 "Namespaces" o MapRoute 方法没有为初始化 Route 对象的 DataTokens 属性提供相应的参数, 如果没有指定命名空间列表,所有通过该方法添加的 Route 对象的 DataTokens 属性总是一个 空的 RouteValueDictionary 对象。 对于针对定义在某个 Con位oller 中的某个 Action 的请求,如果注册的路由表与之匹配, 具体匹配的路由对象的 GetRouteData 方法被调用并返回一个具体的 RouteData 对象。对请求 地址进行解析得到的目标 Controller 和 Action 的名称必须包含在该 RouteData 的 Values 属性 对应的 RouteValueDictionary 对象中,其对应的 Key 分别为 controller 和 action 。 2.2.2 实例演示:注册路由映射与查看路由信息 (8208 ) ASP.NET MVC 通过 RouteCollection 中的扩展方法 MapRoute 进行路由映射,为了让读 者对此有一个深刻的认识,我们来进行一个简单的实例演示。依然沿用之前关于获取天气信 息的场景,看看通过这种方式进行注册的 Route 对象针对匹配的 H口?请求将返回怎样的 RouteData 对象。 我们在创建的 ASP.阳T Web 应用(不是 AS卫阳T MVC 应用)中添加一个 Web 页面 ASP. NET MVC 4 在架揭秘2.2 ASP.NET MVC 扩展臼 63 (Default.aspx) ,并按照之前的做法以内联代码的方式直接将 RouteData 的相关属性显示出 来,页面主体部分的 HTML 如下所示。需要注意的是我们显示的 RouteData 是从定义的方法 GetRouteData 获取的,而不是对应于当前页面的 RouteData 属性。
Route: < 毛 =GetRouteData() .Route != null? GetRouteData() .Route.GetType() .FullNarne:"" 毛 >
RouteHandler: < 毛 =GetRouteData() . RouteHandler != null? GetRouteData() .RouteHandler.GetType() .FullNarne:"" 毛 >
Values:
    <毛 foreach (var variable in GetRouteData() .Values) {毛>
  • <毛 =variable.Key毛>=<毛 =variable.Value 毛 >
  • <毛}毛>
DataTokens:
    <毛 foreach (var variable in GetRouteData() . DataTokens) {毛>
  • <毛 =variable.Key毛>=<毛 =variable.Value 毛 >
  • <毛}毛>
我们将 GetRouteData 方法定义在当前页面的后台代码中,如下面的代码片段所示,我 们手工创建了一个 H忧pRequest 和 HttpResponse 对象, H仕pRequest 的请求的地址为 ''http://localhost:3721/0512/3'' (3 721 是本 Web 应用对应的端口号)。根据这两个对象创建了 HttpContext 对象,并以此创建一个 HttpContextWrapper 对象。最终将其作为参数调用 RouteTable 的 Routes 属性的 GetRouteData 方法并返回。这个方法实际上就是模拟注册的路 由表针对相对地址为 "/0512/3 "的 HTTP 请求的路由处理。 public partial class Default : Systern.Web.UI.Page ASP. NET MVC 4 在架揭秘64 翻第 2 章 URL 路由 private RouteData routeData; public RouteData GetRouteData() if (null != routeData) return routeData; HttpRequest request =口 ew HttpRequest("default.aspx" , "http://localhost:3721/0512/3" , null); HttpResponse response =口 ew HttpResponse(new StringWriter()); HttpContext context =口 ew HttpContext(request , response); HttpContextBase contextWrapper = new HttpContextWrapper(context); return routeData = RouteTable.Routes.GetRouteData(contextWrapper); 具体的路由映射依然定义在添加的 Globa l. asax 文件中,如下面的代码片段所示,通过 调用 RouteTable 的 Routes 属性的 MapRoute 方法注册了一个采用"{缸 eacode } / { days } "作为 URL 模板的路由对象,并指定了默认变量、约束和命名空间列表。由于成功匹配的路由对 象必须具有一个名为" controller" 的路由变量,为了确保程序的成功运行,我们可以直接将 controller 的值设置为 "Home" 。 public class Global.: System.Web.HttpApplication protected void Application_Start(object sender , EventArgs e) object defaults = new { } ; areacode = "010" , days = 2 , defaultCity = "BeiJing" , defaultDays = 2 , controller ="嚣。'me" object constraints = new { areacode = @"0\d{2 , 3}" , days = @" [1-3] {1}" }; string[] namespaces = new string[] { "Artech.Web.Mvc" , "Artech.Web.Mvc.Html" }; RouteTable.Routes.MapRoute("default" , "{areacode}/{days}" , defaults , constra 工 nts , namespaces); 如果我们现在在浏览器中访问 Defaul t. aspx 页面,会得到如图 2-9 所示的结果,从中可 以得到一些有用的信息。 • 与调用 RouteCollection 的 MapPathR oute 方法进行路由映射不同的是,这个得到的 RouteData 对象的 RouteHandler 属性是一个 System. Web.Mvc.MvcRouteHandler 对象。 • 在 MapRoute 方法中通过 defaults 参数指定的两个与 URL 匹配无关的变量 (defaultCity=BeiJing; defaul tD ays=2) 体现在 RouteData 的 Values 属性中。这意味着如 果我们没有在 URL 模板中为 Controller 和 Action 的名称定义相应的变量( {controller} 和 {action}) ,也可以将它们定义成默认变量。 ASP. NET MVC 4 框架揭秘2.2 ASP.NET MVC 扩展 ÌI 65 • DataTokens 属性中包含一个 Key 为 "Namespaces" Value 为字符数组的元素,不难猜出 它对应着我们指定的命名空间列表。 Route: System . Web . R 口 uting.Route RouteHandler: Sys tem . Web.Mvc. t.\vcRou teHandler Values: areacode=0512 days=3 defaultCi ty=Be iJ ing defaul tD ays=2 C 口 n troller=Home DataTokens: Namespaces=System . String[] 图 2-9 采用 ASP.NETMVC 路由映射得到的 RouteData 2.2.3 缺省 URL 参数 当通过 Visual Studio 的 ASP.NETMVC 项目模板创建一个 Web 应用后,它会为我们注册 如下一个 URL 模板为 "{controller}/{action}/{id} "的默认路由对象。三个路由变量( controller 、 action 和 id) 均具有相应的默认值,但是变量名为 id 的默认值为 UrIParameter.Optional 。按 照字面的意思,我们将其称为缺省 URL 参数,那么将默认值进行如此设置与设置一个具体 的默认值有什么区别呢? qd -l 俨目占n o c e ←」U O R s s a 14 C C .-14 .p u p&It public static void RegisterRoutes(RouteCollection routes) jj 其他操作 routes.MapRoute( narne: "Default" , url: "{controller}j{action}j{id}" , defaults: new { controller = "Horne" , action = "1ηdex" , id = UrlParameter. Op tional } 在介绍缺省 URL 参数之前,不妨先来介绍定义它的 System. Web.Mvc. UrIParameter 类型 的定义。如下面的代码片段所示, UrIParameter 是一个不能被实例化的类型(它具有唯一一 个私有构造函数) ,唯一有用的就是用于定义缺省 URL 参数的静态只读字段 Optional 。这是 典型的单例编程模式,意味着多次注册的缺省 Url 参数引用着同一个 UrIParameter 对象。 public sealed class UrlPararneter public static readonly UrlPararneter Optional = new UrlPararneter(); private UrlPararneter() { } ASP. NET MVC 4 框架揭秘66 讪第 2 章 URL 路由 public override string ToString() return string.Empty; 在进行 URL 模式匹配的时候,默认值为缺省 Url 参数的路由变量与其他具有默认值的 路由变量并没有什么差别。它们之间的不同之处在于如果将某个定义在 URL 模板中的变量 的默认值定义为缺省 URL 参数,只有在请求 URL 中真正包含具体的变量值的情况下生成的 RouteData 的 Values 属性中才会包含相应的数据项。 举个简单的例子,我们在 ASP.NET MVC Web 应用 2 中直接使用如上所示的默认注册的 路由。然后我们定义如下一个 HomeController ,默认的 Action 方法 Index 具有一个名为 id 的参数,在该方法中将包含在当前 RouteData 对象的 Values 属性中的所有元素的 Key 和 Value 呈现出来。 public class HomeController : Controller public void Index(string id) foreach (var variable in RouteData.Values) Response.Write(string.Format("{O}: {l}
", variable.Key, variable.Value)); 我们直接运行该程序并在浏览器的地址栏中输入不同的 URL 来访问 HomeController 的 Action 方法 Index ,看看最终包含在 RouteData 的路由变量有何不同。如图 2-10 所示,当直 接通过根地址访问的时候, RouteDa阔的 Values 属性中只包含 controller 和 action 这两个变量, 被设置为缺省 URL 参数的路由变量 id 只有在请求地址包含相应值的情况下才会出现在 RouteData 的 Values 属性中。 (S209) 图 2-10 普通路由变量与缺省 URL 参数的路由变量之间的差别 2 本书用于实例演示而创建的 Web 应用,如果没有特殊说明就是通过 Visual Studio 的 ASP.NETMVC 项目模板创建 的空 Web 应用.在必要的时候我们会添加一些 css 样式,但是具体的样式设直不会出现在给出的代码中. ASP. NET MVC 4 在架揭秘2.2 ASP.NET MVC 扩展也 67 2.2.4 基于 Area 的路由映射 对于一个较大规模的 Web 应用,可以从功能上通过Ar ea 将其划分为较小的单元。每个 Ar ea 相当于一个独立的子系统,具有一套包含 Models 、 Views 和 Controller 在内的目录结构 和配置文件。一般来说,每个Ar ea 具有各自的路由规则 (URL 模板上一般会体现Ar ea 的名 称),而基于Ar ea 的路由映射通过 System. Web.Mvc.Are aRegistration 进行注册。 A 陪 aRegistration 与 AreaRegistrationContext 基于Ar ea 的路由映射通过Ar e aRegistration 进行注册,如下面的代码片段所示, Are aRegistration 是一个抽象类,抽象只读属性Ar e aN ame 返回当前 Area 的名称,而抽象方 法 Register Ar ea 用于实现基于当前Ar ea 的路由注册。 public abstract class AreaRegistratio 口 public static void RegisterAIIAreas()i public static void RegisterAIIAreas(object state)i public abstract void RegisterArea(AreaRegistrationContext context)i public abstract string AreaName { geti } Ar e aR egistration 定义了两个抽象的 RegisterAll Ar eas 方法重载,参数 state 表示传递给具 体Ar e aRegistration 的数据。当 RegisterAll Ar ea 方法执行的时候,当前 Web 应用所有直接或 者间接被引用的程序集会被加载(如果尚未加载),然后从这些程序集中解析出所有继承自 Ar e aRegis位 ation 的类型并通过反射创建相应的Ar e aR egistration 对象。针对每个 Ar e aRegis位 ation 对象,一个 System. Web.Mvc. Ar eaRegistrationContext 对象被创建出来并作为 参数调用它们的 Register Ar ea 方法。 如下面的代码片段所示, Ar e aRegistrationContext 的只读属性Ar eaName 表示Ar ea 的名 称,属性 Routes 是一个代表路由表的 RouteCollection 对象,而 State 是一个用户自定义对象, 它们均通过构造函数进行初始化。具体来说, Ar e aRegistrationContext 对象是在调用 Ar e aRegistration 的静态方法 RegisterAl lAr eas 对所有Ar ea 进行注册时被创建的,其Ar e aN ame 来源于当前Ar eaRegistration 对象的同名属性, Routes 则对应着 RouteTable 的静态属性 Routes 所表示的全局路由表。调用 RegisterAl lAr eas 方法指定的参数 state 值将被作为调用 Ar e aRegistrationContext 构造函数的同名参数。 public class AreaRegistrationContext public AreaRegistrationContext(string areaName , RouteCollection routeS)i public AreaRegistrationCo 口 text(string areaName , RouteCollection routes , object state)i public Route MapRoute(stringηame , string url)i public Route MapRoute(string name , string url , object defaults)i public Route MapRoute(string name , string url , string[] namespaces)i public Route MapRoute(string name , string url , object defaults , ASP. NET MVC 4 在架渴秘68 黯第 2 章 URL 路由 object constraints)i public Route MapRoute(string name , string url , object defaults , string[] namespaces)i public Route MapRoute(string name , string url , object defaults , object constraints , str 工口 g[] namespaces) i public string AreaName { geti } public RouteCollection Routes { geti } public object State { geti } public ICollection Namespaces { geti } Are aR egistrationContext 的只读属性 Namespaces 表示一组优先匹配的命名空间(当多个 同名的 Con位 o l1 er 类型定义在不同的命名空间中)。当针对某个具体 Are aR egis 位 ation 的 Are aRegistrationContext 被创建的时候,如果Ar eaRegistration 类型具有命名空间,在这个命 名空间基础上添加" *"后缀生成的字符串会被添加到 Namespaces 集合中。换言之,对于 多个定义在不同命名空间中的同名 Contro l1 er 类型,会优先选择包含在当前 Are aRegistration 命名空间下的 Contro l1 er 。 Are aR egis 位 ationContext 定义了一系列的 MapRoute 用于进行路由映射注册,方法的使用 以及参数的含义与 RouteCo l1 ection 类的同名扩展方法一致。在这里需要特别指出的是,如果 MapRoute 方法没有指定命名空间,则通过属性 Namespaces 表示的命名空间列表会被使用, 反之,该属性中包含的命名空间会被直接忽略。 当我们通过 Visual Studio 的 AS 卫 NET MVC 项目模板创建一个 Web 应用的时候,在 Globa l. asax 文件中会生成如下通过调用Ar e aR egistration 的静态方法 Register All Ar eas 实现对 所有Area 的注册的代码,也就是说,针对所有 Area 的注册发生在应用启动的时候。 public class MvcApplication : System.Web.HttpApplication protected void Application Start() AreaRegistration.RegisterAllAreas()i AreaRegistration 的缓存 Ar ea 的注册(主要是基于Ar ea 的路由映射注册)通过具体的 Are aRegistration 来实现, 在应用启动的时候,会遍历通过调用 BuildManager 的静态方法 GetReference dA ssemblies 方 法得到的程序集列表,并从中找到所有Ar e aR egistration 类型。如果一个应用涉及太多的程 序集,这个过程可能会耗费很多时间。为了提高性能, ASP.阳 TMVC 会对解析出来的所有 Ar e aRegistration 类型列表进行缓存。 注: Buil dM anag 町的静态方法 GetReference dA ssemblies 返回必须引用的程序集列表,这包括 包含 Web.config 文件的 // 配,直节中指定的用于 编译 Web 应用所使用的程序集和从 App_Code 目录中的自定义代码生成的程序集以及 其他顶级文件夹中的程序集。 ASP. NET MVC 4 在架揭秘2.2 ASP.NET MVC 扩展句 69 AS卫NETMVC 对AreaRegistration 类型列表的缓存是基于文件的,具体来说,当通过程 序集加载和反射得到了所有的AreaRegistration 类型列表后,会对其序列化并保存为一个 刘伍物理文件中,这个名为 MVC-AreaRegistrationTypeCache.xml 的泊位文件被存放在 ASP.NET 的临时目录下,具体的路径如下。 • % Windir% \Microsoft.NE1\Framework\v {version} \ Temporary ASP.NET Files\ {appname } \... \... \U serCache\ • % Windir%\M icrosoft.NEl飞Framework\v {version} \ Temporary AS卫NET Files\root\. .人.人UserCache\ 其中第一个针对寄宿于 IIS 中的 Web 应用,后者针对直接通过 Visual Studio Developer Server 作为宿主的应用。 下面的x1\缸片段体现了这千作为所有AreaRegistration 类型缓存的x1\在L 文件的结构, 从中我们可以看到所有的 AreaRegistration 类型名称,连同它所在的托管模块和程序集名称 都被保存了下来。当调用AreaRegistration 的静态方法 RegisterAIlAreas 被调用之后,系统会 试图加载该文件,如果该文件存在并且具有期望的结构,那么将不再通过程序集加载和反射 来解析所有AreaRegistration 的类型,而是直接对文件内容进行反序列化,从而得到所有 AreaRegistration 类型的列表。 Artech.Admin.AdminAreaRegistration Artech.Portal.PortalAreaRegistration 实例演示:查看基于 Area 路自信息 (8210 ) 通过AreaRegistration 实现的针对Area 的路由注册具有一些特殊的细节差异,我们可以通 过实例演示的方式来说明。直接使用前面创建的演示实例。 208) ,并在项目中创建一个自定 义的 WeatherAreaRegistration 类。如下面的代码片段所示, WeatherAreaRegistration 继承自抽象 基类AreaRegistration ,表示Area 名称的AreaName 属性返回 "Wea出er" 。在实现路由注册的 RegisterArea 方法中调用AreaRegistrationContext 对象的 MapRoute 方法便注册了一个 URL 模 板为 "wea也er/{ 缸.eacode } / { days } "的路由对象,相应的默认变量值、约束也会被提供。 ASP. NET MVC 4 框架揭秘70 醺第 2 章 URL 路由 pub1ic c1ass WeatherAreaRegistration : AreaRegistration { pub1ic override string AreaName get { return "Weather"; } pub1ic override void RegisterArea(AreaRegistrationContext context) object defau1ts = new areacode = "010", days = 2 , defau1tCity = "BeiJing" , defau1tDays = 2 objectconstraints = new { areacode = @"0\d{2 , 3}", days =日 "[1-3]{1}" }; co口 text. MapRoute("weatherDefault" , "weather/{areacode}/{days}", defaults, constraints); 我们可以在 Global. asax 的 Application_ Start 方法中按照如下的方式调用AreaRegistration 的静态方法 RegisterAllAreas 来实现对所有Area 的注册。按照上面介绍的Area 注册原理, RegisterAllAreas 方法的第一次调用会自动加载所有引用的程序集来获取所有的 AreaRegistration (当然会包括我们上面定义的 WeatherAreaRegistration) ,最后通过反射创建 相应的对象并调用 RegisterArea 方法。 pub1ic class Globa1 : System.Web.HttpApp1ication protected void App1ication_Start(object sender, Eve 口 tArgs e) AreaRegistration.RegisterA11Areas(); 在用于获取路由信息的 GetRouteData 方法中,我们对创建的 H即Request 对象略加修改, 使请求地址符合通过 WeatherAreaRegistration 注册的路由规则(" /wea也er/0512/3 ")。 pub1ic partia1 c1ass Defau1t : System.Web.UI.Page private RouteData routeData; pub1ic RouteData GetRouteData() if (nu11 != routeData) return routeData; HttpRequest request = new HttpRequest("defau1t.aspx", "http://1oca1host:3721/weather/0512/3", nu11); HttpResponse response = new HttpResponse(new StringWriter()); HttpContext context = new HttpContext(request, response); HttpContextBase contextWrapper = new HttpContextWrapper(context); return routeData = RouteTab1e.Routes.GetRouteData(contextWrapper); ASP. NET MVC 4 在架揭翻2.2 ASP. NET MVC 扩展白 71 在浏览器中访问 Defaul t. aspx 页面,我们会得到如图 2-11 所示的结果。通过 Ar e aRegistration 注册的路由对象得到的 RouteData 的不同之处主要反映在其 DataTokens 属'性 上。如图 2-11 所示,除了表示命名空间列表的元素, DataTokens 属性表示的 Route ValueDictionary 还具有两个额外的元素,其中一个 Key 为"缸 ea" 的元素代表Ar ea 的 名称,另一个 Key 为 "UseNamespaceFallback" 的元素具有一个布尔值表示是否需要使用后 备的命名空间来解析 Con位。 ller 类型。 ht :JE3 一!同二 I Route: System. Web.Routing.Route RouteHandler: System. Web.Mvc . Mv cR ou teHander Values: areacode=0512 days-3 defaultCitYQBeiJing defaultDays - 2 DataTokens: Namespaces=System.St 时 ng[] area=Weather UseNamespaceFallback-FaL se 图 2-11 采用 AreaRegistration 路由映射得到的 RouteData 如果调用Ar e aRegistrationContext 的 MapRoute 方法是显式指定了命名空间,或者说对 应的Ar e aRegis 位 ation 定义在某个命名空间下,这个名称为 "UseNamespaceFallback" 的 DataToken 元素的值为 False ,反之为 True 。进一步来说,如果在调用 MapRoute 方法时指定 了命名空间列表,那么, Ar e aRegistration 类型所示在的命名空间会被忽略,也就是说,后者 是前者的一个后备,前者具有更高的优先级。 Are aRegistration 类型所示在命名空间也不是直接作为最终 RouteData 的 DataTokens 中的 命名空间,而是在此基础上加上" *"后缀。针对我们的实例来说,包含在 Roωut臼eDa挝ta 的 Da砌t阳aTI曰'o kens臼S 集合中的命名空间为 "We伪bApp 命名空间〉λ。 2.2.5 链援和 URL 的生成 AS 丑陋 T 路由系统通过注册的路由表旨在实现两个切向"的路由功能,即针对入找请 求的路由和出校 URL 的生成。前者通过调用代表全局路由表的 RouteCollection 对象的 Ge tRouteData 方法实现,后者则依赖于 RouteCollection 的 GetVirtua lP athD ata 方法,而最终 还是落在继承自 RouteBase 的路由对象的同名方法的调用上。 ASP.NET MVC 定义了 Ht m1H elper 和 Ur旧 elper 这两个帮助类,可以通过调用它们的 ActionLinklRouteLink 和 Actio nIRouteUrl 方法,并根据注册的路由规则生成相应的链接或者 URL 。从本质上讲, Htm 旧 elper.几Jr 田 elper 实现的对 URL 的生成最终还是依赖于前面所说的 GetVrrtua lP at bD ata 方法。 ASP. NET MVC 4 握架揭秘72 组第 2 章 URL 路由 UrlHelper V.S. HtmlHelper 在介绍如何通过 HtmlHelper 和 UrIHelper 来生成链接或者 URL 之前,先来看看它们的 基本定义。从下面给出的代码片段我们可以看出,一个 UrIHelper 对象实际上对一个表示请 求上下文的 RequestContext 对象和表示路由表的 RouteCollection 对象的封装,它们分别对应 于只读属性 RequestContext 和 RouteCollection ,并且在构造函数中被初始化。如果在构造 UrlHelper 的时候没有指定 RouteCollection 对象,那么通过 RouteTable 的静态属性 Routes 表 示的全局路由表将直接被使用。 r e ny --e 口u14 r u s s a --4 C C .-14 b u p{ //其他成员 public UrlHelper(RequestContext requestContext); public UrlHelper(RequestContext requestContext , RouteCollection routeCollection); public RequestContext RequestContext { get; } public RouteCollection RouteCollection { get;} 再来看看如下所示的 HtmlHelper 的定义,它同样具有一个表示路由对象集合的 RouteCollection 属性。和 UrIHelper 一样,如果在构造函数没有显式指定, RouteTable 的静 态属性 Routes 表示的 RouteCollection 对象将会用于初始化该属性。 r e ny 14 e H 14 m +L UU S s a 14 C C .-14 b u p{ //其他成员 public HtmlHelper(ViewContext viewContext , IViewDataContainer viewDataContainer); public HtmlHelper(ViewContext viewContext , IViewDataContainer viewDataContainer , RouteCollection routeCollection); public RouteCollection RouteCollection { get; } public ViewContext ViewContext { get; } public class ViewContext : ControllerContext //省咯成员 public class ControllerContext //其他成员 public RequestContext RequestContext { get; set; } public virtual RouteData RouteData { get; set; } 由于 HtmIHelper 只是在 View 中使用,所以它具有一个通过 ViewContext 属性表示的针 对 View 的上下文。对于 ViewContext ,我们会在第 8 章 "View 的呈现"中对其进行单独介 绍,在这里只需要知道它是表示 Con位 oller 上下文的 ControllerContext 的子类,而后者通过 RequestContext 和 RouteData 属性获取当前的请求上下文和路由数据(其实 RouteData 属性 表示的 RouteData 对象已经包含在 RequestContext 属性表示的 RequestContext 对象中〉。 ASP. NET MVC 4 在架揭秘2.2 ASP.NET MVC 扩展 • 73 UrIHelper.ActionV.S. HtmlHelper.ActionLi nk UrlHelper 和 HtmlHelper 分别通过 Action 和 ActionLink 方法生成一个针对某个 Controller/ Action 的 URL 和链接。下面的代码片段列出了 UrIHelper 的所有 Action 重载,参 数 actionName 和 controllerName 分别代表 Action 和 Controller 的名称。通过。时 ect 或者 Route ValueDictionary 类型表示的 routeValues 参数表示替换 URL 模板中变量的参数列表。参 数 protocol 和 hostName 代表作为完整 URL 的传输协议(比如 http 和 ht甲 s 等)以及主机名。 r e p -4 e H 14 r 门us s a 14 C C .工14 hu u p{ //其他成员 public string Action(string actionName); public string Action(string actionName, object routeValues); public string Action(string actionName, string controllerName); public string Action(string actionName, RouteValueDictionary routeValues); public string Action(string actionName, string controllerName, object routeValues); public string Action(string actionName, string controllerName, RouteValueDictionary routeValues); public string Action(string actionName, string controllerName, object routeValues, string protocol); public string Action(string actionName, string controllerName, RouteValueDictionary routeValues, string protocol, string hostName); 对于定义在 UrlHelper 中的众多 Action 方法,如果我们显示指定了传输协议 Cprotocol 参数)或者主机名称,返回的是一个完整的 URL ,否则返回的是一个相对 URL 。如果我们 没有显示地指定 Con位oller 的名称 C controllerName 参数),那么当前 Controller 的名称会被 采用。对于 UrlHelper 来说,通过 RequestContext 属性表示的当前请求上下文包含了相应的 路由信息(即 RequestContext 的 RouteData 属性表示的 RouteData) 0 RouteData 的 Values 属 性中必须包含一个 Key 为" controller" 的元素,其值就代表当前 Controller 的名称。 AS卫NETMVC 为 HtmlHelper 定义了如下所示的一系列 ActionLink 扩展方法重载,顾名 思义, ActionL ink 不再仅仅返回一个 URL ,而是生成一个链接 C ... ),但是其中作为 目标 URL 的生成逻辑与 UrlHelper 是完全一致的。 public static class LinkExtensions //其他成员 public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName); public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, object routeValues); public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName); public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, RouteValueDictionary routeValues); public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, object routeValues, ASP. NET MVC 4 在架揭秘74 温第 2 章 URL 路由 object htmlAttributes); pub 工 ic static MvcHtmlString ActionLink(this HtmlHelper htmlHelper , string linkText , string actionName , RouteValueDictionary routeValues , IDictionary htmlAttributes)i public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper , string linkText , string actionName , string controllerName , object routeValues , object htmlAttributes)i public static MvcHtmlStr 工 ng ActionLink(this HtmlHelper htmlHelper , string linkText , string actionName , string controllerName , RouteValueDictionary routeValues , IDictionary htmlAttributes)i public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper , string linkText , string actionName , string controllerName , string protocol , string hostName , string fragment , object routeValues , object htmlAttributes)i public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper , string linkText , string actionName , string controllerName , string protocol , string hostName , string fragment , RouteValueDictionary routeValues , IDictionary htmlAttributes); 实例演示:创建一个 RouteHelper 模拟 UrlHelper 的 URL 生成逻辑 (8211 ) 为了让读者对 UrIHelper 如何利用 ASP.NET 路由系统进行 URL 生成的逻辑具有一个深 刻认识,我们接下来创建一个名为 RouteHelper 的等效帮助类。如下面的代码片段所示, RouteHelper 具有 RequestContext 和 RouteCollection 两个属性,前者在构造函数中指定,后 者直接返回通过 RouteTable 的 Routes 静态属性表示的全局路由表。 r e p -4 e H e ←」u O R s s a 14 C C .-14 b u p{ public RequestContext RequestContext { geti private seti } public RouteCollection RouteCollection { geti private seti } public RouteHelper(RequestContext requestContext) this.RequestContext = requestContexti this.RouteCollection = RouteTable.Routesi public string Action(string actionName , string controllerName=null , object routeValues=null , string protocol=null , string hostName = null) controllerName = controllerName ?? this.RequestContext.RouteData.GetRequiredString("controller")i RouteValueDictionary routeValueDictionary = new RouteValueDictionary(routeValues); routeValueDictionary.Add("action" , actionName)i routeValueDictionary.Add("controller" , controllerName); string virtualPath = this.RouteCollection.GetVirtualPath( this.RequestContext , routeValueDictionary) .VirtualPath; if (string.IsNullOrEmpty(protocol) && string. 工 sNullOrEmpty(hostName)) return virtualPath.ToLower()i AS P. NET MVC 4 在架揭秘2.2 ASP.NET MVC 扩展画 75 protocol = protocol ?? "htt~"; Uri uri = this.RequestContext.HttpContext.Request.Url; hostNarne = hostNarne ?? uri.Host + ":" + uri.Port; return string.Forrnat("{O}://{1}{2}" , protocol , hostNarne , virtualPath) .ToLower(); RouteHelper 定义了一个 Action 方法根据指定的 Action 名称、 Con位 oller 名称、路由参数 列表、网络协议前缀和主机名称来生相应的 URL ,除了第一个表示 Action 的参数,其余参数 均是可以缺省的。具体的逻辑很简单:如果指定的 Controller 名称为 Null ,贝 IJ 通过 R~questContext 获取当前 Con位 oller 名称,然后将 Adion 和 Controller 名称添加到表示路由变参数的 Route ValueDictionary 对象中 (routeValues 参数),对应的 Key 分别是 "action" 和" controller" 。 然后调用 RouteCollection 的 GetVirtuaIPath 得到一个 VirtuaIPat hD ata 对象,如果既没有 显式指定传输协议也没有指定主机名称,直接返回 VirtuaIPat hD ata 对象的 VirtuaIPath 属性体 现的相对路径,否则生成一个完整的 URL 。如果没有指定主机名称,我们采用当前请求的 主机名称,并且使用当前的端口。如果没有指定传输协议,则直接使用 "http" 。 接下来在添加的 Globa l. asax 中通过如下的代码注册一个 URL 模板为" { controller} / {action}/{id}" 的路由对象。 public class Global : Systern.Web.HttpApplication protected void Application Start(object sender , EventArgs e) RouteTable.Routes.MapRoute( narne: "Default" , url: "{controller}/{action}/{id}" , defaults: new controller act 工 on id = " Horne " , = "Index" , = UrlPararneter.Optional 在添加的 Web 页面 (Defaul t. aspx) 中通过如下的代码利用我们自定义的 RouteHelper 生成 5 个 URL 。在页面加载事件处理方法中,我们根据手工创建的 Ht甲 Request (请求地址 为 h即://localhos t: 3 721 /products/ getproductlOO 1 )和 H忧 pResponse 创建一个 H 即 Context 对象, 并进一步创建 HttpContextWrapper 对象。将此 HttpContextWrapper 对象作为参数调用全局路 由表的 GetRouteData 方法得到封装路由数据的 RouteData 对象,并针对创建的 HttpContextWrapper 对象和此 RouteData 进一步创建 RequestContext 对象,最终创建出 RouteHelper 对象。 public partial class Default : Systern.Web.UI.Page ASP. NET MVC 4 框架揭秘76 也第 2 章 URL 路由 protected void Page_Load(object sender, EventArgs e) HttpRequest request = new HttpRequest("default.aspx", ''http://localhost:3721/products/getproduct/00l'', null); HttpResponse response = new HttpResponse(new StringWriter()); HttpContext context = new HttpContext(request, response); HttpContextBase contextWrapper = new HttpContextWrapper(context); RouteData routeData = RouteTable. Routes. GetRouteData (contextWrapper) ; RequestContext requestContext = new RequestContext( contextWrapper, routeData); RouteHelper helper = new RouteHelper(requestContext); Response.Write(helper.Action("GetProductCategories") + "
"); Response.Write(helper.Action("GetAllContacts", "Sales") + "
"); Response.Write(helper.Action("GetAllContact", "Sales", new { id = "001" }) + "
"); Response.Write(helper.Action("GetAllContact", "Sales", new { id = "001" }, "https") + "
"); Response.Write(helper.Action("GetAllContact", "Sales", new { id = "001" }, ..https.....www.artech.com..) + "
"); 运行该程序之后,通过调用 RouteHelper 的 Action 方法生成的 5 个 URL 会以图 2-12 所 示的方式出现在浏览器上。 匮噩噩噩> .. -圃 EZ噩噩罩旦些些旦型企旦空巴金E罩~ Iproductsfgetproductcategories Isal.esl getallcontacts Is al. es/get摇lcon tactl 00 1 https:/ !toca1host:3721/sal.es/getallcon tact/001 https:/ !www.artech.comísales/getallcontact/001 图 2-12 通过自定义 RouteHelper 生成的 URL Url Helper. RouteUrIV.S. HtmlHelper. RouteLink 不论是 UrlHelper 的 Action 方法,还是 HtmIHelper 的 ActionLink , URL 部分都是通过一 个路由表生成出来的,而在默认的情况下这个路由表就是通过 RouteTable 的静态属性 Routes 表示的全局路由表。换句话说,具体使用的总是路由表中第一个匹配的路由对象,但是有时 候我们需要针对注册的某个具体的路由对象来生成 URL 或者对应的链接,在这种情况下就 需要使用到 UrlHelper 和 HtmIHelper 的另外一组方法了。 如下面的代码片段所示, Ur旧elper 定义了一系列的 RouteUrl 方法,除了第一个重载之 外,后面的重载都接受一个路由对象注册名称的参数 routeName 。与 UrlHelper 的 Action 方 法一样,可以指定用于替换定义在 URL 模板中路由变量的参数 C route Values) ,以及传输协 议名称 Cprotocol) 和主机名称 ChostName) 。 ASP.NET MVC 4 在架揭秘2.2 ASP.NET MVC 扩展白 77 r e p ---e H l r 门 us s a ---C C .工14 b u p{ //其他成员 public string RouteUrl(object routeValues); public stri 口 g RouteUrl(string routeName); public string RouteUrl(RouteValueDictionary routeValues); public string RouteUrl(string routeName, object routeValues); public string RouteUrl (string routeName, RouteValueDictionary routeValues) ; public string RouteUrl(string routeName, object routeValues, string protocol); public string RouteUrl(string routeName, RouteValueDictionary routeValues, string protocol, string hostName); 对于没有指定路由对象注册名称的 RouteUrl 方法来说,它还是利用整个路由表进行 URL 的生成。如果显示指定了路由对象的注册名称,那么就会从路由表中获取相应的路由对象。 如果该路由对象与指定的变量列表不匹配,则返回 Null,否则返回生成的 URL 。 HtmlHelper 也同样定义了类似的 RouteLink 方法重载用于实现基于指定路由对象的链接 生成,具体的 RouteLink 方法定义如下。 public static class LinkExtensions //其他成员 public static MvcHtmlString RouteL 工 nk(this HtmlHelper htmlHelper, string linkText, object routeValueS)i public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName); public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, RouteValueD工 ct 工 onary routeValues); public static MvcHtmlString RouteLink(th 工 s HtmlHelper htmlHelper, string linkText, object routeValues, object htmlAttributes); public static MvcHtmlStriηg RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, object routeValues); public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, RouteValueDictionary routeValues); public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, RouteValueDictionary routeValues, IDictionary htmlAttributes); public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, object routeValues, object htmlAttributes); public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, RouteValueDictionary routeValues, IDictionary htmlAttributes); public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, string protocol, string hostName, string fragment, object routeValues, object htmlAttributes); public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary htmlAttributes); ASP. NET MVC 4 擅架揭秘78 • 第 2 章 URL 路由 2.3 动态 HttpHandler 映射 通过第 1 章" ASP.NET + MVC" 对 ASP.NET 管道式设计的介绍,我们知道一个请求最 终是通过-个具体的 HttpHandler 进行处理的。表示一个 Web 页面的 Page 对象就是一个 HttpHandler ,它被用于最终处理基于某个 .aspx 文件的请求,可以通过 HttpHandler 的动态映 射来实现请求地址与物理文件路径之间的分离。 实际上 ASP. 阳 T 路由系统就是采用了这样的实现原理。如图 2-13 所示, AS 卫 NET 路由 系统通过一个注册的自定义 HttpModule 实现对请求进行的拦截,然后动态映射一个用于处 理当前请求的 HttpHandler 0 HtφHandler 对请求进行处理并最终对请求予以响应。 气 / 、 飞 飞 图 2-13 HttpHandler 的动态映射 2.3.1 UrlRoutingModule 图 2-13 所示的作为请求拦截器的 HtψModule 类型为 System. Web.Routing. UrlRoutingM odule 。如下面的代码片段所示, UrIRoutin gM odule 对请求的拦截是通过注册 HttpApplication 的 PostResolveRequestCache 事件实现的。 public class UrlRoutingModule : IHttpModule //其他成员 public RouteCollection RouteCollection { get; set; } public void Init(HttpApplication context) context.PostResolveRequestCache += new EventHand 工 er(this.OnApplicationPostResolveRequestCache); private void OnApplicationPostResolveRequestCache(object sender , EventArgs e); ASP. NET MVC 4 在架揭秘2.3 动态 HttpHandler 映射但 79 UrlRoutin gM odule 具有一个类型为 RouteCollection 的 RouteCollection 属性,在默认的情 况下该属性是对 RouteTable 的静态属性 Routes 的引用。用于最终处理请求的 HttpHandler 的 动态映射就实现在 O nA pplicatio nP ostResolveRequestCache 方法中,具体的实现逻辑非常简 单:通过 HttpApplication 获得当前的 HTTP 上下文,并将其作为参数调用 RouteCollection 的 GetR outeData 方法得到一个 RouteData 对象。 通过 RouteData 的 RouteHandler 属'性可以得到一个实现了 IRouteHandler 的 RouteHandler 对象,调用后者的 GetHttpHandler 方法可以直接获取对应的 HttpHandler 对象,而我们需要 映射到当前请求的就是这么一个 HttpHandler 。下面的代码片段基本上体现了定义在 Ur恨outin gM odule 的 O nA pplicatio nP ostResolveRequestCache 方法中的动态 HttpHandler 映射 逻辑。 public class UrlRoutingModule : IHttpModule //其他成员 private void OnApplicationPostResolveRequestCache (object sender , EventAr gs e) HttpContext context = ((HttpApplication)sender) .Context; HttpContextBase contextWrapper = new HttpContextWrapper(context); RouteData routeData = this. RouteCollection. GetRouteData (contextWrapper) ; RequestContext requestContext = new RequestContext(contextWrapper , routeData); IHttpHandler handler = routeData.RouteHandler.GetHttpHandler(requestContext); context.RemapHandler(handler); 2.3.2 PageRouteHandler 与 MvcRouteHandler 通过前面的介绍我们知道,对于调用 RouteCollection 的 Ge tRouteData 获得的 RouteData 对象,其 RouteHandler 来源于与当前请求 URL 相匹配的 Route 对象。对于通过调用 RouteCollection 的 MapPageRoute 方法注册的 Route 来说,它的 RouteHandler 属性返回一个 类型为 System. Web.Routing.PageRouteHandler 的对象。 由于调用 MapPageRoute 方法的目的在于实现请求地址与某个 .aspx 页面文件之间的映 射,所以我们最终还是要创建 Page 对象来处理相应的请求,所以 PageRouteHandler 的 GetH仕pHandler 方法最终返回的就是针对映射页面文件路径的 Page 对象。此外, MapPageRoute 方法中还可以控制是否对物理文件地址实施授权,而授权在返回 Page 对象之 前进行。 定义在 PageRouteHandler 中的 HttpHandl 町映射逻辑基本上体现在如下的代码片段中, 两个属性 VirtuaIPath 和 Chec kP hysicalUr lA ccess 表示页面文件的地址及是否需要对物理文件 地址实施 URL 授权,它们在构造函数中被初始化,且最初来源于调用 RouteCollection 的 ASP. NET MVC 4 框架揭秘80 • 第 2 章 URL 路由 MapPageRoute 方法传入的参数。 public class PageRouteHandler : IRouteHandler qJ n --·工or ot -bs cc ·工·工141-bb uu p&p ‘ CheckPhysicalUrlAccess { get; private set; } VirtualPath { get; private set; } public PageRouteHandler(string virtualPath , bool checkPhysicalUrlAccess) this.VirtualPath = virtualPath; this.CheckPhysicalUrlAccess = checkPhysicalUrlAccess; public IHttpHandler GetHttpHandler(RequestContext requestContext) if (this.CheckPhysicalUrlAccess) IICheck Physical Url Access return (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath( this.VirtualPath , typeof(Page)) ASP.NET MVC 的 R{)ute 对象是通过调用 RouteCollection 的扩展方法 MapRoute 进行注 册的,它对应的 RouteHandler 是一个类型为 System. Web.Mvc.MvcRouteHandler 的对象。如 下面的代码片段所示, MvcRouteHandler 用于获取处理当前请求的 Ht甲 Handler 是一个 System. Web.Mvc.MvcHandler 对象。 MvcHandler 实现对 Controller 的激活、 Action 方法的执 行以及对请求的响应。毫不夸张地说,整个 MVC 框架是实现在 MvcHandler 之中的。 public class MvcRouteHandler : IRouteHandler 11 其他成员 public IHttpHandler GetHttpHandler(RequestContext requestContext) return new MvcHandler(requestContext) 2.3.3 ASP.NET 路由系统扩展 到此为止,我们已经对 ASP.NET 的路由系统的实现进行了详细地介绍,总的来说,整 个路由系统是通过对 HttpHandler 的动态注册的方式来实现的。具体来说, UrlRoutingModule 通过对代表 Web 应用的 Ht甲 Application 的 PostResolveRequestCache 事件的注册实现了对请 求的拦截。对于被拦截的请求, Ur1 Routinβ,f odule 利用注册的路由表对其进行匹配和解析, 进而得到一个包含所有路由信息的 RouteData 对象,最终借助该对象的 RouteHandler 得到相 应的 H仕pHandler ,并映射到当前请求。从可扩展性的角度来讲,可以通过如下三种方式来 实现我们需要的路由方式。 AS P. NET MVC 4 在架揭秘2.3 动态 HttpHandler 映射 翻 81 • 通过集成抽象类 RouteBase 创建自定义 Route 定制路由逻辑。 • 通过实现接口 IRouteHandler 创建自定义 RouteHandler 定制 HttpHandler 提供机制。 • 通过实现 IHttpHandler 创建自定义 H忧pHandler 来对请求处理作最终的处理。 实例演示:通过自定义 Route 对 ASP.NET 路由系统进行扩展 (S212 ) 定义在 ASP.NET 路由系统中默认的路由类型 Route 实现了 URL 模式与物理文件之间的 映射,如果我们对 WCF 阻 ST 有一定的了解,应该知道其中也有类似的实现。具体来说, WCFREST 借助于 System. UriTemplate 这个对象实现了同样定义成某个文本模板的 U阳模式 与目标操作之间的映射。篇幅所限,我们不能对 WCFREST 的 UriTemplate 作详细的介绍, 如果读者朋友对此有兴趣可以参阅笔者在 2012 年 4 月出版的 ((WCF 全面解析)) (上、下册)。 我们创建一个新的 ASP.NET Web 应用,并且添加针对程序集 S ystem. S erviceModel. dll 的引用( Ur iTemplate 定义在该程序集中),然后创建如下一个针对 UriTemplate 的路由类型 UriTemplateRoute 。 public class UriTemplateRoute:RouteBase public UriTemplate public IRouteHandler public RouteValueDictionary UriTemplate { geti private seti } RouteHandler { geti private seti } DataTokens { geti private set; } public UriTemplateRoute(string template , string physicalPath , object dataTokens = null) this.UriTemplate = new UriTemplate(template); this.RouteHandler = new PageRouteHandler(physicalPath); if (null != dataTokens) this.DataTokens =口 ew RouteValueDictionary(dataTokens); else this.DataTokens = new RouteValueDictionarY()i public override RouteData GetRouteData(HttpContextBase httpContext) Uri uri = httpContext.Request.Urli Uri baseAddress = new Uri(string.Format("{O}://{l}" , uri.Scheme , uri.AuthoritY))i UriTemplateMatch match = this.UriTemplate.Match(baseAddress , uri); if (null == match) return null; RouteData routeData = new RouteData(); routeData.RouteHandler = this.RouteHandler; routeData.Route = this; foreach (string name in match.BoundVariables.Keys) AS P. NET MVC 4 框架揭秘82 '. 第 2 章 URL 路由 routeData.Values.Add(name,match.BoundVariables[name]); foreach (var token in this.DataTokens) routeData.DataTokens.Add(token.Key, token.Value); return routeData; public override VirtualPathData GetVirtualPath (RequestContext requestContext, RouteValueDictionary values) Uri uri = requestContext.HttpContext.Request.Url; Uri baseAddress = new Uri(string.Format("{O}://{l}", uri.Scheme, uri.Authority)); Dictionary variables = new Dictionary(); foreach(var item in values) variables.Add(item.Key, item.Value.ToString()); //确定段变量是否被提供 foreach (var name in this.UriTemplate.PathSegmentVariableNames) if(!this.UriTe~plate.Defaults.Keys.Any( key=> string.Compare(name, key, true) == 0) && !values.Keys.Any(key=> string.Compare(name, key, true) == 0)) return null; //确定查询变量是否被提供 foreach (var name in this.UriTemplate.QueryValueVariableNames) if(!this.UriTemplate.Defaults.Keys.Any( key=> string.Compare(name, key, true) == 0) && !values.Keys.Any(key=> string.Compare(name, key, true) == 0)) return null; Uri virtualPath = this.UriTemplate.BindByName(baseAddress, variables); string strVirtualPath = virtualPath.ToString() .ToLower() .Replace(baseAddress.ToStr 工 ng () . ToLower ( ) , "") ; VirtualPathData virtualPathData = new VirtualPathData(this, strVirtualPath); foreach (var token 工 n this.DataTokens) virtualPathData.DataTokens.Add(token.Key, token.Value); return virtualPathData; ASP. NET MVC 4 框架揭秘2.3 动态 HttpHandler 映射 • 83 如上面的代码片段所示, UriTemplateRoute 具有 UriTemplate 、 DataTokens 和 RouteHandler 三个只读属性,前两个通过构造函数的参数进行初始化,后者则是在构造函数中创建的 PageRouteHandler 对象。 在用于对入校请求进行匹配并获取路由数据的 GetRouteData 方法中,我们解析出基于 应用的基地址并连同请求地址作为参数调用 UriTemplate 的 Match 方法,如果返回的 UriTemplateMatch 对象不为 Null ,则意味着 URL 模板的模式与请求地址匹配。在匹配的情 况下我们创建并返回相应的 RouteData 对象,否则直接返回 Null 。 在用于生成出校 URL 的 GetVirtualPath 方法中,通过判断定义在 URL 模板中的变量(包 括变量名包含在属性 PathSegmentVariableN ames 的路径段变量和包含在 QueryValueVariableNames 属性的查询变量)是否在提供的 Route ValueDictionary 字段或者默 认变量列表(通过属性 Defaults 表示)中来确定 URL 模板是否与提供的变量列表匹配。在 匹配的情况下通过调用 UriTemplate 的 BindByName 方法得到一个完整的 Uri 。由于该方法返 回的是相对路径,所以我们需要将应用基地址剔除并最终创建并返回一个 VirtualPathD ata 对 象。如果不匹配,则直接返回 Null 。 在创建的 Globa l. asax 文件中采用如下的代码对我们自定义的 UriTemplateRoute 进行注 册,选用的场景还是之前采用的天气预报的例子。我个人觉得基于 UriTemplate 的 URI 模板 比针对 Route 的 URL 模板更好用,其中一点就是它定义默认值的方式更为直接。如下面的 代码片段所示,可以直接将默认值定义在模板中(" {町 eacode=010}/{days=2} 勺。 public class Global : System.Web.HttpApplication protected void Application Start(object sender , EventArgs e) UriTemplateRoute route =ηew UriTemplateRoute (" {areacode=010} / {days=2}" , "-/Weather.aspx" , ηew { defual tCi ty = "BeiJing" , defaul tDays = 2}) ; RouteTable.Routes.Add("default" , route); 在注册的路由对应的目标页面 Weather.aspx 的后台代码中,我们定义了如下一个 GenerateUrl 根据指定的区号(缸 eacode) 和预报天数 (days) 创建一个 Url ,而 Url 的生成直 接通过调用 RouteTable 的 Routes 属性的 GetVirtuaIPat hD ata 方法完成。 public partial class Weather : System.Web.UI.Page public string GenerateUrl(string areacode , int days) var values = new { areacode = areacode , days = days }; RequestContext requestContext = new RequestContext(); requestContext.HttpCo 口 text =口 ewHttpContextWrapper(HttpContext.Current); requestContext.RouteData = RouteData; return RouteTable.Routes.GetVirtualPath(requestContext , new RouteValueDictionary(values)) .VirtualPath; AS P. NET MVC 4 框架揭秘84 111 第 2 章 URL 路由 通过调用 GenerateU r1方法生成的 URL (町 eaCode=0512; days=3) 连同当前页面的 RouteData 的属性通过如下所示的 HT岛E 代码输出出来。
RouteData.Route.GetType() .FullName:"" 毛 >
Router: < 毛 =RouteData.Route != null?
RouteHandler: < 毛 =RouteData.RouteHandler != null? RouteData.RouteHandler.GetType() .FullName:"" 毛 >
Values:
    <毛 foreach (var variable in RouteData.Values) {毛>
  • <毛 =variable.Key 毛>=<毛 =variable.Value 毛 >
  • <毛}毛>
DataTokens:
    <宅 foreach (var variable in RouteData.DataTokens) {毛>
  • <毛 =variable.Key 毛>=<毛 =variable.Value 毛 >
  • <毛}毛>
Generated Url: <毛 =GenerateUrl("0512" , 3) 毛>
由于注册的 URL 模板所包含的段均由具有默认值的变量构成,所以当我们请求根地址 时,会自动路由到 Weather.aspx 。图 2-14 是我们在浏览器访问应用根目录的截图,上面显示 了我们注册的 UriTemplateRoute 生成的 RouteData 的信息和生成的 URL (/0512/3) 。 ASP. NET MVC 4 框架揭秘Router: WebApp.U 时 T emplateRou te RouteHandler: 5ys tem. Web. Rou tin g. PageRou teHan dler Values: AREACODE=OI0 DAY5=2 DataTokens: defu al.tCity=BeiJing defaultDays=2 Generated Url: .:0512'3 本章小结 .. 85 图 2-14 通过自定义 UriTemplateRoute 得到的 RouteData 和生成的 URL 本章小结 ASP.NET MVC 应用下 HTTP 请求的访问目标是定义在某个 Controller 类型中的某个 Action 方法, URL 路由系统通过对请求地址进行解析从而得到以目标 Controller/Action 名称 为核心的路由数据。 U虹路由系统是建立在 ASP.NET 上,而非专属于 ASP.NET MVC ,它 最初是为了实现请求 URL 与物理文件路径的分离而建立的。 ASP.阳TMVC 通过自定义的路 由类型实现对 ASP.NET 路由系统的扩展,将 URL 与物理文件路径的映射转移到与目标 Controller/ Action 的映射。 ASP.NET 路由系统具有一个针对整个 Web 应用的全局路由表,路由表中的每个路由对 象具有一个可以包含变量的 URL 模板。路由对象一方面利用 URL 模板与入校请求的 URL 进行模式匹配并得到相应的路由数据,另一方面还可以根据指定的路由变量参数列表生成相 应的 URL 。 ASP.NET 路由系统是通过对 H忧pHandler 的动态映射来实现的,作为自定义 Ht甲Module 的 UrlRoutingModule 通过注册 HtφApplication 的 PostResolveRequestCache 事件对请求进行 拦截,并利用路由表与请求 URL 进行模式匹配得到相应的路由数据。与请求 URL 相匹配路 由对象关联的 HttpHandler 被提取出来用于最终处理当前请求。 ASP. NET MVC 4 在架揭秘第 3 章 Controller 的激活 ASP.NETMVC 应用中请求的目标不再是具体某个物理文件,而是定义在 某个 Con位oller 中的 Action 方法。每个请求经过 AS卫NETURL 路由系统的拦 截后,会生成以 ControllerlAction 名称为核心的路由数据。 ASP.NETMVC 据 此解析出目标 Controller 的类型,并最终激活具体的 Conrtoller 实例来处理当 前的请求。 ASP. NET MVC 4 在架揭翻3.1 总体设计 、 87 3.1 总体设计 我们将整个 ASP.NETMVC 框架人为地划分为若干个子系统,那么针对请求上下文激活 目标 Controller 对象的子系统可以称为 Con位 oller 激活系统。在正式讨论 Controller 对象具体 是如何被创建之前,我们先来了解 Contro l1 er 激活系统在 ASP.NET MVC 中的总体设计,看 看它大体上由哪些组件构成。 3.1.1 Controller 我们知道作为 Con位 oller 的类型直接或者间接实现了 System. Web.Mvc .I Controller 接口。 如下面的代码片段所示, ICon位 oller 接口仅仅包含一个参数类型为 RequestContext 的 Execute 方法,当一个 Con位 oller 对象被激活之后,其核心的操作就是:从包含在当前请求上下文的 路由数据中获取 Action 名称并据此解析出对应的方法,将通过 Model 绑定机制从当前请求 上下文中提取相应的数据并调用 Action 方法生成对应的参数列表。所有这些后续操作都是 间接地通过调用 Controller 的 Execute 方法来完成的。 public interface IController void Execute(RequestContext requestContext); 定义在 IContro l1 er 接口中的 Execute 是以同步的方式执行的。为了支持以异步方式对请 求的处理, IController 接口的异步版本 System. Web.Mvc.IAsyncCon位 oller 被定义出来。如下 面的代码片段所示,实现了 IAsyncC 。由 o l1 er 接口 Contro l1 er 的执行通过 Beg inE xecuteÆndExecute 方法以异步的形式完成。 public interface IAsyncController : IController IAsyncResult BeginExecute(RequestContext requestContext , AsyncCallback callback , object state); void EndExecute(IAsyncResult asyncResult); 抽象类 System. Web.Mvc.ControllerBase 实现了 IController 接口。如下面的代码片段所示, Con仕 ollerBase 以"显式接口实现"的方式定义了 Execute 方法,该方法在内部直接调用受保 护的 Execute 虚方法,而后者最终会调用抽象方法 ExecuteCore 方法。 public abstract class ControllerBase : IController //其他成员 protected virtual void Execute(RequestContext requestContext); protected abstract void ExecuteCore(); void IController.Execute(RequestContext requestContext); ASP. NET MVC 4 框裂揭秘88 诵第 3 章 Controller 的激活 public ControllerContext public TempDataDictio口 ary public object public ViewDataDictionary ControllerContext { get; set; } TempData { get; set; } ViewBag { [return: Dynamic] get; } ViewData { get; set; } ControllerBase 具有如下几个重要的属性: TempData 、 ViewBag 和 ViewData ,它们用于 存储从 Controller 向 View 传递的数据或者变量。其中 TempData 和 ViewData 具有基于字典 的数据结构, Key 和 Value 分别表示变量的名称和值,两者的不同之处在于前者仅仅用于存 储临时数据,并且设置的变量被第一次读取之后会被移除,换句话说通过 TempD剖a 设置的 变量只能被读取一次。 ViewBag 和 ViewData 共享着相同的数据,它们之间的不同之处在于 前者是一个动态对象,我们可以为其指定任意属性(动态属性名将作为数据字典的 Key) 。 在 ASP.NETMVC 中我们会陆续遇到一系列的上下文 (Context) 对象,之前已经对表示 请求上下文的 RequestContext (HttpContext + RouteData) 进行了详细的介绍,现在来介绍另 一个具有如下定义的上下文类型 System. Web.Mvc.ControllerContext 。 public class ControllerContext //其他成员 public ControllerContext(); public ControllerContext(RequestContext requestContext, ControllerBase controller); public ControllerContext(HttpContextBase httpContext, RouteData routeData, ControllerBase controller); public virtual ControllerBase public RequestCo口 text public virtual HttpContextBase public virtual RouteData Controller { get; set; } RequestContext { get; set; } HttpContext { get; set; } RouteData { get; set; } 顾名思义, ControllerContext 就是基于某个 Controller 对象的上下文。从如上的代码可以 看出一个 ControllerContext 对象实际上是对一个 Controller 对象和 RequestContext 的封装。 这两个对象分别对应着 ControllerContext 中的同名属性,可以在构建 ControllerContext 的时 候为调用的构造函数指定相应的参数来初始化它们。 通过 H忧pContext 和 RouteData 属性返回的 HttpContextBase 和 RouteData 对象在默认情 况下实际上就是 RequestContext 的核心组成部分。当 ControllerBase 的 Execute 方法被执行的 时候,它会根据传入的 ReuqestContext 创建 Con位ollerContext 对象,后续的操作可以看成是 在该上下文中进行。 通过 Visual Studio 的 Controller 创建向导创建的 Controller 类型实际上继承自抽象类 System. Web.Mvc.Controller ,它是 Con位ollerBase 的子类。如下面的代码片段所示,除了直接 继承 ControllerBase 之外, Controller 类型还显式地实现了 ICon位oller 和 IAsyncCon位oller 接 口,以及代表 ASP.NETMVC 四大筛选器( AuthorizationF ilter 、 ActionF ilter 、 ResultFilter 和 ExceptionF ilter) 的 4 个接口(我们会在第 7 章" Action 的执行"中对筛选器进行详细介绍〉。 ASP. NET MVC 4 框架揭秘public abstract class Controller Contr o. llerBase , IController , IAsyncController , IActionFilter , IAuthorizationFilter , IExceptionFilter , IResultFil ter , IDisposable , //省咯成员 同步还是异步 3.1 总体设计 • 89 从抽象类 Controller 的定义可以看出它同时实现了 IController 和 IasyncController 这两个 接口,意味着它既可以采用同步的方式〈调用 Execute 方法)执行,也可以采用异步的方式 (调用 Begi nE xecuteÆn dE xecute 方法)执行。但是即使执行 Beg inE xecuteÆndExecute 方法, Controller 也不一定是以异步方式执行的。 如下面的代码片段所示, Controller 具有一个布尔类型的属性 DisableAsyncSupport ,表 示是否关闭对异步执行的支持。在默认的情况下该属性总是返回 False ,即支持以异步方式 执行 Controller 0 Beg inE xecute 方法会根据 DisableAsyncSupport 属性决定究竟是调用 Execute 方法以同步的方式执行,还是调用 Beg inE xecuteCoreÆn 伍 xecuteCore 方法以异步的方式执 行。换句话说,如果我们希望 Controller 总是以同步的方式来执行,可以将 DisableAsyncSupport 属性设置为 True 。 public abstract class Controller: //其他成员 protected virtual bool DisableAsyncSupport get{return falsei} protected virtual IAsyncResult Begi 口 Execute(RequestContext requestContext , AsyncCallback callback , object state) if (this.DisableAsyncSupport) //通过调用 Execute 方法同步执行 Controller else //通过调用 BeginExecuteCore/EndExecuteCore 方法异步执行 Controller protected virtual IAsyncResult BeginExecuteCore(AsyncCallback callback , object state)i protected virtual void EndExecuteCore(IAsyncResult asyncResult)i ASP. NET MVC 4 框架揭秘90 市第 3 章 Controller 的激活 现在我们通过一个简单的实例来演示属性 DisableAsyncSupport 对默认创建的 Controller 执行的影响。我们在一个 ASP.NET MVC 应用中定义了一个具有如下定义的默认 Horne Controller ,它重写了 Execute 、 ExecuteCor队 Beg inE xecuteÆndExecute 和 Beg inE xecuteCore/ En dE xecuteCore 六个方法,同时将相应的方法名写入响应并最终呈现在浏览器上。 public class HorneController : Controller public new HttpResponse Response get { return Systern.Web.HttpCo 口 text.Current.Response; } protected override void Execute(RequestContext requestContext) Response.Write("Execute();
"); base.Execute(requestContext); protected override void ExecuteCore() { Response.Write("ExecuteCore();
"); base.ExecuteCore(); protected override IAsyncResult BeginExecute(RequestContext requestContext , AsyncCallback callback , object state) Response.Write("BeginExecute();
"); return base.BeginExecute(requestContext , callback , state); protected override void EndExecute(IAsyncResult asyncResult) Response.Write("EndExecute();
"); base.EndExecute(asyncResult); protected override IAsyncResult BeginExecuteCore(AsyncCallback callback , object state) Respo 口 se.Write("BeginExecuteCore();
"); return base.Beg 工 nExecuteCore(callback , state); protected override void EndExecuteCore(IAsyncResult asyncResult) Response.Write("EndExecuteCore();
"); base.EndExecuteCore(asyncResult); public ActionResult Index() return Content(" 工 ndex() ;
"); 虽然抽象类中定义了一个表示当前 H仕pResponse 的属性 Response ,但是当 Beg inE xecute ASP. NET MVC 4 在架揭秘3.1 总体设计 ... 91 方法执行的时候该属性尚未初始化,所以上面代码中使用的 Response 属性是我们自行定义 的。运行该程序后会在浏览器中呈现出如图 3-1 所示的输出结果。从输出方法的调用顺序中 不难看出在默认的情况下 Con位 o l1 er 是以异步的方式执行的。 (S301) Be 民lExe咀eQ; Be gjnEx ec回eC or eQ; EndEX仅回e(); EndEx ecuteC oreQ; IndexO; 图 3-1 Controller 在默认情况下的异步执行方式 现在按照如下的方式重写虚属性 DisableAsyncSupport ,使它直接返回 True 以关闭对 Con位 o l1 er 异步执行的支持。 public class HomeController : Controller //其他成员 protected override bool DisableAsyncSupport get{return true;} 再次执行我们的程序将会得到如图 3-2 所示的输出结果,可以看出由于 HomeController 间接地实现了 IAsyncController 接口, Con仕 o l1 er 的执行总是以调用 Begi nE xecute.厄 n dE xecute 方法的方式来执行,但是由于 DisableAsyncSupport 属性被设置为 True , Beg inE xecute 方法 内部会以同步的方式调用 ExecuteÆxecuteCore 方法。 (S302) Be 自lExect监eQ; EndEx eαrteQ; Ex ecute(); Ex ecuteC oreQ; IndexO ; 图 3-2 Controller 在 DisableAsyncSupport 属性为 True 的情况下的同步执行方式 ASP.NETMVC 应用编程接口中还定义了一个 System. Web.Mvc.AsyncController 类型,从 名称上看, AsyncController 是一个基于异步的 Controller ,但是这里的异步并不是指 Controller 的异步执行,而是 Action 方法的异步执行。从如下的代码片段中可以看出,这个直接继承 自抽象类 Controller 的 AsyncController 是一个"空"类型〈没有额外定义和重写基类的类型 成员〉。在上一个版本中,以XxxA sync!Xxx Completed 形式定义的异步 Action 方法均定义在 ASP. NET MVC 4 握架揭秘92 路 第 3 章 Controller 的激活 继承自 AsyncController 的 Con位 oller 类型中,考虑到向后兼容性, AsyncController 在新的版 本中保留下来。 public abstract class AsyncController : Controller { } 只有以传统方式( X xxAsync/XxxCompleted )定义的异步 Action 方法才需要定义在 AsyncController 中。 AS 卫 NET MVC 4.0 提供了新的异步 Action 方法定义方式,使我们可以 通过一个返回类型为 Task 的方法来定义以异步方式执行的 Action ,这样的 Action 方法不需 要定义在 AsyncController 中。 3.1 .2 ControllerF actory ASP.NETMVC 为 Controller 的激活定义相应的工厂,我们将其统称为 ControllerFactory , 所有的 Con位 ollerFactory 实现了接口 System. Web.Mvc .l ControllerFactory 接口。如下面的代码 片段所示, Controller 对象的激活最终通过 ICon位 ollerFactory 的 CreateController 方法来完成, 该方法的两个参数分别表示当前请求上下文和从路由信息中获取的 Controller 的名称。 public interface IControllerFactory IController CreateController(RequestContext requestContext , string controllerName); SessionStateBehavior GetControllerSessionBehavior( RequestContext requestContext , string controllerName); void ReleaseController(IController controller); public enum SessionStateBehavior { ,, 'dyd te--e lrn1-uiob au ‘Ga fιq ‘ as eee-­DRRD 除了负责创建 Controller 处理请求之外, ControllerFactory 还需要在完成请求处理之后释 放 Controller ,对激活 Controller 对象的释放定义在 ReleaseController 方法中。 IControllerFactory 的另一个方法 GetControllerSessio nB ehavior 返回一个 System. Web. SessionState.Session StateBehavior 枚举。熟悉 ASP.NET 的读者对 SessionStateBehavior 应该不会感到陌生,它用 于表示请求处理过程中会话状态支持的模式,它的四个枚举值分别具有如下的含义。 • Default: 使用默认 ASP.NET 逻辑来确定请求的会话状态行为。 • Requ 让ed: 为请求启用完全的读写会话状态行为。 • ReadOnly: 为请求启用只读会话状态。 • Disabled: 禁用会话状态。 ASP. NET MVC 4 在架揭秘3.1 总体设计 磁 93 对于 Default 选项来说, ASP.NET 通过映射的 HttpHandler 类型是否实现了相关接口来决 定具体的会话状态控制行为。在 System. Web. SessionState 命名空间下定义了 IRequiresSessionState 和 IRequiresSessionState 接口,如下面的代码片段所示,这两个都是不 具有任何成员的空接口(我们一般称之为标记接口) ,而 IReadOnlySessionState 继承自 IRequiresSessionState 。如果 HttpHandler 实现了接口 IReadOnlySessionSta旬,则意味着采用 ReadOnly 模式,如果只实现了 IRequiresSessionState 则采用 Required 模式。 public interface IRequiresSessionState { } public interface IReadOnlySessionState : IRequiresSessionState { } 具体采用何种会话状态行为取决于当前 HTTP 上下文(通过 HttpContext 的静态属性 Current 表示)。对于之前的版本,我们不能对当前 HTTP 上下文的会话状态行为模式进行动 态的修改, ASP.NET 4.0 为 HttpContext 定义了如下一个 SetSessionStateBehavior 方法使我们 可以臼由地选择会话状态行为模式。相同的方法同样定义在 HttpContextBase 中,它的子类 HttpContextWrapper 重写了这个方法并在内部会调用封装的 HttpContext 的同名方法。 public sealed class HttpContext : IServiceProvider , IPrincipalContainer //其他成员 public void SetSessionStateBehavior( SessionStateBehavior sessionStateBehavior); public class HttpContextBase: IServiceprovider //其他成员 public void SetSessionStateBehavior( SessionStateBehavior sessionStateBehavior); 3.1.3 ControllerBuilder 用于激活 Controller 对象的 ControllerFactory 最终通过 System. Web.M vc. ControllerBuilder 注册到 ASP.NET MVC 应用中。如下面的代码所示, Con 位 ollerBuilder 定义了一个静态只读 属性 Current 返回当前 ControllerBuilder 对象,这是针对整个 Web 应用的全局对象。两个 SetControllerFactory 方法重载用于注册 ControllerF actory 的类型或者实例,而 GetControllerFactory 方法返回一个具体的 ControllerFactory 对象。 public class ControllerBuilder public IControllerFactory GetControllerFactory(); public void SetControllerFactory(Type controllerFactoryType); AS P. NET MVC 4 框架揭秘94 :. 第 3 章 Controller 的激活 public void SetControllerFactory(IControllerFactory controllerFactory); public HashSet DefaultNamespaces { geti } public static ControllerBuilder Current { get; } 具体来说,如果我们注册的是 ControllerFactory 的类型,那么 GetControllerFactory 在执 行的时候会通过对注册类型的反射(调用 Activator 的静态方法 CreateInstance) 来创建具体 的 ControllerFactory (系统不会对创建的 Controller 进行缓存)。如果注册的是一个具体的 ControllerFactory 对象,该对象直接从 GetControllerFactory 返回。 通过第 2 章 "URL 路由"的介绍我们知道,被 ASP.NET 路由系统进行拦截处理后会生 成一个用于封装路由信息的 RouteData 对象,而目标 Controller 的名称就包含在通过该 RouteData 的 Values 属性表示的 RouteValueDictionary 对象中,对应的 Key 为 "con位oller" 。 而在默认的情况下,这个作为路由数据的名称只能帮助我们解析出 Controller 的类型名称, 如果在不同的命名空间下定义了多个同名的 Controller 类,会导致激活系统无法确定具体的 Con位oller 的类型从而抛出异常。 为了解决这个问题,我们必须为定义了同名 Controller 类型的命名空间设置不同的优先 级。具体来说有两种提升命名空间优先级的方式。第一种方式就是在调用 RouteCollection 如 下所示的扩展方法 MapRoute 时指定一个命名空间的列表。通过第 2 章 "URL 路由"的介绍 我们知道,通过这种方式指定的命名空间列表会保存在 Route 对象的 DataTokens 属性表示的 Route ValueDictionary 字典中,对应的 Key 为 "Namespaces" 。 public static class RouteCollectionExtensions //其他成员 public static Route MapRoute(this RouteCollection routes, string name, string url, string[] namespaces)i public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces)i public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces) i 另一种提升命名空间优先级的方式就是将其添加到当前的 ControllerBuilder 中的默认命 名空间列表中。从上面的给出的 ControllerBuilder 的定义可以看出,它具有-个 HashSet类型的只读属性 DefaultNamespaces 代表了这么一个默认命名空间列表。对 于这两种不同的命名空间优先级提升方式,前者〈通过路由注册〉指定命名空间具有更高的 优先级。 实例演示:如何提升命名空间的优先级( S303 , S304 , S305) 为了让读者对如何提升命名空间优先级有一个深刻的印象,我们来进行一个简单的实例 演示。在一个 ASP.NETMVC 应用创建两个同名的 HomeController 类,如下面的代码片段所 示,这两个 HomeController 类分别定义在命名空间Artech.MvcApp 和 Artech.MvcApp. ASP.NET MVC 4 框架揭秘3.1 总体设计也 95 Con位ollers 之中,而 Index 操作返回的是一个将 Controller 类型全名作为内容的 System. Web. Mvc.ContentResult 对象。 namespace Artech.MvcApp.Controllers public class HomeController : Controller public ActionResult Index() return this.Content(this.GetType() .FullName); P P A C V M h c e •L X A e c a p s e m a ni ‘ public class HomeController : Controller public ActionResult Index() return this.Content(this.GetType() .FullName); 现在我们直接运行该 Web 应用。由于具有多个 Controller 与注册的路由规则相匹配,这 会导致 Con位oller 激活系统无法确定哪个类型的 Controller 应该被选用,所以会出现如图 3-3 所示的错误。 (S303 ) 圃EEEEEE·E·-E·E·-·· 眶里回B:l OI口 calhos t: ï08.l 食 E噩田' Server Error in '/', Application 户 一 ­ M叫e 仰 es were found that ma灿的e controller named 'Home' 时 : can happen if the route that services this request ('{controller}/{action}/{id}') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overfoad of the 'MapRoute' method that takes a 'namespaces' parameter. The request for 'Home' has found the foflowìng matching controllers: Artech.MvcApp. Controllers. HomeControlfer Artech. MvcAPP. HomeController 一一一一-~ i m 图 3-3 具有多个匹配 Controller 导致的异常 目前定义了 HomeContro l1 er 的两个命名空间具有相同的优先级,现在将其中一个定义在 当前 Contro l1 erBuilder 的默认命名空间列表中以提升匹配优先级。如下面的代码片段所示, 在 Global. asax 的 Application_ Start 方法中,将命名空间" Artech.MvcApp.Con位ollers" 添加 到当前 ControllerBuilder 的 DefaultNamespaces 属性所示的命名空间列表中。 ASP. NET MVC 4 框架揭秘96 组第 3 章 Controll 町的激活 public class MvcApplication : System.Web.HttpApplication protected void Application Start() jj 其他操作 ControllerBuilder.Current.DefaultNamespaces .Add("Artech.MvcApp.Controllers"); 对于同时匹配注册的路由规则的两个 HomeControll町来说,由于" Artech.MvcApp.Controllers" 命名空间具有更高的匹配优先级,所有定义其中的 HomeController 会被选用,这可以通过如 图 3-4 所示的运行结果看出来。 (S304) Art ech .MvcApp.Controners.H 锢 eC∞troner 图 3 -4 通过 ControllerBuilder 提升命名空间匹配优先级 为了检验在路由注册时指定的命名空间和作为当前 ControllerBu i1 der 的命名空间哪个具 有更高匹配优先级,修改定义在" App _ StartlR outeConfig.cs "中的路由注册代码,如下面的 代码片段所示,在调用 RouteTable 的静态属性 Routes 的 MapRoute 方法进行路由注册的时候 指定了命名空间(" Art ech.MvcApp 勺。 public class RouteConfig public static void RegisterRoutes(RouteCollection routes) jj 其他操作 routes.MapRoute( name: "Default" , url: "{controller}j{action}j{id}" , defaults: new { controller = "Home" , action = "Index" , id = UrlParameter.Optional }, namespaces: new string[] { "Ar tech.MvcApp" } 再次运行我们的程序会在浏览器中得到如图 3-5 所示的结果,从中可以看出定义在命名 空间" Artech.MvcApp" 中的 HomeController 被最终选用,可见较之作为当前 ControllerBuilder 的默认命名空间,在路由注册过程中执行的命名空间具有更高的匹配优先级,前者可以视为 后者的一种后备。 (S305) ASP. NET MVC 4 框架揭秘3.1 总体设计程 97 Art民h_:yJvcApp_ HomeConlrOner 图 3-5 在路由注册时指定的命名空间具有更高的匹配优先级 在路由注册时指定的命名空间比当前 ControllerBuilder 的默认命名空间具有更高的匹配 优先级,但是对于这两个集合中的所有命名空间却具有相同的匹配优先级。换句话说,用于 辅助解析 Controller 类型的命名空间分为三个梯队,分别简称为路由命名空间、 ConrollerBuilder 命名空间和 Controller 类型命名空间。如果前一个梯队不能正确解析出目标 Con位oller 的类型,则后一个梯队的命名空间将作为后备,反之,如果根据某个梯队的命名 空间进行解析得到多个匹配的 Con位oller 类型,会直接抛出异常。 针对 Area 的路由对象的命名空间 针对某个 Area 的路由映射是通过相应的AreaRegistration 进行注册的,具体来说是在 AreaRegistration 的 RegisterArea 方法中调用AreaRegistrationContext 对象的 MapRoute 方法进 行注册的。如果在调用 MapRoute 方法中指定了表示命名空间的字符串,它将自动作为注册 的路由对象的命名空间,否则会将AreaRegistration 的命名空间加上飞*"后缀得到的字符串 作为路由对象的命名空间。 这里所说的"路由对象的命名空间"存在于 Route 对象的 DataTokens 属性表示的 Route ValueDictionary 对象中,对应的 Key 为 "Namespaces" , Value 就是一个包含字符串数 组的命名空间列表。通过第 2 章 "URL 路由"的介绍, Route 对象的 DataTokens 属性包含的 变量会转移到由它生成的 RouteData 的同名属性中。 除此之外,在调用AreaRegistrationContext 的 MapRoute 方法时还会在注册 Route 对象的 DataTokens 属性中添加一个 Key 为 "useN amespaceFallback" 的条目,它表示是否采用后备 命名空间对 Controller 类型进行解析。如果注册的路由对象具有命名空间(调用 MapRoute 方法时指定了命名空间或者对应的 AreaRegis位副ion 类型定义在某个命名空间下) ,该条目的 值为 False ,否则为 True 。该条目同样反映在通过该 Route 对象生成的 RouteData 对象的 DataTokens 属性中。 在解析 Controller 真实类型的过程中,会先使用 RouteData 包含的命名空间。如果解析 失败,则通过由 RouteData 的 DataTokens 属性得到的这个名为 "useNamespaceFallback" 的 变量值来判断是否使用"后备"命名空间进行解析。具体来说,如果该值为 True 或者不存 在,则先通过当前 Con位ollerBuilder 的命名空间解析,如果失败则忽略命名空间直接采用类 型名称进行匹配,否则会因找不到匹配的 Controller 而直接抛出异常。 ASP. NET MVC 4 翻揭秘98 • 第 3 章 Controller 的激活 我们通过具体的例子来说明这个问题。在一个 ASP.NET MVC 应用中通过Ar ea 添加向 导创建一个名称为 Admin 的 Area ,此时 IDE 会默认为我们添加了如下一个 Adm inAr e aR egistration 类型。 NamespaceMvcApp.Areas.Admin public class AdminAreaRegistration : AreaRegistration public override string AreaName get{return "Admin";} public override void RegisterArea(AreaRegistrationContext context) context.MapRoute( "Admin default" , "Admin!{controller}!{act 土 on}!{ 土 d}" , new { action = "Index" , id = UrlParameter.Optional } Adm inAr e aR egistration 类型定义在命名空间 MvcApp. Ar eas.Admin 中。现在我们在该 Area 中添加如下一个 HomeController ,在默认的 Action 方法 Index 中,我们从当前 RouteData 的 DataTokens 中提取这个名为 "u seNamespaceFallback" 的变量值,并将它和解析出来的 Con位 o l1 er 类型名称写入当前 HtφResponse 而最终呈现在客户端浏览器中。在默认情况下, 添加的 HomeCon位 oller 类型被定义在 MvcApp.Areas.A 但由 .Controllers 命名空间下,现在我 们刻意将命名空间改为 MvcApp.Areas.Controllers 。 namespaceMvcApp. Areas.Controllers public class HomeController : Controller public void Index() Response.Write(string.Format("UseNamespaceFallback: {O}" , RouteData.DataTokens["UseNamespaceFallback"])); Response.Write(string.Format("Controller Type: {O}" , this.GetType() .FullName)); 现在我们在浏览器中通过匹配的 URL (/ Admi nIH ome/I ndex) 来访问Ar ea 为 Admin 的 HomeController 的In dex 操作,会得到如图 3-6 所示的 HTTP 状态为 "404 , Not Found" 的 错误。这就是因为在对 Controller 类型进行解析的时候是严格按照对应的Ar e aRegistration 所 在的命名空间来进行的,很显然在这个范围内是不可能找得到对应的 Controller 类型的。 (S306) ASPNETMVC4 在架揭秘Server Error in '/' Application. The resource cannot be found. Oescription : HTTl' -4 04 刊 e resour 臼 you 8re Io ok in g for (or one of 阳 dependencies) ∞ ukl h8ve been removed , had ils name changed , or 国 lemporarily un8V8 阳协同easerl刷刷 Ihe follow in g URl and rnake sure 11181 rt is 5 p-创1ed correctly 3' . 1 总体设计电 99 图 3-6 Controller 和 AreaRegistration 命名空间不匹配导致的 404 错误 但是如果我们去掉 Adm inAr e aR egis 位 ation 的命名空间,那么将会导致路由变量 U seNamespaceFallback 的值变为 True ,这会促使 Controller 激活系统选择"后备"的命名空 间。由于整个 Web 应用中仅仅定义了唯一匹配的 MvcApp. Ar eas.Controllers . HomeController , 很显然这个 Con位 oller 会被激活,如图 3-7 所示的程序运行结果也说明了这一点。 (S307) UseN1IDlespaceFaDb aclc True Co nlroDer Type: MvcApp .Areas .ControDers .Hom eCo ntroBer 图 3-7 去掉 AdminAreaRegistration 命名空间以采用后备命名空间 3.1.4 Controller 的激活与 URL 路由 ASP.NET 路由系统是 Hη? 请求抵达服务端的第一道屏障,它根据注册的路由规则对拦 截的请求进行匹配并解析包含目标 Controller 和 Action 名称的路由信息。而当前 ControllerB uilder 具有用于激活 Controller 对象的 Con位 ollerFactory ,现在看看两者是如何结 合起来的。 通过第 2 章 "URL 路由"的介绍我们知道, ASP. 阳T 路由系统的核心是一个叫做 UrlRoutin民.f odule 的 H 即 Module ,路由的实现是它通过注册代表 HttpApplication 的 PostResolveRequestCache 事件对 HttpHandler 的动态映射来实现的。具体来说,它通过以 RouteTable 的静态属性 Routes 代表的全局路由表对请求进行匹配并得到一个 RouteData 对 象。 RouteData 具有一个实现了接口 IRouteHandler 的属性 RouteHandler ,通过该属性的 Ge旧址pHandler 方法可以得到最终被映射到当前请求的 H即 Handler 对象。 对于 ASP. NETMVC 应用来说, RouteData 的 RouteHandler 属'性类型为 MvcRouteHandler , 实现在 MvcRouteHandler 中的 H仕pHandler 提供机制基本上(不是完全等同〉可以通过如下 的代码来体现。 MvcRouteHandler 维护着一个 ControllerFactory 对象,该对象可以在构造函 ASP. NET MVC 4 框架缉翻100 翻 第 3 章 Controll町的激活 数中指定,如果没有显示指定则直接通过调用当前 ControllerBuilder 的 GetControllerFactory 方法获取。 public class MvcRouteHandler : IRouteHandler private IControllerFactory controllerFactory; public MvcRouteHandler(): this(ControllerBuilder.Current .GetControllerFactory()) { } public MvcRouteHandler(IControllerFactory controllerFactory) controllerFactory = controllerFactory; IHttpHandlerIRouteHandler.GetHttpHandler(RequestContextrequestContext) string controllerName = (string)requestContext.RouteData .GetRequiredString("controller"); SessionStateBehavior sessionStateBehavior = controllerFactory . GetControllerSessionBehavior (requestContext, controllerName); requestContext.HttpContext.SetSessionStateBehavior(sessionStateBehavior); return new MvcHandler(requestContext); 在用于提供 Ht甲Handler 的 GetHttpHandler 方法中,除了返回一个实现了 IH忧pHandler 接口的 MvcHandler 对象之外,还需要对当前 HTTP 上下文的会话状态行为模式进行设置。 具体的实现是:先通过包含在 RequestContext 的 RouteData 对象得到 Controller 的名称,该 名称连同 RequestContext 对象一起传入 ControllerFactory 的 GetControllerSessionB ehavior 方 法得到一个类型为 SessionStateBehavior 的枚举。最后通过 RequestContext 得到当前 HTTP 上 下文(实际上是一个 HttpContextWrapper 对象),并调用其 SetSessionStateBehavior 方法对会 话状态行为进行设置。 通过第 2 章 "URL 路由"的介绍我们知道, RouteData 中的 RouteHandler 属性最初来源 于对应的路由对象,而当我们调用 RouteCollection 的扩展方法 MapRoute 方法时注册的 Route 对象对应的 RouteHandler 是一个 MvcRouteHandler 对象。由于在创建 MvcRouteHandler 对象 时并没有显式指定 ControllerFactory ,所以通过当前 Con位ollerBuilder 的 GetCon位ollerFactory 方法得到的 ControllerFactory 默认被使用。 通过当前 ControllerBuilder 的 GetControllerFactory 方法得到的 ControllerFactory 仅仅用 于获取会话状态行为模式,而 MvcHandler 真正将它用于创建 Controller 。如下的代码片段基 本上体现了 MvcHandler 的定义,它对请求处理的逻辑定义在 BeginProcessRequest 方法中。 public class MvcHandler : IHttpAsyncHandler, IHttpHandler, IRequiresSessionState //其他成员 public RequestContext RequestCo口 text [ get; private set; } e 1·­b a s u e R S T4 1 0 o b c .-1-b u p{ ASP. NET MVC 4 框架揭秘3.2 默认实现 醺 101 get { return false; } public MvcHandler(RequestContext requestContext) this.RequestContext = requestContext; IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory(); string controllerNarne = this.RequestCoηtext.RouteData.GetRequ 工 redString("controller"); IController controller = controllerFactory .CreateController(this.RequestContext, controllerNarne); if (controller is IAsyncController) try //调用 BeginExecute/EndExecute 方法以异步的方式执行 Controller V4 14 14 a 口.工 lJZL ,4 、 controllerFactory.ReleaseController(controller); else try //调用 Execute 方法以异步的方式执行 Controller V4 14 14 a n -- 、}Jfι 『 1 controllerFactory.ReleaseController(controller); 由于 MvcHandler 同时实现了 IH仕pHandler 和 IH仕pAsyncHandl町接口,所以它总是以异 步的方式被执行(调用 BeginProcessRequestÆndProcessRequest 方法) 0 BeginProcessRequest 方法通过 RequestContext 对象得到目标 Controller 的名称,然后利用当前 ControllerBuilder 创建的 ControllerFactory 激活 Controller 对象。如果 Controller 类型实现了 IAsyncController 接口,则以异步的方式执行 Controller ,否则采用同步执行方式。在被激活 Controller 对象被 执行之后, MvcHandler 会调用 ControllerF actory 的 ReleaseController 对其进行释放清理工作。 3.2 默认实现 Con位oller 激活系统最终通过注册的 ControllerFactory 创建相应的 Controller 对象,如果 ASP. NET MVC 4 框架揭秘102 黯 第 3 章 Controll町的激活 没有对 ControllerFactory 类型或者实例进行注册〈通过调用当前 ControllerBuilder 的 SetControllerFactory 方法),默认使用的 ControllerFactory 类型为 System. Web.Mvc.Default Con位ollerFactory 。我们现在就来讨论实现在 DefaultControllerFactory 中的默认 Controller 激 活机制。 3.2.1 Controller 类型的解析 激活目标 Controller 对象的前提是能够正确解析出 Controller 类型。对于 DefaultControllerFactory 来说,用于解析目标 Controller 类型的辅助信息包括:通过与当前请 求匹配的路由对象生成的 RouteData (其中包含 Controller 的名称和命名空间〉和包含在当前 ControllerBuilder 中的命名空间。很多读者可能首先想到的是通过 Controller 名称得到对应的 类型,并通过命名空间组成 Controller 类型的全名,最后遍历所有程序集并以此名称去加载 相应的类型即可。 这貌似是一个不错的解决方案,实际上则完全行不通。不要忘了作为请求地址 URL 一 部分的 Controller 名称是不区分大小写的,而类型名称则是大小写敏感的。此外,不论是注 册路由时指定的命名空间还是当前 ControllerBuilder 的默认命名空间,有可能包含统配符 - (*)。由于我们不能通过给定的 Controller 名称和命名空间得到 Controller 的真实类型名称, 自然就不可能通过名称去解析 Con位oller 的类型了。 AS卫NETMVC 的 Controller 激活系统则反其道而行之,它先遍历通过 BuildManager 的 静态方法 GetReferencedAssemblies 方法得到所有引用程序集,通过反射的方式得到定义在它 们中的所有实现了接口 IController 的类型,最后通过 Controller 的名称和命名空间作为匹配 条件去选择对应的 Con位oller 类型。 实例演示:创建一个自定义 ControllerF actory 模拟 Controller 默认激活机制 (S308 ) 为了让读者对默认采用的 Controller 激活机制,尤其是 Controller 类型的解析机制有一 个深刻的认识,通过一个自定义的 ControllerFactory 来模拟其中的实现。由于采用反射的方式 来创建 Controller 对象,所以将该自定义 ControllerFactory 起名为 ReflectedCon位ol1erFactory 。 public class ReflectedControllerFactory : IControllerFactory //其他成员 private static List controllerTypesi static ReflectedControllerFactory() controllerTypes = new List(); foreach (Assembly assembly in BuildManager.GetReferencedAssemblies()) controllerTypes.AddRange (assembly.GetTypes () .Where( type => typeof( 工 Controller) .IsAss 工 gnableFrom(type)))i ASPNETMVC4 框架揭秘3.2 默认实现 • 103 public ICo 日 troller CreateController(RequestContext requestContext , string controllerName) Type controllerType = this.GetControllerType(requestContext.RouteData , controllerName); if (null == controllerType) return null; return (IController)Activator.Createlnstance(controllerType); private static bool IsNamespaceMatch(string requestedNamespace , string targetNamespace) if (!requestedNamespace.EndsWith(". 女", StringComparison.OrdinalIgnoreCase)) return string.Equals(requestedNamespace , targetNamespace , StringComparison. OrdinalIgnoreCase) ;. requestedNamespace = requestedNamespace.Substring(O , requestedNamespace.Length - ".女". Length) ; if (!targetNamespace.StartsWith(requestedNamespace , StringComparison.OrdinalIgnoreCase)) return false; return ((requestedNamespace.Length == targetNamespace.Length) II (targetNamespace[requestedNamespace.Length] == '. ')); private Type GetControllerType(IEnumerable namespaces , Type[] controllerTypes) var types = (from type in controllerTypes where namespaces.Any(ns => IsNamespaceMatch( ns , type.Namespace)) select type) .ToArray(); switch (types.Length) case 0: return null; case 1: return types[O]i default: throw new InvalidOperationException ("具有多个匹配的 Controller 类型") ; protected virtual Type GetControllerType(RouteData routeData , string controllerName) //省略实现 如上面的代码片段所示, ReflectedContro l1 erFactory 具有一个静态的 controllerTypes 字段用 于保存所有被解析出来的 Contro l1 er 的类型。在静态构造函数中,调用 Buil dM anager 的 ASPNETMVC4 框架揭秘104 • 第 3 章 Controller 的激活 GetReference dA ssemblies 方法得到所有被引用的程序集,并得到所有定义其中的实现了 ICon位'Ol1 er 接口的类型,这些类型全部被添加到通过静态字段 con位 o l1 erTypes 表示的类型列表。 Con位 o l1 er 类型的解析实现在受保护的 GetCon位 o l1 erType 方法中。在用于最终激活 Contro l1 er 对象的 CreateController 方法中,通过调用该方法得到与指定 RequestContext 和 Contro l1 er 名称相匹配的 Controller 类型,最终通过调用 Activator 的静态方法 CreateInstance 创建相应的 Con位 o l1 er 对象。 Ref1 ectedControl1 erF actory 中定义了两个辅助方法,其中 IsNamespaceMatch 用于判断 Contro l1 er 类型真正的命名空间是否与指定的命名空间(可能包含统配符〉相匹配,进行字符比 较是忽略大小写的。私有方法 GetCon位'Ol1 erType 根据指定的命名空间列表和类型名称匹配的类 型数组得到一个完全匹配的 Contro l1 er 类型。如果得到多个匹配的类型,直接抛出 InvalidOperation 异常,并提示具有多个匹配的 c 。由o l1 er 类型,如果找不到匹配类型,则返回 Nu l1。 在如下所示的用于解析 Contro l1 er 类型的 GetContro l1 erType 方法中,从预先得到的所有 Contro l1 er 类型列表中筛选出类型名称与传入的 Contro l1 er 名称相匹配的类型。首先通过路由 对象的命名空间对之前得到的类型列表进行进一步筛选,如果能够找到一个唯一的类型,则 直接将其作为 Contro l1 er 的类型返回。为了确定是否采用后各命名空间对 Controller 类型进 行解析,可以从作为参数的 RouteData 对象中得到其 DataTokens 属性,并从中获取路由变量 U seN amespaceFallback 的值。如果该路由变量存在并且值为 False ,则直接返回 Null 。 public class ReflectedControllerFactory : IControllerFactory //其他成员 protected virtual Type GetControllerType (RouteData routeData , string controllerName) //根据类型名称筛选 var types = controllerTypes.Where(type => string.Compare( controllerName + "Controller" , type.Name , true) == 0) .ToArray(); if (types.Length == 0) return null; //通过路由对象的命名空间进行匹配 var namespaces = routeData.DataTokens["Namespaces"] as IEnumerable; namespaces = namespaces ?? new string[O]; Type contrllerType = this.GetControllerType(namespaces , types); if (null != contrllerType) return contrllerType; //是否九许采用后备命名空间 bool useNamespaceFallback = true; if (null != routeData.DataTokens["UseNamespaceFallback"]) AS P. NET MVC 4 在架揭秘3.2 默认实现 黯 105 useNamespaceFallback = (bool) (routeData.DataTokens["UseNamespaceFallback"])i //如果不九许采用后备命名空间,返回 Null if (!useNamespaceFallback) return nulli //通过当前 ControllerBuilder 的默认命名空间进行匹配 contrllerType = th 工 s.GetControllerType( ControllerBuilder.Current.DefaultNamespaces, types)i if (null != contrllerType) return contrllerTypei //女口呆只存在一个类型名称匹配的 Controller ,则返回之 if (types.Length == 1) return types[O]i //女口呆具有多个类型名称匹配的 Controller ,则抛出异常 throw new InvalidOperationException(" 具有多个匹配的 Controller 类型") i 如果 RouteData 的 DataTokens 中不存在这样一个 UseN amespaceFallback 路由变量,或者 它的值为 True ,则先采用当前 ControllerBuilder 的默认命名空间列表进一步对 Con位oller 类 型进行解析,如果存在唯一的类型则直接当作目标 Controller 类型返回。如果通过两组命名 空间均不能得到一个匹配的 ControllerType ,并且只存在唯一一个与传入的 Controller 名称相 匹配的类型,则直接将该类型作为目标 Controller 返回。如果这样的类型具有多个,则直接 抛出 InvalidOperationException 异常。 3.2.2 Controller 类型的缓存 为了避免频繁地遍历所有程序集对目标 Controller 类型进行解析, ASP.NET MVC 对解 析出来的 c。由oller 类型进行了缓存以提升性能。与针对用于 Area 注册的AreaRegistration 类型的缓存类似, Controller 激活系统同样采用基于文件的缓存策略,用于保存 Controller 类 型列表的名为 MVCControllerTypeCache.xml 的文件保存在 ASP.NET 的临时目录下面。具体 的路径如下: • % Windir%\Microsoft.NET飞Framework\v {version} \ Temporary ASP.NET Files\ {appname } \..人.人UserCache\ • % Windir% \Microsoft.NET\Framework\v {version}\ Temporary ASP.NET Files\root\. .人 ..\UserCache\ ASP. NET MVC 4 框架揭秘106 • 第 3 章 Controller 的激活 其中第一个针对寄宿于 IIS 中的 Web 应用,后者针对直接通过 Visual Studio Developer Server 作为宿主的应用。而用于保存所有AreaRegistration 类型列表的 MVC-AreaRegistrationTypeCache. xml 文件也保存在这个目录下面。 当接收到 Web 应用被启动后的第一个请求时, Controller 激活系统会读取这个用于缓存 所有 Controller 类型列表的 ControllerTypeCache.xml 文件并反序列化成一个 List对象。 只有在该列表为空的时候才会通过遍历程序集和反射的方式得到所有实现了接口 ICon位oller 的类型,而被解析出来的 Controller 类型重新被写入这个缓存文件中。这个通过读取缓存文 件或者重新解析出来的 Controller 类型列表被保存到内存中,在 Web 应用活动期间内被 Con位oller 激活系统使用。 下面的 )a缸片段反映了这个用于 Controller 类型列表缓存的 MVC-ControllerTypeCache. xml 文件的结构,从中可以看出它包含了所有的 Controller 类型的全名和所在的程序集和托 管模块的名称。 Artech.Admin.HomeController Artech.Admiη.EmployeeController Artech.Portal.Controllers.HomeController Artech.Portal.ProductsController 3.2.3 Controller 的释放和会话状态行为的控制 作为激活 Controller 对象的 ControllerFactory 不仅仅用于创建目标 Controller 对象,还具 有两个额外的功能,即通过 ReleaseController 方法对激活的 Controller 对象进行释放和回收, 以及通过调用 GetControllerSessionB ehavior 方法返回用于控制当前会话状态行为的 SessionStateBehavior 枚举对象。 对于默认使用的 DefaultControllerF actory 来说,它对 Controller 对象的释放操作很简单, 即如果 Controller 类型实现了 IDisposable 接口,则直接调用其 Dispose 方法即可。我们将这 个逻辑也实现在了我们自定义的 ReflectedControllerFactory 中。 ASP.NET MVC 4 框架揭秘3.2 默认实现 部 107 public class ReflectedControllerFactory : IControllerFactory //其他操作 public void ReleaseController(IController controller) IDisposable disposable = controller as IDisposablei if (null != disposable) disposable.Dispose()i 至于用于返回 SessionStateBehavior 枚举的 GetControllerSessio nB ehavior 方法,在默认的 情况下它的返回值为 SessionStateBehavior.Default 。通过前面的介绍我们知道在这种情况下具 体的会话状态行为取决于创建的 HttpHandler 所实现的标记接口。对于 ASP.NET MVC 应用 来说,默认使用的 HttpHandler 是-个 MvcHandler 的对象,如下面的代码片段所示,它实现 了 IRequiresSessionState 接口,意味着默认情况下会话状态是可读写的(相当于 S essionStateB ehavior.Requried) 。 public class MvcHandler 工 HttpAsyncHandler , IHttpHandler , IRequiresSessionState //其他成员 可以通过在 Controller 类型上应用 System. Web.Mvc.SessionStateAttribute 特性来具体控制 会话状态行为。如下面的代码片段所示, SessionStateAttribute 具有一个 SessionStateBehavior 类型的只读属性 Behavior 用于返回具体行为设置的会话状态行为选项,该属性是在构造函数 中被初始化的。 [AttributeUsage(AttributeTargets.Class , AllowMultiple = false , Inherited = true) ] public sealed class SessionStateAttribute : Attribute public SessionStateAttribute(SessionStateBehavior behavior)i public SessionStateBehavior Behavior { geti } DefaultControllerF actory 会试着获取应用在 Controller 类型上的 SessionStateAttribute 特 性,如果这样的特性存在则直接返回它的 Behavior 属性所表示的 SessionStateBehavior 枚举, 如果不存在则返回 SessionStateBehavior.Default ,具体的逻辑也反映在我们自定义的 ReflectedControllerFactory 的 GetControllerSessio nB ehavior 方法中。 public class ReflectedControllerFactory : IControllerFactory //其他成员 public SessionStateBehavior GetControllerSessionBehavior( RequestContext requestContext , str 工 ng controllerNarne) AS P. NET MVC 4 框架揭秘108 醺 第 3 章 Controller 的激活 Type controllerType = this.GetControllerType(requestContext.RouteData, controllerName); if (ηull == controllerType) return SessionStateBehavior.Default; SessionStateAttribute attribute = controllerType . GetCustornAttributes (true) .OfType() .FirstOrDefault(); attribute = attribute ?? new SessionStateAttribute(SessionStateBehavior.Default)i return attribute.Behaviori 3.3 loC 的应用 所谓控制反转 C Inversion of Con衍。1, IoC) ,简单地说,就是应用本身不负责依赖对象的 创建和维护,而交给一个外部容器来负责。这样控制权就由应用转移到了外部 IoC 容器,控 制权就实现了所谓的反转。比如在类型 A 中需要使用类型 B 的实例,而 B 实例的创建并不 由 A 来负责,而是通过外部容器来创建。通过 IoC 的方式实现针对目标 Con位oller 的激活具 有重要的意义。 3.3.1 从 Unity 来认识 loC 有时又将 IoC 称为依赖注入 CDependency Injection, DI)。所谓依赖注入,就是由外部 容器在运行时动态地将依赖的对象注入到组件之中。 Martin Fowler 在那篇著名的文章 Inversion o[ Control Containers and the Dependency .L吃jection pattern 中将具体的依赖注入划分 为三种形式,即构造器注入、属性(设置)注入和接口注入,而我个人习惯将其划分为一种 (类型)匹配和三种注入。 • 类型匹配 C Type Mapping): 虽然我们通过接口〈或者抽象类)来进行服务调用,但是 服务本身还是实现在某个具体的服务类型中,这就需要某个类型注册机制来解决服务接 口和服务类型之间的匹配关系。 • 构造器注入 C Constructor 1时 ection): IoC 容器会智能地选择和调用适合的构造函数以创 建依赖的对象。如果被选择的构造函数具有相应的参数, IoC 容器在调用构造函数之前 解析注册的依赖关系并自行获得相应参数对象。 • 属性注入 CPrope即 I时 ection): 如果需要使用到被依赖对象的某个属性,在被依赖对象 被创建之后, IoC 容器会自动初始化该属性。 • 方法注入 CMethod 1时 ection): 如果被依赖对象需要调用某个方法进行相应的初始化, 在该对象创建之后, IoC 容器会自动调用该方法。 ASP.NET MVC 4 框架揭翻3.3 loC 的应用 黯 109 开源社区具有很有流行的 IoC 框架,如 Castle Windsor 、 Unity 、 Spring.NET 、 StructureMap 和 Niniect 等。 Unity 是微软 Patterns& Practices 部门开发的一个轻量级的 IoC 框架,该项目 在 Codeplex 上的地址为 h即 ://unity.codeplex.co m/ ,我们可以下载相应的安装包和开发文档。 在本书出版之时, Unity 的最新版本为 2 .1。出于篇幅的限制,我们不可能对 Unity 进行详细 的讨论,但是为了让读者了解 IoC 在 Unity 中的实现,我们写了一个简单的程序。 创建一个控制台程序,定义如下几个接口(l A 、 IB 、 IC 和 ID) 和它们各自的实现类 (A 、 B 、 C 、 D) 。在类型 A 中定义了 B 、 C 和 D3 个属性,其类型分别为接口 IB 、 IC 和 ID 。属性 B 在函数中被初始化,意味着它会以构造器注入的方式被初始化:属性 C 上应用了 Microsoft.Practices. Uni 可 .Dependency A忧 ribute 特性,意味着这是一个需要以属性注入方式被 初始化的依赖属性:属性 D 则通过方法 Initialize 初始化,该方法上应用了特性 Microsoft.Practices. U nity.In j ectionMethodAttribute ,意味着这是一个注入方法,它会在 A 对象 被 IoC 容器创建的时候会被自动调用。 o m e nυ V 」 卡」E工η nu e c a p& S e m a n-- ‘ 1,J {{{{ ABCD T4γi-T4TI-eeee cccc aaaa ffff rrrr eeee 』L ←L ←」←」 nnnη -1·l· 工 -l cccc ·工 -1· 工 -1 141411 bbbb uuuu pppp public class A : 1A public 1B B { geti seti } [Oependency] public 1C C { geti set; public 10 0 { geti seti public A(1B b) this.B = bi [1njectionMethod] public void 1nitialize(10 d) this.O = di public class B: 1B{} public class C: 1C{} public class 0: 10{} 然后为该应用添加一个配置文件,并定义如下一段关于 Unity 的配置。这段配置定义了 一个名称为 defaultContainer 的 Unity 容器,并在其中完成了上面定义的接口和对应实现类之 间映射的类型匹配。
最后在作为程序入口的 Main 方法中创建一个代表 IoC 容器的 UnityContainer 对象,并 加载配置信息对其进行初始化。然后调用它的泛型方法 Resolve 创建一个实现了泛型接口 IA 的对象。最后将返回对象转变成类型 A ,并检验其 B 、 C 和 D 属性是否为 Null 。 static void Main(string[] args) IUnityContainer container = new UnityContainer(); UnityConfigurationSection configuration = ConfigurationManager.GetSection(UnityConfigurationSection.SectionNarne) as UnityConfigurationSection; configuration.Configure(container , "defaultContainer"); A a = container.Resolve() as A; if (null != a) { .,.,., ))) OOO NNN sss eee yyy nfnf 町,- 14141-1411 uuu nnn === === BCD aaa ,,, 、.•. ,、,「,、,, J nununu {{{ 町,·町,.。-141-1 14 『l-14 uuu nnn === === BCD aaa ((( eee nnn ·工·工·工于μ''UTμeee ttt -l· 工·工 rrr www ... eee l--14 000 sss nnn 000 户U 户U 户」 从如下给出的执行结果可以得到这样的结论:通过 Resolve 方法返回的是一个类型 为 A 的对象,该对象的三个属性被进行了有效的初始化。这个简单的程序分别体现了接口 注入(通过相应的接口根据配置解析出相应的实现类型)、构造器注入(属性 B) 、属性注入 (属性 C) 和方法注入(属性 D) 0 (S309) a.B == null ? No a.C == null ? No a.D == null ? No 3.3.2 Controller 与 Model 的分离 在第 1 章" ASP.NET + MVC" 中我们谈到过 ASP.NETMVC 是基于 MVC 的变体 Mode12 设计的。 ASP.NET MVC 所谓的 Model 仅仅表示绑定到 View 上的数据,我们一般称之为 View Model 。而真正的 Model 一般意义上指维护应用状态和提供业务功能操作的领域模型, 或者是针对业务层的入口或者业务服务的代理。真正的 MVC 在 ASP.NETMVC 中的体现如 图 3-8 所示。 ASP. NET MVC 4 在架揭秘3.3 loC 的应用 磁 111 ASP.NET MVC |View |长--- |Model I 图 3-8 AS P. NET MVC + Model 对于一个 ASP.NET MVC 应用来说,用户交互请求直接发送给 Controller 。如果涉及针 对某项业务功能的调用, Controller 会直接调用 Model 。如果需要呈现业务数据, Controller 会通过 Model 获取相应业务数据并转换成 ViewModel ,最终通过 View 呈现出来,这样的交 互协议方式反映了 Controller 针对 Model 的直接依赖。 如果我们在 Controller 激活系统中引入 IoC ,并采用 IoC 的方式提供用于处理请求的 Controller 对象,那么 Controller 和 Model 之间的依赖程度在很大程度上被降低了,甚至可以 像图 3-9 所示的一样,以接口的方式对 Model 进行抽象,让 Controller 依赖于这个抽象化的 Model 接口,而不是具体的 Model 实现。 ASP.NET MVC View ~--- 图 3-9 ASP.NET MVC + IModel +Model 3.3.3 基于 loC 的 ControllerF actory AS 卫 NET MVC 的 Controller 激活系统最终通过 ControllerFactory 来创建目标 Controller 对象,要将 IoC 引入 ASP.NET MVC 并通过对应的 IoC 容器实现对目标 Controller 的激活, 我们很自然地会想到自定义一个基于 IoC 的 ControllerFactory 。 对于自定义 ControllerFactory ,可以直接实现 IControllerFactory 接口创建一个全新的 ControllerF actory 类型,这需要实现包括 Controller 类型的解析、 Controller 实例的创建与释 放以及会话状态行为选项的获取在内的所有功能。一般来说, Controller 实例的创建才需要 IoC 容器的控制,为了避免重新实现其他的功能,可以直接继承 DefaultControllerFactory ,重 写 Con位 oller 实例创建的逻辑。 ASP. NET MVC 4 在架揭秘112 • 第 3 章 Controller 的激活 实例演示:创建基于 Unity 的 ControllerFactory (8310) 现在我们通过一个简单的实例演示如何通过自定义 ControllerF actory 利用 Unity 进行 Contro l1 er 的激活。为了避免针对 Controller 类型解析、会话状态行为选项的获取和对 Con位 oller 对象的释放逻辑的重复定义,我们直接继承 DefaultControllerF actory 。将该自定义 ControllerF actory 命名为 UnityControllerFactory 。如下面的代码片段所示, UnityControllerFacωry 仅仅重写了受保护的虚方法 GetCon位 olle rI nstance ,将成功解析的 Controller 类型作为调用 Uni可 Container 的 Resolve 方法的参数,而返回值就是需要被激活的 Controller 实例。 public class UnityControllerFactory: DefaultControllerFactory public IUnityContainer UnityContainer { geti private seti } public UnityControllerFactory(IUnityContainer unityContainer) this.UnityContainer = unityContaineri protected override IController GetControllerlnstance( RequestContext requestContext , Type controllerType) if (null == controllerType) return nulli return (IController)this.UnityContainer.Resolve(controllerType)i 整个自定义的 UnityCon位 o l1 erFactory 就这么简单。为了演示 IoC 在它身上的体现,我们 在一个简单的 ASP. MVC 实例中来使用它。我们沿用在第 2 章 "URL 路由"中使用过的关于 "员工管理"的场景,如图 3-10 所示,本实例由两个页面〈对应着两个 View) 组成,一个 用于显示员工列表,另一个用于显示基于某个员工的详细信息。 撞击曹甲EEE 图 3-10 员工列表和员工详细信息页面 在一个 ASP.NETMVC 应用中添加对 Unity 的程序集 Microsoft.Practices. Unity.dll 的引用 (如果读者不想安装 Unity ,可以通过下载本实例的源代码的方式获取该程序集),然后在 AS P. NET MVC 4 在架揭秘3.3 loC 的应用 • 113 Models 目录下定义如下一个表示员工信息的 Employee 类型。 e e y o -­p m GetErnployees(string id = "")i public class ErnployeeRepository : IErnployeeRepository private static IList ernployeesi static EmployeeRepository() ernployees = new List()i employees .Add (new Employee ("001" , "张三","男" , new Da teT ime (1981 , 8 , 24) , "销售部") ) i ernployees .Add (new Employee ("002", "李四","女", new DateTirne (1982, 7 , 10) , "人事部") ) i ernployees.Add( 口 ew Employee ("003", "王五","男", new DateTirne (1981, 9, 21) , "人事部") ) ; ASP.NET MVC 4 在架揭秘114 • 第 3 章 Controller 的激活 public IEnumerable GetEmployees(string id = "") return employees.Where(e => e.ld == id II string.IsNullOrEmpty(id)); 我们创建了一个具有如下定义的 EmployeeController ,它具有一个类型为 IEmployeeRepository 的属性 Repository ,应用在上面的 DependencyAttribute 特性告诉我们 这是一个"依赖属性"。当我们采用 UnityContainer 来激活 EmployeeController 对象的时候, 会根据注册的类型映射来实例化一个实现了 IEmployeeRepository 的类型的实例来初始化 该属性。 public class EmployeeController : Controller [Dependency] public I EmployeeRepository Repository { get; set; } public ActionResult GetAIIEmployees() var employees = this.Repository.GetEmployees(); return View("EmployeeList", employees); public ActionResult GetEmployeeByld(string id) Employee employee = this.Repository.GetEmployees(id) .FirstOrDefault(); if (null == employee) throw new HttpException (404 , string. Format ("ID 为 {O} 的员工不存在", id) ) ; } return View("Employee", employee); EmployeeController 定义了两个基本的 Action 方法。 GetAllEmployees 通过 Repository 获 取所有员工列表并将其通过名位 EmployeeLi st 的 View 呈现出来。另一个 Action 方法 GetEmployeeBy Id 根据指定的 D 获取相应的员工信息,最终用于呈现单个员工信息的 View 为 Employee 。如果根据指定的 ID 找不到相应的员工,直接抛出一个状态为 "404" 的 HttpException 异常。 如下所示的是用于显示员工列表的 View (EmployeeList) 的定义,它的 Model 类型为 IEnumerable 。在该 View 中,通过一个表格来显示员工列表,值得一提的是,可 以通过调用 HtmlHelper 的 ActionL ink 方法将员工的名称显示为一个指向 Action 方法 GetEmployeeByld 的链接。 @model IEnumerable 员工列表 ASP. NET MVC 4 蓓架揭秘3.3 loC 的应用自 115 @{ foreach(Ernployee ernployee in Model)
姓名 't生别 出生日期 部门
@Htrnl.ActionLink(ernployee.Narne , "GetErnployeeByld" , new { narne = ernployee.Narne , id = ernployee.ld }) @Htrnl.DisplayFor(rn=>ernployee.Gender) @Htrnl.DisplayFor(rn=>ernployee.BirthDate) @Htrnl.DisplayFor(rn=>ernployee.Departrnent)
用于显示单个员工信息的名为 Employee 的 View 定义如下,这是一个 Model 类型为 Employee 的强类型的 View ,通过表格的形式将员工的详细信息显示出来。 @rnodel Ernployee @Model.Narne rn=>rn.Gender) AS P. NET MVC 4 在架揭秘116 源 第 3 章 Controll 凹的激活
@Htrnl.LabelFor(rn=>rn.ld)@Htrnl.DisplayFor(rn=>rn.Id)
@Htrnl.LabelFor(rn=>rn.Narne)@Htrnl.DisplayFor( rn=>rn.Narne)
@Htrnl.LabelFor(rn=>rn.Gender)@Htrnl.DisplayFor(
@Htrnl.LabelFor(rn=>rn.BirthDate)@Htrnl.DisplayFor( rn=>rn.BirthDate)
@Html.LabelFor(m=>m.Department)@Html.DisplayFor( m=>m.Department)
我们对两个页面的 U也进行了相应的设计,主页用于显示所有员工列表,它指向 EmployeeController 的 Action 方法 Ge tA llEmployees 。用于显示单个员工详细信息的页面的 URL 的结构为"/{员工姓名}/{员工 ID}" C 比如"/李四 /002 勺,它自然指向另一个 Action 方 法 GetEmployeeByld ,为此我们在自动生成的 RouteConfig 类型中按照如下的方式注册两个 路由。 9 ·工FL n o c e ←」u O R s s a 14 C C .l l b u p4It public static void RegisterRoutes(RouteCollection routes) //其他操作 routes.MapRoute( name: "Home" , url: "", defaults: new { controller = "Employee" , action = "GetAIIEmployees" } routes.MapRoute( name: "Detail" , url: "{name}/{id}" , defaults: 口 ew { controller = "Employee" , action = "GetEmployeeByld" } 自定义的 ControllerFactory CUnityControllerFactory) 在 Globa l. asax 中通过如下的代码进 行注册。用于创建 U nityControllerF actory 的 U nityContainer 对象注册了 IEmployeeRepository 和 EmployeeRepository 之间的映射关系。 public class MvcApplication : System.Web.HttpApplication protected void Application Start() //其他操作 UnityContainer unityContainer = new UnityContainer(); unityContainer.RegisterType(); UnityControllerFactory controllerFactory = new UnityControllerFactory(unityContainer); ControllerBuilder.Current.SetControllerFactory(controllerFactory); 除此之外,我们还为该实例应用定义相应的布局文件和 CSS 样式,在这里就不一一介 AS P. NET MVC 4 在架揭翻3.3 loC 的应用 融 117 绍了。这个例子旨在演示通过自定义 ControllerFactory 实现以 IoC 的方式激活目标 Controller 对象,这样可以最大限度地降低 Controller 和其他组件之间的依赖关系,因为这些 依赖会被用于激活 Controller 的 IoC 容器动态注入。 3.3.4 基于 loC 的 ControllerActivator 除了通过自定义 ControllerF actory 的方式引入 IoC 之外,在使用默认 DefaultControllerFactory 情况下也可以通过一些扩展使基于 IoC 的 Controller 激活成为可能。不过这就需要我们具体 了解实现在 DefaultControllerFactory 内部的 Controller 激活机制了。 DefaultControllerFactory 针对目标 Controller 的激活其实是通过另一个名为 ControllerActivator 的组件来完成的,所有的 ControllerActivator 实现了 System. Web.Mvc. IControllerActivator 接口。如下面的代码片段所示, IControllerActivator 定义了唯一的用于创 建 Controller 对象的 Create 方法,而 DefaultControllerFactory 使用的 Controller Activator 可以 直接通过构造函数参数的方式来指定。 public interface IControllerActivator IController Create(RequestCo 口 text requestContext , Type controllerType)i public class DefaultControllerFactory : IControllerFactory //其他成员 public DefaultControllerFactorY()i public DefaultControllerFactory(IControllerActivator controllerActivator)i 实例演示:创建基于 Ninject 的 ControllerActivator (8311 ) 如果我们基于一个 ControllerActivator 对象来创建一个 DefaultControllerFactory ,它最终 会被用于 Controller 对象的激活,那么可以通过自定义 ControllerActivator 的方式将 IoC 引入 Con位 o l1 er 激活系统。接下来自定义的 ControllerActivtor 基于另一个 IoC 框架 Niniect ,较之 Unity , Ninject 是一个更加轻量级也更适合 ASP.NET MVC 的 IoC 框架,将自定义的 ControllerActivator 起名为 NiniectControllerActivator 。如下面的代码所示,针对目标 Controller 的创建是通过一个 StandardKemel 对象来完成的,为了方便实现类型的映射,我们定义了一 个泛型的 Register 方法。 public class NinjectControllerActivator : IControllerActivator public IKernel Kernel { geti private seti } public NinjectControllerActivator() this.Kernel = new StandardKernel()i ASPNETMVC4 框架揭秘118 盟 第 3 章 Controller 的激活 public IController Create (RequestContext requestCo 口 text , Type controllerType) return (IController)this.Kernel.TryGet(controllerType); public void Register() where TTo: TFrom this.Kernel.Bind() .To(); 接下来我们使用的还是之前演示过的关于员工管理的例子,前面我们演示了属性注入的 方式在激活 EmployeeController 的时候对 Repository 进行初始化,现在来演示另一种依赖注 入形式一一构造器注入。如下面的代码片段所示,只读的 Repository 是在构造函数中通过指 定的参数初始化的,而该参数的类型是 IEmployeeRepository 。 public class EmployeeController : Controller //其他成员 public IEmployeeRepository Repository { get; private set; } public EmployeeCo 口 troller(IEmployeeRepository repository) this.Repository = repository; 为了让 ASP.NETMVC 的 Controller 激活系统采用我们自定义的 ControllerActivator 来创 建目标 Controller ,我们需要创建并注册一个相应的 DefaultControllerFactory 对象。如下面的 代码片段所示,我们在 Globa l. asax 中创建一个 NinjectControllerActivator 对象,并注册了接 口 IEmployeeRepository 和实现类型 EmployeeRepository 之间的匹配关系。最后据此创建一 个 DefaultControllerFactory 对象,通过当前的 ControllerBuilder 进行注册。 public class MvcApplication : System.Web.HttpApplication protected void Application Start() //其他成员 NinjectControllerActivator controllerActivator = new NinjectControllerActivator(); controllerActivator.Register(); DefaultControllerFactory controllerFactory = new DefaultControllerFactory(controllerActivator); ControllerBuilder.Current. SetControllerFactory (control lerFactory); 再次运行我们的程序,依然会得到如图 3-10 所示的结果,其实自定义 ControllerActivator 实现 IoC 的方式并不是很常用,接下来我们介绍第三种更加常用的 IoC 实现方式。 ASP. NET MVC 4 在架揭秘3.3 loC 的应用 露 119 3.3.5 基于 loC 的 DependencyResolver 如果在构建 DefaultControllerFactory 的时候没有显式指定采用 ControllerActivator ,它默 认使用的是一个类型为 DefaultControllerActivator 的对象。如下面的代码片段所示,这只是 一个实现了 IControllerActivator 接口的私有类型,不能直接通过编程的方式使用它。 private class DefaultControllerActivator : IControllerActivator public DefaultControllerActivator(); public DefaultControllerActivator(IDependencyResolver resolver); public IController Create(RequestCoηtext requestContext, Type controllerType); 即使 DefaultControllerFactory 采用了默认的 DefaultControllerActivator ,依然可以将 IoC 引入到 Controller 的激活系统中,而这就需要进一步了解实现在 DefaultControllerActivator 的 Controller 激活逻辑了。 其实 DefaultControllerActivator 完成对 Controller 的激活依赖于另一个名为 Dependency Resolver 的对象。 DependencyResolver 是一个非常重要的组件,可以将其视为 ASP.NET MVC 框架内部使用的 IoC 容器。它不只是用于针对 Controller 的激活,框架内部 很多组件的提供最终都依赖于它。 De叩pend由enc叮yRe臼solve町r 实现了具有如下定义的 S句y嘻吼S卧阳t饨em 定的类型获取单个和所有实例。 public interface IDepe 口 de 口 cyResolver object GetService(Type serviceType); IEnumerable GetServices(Type serviceType); 整个 Web 默认使用的 DependencyResolver 可以通过 System. Web.Mvc.Dependency Resolver 类型进行注册。如下面的代码片段所示, DependencyResolver 类型具有一个静态的 Current 属性表示当前 DependencyResolver ,具体对 DependencyResolver 的注册通过调用静态 方法 SetResolver 来完成。顺便说一下, DependencyResolver 类型并没有实现 IDependencyResolver 接口,并不是真正意义上的 DependencyResolver 。 public class Depende口 cyResolver //其他成员 private static Depe 口 de 口 cyResolver instance; public void InnerSetResolver(object commonServiceLocator); public void InnerSetResolver( 工 Depe 口 de 口 cyResolver resolver); public void InnerSetResolver(Func getService, Func> getServices); public static void SetResolver(object commonServiceLocator); public static void SetResolver(IDependencyResolver resolver); ASP. NET MVC 4 框架揭秘120 磁第 3 章 Controller 的激活 public static void SetResolver(Func getService , Func> getServices)i public static IDependencyResolver Current { geti } public IDependencyResolver 工口 nerCurrent { geti } 这个被封装的 DependencyResolverC 指实现了接口 IDependency Resolver 的某个类型的对 象,不是指 DependencyResolver 类型的对象,对于后者我们会采用 "DependencyResolver 类 型对象"的说法)通过只读属性 InnerCurrent 表示,而三个 InnerSetResolver 方法重载用于初 始化该属性。静态字段 instance 表示当前的 DependencyResolver 类型对象,静态只读属性 Current 则表示该对象内部封装的 DependencyResolver 对象,而它通过三个静态的 SetResolver 进行初始化。 如果没有对 DependencyResolver 进行显式注册,系统默认使用的是一个类型为 DefaultDependency Resolver 的对象。如下面的代码片段所示,这是一个私有类型,用于根据 类型提供"服务实例"的 GetService 方法直接以反射的方式根据类型创建并返回对应的实例。 对于类型为接口/抽象类,或者不曾定义默认公有构造函数的类型,我们直接返回 Null 。也 就是说在默认的情况下, Con位 oller 的激活最终是通过对 Controller 类型的反射来实现的。 DefaultDependencyResolver 的另一个 GetServices 方法直接返回一个空的对象列表。 private class DefaultDependencyResolver : IDependencyResolver public object GetService(Type serviceType) if (serviceType.Islnterface II serviceType.IsAbstract) return nulli try retur 口 Activator.Createlnstance(serviceType)i catch return nulli public IEnumerable GetServices(Type serviceType) return Enumerable.Empty()i 上面介绍的类型 DefaultCon衍ollerFactory 、 ICon位 ollerActivator 、 DefaultCon位ollerActivator 、 IDependencyResolver 、 DefaultDependency Resolver 和 DependencyResolver 之前的关系基本上 可以通过如图 3-11 所示的类图来体现。 ASP. NET MVC 4 在架揭秘DefaultControllerFactory 斗 <> IControllerActivator 3.3 loC 的应用 蟹 121 图 3-11 DefaultControllerFactory + ControllerActivator + DependencyResolver 实例演示:创建基于 Ninject 的 DependencyResolver (8312 ) 通过前面的介绍我们知道,当调用构造函数创建一个 DefaultControllerFactory 的时候, 如果调用的时候默认无参构造函数,后者将作为参数的 ControllerActivator 对象设置为 Null , 那么默认请求用于激活 Controller 实例的是通过 DependencyResolver 类型的静态属性 Current 表示的 Dependency Resolver 对象,换言之,我们可以通过自定义 DependencyResolver 的方式 来实现基于 IoC 的 Controller 激活。 同样是采用 Ni 时 ect ,我们定义了一个具有如下定义的 NinjectDependencyResolver 。与 上面定义的 NiniectControllerActivator 类似, NinjectDependencyResolver 具有一个 IKemel 类型的只读属性 Kemel ,该属性在构造函数中被初始化为一个 Standar dK emel 对象。对于 实现的 GetService 和 GetServices 方法,直接调用 Kernel 的 TηrGet 和 GetAll 返回指定类型 的实例和实例列表。为了方便进行类型映射,我们定义了泛型的 Register 方法。 public class NinjectDepende 口 cyResolver : IDepeηde 口 cyResolver public IKernel Kernel { geti private seti } public NinjectDependencyResolver() this.Kernel =口 ew StandardKernel()i public void Register() where TTo: TFrom this.Kernel.Bind() .To()i public object GetService(Type serviceType) return this.Kernel.TryGet(serviceType)i ASP. NET MVC 4 在架揭秘122 幽 第 3 章 Conìroller 的激活 public IEnumerable GetServices(Type serviceType) return this.Kernel.GetAll(serviceType); 我们只需要创建一个自定义的 Ninjec tD ependencyResolver 对象并将其作为当前的 DependencyResolver 即可。如下面的代码片段所示,我们创建了一个 Ninjec tD ependencyResolver 对象并注册了 IEmployeeRepository 和 EmployeeRepository 之间的映射关系,然后调用 Dependency Resolver 的静态方法 SetResolver 将创建的 NinjectDependencyResolver 注册为 当前的 DependencyResolver 对象。再次运行我们的程序,依然会得到如图 3-10 所示的 效果。 public class MvcApplication : System.Web.HttpApplication //其他成员 protected void Application_Start() //其他操作 NinjectDependencyResolver depe 口 dencyResolver = new NinjectDependencyResolver(); dependencyResolver.Register(); Depende 口 cyResolver.SetResolver(dependencyResolver); 本章小结 当目标 Controller 的名称通过 URL 路由被解析出来之后, ASP.NET MVC 利用注册的 Con位 ollerFactory 根据该名称实现对目标 Con位 oller 的激活。除了完成对 Controller 的激活之 外, ControllerFactory 还负责对 Controller 的释放工作,以及获取用于控制会话状态行为的 SessionStateBehavior 枚举。 ControllerFactory 的注册通过 ControllerBuilder 来完成。 ASP.NET MVC 默认使用的 ControllerFactory 类型为 DefaultControllerFactory ,它在对 Con位 oller 类型进行解析的时候对所有 Controller 类型采用了基于文件的缓存以提升性能。在 DefaultCon位 ollerFactory 内部,它将解析得到的 Controller 类型递交给 ControllerActivator 对 象对 Controller 实施最终的激活。默认使用 DefaultControllerActivator 内部利用了当前注册的 DepedencyResolver 来提供具体的 Controller 对象。如果没有对 DepedencyResol ver 进行显式 注册,默认提供的 DepedencyResolver 将采用对提供类型的反射方式创建相应的实例 将 IoC 应用到 Controller 的激活过程中具有重要的意义,可以极大地降低 Con位oller 和 其他组件的依赖关系。通过对 Con位oller 激活流程的分析,我们提供了三种实现方法,即自 定义 ControllerF actory 、 ControllerActivator 和 DepedencyResolver 。 ASP. NET MVC 4 握主导毒秘第 4 章 Model 元数据的解析 AS 卫 NETMVC 的 Model 为 ViewModel ,表示最终呈现在 View 上的数据, 而 Model 元数据的一个主要的作用在于控制 Model 对象在 View 上的呈现方 式。说得更加具体点,基于某种数据类型的 Model 元数据用于指导最终生成 怎样的 HTML 来呈现对应的 Model 对象。 Model 元数据的存在使模板化的 HTh在 L 呈现机制成为可能。除此之外, Model 元数据还服务于 Model 绑定和 Model 验证。 ASP. NET MVC 4 在架揭秘124 • 第 4 章 Model 元数据的解析 4.1 Model 元数据及真定制 Model 元数据是针对数据类型的一种描述信息,主要用于控制数据类型本身及其成员属 性在界面上的呈现方式,同时也为 Model 绑定和验证提供必不可少的元数据信息。一个复杂 数据类型通过属性的方式定义了一系列的数据成员,而 Model 元数据不仅仅是数据类型本身 的描述,对数据成员的描述也包含其中,所以 Model 元数据具有一个层次化结构。 4.1.1 Model 元数据层次化结构 举个例子,假设我们的 Model 类型是如下一个表示联系人的 Contact 类,其属性 Name 、 PhoneNo 、 EmailAddress 和 Address 分别代表姓名、电话号码、邮箱地址和联系地址。联系 地址通过另一个数据类型 Address 表示,属性 Province 、 City 、 District 和 Street 分别表示所 在省份、城市、城区和街道。 ←』c a ←」n o 户」s s a 14 C C .-14 b u p{ s qJGJαJS nRne ·工·工 -lr rrrd tttd SSSA CCCC ·-·工·工·工141414 可牛 bbbb uuuu p 晶。‘ P-P Narne { get; set; } PhoneNo { get; set; } ErnailAddress { get; set; } Address { get; set; } s s e r d d A S s a 14 C C .l l b u p ‘ r1 public string Province { get; set; } public string City { get; set; } public string District { get; set; } public string Street { get; set; } 基于 Contact 类型的 Model 元数据不仅仅具有 Contact 类型本身和其属性成员的描述, 由于其 Address 属性依然是一个复杂类型,元数据还需要描述定义在该类型中的四个属性成 员。图 4-1 给出了基于 Contact 类型的 Model 元数据的层次化结构。 ProvinceM etadata Cityfdetadata 71 Districtldetadata StreetMetadata 图 4-1 基于 Contact 类型的 Model 元数据的层次化结构 ASP. NET MVC 4 框架揭秘4.1 Model 元数据及其定制 穰才 25 ASP.NET MVC 通过类型 System. Web.Mvc.ModelMetadata 来表示 Model 元数据, Model 元数据的层次化结构同样可以从它的定义看出来。如下面的代码片段所示, ModelMetadata 具有一个类型为 ModelMetadata 列表的只读属性 Properties ,表示用于描述属性的 Model 元 数据列表。 public class ModelMetadata //其他成员 public virtual IEnumerable Properties { get; } 由于基于类型的 ModelMetadata 和基于数据成员的 ModelMetadata 是一种包含关系,可 以将前者称为后者的容器( Container ) 0 ModelMetadata 的层次化结构可以通过图 4-2 所示的 u:t\在L 图来体现。 ModelMetadata 图 4-2 ModelMetadata 层次化结构 4.1.2 基本 Model 元数据信息 用于描述数据类型的 Model 元数据主要在 View 中为绑定的数据实现模板化的 HTML 呈现,所以很多元数据信息都与数据呈现有关。对于这些元数据信息及其如何控制 HTML ,我们会在后面进行单独介绍,在这里先来看看 ModelMetadata 类型中与此无关 的一些属性。 public class ModelMetadata //其他成员 14 0 o b 14 a u etle prop VTlov4 TvbT CCCC ·工 -1·1· 工 吨'4141414bbbb uuuu p&p ‘ D&p ‘ ModelType { get; } IsComplexType { get; } IsNullableValueType { get; } ContainerType { get; } tqJ cn eE 工 -1r bt os cc ·工·工1414 'pb uu pp -PJ }., ←」 ·'e +LαJ e sr1 ., e +」 m ea qJN Y Jtt r 1{e ep ,do or MP public virtual Dictionary AdditionalValues { get; } protected ModelMetadataProvider Provider { get; set; } 如上面的代码片段所示, ModelMetadata 具有四个与类型相关的只读属性。 ModelType ASP. NET MVC 4 框架揭秘126 • 第 4 章 Model 元数据的解析 表示 Model 本身的类型,比如说针对上面定义的 Contact 类型的 ModelMetadata 对象,其 Mode lType 属性值就是 Contact 类型。而针对其属性的 ModelMetadata 对象,则具体的属性 类型将作为它的 Mode lType 属性。属性 IsComplexType 和 IsNullable ValueType 分别表示以 ModelType 属性表示的 Model 类型是一个复杂类型和可空值类型。 Model 元数据具有树形的层次结构,某个节点可以看成其子节点的容器,而 ContainerType 则是表示容器的类型。同样以前面定义的 Contact 类型为例,基于该类型本身 的 Mode lM etadata 是整个层次树的根节点,所以 ContainerType 返回 Null 。基于属性 Address 的 ModelMetadata 的 ContainerType 属性返回 Contact 类型,而基于 Address 的属性 Province 的 Mode lM etadata 的 ContainerType 属性值则是 Address 类型。 Mode lM etadata 的 Model 属性表示具体的数据对象。针对类型 Contact 的 Mode lM etadata 的 Model 属性值为具体的某个 Contact 对象,而针对其中某个属性的 ModelMetadata 的 Model 属性则对应着相应的属性值。值得一提的是, Model 属性是可读可写的,可以随时根据需要 改变它。另一个属性 Prope町 Name 表示对应的属性值,对于根节点 ModelMetadata 来说,该 属性总是返回 Null 。 Mode lM etadata 的 AdditionalValues 属性返回一个字典对象,用于存储一些自定义的属性, 字典元素的 Key 和 Value 分别代表自定义属性的名称和值。对于自定义属性的添加,可以在 数据类型或者其数据成员上应用 System. Web.Mvc.AdditionalMetadat aA ttribute 特性来实现。 如下面的代码片段所示, AdditionalMetadat aA ttribute 具有 Name 和 Value 两个只读属性,它 们分别表示自定义属性的名称和对应的值,直接通过构造函数进行初始化。 Additiona lM etadat aA ttribute 实现了 System. Web.Mvc .I Metadat aAware 接口,对于 Model 元数 据的定制来说,这是一个非常重要并且实用的接口,将在下一节对其进行单独介绍。 [AttributeUsage(AttributeTargets.lnterface I AttributeTargets.Property I AttributeTargets.Class , AllowMultiple=true)] public sealed class AdditionalMetadataAttribute : Attribute , IMetadataAware public AdditionalMetadataAttribute(string narne , object value); public void OnMetadataCreated(ModelMetadata rnetadata); public string Narne { get; } public object Value { get;} Mode lM etadata 的属性 Provider 是一个 System. Web.Mvc.ModelMetadat aP rovider 对象,从 类型命名不难看出它是 Mode lM etadata 的提供者。我们将在本章后续部分对以 Mode lM etadat aP rovider 为核心的 Model 元数据提供机制进行详细介绍。 复杂类型还是简单类型? Mode lM etadata 的 IsComplexType 属性用于判断 Model 类型是简单类型还是复杂类型。 ASP. NET MVC 4 框架揭秘4.1 Model 元数据及真定制 • 127 在这里判断某个类型是否是复杂类型的条件只有一个,即是否允许字符串类型向该类型的转 换。所以所有的基元类型 C Primative Type) 和可空值类型 CNullable Type) 均不是复杂类型。 对于一个默认为复杂类型的自定义数据类型,可以在它上面应用 TypeConverter At位 ibute 特性 并指定一个支持字符串转换的 TypeConverter 类型,使之转变成简单类型。 如下面的代码片段所示,我们定义了一个表示二维坐标的 Point 类型,并通过应用在上 面的 TypeConverterA 忧 ribute 特性指定了一个类型为 PointTypeConverter 的 TypeConverter 。由 于 PointTypeConverter 支持从字符串到 Point 类型的转换,所以 Point 并不是一个复杂类型。 [TypeConverter( 乞.ypeof (PointTypeConverter)) ] public class point public double X { geti seti } public double Y { geti seti } public Point(double x , double y) this.X = Xi this.Y = yi public static Point Parse(string point) string[] split = point.Split(' , '); if(split.Length != 2) throw new FormatException("Invalid point expression.")i double Xi double yi if (! double. TryParse (spli t [0] , out x) I I !double.TryParse(split[l] , out y)) throw new FormatException("Invalid point expression.")i return new Point(x , y)i public class PointTypeConverter : TypeConverter public override bool CanConvertFrom(ITypeDescriptorContext context , Type sourceType) return sourceType == typeof(string)i public override object ConvertFrom(ITypeDescriptorContext context , Culturelnfo culture , object value) if (value is string) AS P. NET MVC 4 框架揭秘128 • 第 4 章 Model 元数据的解析 return Point.Parse(value as string); } return base.ConvertFrorn(context , culture , value)i 4.1.3 Model 元数据的定制 ASP.NETMVC 采用基于数据注解特性 (Data Annotation At位 ibute) 的声明式 Model 元数 据定义方法,我们将相应的数据注解特性应用在数据类型及其属性上对 Model 元数据进行定 制。这些用于声明式元数据定义的特性大都定义在 System. ComponentModel. Dat aAnn otations.dll 程序集中,程序集的名称同时也是对应的命名空间名称,接下来介绍一 些常用的数据注解特性以及它们对元数据的影响。 U I H i ntA ttribute HtmlHelper 和 HtmlHelper 定义了一系列模板方法,比如 Disp lay /D isplay F or 、 EditorÆditorF or 、 DisplayForMode lÆ di tF orModel 、 LablelL abe lF or 和 DisplayTextID isplayTextF or 。 所谓模板方法,就是说我们在通过调用这些方法将代表 Model 的数据呈现在 View 中的时候, 并不对最终呈现的 UI 元素进行显式地控制,而采用默认或者指定的模板来决定最终呈现在 浏览器中的 HTML 。 每个模板均具有相应的名称,这些模板方法在进行 Model 呈现的时候根据对应的 Model 元数据得到对应的模板名称。模板的名称通过 ModelMetadata 的 TemplateHint 属性表示,如 下面的代码片段所示,这是一个字符串类型的可读写属性。 public class ModelMetadata //其他成员 public virtual string TernplateHint{get;set;} Mode lM etadata 的 TemplateHint 属性可以通过 UIHintAttribute 特性来定制。如下面的代 码片段所示, UIHintAttribute 具有 Presentatio nL ayer 和 UIHint 两个只读属性,分别用于限制 展现层的类型(比如咀 T :r..缸"、" Silverlight" 、 "WPF" 、 "Wi nF orms" 和 "MVC" 等)和模 板名称,这两个属性均在构造函数中初始化。 [AttributeUsage(AttributeTargets.Field I AttributeTargets.Property , AllowMultiple=true)] public class UIHintAttribute : Attribute //其他成员 public UIHintAttribute(string uiHint); public UIHintAttribute(string uiHint , string presentationLayer); public string PresentationLayer { get; } public string UIHint { get; } ASP. NET MVC 4 在架揭秘4.1 Model 元数据及其定制 黯 129 通过应用在 UIHintAttribute 上的 AttributeUsageAttribute 特性定义不难看出, AllowMultiple 属性被设置为 True ,意味着我们可以在相同的目标元素上应用多个 UIHintAttribute 特性,那么哪一个会被选择用于定制 Model 元数据呢? 如果多个 UIHintAttribute 应用到了相同的元素(类型或者属性) ,会优先选择 Presentatio nL ayer 属性为 "Mvc" (不区分大小写)的 UIHintAttribute 。如果这样的 UIHintAttribute 不存在,则选择一个 Presentatio nL ayer 属性值为空的 UIHintAttribute 。值得一 提的是,如果具有多个匹配的 UIHintAttribute 可供选择,系统会选择第一个,但是通过反射 的方式获取的"第一个特性"并不一定是最先被声明的那个特性。 接下来我们创建一个简单的实例来演示 UIHintAttribute 特性对 Model 元数据的定制,在 本节后续部分我们同样用该实例程序测试其它数据注解特性对 Model 元数据的影响。在一个 ASP.NETMVC 应用中先创建了如下一个 ModelMetadatalnfo 类型,它的 ModelMetadata 属性 表示基于某个数据类型的 Model 元数据, Property Accessors 属性是一个表达式数组,每个表 达式用于根据指定的 Mode lM etadata 对象得到相应的属性值。 public class ModelMetadataInfo public ModelMetadata ModelMetadata { get; private set; } public Expression>[] PropertyAccessors { get; private set; } public ModelMetadataInfo(Type rnodelType , pararns Expression>[] propertyAccessors) this.ModelMetadata = newModelMetadata(ModelMetadataProviders.Curre 口 t , null , null , rnodelType , null); this.PropertyAccessors = propertyAccessors; 然后定义了如下一个 HomeController 。默认的 Action 方法的 Index 中我们会根据自定义 的数据类型 DemoModel 创建了一个 ModelMetadatalnfo 对象,其 Prope 向rAccessors 属性包含 一个根据 ModelMetadata 对象得到其 TemplateHint 属性的表达式,最终将创建的 Mode lM etadat aI nfo 对象作为 Model 显示在默认的 View 中。 public class HorneController : Controller public ActionResult Index() ModelMetadataInfo rnetadataInfo = newModelMetadataInfo(typeof(DernoModel) , rnetadata => rnetadata.TernplateHint); return View(rnetadataInfo); 14 e d O M o m e D s s a --C C .工14 b u PJL AS P. NET MVC 4 框架揭秘130 跚 第 4 章 Model 元数据的解析 public string Foo { get; set; } [UIHint("Ternplate A")] [UIHint("Ternplate B" , "Mvc")] public string Bar { get; set; } [UIHint ("Ternplate A")] [UIHint ("Ternplate B")] public string Baz { get; set; } 如上面的代码片段所示, DemoModel 具有 Foo 、 B 缸和 Baz 三个属性,后两个属性上应 用了两个 UIHin tAttribute 特性分别将模板名称设置为 "Template A" 和 "Template B" ,应用 在属性 B 盯上的一个 UIHintAttribute 特性对 Presentatio nL ayer 属性进行了设置 ("Mvc") 。 然后我们为 Action 方法 Index 定义相应的 View 。从如下的代码片段中(省略掉 css 样 式设置的代码)可以看出,这是一个基于 ModelMetadatalnfo 的强类型 View 。在该 View 中, 用于描述属性 Model 元数据的 Mode lM etadata 的指定属性值通过表格的形式呈现出来。 @using Systern.Linq.Expressions @using System.Reflection @rnodel ModelMetadatalnfo Model 元数据 @foreach (Expression> accessor in Model.PropertyAccessors) MernberExpression rnernberExpression = accessor.Body as MernberExpression; if (null == rnernberExpression) { UnaryExpression convertExpression = accessor.Body as UnaryExpressio 口; if (null != co 口 vertExpression) { rnernberExpression = (MernberExpression)convertExpression.Operand; Propertylnfo propertylnfo = (Propertylnfo)rnernberExpression.Mernber; @foreach (ModelMetadata metadata in Model.ModelMetadata.Properties) @foreach (Express 工 on> accessor in Model.PropertyAccessors) ASP. NET MVC 4 在架缰秘4 . 1 Model 元数据及真定制 1:. 131
Property
@propertylnfo.Narne
@rnetadata.PropertyNarne@ (accessor. Cornpile () (rnetadata) ?? "N/A")
该程序运行之后会在浏览器中呈现出如图 4-3 所示的输出结果,可以清楚地看到对于同 时应用了两个 UIHintAttribute 特性,并对模板名称进行了不同设置的属性 B 缸和 B 缸, Presentatio nL ayer 被设置为 "Mvc" 的 illHintAttribute 特性具有更高的优先级。如果多个 UIHintAttribute 特性具有相同的优先级,那么会选择获取的第一个 UIHintAttribute 特性。 (S401) 幢莹晋 7290 曹 N/A Template B Template A 图 4-3 针对多个 UIHin tAtlribute 特性的选择 Hiddenlnpu tA ttribute 与 ScaffoldColumnAttribute 一个作为 Model 的数据类型有时候具有一个唯一标识 (ID) ,当我们以编辑模式将 Model 对象在 View 中呈现的时候,往往不允许对作为唯一标识的属性进行修改。如果 D 不具有可 读性(比如是-个随机数或者 GillD) ,甚至不希望让它显示在界面上,这个时候就会使用 到特性 Hidde nI nputAttribute 。 Hidde nI nputAttribute 并没有定义在 System.ComponentM ode l. DataAnn otations 命名空间 下,它的命名空间为 System. Web.Mvc ,所以该特性是专门为 ASP.NET MVC 设计的。顾名 思义, Hidde nInpu tA ttribute 会将目标对象以类型为 hidden 的 7G素呈现出来。在默认 的情况下,应用了 Hidde nI npu tA ttribute 特性的目标对象依然会以只读的形式显示出来。如果 不希望显示,可以将如下所示的布尔类型的 DisplayValue 设置为 False (默认值为 True) 。 [AttributeUsage(AttributeTargets.Property I AttributeTargets.Class , AllowMultiple=false , Inherited=true)] public sealed class HiddenInputAttribute : Attribute public HiddenInputAttribute(); public bool DisplayValue { get;set; } 同样以前面定义的 DemoModel 类型为例,通过如下的方式将 Hidde nI npu tAttribute 特性 应用在属性 Foo 和 B 缸上,后者将 DisplayValue 属性设置为 False 。 ASP. NET MVC 4 握架揭秘132 • 第 4 章 Model 元数据的解析 14 e d O M o m e D S s a 14 C C .工l b u p ‘ f1 [Hiddenlnput] public string Foo { geti seti } [Hiddenlnput(DisplayValue = false)] public string Bar { geti seti } public string Baz { geti seti } 在一个基于 DemoModel 类型的强类型 View 中,通过如下的代码调用 Htr恤恤n世lH如蚓elp严erκ> 的扩展方法 Ed副itωorFo町r 将 Demo仙岛M岛odel 对象三个属性 (ωFo∞0="叩Fo∞0" , Bar ="咀B 盯", Ba缸z="咀B 但az" 以编辑模式呈现出来。 @model DemoModel @Html.EditorFor(m=>m.Foo) @Html.EditorFor(m=>m.Bar) @Html.EditorFor(m=>m.Baz) 如下所示的是最终生成的 HTML ,可以看出应用了 HiddenlnputAttribute 特性的两个属 性 Foo 和 B 缸均以类型为 "hidden" 的 JG 素进行呈现,不过 Foo 属性值会以文本的形 式显示出来,但 B 町的属性值则不会。 CS402) Foo Hidde nI npu tA ttribute 针对 Model 元数据的定制体现 ModelMetadata 的如下两个属性上, 其中一个就是上面介绍的 TemplateHint ,另一个则是布尔类型的属性 HideSurroundingHtml , 它表示目标元素是否需要通过相应的 HTML 呈现在 UI 界面上。具体来说, Hidde nI nputA伽 ibute 特性 Mode lM etadata 对象的 TemplateHint 属性设置为 "Hidde nI nput" , 其 HideSurroundingHtrnl 属性则对应着 HiddenI npu tA ttribute 的 DisplayValue 属性。 public class ModelMetadata //其他成员 qd n ·工 14 ro fwO S-b 1414 aa uu ←」← L rr -1·l vv cc ·工 -L 1-14 bb uu pp TemplateHiηt{getiseti} HideSurroundingHtml { geti seti } 针对上面定应在 DemoModel 中的三个属性,我们通过现有的测试程序来检测一下对应 Mode lM etadata 的 TemplateHint 和 HideSurroundin gH trnl 属性因应用了 HiddenlnputAttr协 ute 特性而有何不同,只需要将定义在 HomeController 的 Index 方法作如下的修改即可。 public class HomeController : Controller public ActionResult Index() ModelMetadataInfo metadataInfo = newModelMetadataInfo(typeof(DemoModel) , metadata => metadata.TemplateHint , ASP. NET MVC 4 框架揭秘4.1 Model 无数据及奠定制 也 133 metadata => metadata.HideSurroundingHtml) i return View(metadatalnfo)i 运行该程序会在浏览器中呈现出如图 4-4 所示的输出结果,可以看到对于应用了 Hidde nI npu tA ttribute 的两个属性 Foo 和 B 缸,对应 Mode lM etadata 的 TemplateHint 属性均为 "HiddenIn put" ,而将 DisplayValue 属性设置为 False 的属性 B 缸,其 HideSurroundingHtml 属性变成了 True 0 ( S403 ) 民尘町叩9' 证3 ………·程 ZJI写E圈 。 。 ωhos t: 'ì 290 合 E 望o HiddenInput HiddenInput N/A 图 4 -4 Hiddenlnpu tAttribute 对 Model 元数据的控制 有的读者可能会问这样一个问题, UIHin tA t位 ibute 和 Hidde nI npu tA ttribute 都会设置表示 Model 元数据的 ModelMetadata 对象的 TemplateHint 属性,如果两个特性均应用到相同的目 标元素上,最终生成的 Mode lM etadata 对象具有怎样的 TemplateHint 属性值呢?答案是: UIHintA ttribute 具有更高的优先级。 对于应用了 Hidde nI nputAt位ibute 特性的目标元素,不论其 DisplayValue 具有怎样的值, 都会出现在通过模板方法生成的 HTML 中。如果我们希望将它从 HTML 中移除,可以应用 另一个叫作 ScaffoldCol umnA ttribute 的特性。将通过预定义模板自动生成 H Th伍的方式称为 "基架 (Scaffolding)" , ScaffoldColumnAttribute 中的 ScaffoldColumn 代表存在于"基架" 中并最终呈现在 HT l\伍中的字典,而该特性本身则用于控制目标元素是否应该存在于基架 之中。 如下面的代码片段所示, ScaffoldColu mnA ttribute 具有一个布尔类型的只读属性 Scaffold 表示目标元素是否应该存在于呈现在最终生成的 HTML 的基架中,该属性在构造函数中初 始化。 [AttributeUsage(AttributeTargets.Field I AttributeTargets.Property , AllowMultiple=false)] public class ScaffoldColumnAttribute : Attribute public ScaffoldColumnAttribute(bool scaffold)i public bool Scaffold { geti } ScaffoldColu mnA ttribute 最终用于控制 ModelMetadata 的 ShowF orDisplay 和 ShowForEdit 属性。如下面的代码所示,这是两个布尔类型的属性,分别表示目标元素是否应该出现在显 ASP. NET MVC 4 在架揭秘134 酸 第 4 章 Model 元数据的解析 示和编辑模式下的"基架"中。如果 ShowForDisplay 的属性为 False ,在调用模板方法 EditorFor旭ditorForModel 方法时目标元素将不会出现在最终生成的 HTML 中。通过 DisplayFor/DisplayForModel 方法生成的 HTi\在L 将不会包含 ShowForDisplay 属性为 False 的 元素。这两个属性值在默认情况下均为 True 。 public class ModelMetadata //其他成员 public virtual bool ShowForDisplay { get; set; } public virtual bool ShowForEdit { get; set; } Data TypeAttribute 与 DisplayFormatAttribute 用于指定数据类型的 DataTypeAttribute 特性是经常使用的数据标注特性。这里所说的数据 类型不是我们所理解的 CLR 类型,而是通过 System.ComponentMode l. DataAnnotations.DataType 枚举表示的具有某种显示格式的数据类型。如下面的代码片段所示, DataType 枚举定义了一 系列包括时间、日期、电话号码、货币、 Html 、电子邮箱地址在内的数据类型。 e p y 巾 4 a 卡」a hu m u n e c .工14 b u p{ Custorn, DateT 工 rne , Date, Tirne, Durat 工 0 口, PhoneNurnber, Currency, Text, Htrnl, MultilineText, ErnailAddress, Password, Url, IrnageUrl, CreditCard, PostalCode, Upload 为 Model 元数据设置数据类型的 DataTypeAttribute 实际上是一个验证特性。如下面的代 码片段所示, DataTypeAttribute 直接继承自 ValidationAttribute 。关于验证和验证特性,我们 会在第 6 章 "Model 的验证"中进行单独介绍。除了具有一个 DataType 枚举类型的 DataType 只读属性之外, DataTypeAt位ibute 还具有一个字符串类型的表示自定义数据类型的 CustomDataType 属性,它们均在相应的构造函数中初始化,方法 GetDataTypeName 返回一 个代表数据类型名称的字符串。 [AttributeUsage(AttributeTargets.Pararneter I AttributeTargets.Field I AttributeTargets.Property I AttributeTargets.Method, AllowMultiple=false)] public class DataTypeAttribute : ValidationAttribute ASP. NET MVC 4 在架揭秘4.1 Model 元数据及真定制 混 135 public DataTypeAttribute(DataType dataType)i public DataTypeAttribute(string customDataType)i public virtual string public override bool public string public DataType public DisplayFormatAttribute 、 KFB , .,} etJ uet J14αJIJe )aqJ I 、 VJt eJfk mtet acp ‘et Neyαda e--Tm pbaItr vdo ←」 O Tf 、 aeF adDPy t--mv4a a14OT-­Datap tvsts esua· 工 GICDD DataTypeA忧ribute 的只读属性 DisplayFormat 涉及另一个用于进行格式化的 Display F ormatAttribute 特性,它可以指定一个格式化字符串以控制数据在 UI 界面上的显示 格式。如下面的代码片段所示,格式化字符串通过属性 DataF ormatString 表示,布尔类型的 属性 ApplyF ormatlnEditMode 表示格式化规则是否需要应用到编辑模式,而 Htmffincode 属 性表示是否需要对目标内容实施 HTML 编码,默认情况下这两个属性值分别为 False 和 True 。 Display F ormatAttribute 的属性 NullDisplayText 和 ConvertEmptyStringToNull 与空值/空字符串 的处理有关,前者表示针对空值 CNull) 对象的显示文本,后者表示是否将传入的空字符串 转换成 Null 。 [AttributeUsage(AttributeTargets.Field I AttributeTargets.Property, AllowMultiple=false)] public class DisplayFormatAttribute : Attribute public DisplayFormatAttribute()i qdqJ nn il--114 rooro tooto sbbsb ccccc ·工·工 -1·1· 工 1114141 bbbbb uuuuu ppppp DataFormatString { geti seti } ApplyFormatlnEditMode { geti seti } HtmlEncode { geti seti } NullDisplayText { geti seti } ConvertEmptyStringToNull { geti seti } 定义在 DataType 枚举中的部分数据类型(比如 Date 、 Time 和 Currency 等)都具有各自 的格式(它们的格式化字符串分别是" {O:d}" 、 "{O:t} "和" {O:C} 勺。当 DataTypeAttribute 通过指定的 DataType 枚举值被创建的时候,会根据对应的格式创建一个 Display F ormatAttribute 对象作为其 DisplayFormat 属性值。顺便提一下,针对数据类型 Data 、 Time 和 Currency 的 DataTypeAttribute 对应的 DisplayFormatAttribute .前两个的 Apply F ormatlnEditMode 属性为 True. 最后一个的 ApplyFormatlnEditMode 属性为 False 。这 也很好理解,针对日期和时间的编辑具有对应的格式。对于货币来说,编辑的时候一般就是 一个无格式的数字。 DataTypeAttribute 和 DisplayFormatAttribute 对 Model 元数据的定制涉及 ModelMetadata 的如下属性。其中 DataTypeAttribute 中设置的数据类型对应于 ModelMetadata 的 DataTypeName 属性,而 DisplayF ormatAttribute 的 ConvertEmp可 S位ingToNull 和 NullDisplayText 属性对应着 ModelMetadata 的同名属性。 ASP. NET MVC 4 在架揭秘136 翻 第 4 章 Model 元数据的解析 public class ModelMetadata //其他成员 public virtual string public virtual bool public virtual string public virtual string public virtual string DataTypeNarne { geti seti } Co 口 vertEmptyStringToNull { get; seti } NullDisplayText { geti seti } DisplayForrnatString { geti set; } EditForrnatString { geti seti } 通过 Display F ormatAttribute 的 Dat aF ormatString 属性设置的格式化字符串会赋值给 ModelMetadata 的 DisplayF ormatString 属性,该属性表示显示模式下的格式化字符串。如果 ApplyF ormatl nE ditMode 属性为 True ,该属性会赋值给 Mode lM etadata 的 Edi tF ormatString 属 性,表示编辑模式下的格式化字符串。 ModelMetadata 表示数据类型名称的 DataTypeName 属性类型为字符串,如果 DataTypeAttribute 特性的 DataType 属性为 Custom ,那么以字符串设置的自定义数据类型将 会作为 Mode lM etadata 的 DataTypeName 属性,否则直接将设置的 DataType 枚举对象转换为 字符串(直接调用 ToS 位ing 方法)并直接作为 DataTypeName 属性值。 DataTypeAttribute 并不是一个封闭的 C Sealed) 类型,可以通过继承它创建自定义的 DataTypeAttribute 。这种情况下 Mode lM etadata 的 DataType 属性是通过调用 DataTypeA 时 ibute 的虚方法 CJetDataTypeName 获取的。如果我们对 Model 元数据的数据类型名称具有特殊的 定制方式,则需要重写这个方法。 如果通过 DataTypeAttribute 特性以字符串的方式指定一个自定义数据类型,该字符串将 直接作为 ModelMetadata 的 DataTypeName 属性值。如果没有显式地对数据类型进行设置, 并且 DisplayF ormatA ttribute 的 Htmffincode 属性为 False C 不需要对目标内容进行 HTML 编 码),生成的 ModelMetadata 对象的 DataTypeName 属性值则为 HtmlC 相当于 DataType.Html) 。 由于有的 DataTypeAttribute 对应着一个 DisplayF ormatAttribute ,如果它们同时应用在了 相同的目标元素上,在它们的设置互相冲突的情况下后者 C Display F ormatA ttribute) 将具有 更高的优先级。 照例利用前面创建的测试程序来检测一下 DataTypeAttribute 和 DisplayFormatA ttribute 对目标元素的 Model 元数据的影响。为此将 DemoModel 进行了如下的改写:属性 Foo 和 B 缸 分别应用了 DataTypeAttribute 特性将数据类型设置为预定义的 Emai lA ddress C 通过 DataType 枚举)和自定义的 "B 町 code" C 通过字符串),属性 Qux 上则应用了 Display F ormatAttribute 特性将 Htmffincode 属性设置为 False 。 『ie d O M o m e nu s s a --C C .工14 .b u p{ [DataType(DataType.ErnailAddress)] public string Foo{ get; set; } [DataType("Barcode")] public string Bar { geti set; } ASP. NET MVC 4 框架揭秘4.1 Model 元数据及其定制 毡 137 public string Baz { get; set; } [DisplayForrnat(HtrnlEncode = false)] public string Qux { get; set; } 只需要对定义在 HomeCon位 oller 的 Index 方法通过如下的代码让用于描述所有属性的 Mode lM etadata 的 DataTypeName 属性呈现出来。 public class HorneController : Controller public ActionResult Index() ModelMetadatalnfo metadatalnfo = newModelMetadatalnfo(typeof(DernoModel) , metadata => metadata.DataTypeName); return View(rnetadatalnfo); 运行该程序后会在浏览器中呈现出如图 4-5 所示的输出结果,上面介绍的 Model 元数据 的数据类型名称的设置规则在这里得到了很好地体现。 CS404) 图 4-5 DataTypeAttribute/DisplayForma tA ttribute 对 Model 元数据数据的定制 EditableAttribute 与 ReadOnlyAttribute EditableAttribute 和 ReadonlyAt位 ibute 用于控制目标元素的可读写性。如下面的代码片段 所示, EditableAt位 ibute 和 Readonly Attribute 分别具有一个布尔类型的属性 AllowEdit 和 IsReadOnly 分别表示是否允许编辑和是否只读。 [AttributeUsage(AttributeTargets.Field I AttributeTargets.Property , AllowMultiple=false , Inherited=true)] public sealed class EditableAttribute : Attribute //其他成员 public EditableAttribute(bool allowEdit); public bool AllowEdit { get; private set; } [AttributeUsage(AttributeTargets.All)] public sealed class ReadOnlyAttribute : Attribute ASP. NET MVC 4 框架揭秘138 建总 第 4 章 Model 元数据的解析 //其他成员 public ReadOnlyAttribute(bool isReadOnly); public bool IsReadOnly { get; } 不允许编辑即为只读,所以这两个标注特性具有相同的作用。它们共同控制着 Mode lM etadata 的 IsReadOnly 属性。如果同时将 EditableAttribute 和 ReadonlyAt位 ibute 应用 到相同的目标元素上并且作出相反的设置(让 EditableAttribute 的 AllowEdit 属性和 Readonly Attribute 的 IsReadOnly 属性具有相同的布尔值) , EditableAttribute 特性具有更高的 优先级。 public class ModelMetadata //其他成员 public virtual bool IsReadOnly{get; set;} 通过如下方式将特性 EditableAttribute 和 Readonly Attribute 同时应用到类型 DemoModel 的 B 缸和 B 但属性上,并在读写性上作出相反的设置,而在属性 Foo 上应用了 ReadonlyAttribute 特性并将其设为只读。 14 e d O M o m e nu s s a 14 C C .工14 b u p{ [ReadOnly(true) > public string Foo { get; set; } [Editable(true)] [ReadOnly(true)] public string Bar { get; set; } [Editable(false)] [ReadOnly(false)] public string Baz { get; set; } 然后对定义在 HomeController 中的 Index 方法作了如下的改动,使 DemoModel 的三个 属性对应 Mode lM etadata 的 IsReadOnly 属性显示出来。 public class HomeController : Controller public ActionResult Index() ModelMetadataInfo metadataInfo = newModelMetadataInfo(typeof(DemoModel) , metadata => metadata.IsReadOnly); return View(metadataInfo); 上面的程序运行之后会在浏览器上呈现出如图 4-6 所示的输出结果。由于 Foo 属性上仅 仅应用了 Readonly Attribute 特性,所以它控制了 Mode lM etadata 的 IsReadOnly 属性。而 Bar 和 Baz 属性则同时应用 EditableAttribute 和 ReadonlyAttribute 两个特性, Mode lM etadata 的 IsReadOnly 属性最终通过 EditableAttribute 特性来控制。 (S405) AS P. NET MVC 4 框架揭秘4.1 Model 元数据及真定制 l. 139 盟军localhost: i 290 曹 ME-M 图 4-6 EditableAttribute/ReadonlyAttribute 对 Model 元数据的定制 DisplayAttribute 与 DisplayNameAttribute Display Attribute 特性为目标元素定义一些说明性文字。如下面的代码片段所示, Display Attribute 具有 5 个基本属性,其中 Name 和 ShortName 是为目标元素设置一个显示名 称和一个简短的显示名称。属性 Description 和 Order 为目标元素设置描述性文字和用于排序 的权重。字符串类型的 Prompt 属性为目标元素设置一个字符串,它在 UI 界面上以水印的方 式呈现。 [AttributeUsage(AttributeTargets.Pararneter I AttributeTargets.Field I AttributeTargets.Property I AttributeTargets.Method, AllowMultiple=false)] public sealed class DisplayAttribute : Attribute //其他成员 public DisplayAttribute(); public string GetNarne(); public string GetShortNarne(); public string GetDescription(); public int? GetOrder(); public string GetPrornpt(); public string Narne { get; set; } public string ShortNarne { get; set; } public string Description { get; set; } public int Order { get; set; } public string Prornpt { get; set; } public Type ResourceType { get; set; } 由于 DisplayAttribute 特性设置的文字都是面向最终用户的,所以有必要对其进行本地 化( Localization ) ,为此该特性允许我们通过资源文件的方式来定义它们。 DisplayAttribute 特性的 ResourceType 代表采用的资源文件生成的类型。如果我们对该属性进行了显式设置, 上述 5 个属性值将会被认为是对应的资源条目的名称。正因为如此,如果我们需要得到最终 用于显示的文字,不能通过相应的属性而需要通过相应的 GetXxx方法来获取。 另一个定义在命名空间 System.ComponentModel 下的 DisplayNameAttribute 特性则专门 用于设置目标元素的显示名称。如下面的代码片段所示,目标元素的显示名称通过只读属性 ASP. NET MVC 4 框架揭秘140 • 第 4 章 Model 元数据的解析 DisplayName 表示,该属性在构造函数中被初始化。如果调用默认的构造函数,该属性会被 设置为空字符串。 [AttributeUsage(AttributeTargets.Event I AttributeTargets.Property I AttributeTargets.Method I Attr 工 buteTargets.Class)] public class DisplayNameAttribute : Attribute public DisplayNameAttribute()i public DisplayNameAttribute(string displayName)i public virtual string DisplayName { geti } Display Attribute 和 DisplayNameAttribute 特性对 Model 元数据的定制涉及五个属性。 Display Attribute 的 GetName 方法的返回值和 DisplayNameAttribute 的属性 DisplayName 对应 于 Mode lM etadata 的 DisplayName 属性。 Display Attribute 的 GetShortName 方法则获取 Mode lM etadata 的 Sho rtD isplayName 属性。而 Ge tD escription 和 GetOrder 方法返回 Mode lM etadata 的 Description 和 Order 属性(默认值为 10000) 0 ModelMetadata 的 Watermark 属性通过 Display Attribute 的 Ge tP romp 方法的返回值来初始化。 public class ModelMetadata //其他成员 public virtual string DisplayName { geti seti } public virtual stri 口 g ShortDisplayName { geti seti } public virtual string Description { geti seti } public virtual int Order { geti seti } public virtual string Watermark { geti seti } 由于 Display Attribute 的 GetN ame 方法的返回值和 DisplayNameAttribute 的 DisplayName 属性最终都用于设置 Mode lM etadata 的 DisplayName 属性,如果这两个属性同时应用到相同 的目标元素上并且对显示名称作出了不同的设置,那么 Display Attribute 特性具有更高的优 先级。 如下面的代码片段所示,我们将 Display Attribute 和 DisplayN ameAttribute 特性应用到了 定义在测试程序中的数据类型 DemoModel 的相应的属性上。其中属性 B 盯上应用了 DisplayN ameAttribute 并将显示名称设置为 "B 缸",而属性 B 但上同时应用了 Display Attribute 和 DisplayNameAttribute 特性并分别将显示名称设置为 "BAZ" 和 "baz" 。应用在属性 B 盯 上的 DisplayAttribute 还对其他相关属性作了相应的设置。 public class DemoModel public string Foo { geti seti } [DisplayName("Bar")] public string Bar { get; set; } [Display(Name = "BAZ" , Description ="Desc" , ShortName="B" , Prompt="Watermark..." , Order=999)] AS P. NET MVC 4 在架揭秘4.1 Model 元数据及真定制 组 141 [DisplayName ("baz")] public string Baz { get; set; } 我们对定义在 HomeController 的 Index 方法作了如下的改动,使基于属性的 ModelMetadata 的与 DisplayAttributelDisplayN ameAttribute 特性相关的属性显示出来。 public class HomeController : Controller public ActionResult Index() ModelMetadatalnfo metadatalnfo = newModelMetadatalnfo(typeof(DemoModel) , metadata => metadata.DisplayName, metadata => metadata.Description, metadata => metadata.ShortoisplayName, metadata => metadata.Water.mark, metadata => metadata.Order); return View(metadataInfo); 上面的程序运行之后会在浏览器上呈现出如图 4-7 所示的输出结果。可以看到对于同时 应用了 DisplayAttribute 和 DisplayNameAttribute 特性的 B但属性,对应 ModelMetadata 的 DisplayName 属性与 DisplayAttribute 是一致的。对于没有通过 DisplayAttribute 特性对 Order 进行设置的属性 Foo 和 B缸,该属性默认为 100000 (S406) 噩噩曹=…曹 图 4-7 DisplayAttribute/DisplayNameAttribute 对 Model 元数据的定制 RequiredAttribute 我们来介绍最终一个标注特性 RequiredAttribute 。顾名思义, RequiredAttribute 特性将目 标元素设置为必需的数据成员。如下面的代码片段所示, RequiredAttribute 和 DataTypeAttribute 是一个验证特性。其 AllowEmptyStrings 属性表示作为必需数据成员的目 标元素是否接受一个空字符串,默认情况下是不允许的。· [AttributeUsage(AttributeTargets.Parameter I AttributeTargets.Field I AttributeTargets.Property, AllowMultiple=false)] public class RequiredAttribute : ValidationAttribute public RequiredAttribute(); public bool AllowEmptyStrings { get; set; } ASP. NET MVC 4 框架揭秘142 海 第 4 章 Model 元数据的解析 对于应用了 RequiredAttribute 特性的数据成员,对应 ModelMetadata 的 IsRequired 属性 将会被设置为 True 。如下面的代码片段所示,该属性是一个可读写的属性。 public class ModelMetadata //其他成员 public virtual bool IsRequired{ get; set; } 4.1.4 IMetadataAware ~妾口 在介绍用于设置 Model 元数据自定义属性的 AdditionalMetadataAttribute 特性时,我们 提到了它实现的接口 IMetadataAware ,这是一个非常重要并且有用的接口,通过自定义实现 该接口的特性可以对最终生成的 Model 元数据进行自由地定制。如下面的代码片段所示, IMetadataAware 接口具有唯一的方法成员 OnMetadataCreated ,针对作为参数的 ModelMetadata 对象的定制就体现在该方法中。 public interface IMetadataAware void OnMetadataCreated(ModelMetadata metadata); 当 Model 元数据被创建出来后,上述的这一系列数据注解特性会被提取出来对其进行 初始化,然后获取应用在目标元素上所有实现了 IMetadataAware 接口的特性,并将初始化 的 ModelMetadata 对象作为参数调用 OnMetadataCreated 方法。通过自定义实现该接口的 特性不仅仅可以添加一些额外的元数据属性,还可以修改己经通过相应的标注特性初始化 的相关属性。 ASP.NETMVC 定义了两个实现了 IMetadataAware 接口的特性,一个就是已经介绍过的 AdditionalMetadataAttribute ,另一个则是 System. Web.Mvc.AllowHtmlAttribute 。 AllowHtmlA忧 ribute 出于安全考虑, ASP.NET MVC 在进行 Model 绑定过程中会对请求数据进行验证以确保 没有任何 HTML 标记被包含其中。 ModelMetadata 的 RequestValidationEnabled 属性开启/关 闭请求验证的开关。该属性在默认情况下为 True ,意味着默认开启针对 HTI\在L 标记的请求 验证。 public class ModelMetadata //其他成员 public virtual bool RequestValidationEnabled { get; set; } 如果在数据类型或者属性上应用了 AllowHtmlAttribute 特性,意味着允许绑定到目标元 ASP.NET MVC 4 在架揭秘4.1 Model 元数据及其定制 黯 143 素的原始内容包含 HTML 标记,换言之需要忽略针对请求的验证。如下面的代码片段所示, AllowHtm lA ttribute 是实现了 IMetadat aAware 接口,在 O nM etadataCreated 方法中它直接将 作为参数的 ModelMetadata 对象的 RequestValidatio nE nabled 属性设置为 False ,从而使针对 目标对象的请求验证被忽略掉。 [AttributeUsage(AttributeTargets.Property , AllowMultiple=false , Inherited=true)] public sealed class AllowHtmlAttribute : Attribute ,工 MetadataAware public void OnMetadataCreated(ModelMetadata metadata) //其他操作 metadata.RequestValidationEnabled = false; 为了验证 ASP.NETMVC 针对 HTML 标记的请求验证和 Allow HtmlAttribute 的作用,我 们来做一个简单的实例演示。在一个 ASP.NET MVC 应用中定义了如下一个数据类型 DemoModel , DemoModel 定义了两个字符串类型的属性 Foo 和 B 町,后者应用了 AllowHtmlAttribute 特性。 14 e d O M o m e D s s a --4 C C .l l b u p{ public string Foo { get; set; } [AllowHtml] public string Bar { get; set; } 然后创建如下一个默认的 HomeController ,默认的 Action 方法 Index 中具有一个类型为 DemoModel 的参数,该参数直接作为 Model 呈现在默认的 View 中。 public class HomeController : Controller public ActionResult Index(DemoModel model) return View(model); 如下所示的是 Action 方法 Index 所对应 View 的定义,这是一个以 Foo 为 Model 的强类 型 View 。在该 View 中,直接调用 HtmIHelper 的 EditorF orModel 方法将 Foo 对象以 编辑模式呈现出来。 @model DemoModel AllowHtml @Html.EditorForModel() ASP. NET MVC 4 框架揭秘144 阳 第 4 章 Model 元数据的解析 现在直接运行该 Web 应用。根据 Model 绑定的规则我们知道,如果通过浏览器访问 HomeCon位oller 的 lndex 操作,可以分别指定名称为 Foo 和 B缸的查询字符串对作为参数的 DemoModel 对象的两个属性进行初始化。为了验证对包含 HTh在L 标记的输入的验证,将最 终绑定到 Model 上的查询字符串设置为" "。 如图 4-8 所示,由于属性 B町上应用了 AllowHtmlAttribute 特性使之支持包含 HTML 标 记的数据,所以以查询字符串方式指定的包含 HTML 标记的内容 ("")直 接显示在相应的文本框中。但是 B缸属性在默认情况下不允许绑定的数据具有任何 HTML 标记,所以会将输入的数据视为恶意注入的 HTML ,直接抛出异常。 (S407) ~'芝~p! > 图 4-8 针对 HTML 标记请求的验证与 AllowHtmlAttribute 作用 实例演示:创建实现 I MetadataAware 接口的特性定制 Model 元数据 (8408 ) 我们知道,数据项显示名称可以通过在数据类型或者属性成员上应用 DisplayAttribute 特性来定义,在使用该特性的时候,需要显式指定表示显示名称的 Name 属性。如果需要进 行本地化处理,需要将显示内容定义在某个资源文件中,并通过 Resource乃pe 属性指定该 资源文件生成的类型。 为了简化,通过实现 IMetadataAware 接口的方式定义了如下一个 DisplayTextAttribute 特性。该特性的属性 DisplayNamelResourceType 与 DisplayAttribute 的 NamelResourceType 具有相同的作用,唯一不同的是 DisplayTextAttribute 的这两个属性均是可以缺省的。如果 DisplayName 没有显式指定,则默认使用属性名称或者类型名称。如果 ResourceType 没有显 式指定,则采用通过静态字段 staticResourceType 表示的默认资源类型,该类型通过静态方 法 SetResourceType 进行注册。 [AttributeUsage(AttributeTargets.ClassI AttributeTargets.Property)] public class DisplayTextAttribute: Attribute, IMetadataAware private static Type staticResourceType; public string DisplayName { get; set; } public Type ResourceType { get; set; } ASP. NET MVC 4 框架揭秘4.1 Model 元数据及其定制 穗 145 public DisplayTextAttribute() this.ResourceType = staticResourceTypei public void OnMetadataCreated(ModelMetadata metadata) this.DisplayName = this.DisplayName ?? (metadata.PropertyName ?? metadata.ModelType.Name)i if (null == this.ResourceType) metadata.DisplayName = this.DisplayNamei returni PropertyInfo property = this.ResourceType.GetProperty(this.DisplayName , BindingFlags.NonPublicI BindingFlags.PublicI BindingFlags.Static)i metadata.DisplayName = property.GetValue(null , null) .ToString()i public static void SetResourceType(Type resourceType) staticResourceType = resourceTypei DisplayTextAttribute 对 Model 元数据的定制实现在 O nM etadataCreated 方法中。该方法 根据设置的 DisplayName 和 ResourceType 属性解析出最终作为显示名称的文本,并作为 Mode lM etadata 的 DisplayName 属性值。 接下来演示如何使用这个 DisplayTextA t位 ibute 特性来替换 Display Attribute 特性进行显 示名称的设置。我们在一个 ASP.NETMVC 应用中定义如下一个表示员工的 Employee 类型。 Employee 所有的属性上均应用了 DisplayTextAttribute 特性,而 DisplayName 和 RerourceType 属性没有显式指定。 e e vd o --4 p m pu s s a 14 C C .工1-b u PJL [DisplayText] public string Name { geti seti } [DisplayText] public string Gender { geti seti } [DisplayText] [DataType(DataType.Date)] public DateTime BirthDate { geti seti } [DisplayText] public string Department { geti seti } 接下来打开项目的属性对话框并选择"资源 (Resources)" Tab 页,按照如图 4-9 所示为 Employee 中的四个属性定义相应的资源字符串作为显示的名称,资源字符串条目的名称为 属性名。 ASP. NET MVC 4 握架揭翻146 留 第 4 章 Model 元数据的解析 坦国 Strin9~' .J Add ßesource .,. 坏 Re血。 vεR 国 ource 自 牛 j ..1 . Mv cA pp Application | 龟帽 一叫时 Build Web Packag e/ PubJish Web R 田 ources 亨 图 4-9 基于属性名称的资源字符串的定义 该资源文件会自动生成一个类型为 Resources 的内部类型。由于应用在 Employee 属性上 的 DisplayTextAttribute 特性并没有显式指定资源类型,所以需要在 Globa l. asax 文件中通过 如下的方式将 Resources 类型注册为默认的资源类型。 public class MvcApplication : System.Web.HttpApplication //其他成员 protected void Application Start() //其他操作 DisplayTextAttribute.SetResourceType(typeof(Resources)}; 现在通过调用 HtmlHelper 的 EditorF orModel 方法将一个具体的 Employee 对象 以编辑模式显示在某个 Model 类型为 Employee 的强类型 View 上,会呈现出如图 4-10 所示 的效果。可以看到,作为标签显示的文字正是我们定义在资源文件中的内容。 醒曹曹雪曹 姓名 性别 出生日期 二三i 部门 也到 图 4-10 Employee 对象在编辑模式下默认的呈:现效果 4.2 Model 元数据与 Model 模板 Model 元数据的一个主要的作用就是为定义在 HtmlHelper 中的模板方法(这 ASP. NET MVC 4 在架揭秘4.2 Model 元数据与 Model 模板 、 147 些模板方法包括 DisplaylD isplayFor 、 EditorÆditorF or 、 DisplayForModelÆditF orModel 、 LablelLabelF or 和 DisplayTextIDisplayTextFor 等)提供辅助生成 HTh伍的元数据信息。 在调用这些方法的时候,如果指定了一个具体的通过 Partial View 定义的模板,或者对应 的 ModelMetadata 的 TemplateHint 属性被设置了相应的模板名称,会自动采用该模板来生成最 终的 Hτ'ML。如果没有指定模板名称,则会根据数据类型在预定义的目录下去寻找作为模板 的 Partial View 。为了让读者对模板及其作用有一个大体的认识,我们来做一个简单的实例演示。 4.2.1 实例演示:通过模板将布尔值显示为 RadioButton ( S409 ) 在默认的情况下,不论是对于编辑模式还是显示模式,一个布尔类型的属性值总是以一 个 CheckB ox 的形式呈现出来。创建如下一个表示员工的类型 Employee. 他具有一个布尔类 型的属性 IsPartTime 表示该员工是否为兼职。 e e y 0 14 p m nb s s a 14 C C .-14 b u pi ‘ [DisplayName(" 姓名") ] public string Name { get; set; } [DisplayName(" 部门") ] public string Department { get; set; } [DisplayName(" 是否兼职"门 public bool IsPartTime { get; set; } 如果直接调用 Htm旧elper 的 EditorForModel 方法将一个 Employee 对象显示在 某个 Model 类型为 Employee 的强类型 View 中,最终会呈现出如图 4-11 的所示的效果,可 以看到,表示是否为兼职的 IsPartTime 属性最终以 CheckB ox 形式被呈现出来。 IrM. i f#i lffffl!脯 '而 4咱瞌 l IrR "'5!...._、 t 11I 均 ~ 到; 国E回Ihost:i醒圃E咽~、 l 姓名 l 张三 部门 町部 是否兼职 生 图 4-11 默认模板对布尔值的呈现效果 现在我们希望的是将所有布尔类型对象显示为两个 RadioButton. 具体的显示效果如 图 4-12 所示。可以通过创建一个 Model 类型为 Boolean 的 Partial View 来创建一个模板,使 之改变所有布尔类型对象的默认呈现效果。 ASP. NET MVC 4 框架揭秘148 也 第 4 章 Model 元数据的解析 姓名 部门 IT 是否兼职 ⑨ 是 。 否 (豆E 图 4-12 自定义模板对布尔值的呈现效果 由于我们需要改变的是布尔类型对象在编辑模式下的呈现形式,所以需要将作为模板的 View 定义在 EditorTemplates 目录下。这个目录可以存在于 "Views/Sh 缸 ed" 下,也可以存在 于句iews/ {ControllerN ame } "下。由于 ASP. NETMVC 是采用数据类型作为匹配条件来寻找 对应的模板的,所以我们需要将模板 View 命名为 Boolean 。下面的代码片段体现了整个 Partial View 的定义,通过调用 Htm 旧 elper 的 RadioButton 方法将两个布尔值 (True lF alse) 映射为 对应的 RadioButton ,并且采用 来布局。 @model bool
@Html.RadioButton("" , true , Model) 是 @Html.RadioButton("" , false , !Model) 否
值得一提的是,我们没有指定 RadioButton 的名称,而是指定一个空字符串。 Html 本身 会对其进行命名,而命名的依据就是 Model 元数据。 Employee 的 IsPartTime 属性呈现在界 面上对应的 HTML 如下所示,可以看到两个类型为 radio 的 元素的 name 被自动赋上 了对应的属性名称。美中不足的是它们具有相同的 ID ,如果希望让 D 具有唯一性,可以对 模板进行更加细致的定制。
4.2.2 预定义模板 上面我们介绍了如何通过 Partial View 的方式创建模板,进而控制某种数据类型或者某 ASP. NET MVC 4 框架揭秘4.2 Model 元数据与 Model 模板 • 149 个目标元素对应的呈方式。实际上在 ASP. 阳 TMVC 的内部定义了一系列的预定义模板。当 我们调用 H由f扯tm时n旧l旧H让蚓elpe町r但/ 呈现的时候,系统会根据当前的呈现模式(显示模式和编辑模式)和 Model 元数据获取一个 具体的模板(白定义模板者预定义模板。由于 Model 具有显示和编辑两种呈现模式,所以定 义在 AS 卫 NETMVC 内部的默认模板可以划分为这两种基本的类型。接下来就逐个介绍这些 预定义模板及最终的 HTML 呈现方式。 EmailAddress 该模板专门针对用于表示 Email 地址的字符串类型的数据成员,它将目标元素呈现为一 个 href 属性具有 "mailto:" 前缀的链接 ( )。由于该模板仅仅用于 Email 地址的显示, 所以只在显示模式下有效,或者说 ASP.NETMVC 仅仅定义了基于显示模式的 EmailAddress 模板。为了演示数据在不同模板下的呈现方式,定义了如下一个简单的数据类型 Model ,通 过在属性 Foo 上应用 UIHintAttribute 特性将模板名称设置为 "Emai lA ddress" l e d O M o m e nυ s s a --C C .工14 b u p{ [UIHint(" Em ailAddress")] public string Foo { get; set; } 然后在一个基于 DemoModel 类型的强类型 View 中,通过调用 HtmlHelper 的 DisplayFor 方法将一个具体的 Model 对象的 Foo 属性以显示模式呈现出来。 @model Model @Html.DisplayFor(m=>m.Foo) 如下的代码片段表示 Model 的 Foo 属性对应的 HTML ,可以看到它就是一个针对 E-mail 地址的链接。当我们点击该链接的时候,相应的 E-mail 编辑软件(比如 Outlook) 会被开启 用于针对目标 E-mail 地址的邮件编辑。 foo@gmail.com Hiddenlnput 关于默认模板 Hidde nI nput 我们不应该感到陌生,前面介绍的 Hiddenlnpu tA ttribute 特性 就是将 ModelMetadata 对象的 TemplateHint 属性设置为 "Hiddenlnput" 。如果目标元素采用 Hidde nI nput 模板,在显示模式下内容会以文本的形式显示:在编辑模式下不仅会以文本的 方式显示其内容,还会生成一个对应的类型 "hidden" 的 元素。如果表示 Model 元数 据的 ModelMetadata 对象的 HideSurroundingHtml 属性为 True (将应用在目标元素上的特性 Hidde nI nputAttribute 的 DisplayValue 属性设置为 False) ,不论是显示模式还是编辑模式下显 示的文本都将消失。 同样以上面定义的数据类型 DemoModel 为例,通过在 Foo 属性上应用 UIHintAttribute ASP. NET MVC 4 框架揭秘150 部 第 4 章 Model 元数据的解析 特性将模板名称设置为 "HiddenInput" 。 14 e d O M o m e nu s s a --C C .工14 b u PJL [UIHint ("Hiddenlnput") ] public string Foo { get; set; } 然后在一个基于 DemoModel 类型的强类型 View 中分别调用 HtmlHelper 的 DisplayFor 和 EditFor 方法将一个具体的 Model 对象的 Foo 属性以显示和编辑模式呈 现出来。 @model Model @Html.DisplayFor(m=>m.Foo) @Html.EditorFor(m=>m.Foo) 分别以两种模式呈现出来的 Foo 属性对应的 HTML 如下(包含在花括号中的 GUID 表 示属性值〉。第一行是针对显示模式的,可以看出最终呈现出来仅限于表示属性值的文本。 编辑模式对应的 HTML 中不仅包含属性值文本,还具有一个对应的类型为 "hidden" 的 元素。 {42A1E9B7-2AED-4C8E-AB55-78813FC8C233} {42A1E9B7-2AED-4C8E-AB55-78813FC8C233} 现在我们对数据类型 Model 做一下简单修改,将应用在属性 Foo 上的 UIHintAttribute 特性替换成 HiddenInputAttribute 特性,并将其 DisplayValue 属性设置成 False 。 14 e d O M o m e nu s s a 14 C C .工14 b u p{ [Hiddenlnput(DisplayValue = false)] public string Foo { get; set; } 由于应用在目标元素上的 HiddenInputAttribute 特性的 DisplayValue 属性会最终控制对应 ModelMetadata 的 HideSurroundin出tml 属性,而后者控制是否需要生成用于显示目标内容的 HTC\伍,所以针对的 Model 定义,最终会生成如下一段 HTML 。 Html 如果目标对象的内容包含一些 HTh伍,并需要在 UI 界面中原样呈现出来,我们可以采 用 Html 模板。和 EmailAddress 模板一样,该模板仅限于显示模式。为了演示 Html 模板对 目标内容的呈现方法与默认呈现方式之间的差异,我们定义了如下一个数据类型 DemoModel 。该数据类型具有两个字符串类型的属性 Foo 和 Bar ,其中 Foo 上面应用 UIHintAttribute 特性将模板名称设置为 "Html" 。 ASP. NET MVC 4 框架揭秘4.2 Model 元数据与 Model 模板 知 151 14 e AU O M o m e nu s s a 14 C C .-14 .p u p-t [UIHint ("Html") ] public string Foo { geti seti } public string Bar { geti seti } 现在我们创建一个具体的 DemoModel 对象,并将 Foo 和 B缸设置为一段表示链接的文 本 (google.com ),最终在一个基于 Model 类型的强类型 View 中通过调用 HtmlHelper 的 DisplayFor 方法将这两个属性以显示模式呈现出来。 @model DemoModel @Html.DisplayFor(m=>m.Foo) @Html.DisplayFor(m => m.Bar) 从如下所示的表示 Foo 和 B町两属性的 HTML 中我们不难看出:采用 Html 模板的 Foo 属性的内容原样输出,而包含在属性 B缸中的 HTML 都进行了相应的编码。 google.com <ia href="iWWW.google.com"i&9tigoogle.com<i/a>i Text 与 String 不论是在显示模式还是编辑模式, Text 和 S往ing 着两个模板具有相同的 HTML 呈现方 式(实际上在 ASP.NETMVC 内部,两种模板终生成的 HTh也是通过相同的方法产生的〉。 对于这两种模板说,目标内容在显示模式下直接以文本的形式输出,而在编辑模式下则对应 着一个单行的文本框。 为了演示两种模板的同一性,我们对上面定义的数据类型 DemoModel 略作修改,在属 性 Foo 和 B盯上应用 UIHintAttribute 特性并将模板称分别设置为 String 和 Text 。 14 e d O M o m e nu s s a 14 C C .-14 .b u p{ [UIHint("String")] public string Foo { geti seti } [UIHint ("Text") ] public string Bar { geti seti } 然后创建一个具体的 DemoModel 对象,将其作为 Model 呈现在具有如下定义的 View 中。这是一个 Model 类型为 DemoModel 的强类型 View 中,在该 View 中分别调用 HtmlHelper 的 DisplayFor 和 EditorFor 方法将 Model 对象的两个属性以显示和编辑 模式呈现出来。 @model DemoModel @Html.DisplayFor(m=>m.Foo) @Html.DisplayFor(m => m.Bar) @Html.EditorFor(m=>m.Foo) @Html.EditorFor(m => m.Bar) 如下所示的代码片段体现了上述四个元素对应的 HTML ("Dummy text... "是 Foo 和 B缸 ASP. NET MVC 4 框架揭秘152 如 第 4 章 Model 元数据的解析 的属性值),可以看到采用了 Text 和 String 模板的两个属性在显示和编辑模式下具有相同的 HTML 输出。编辑模式下输出的类型为 "text" 的 5t素,表示 CSS 特性类型的 class 属性被设置为 "text-box single-line" ,意味着这是一个基于单行的文本框。 Durnmy text Durnmy text 值得一提的是, ASP. 阳 TMVC 内部采用基于类型的模板匹配机制,对于字符串类型的 数据成员,如果没有显式设置采用的模板名称,默认情况下会采用 String 模板。 Url 与 EmailAddress 和 Html 一样,模板 Url 也仅限于显示模式。对于某个表示为 Url 的字 符串,如果我们希望它最终以一个链接的方式呈现在最终生成的 HT l\伍中,可以选择该模 板。如下面的代码片段所示,通过应用 UIHintAttribute 特性将模板 Url 应用到属性 Foo 中。 14 e d O M o m e nu s s a l c c .工14 ku u p ‘ it [UIHin t ("Url") ] public string Foo { geti seti } 创建一个具体的 DemoModel 对象,并将 Foo 属性设置为一个表示 Url 的字符串 "问://www.asp.net" ,最后通过如下的方式将该属性以显示模式呈现出来。 @model DemoModel @Html.DisplayFor(m=>m.Foo) 如下面的代码片段所示,该属性最终呈现为一个 href 属性和文本内容均为指定属性值的 链接( ... )。 http://www.asp.net MultilineText 一般的字符串在编辑模式下会呈现为一个单行的文本框(类型为 "text" 的 5t素), 而 Multiline Text 模板会将表示目标内容的字符串通过一个 Password 对于表示密码的字符串来说,在编辑模式下应该呈现为一个类型为" password" 的 元素,以使我们输入的内容以掩码的形式显示出来以保护密码的安全性。在这种情 况下可以采用 Password 模板,该模板和 Mult i1i neText 一样也仅限于编辑模式。如下面的代 码片段所示,我们在 DemoModel 的 Foo 属性上应用 UIHintAttribute 特性将模式名称设置 为 "Password" 。 14 e d O M o m e nυ s s a ---C C .工14 b u p{ [U 工 Hint("Password")] public string Foo { get; set; } 创建一个具体的 DemoModel 对象,并通过如下的形式将 Foo 属性以编辑模式呈现在某 个基于 DemoModel 的强类型 View 中。 @model DemoModel @Html.EditorFor(m=>m.Foo) 该 Foo 属性最终会以如下的形式通过一个类型为 "Password" 的 :7G素呈现出来, 表示 css 样式类型的c1 ass 属性被设置为 "text七 ox single-line password" ,意味着呈现效果为 一个单行的文本框。 Oecimal 如果采用 Decimal 模板,代表目标元素的数字不论其小数位数是多少,都会最终被格式 化为两位小数。在显示模式下,被格式化的数字直接以文本的形式呈现出来,而在编辑模式 下则对应着一个单行的文本框。如下面的代码片段所示,我们在数据类型 DemoModel 中定 义了两个对象类型属性 Foo 和 B 缸,它们应用了 UIHin tA ttribute 特性并将模板名称指定为 "Decimal" 。 ASP. NET MVC 4 框架揭秘154 • 第 4 章 Model 元数据的解析 14 e d O M o m e nu s s a 14 C C .工14 嘈 DU P-{ [UIHint ("Decimal")] public object Foo { geti seti } [UIHint ("Decimal") ] public object Bar { geti seti } 我们来创建一个具体的 DemoModel 对象,将它的 Foo 和 B 缸属性分别设置为整数 123 和浮点数 3.1415 (4 位小数),最终通过如下的形式将它们以显示和编辑的模式呈现在一个 基于 Model 类型的强类型 View 中。 @model DemoModel @Html.DisplayFor(m=>m.Foo) @Html.DisplayFor(m=>m.Bar) @Html.EditorFor(m=>m.Foo) @Html.EditorFor(m =>m.Bar) 上述四个元素在最终呈现的 UI 界面中对应着如下的 H 刊1L 代码,可以看到最终显示的 都是具有两位小数的数字。 123.00 3.14 Boolean 通过本章最开始的实例演示我们知道,一个布尔类型的对象在编辑模式下会以一个类型 为 "checkbox" 的 5t素的形式呈现,实际上在显示模式下它依然对应着这么一个元素, 只是其 disabled 属性会被设置为 True 使之处于只读状态。布尔类型的这种默认呈现方式源自 "Boolean" 模板默认被使用。 当布尔类型的目标元素以编辑模式进行呈现的时候,除了生成一个类型为 "checkbox" 的 元素之外还会附加产生一个类型为 "hidden" 的 元素。如下面的代码片段所 示,这个 hidden 元素具有与 Chec kB ox 相同的名称,但是值为 False ,它存在的目的在于当 Chec kB ox 没有被勾选的情况下,通过对应的 hidden 元素向服务器提交相应的值 (False) , 因为没有被勾选的 Chec kB ox 的值是不会包含在请求中的。 Boolean 和 String 、 Decimal 以及后面我们将要介绍的 Object 都属于基于 CLR 类型的模 板,由于 ASP.NET 在内部采用基于类型的模板匹配策略,如果没有显示设置采用的模板类 型,相应类型的元素会默认采用与之匹配的模板。 ASP. NET MVC 4 在架揭秘4.2 Model 元数据与 Model 模板 耀 155 Collection 顾名思义, Collection 模板用于集合类型的目标元素的显示与编辑。对于采用该模板的 类型为集合(实现了 IEnumerable 接口)的目标元素,在调用 Htm lH elper 或者 HtmlHelper 以显示或者编辑模式对其进行呈现的时候会遍历其中的每个元素,并根 据基于集合元素的 Model 元数据决定对其的呈现方法。同样以我们定义的数据类型 DemoModel 为例,按照如下的方式将它的 Foo 属性类型改为对象数组,上面应用了 UIHintAttribute 特性并将模板名称设置为" Collection "。 14 e d o M o m e nu s s a 14 C C .l l b u p{ [UIHint ("Collection") ] public object[] Foo { get; set; } 然后我们按照如下的方式创建一个包含三个对象的数组,作为数据元素的三个对象类型 分别是数字、字符串和布尔,然后将该数组作为 Foo 属性创建一个具体的 Model 对象。 object[] foo = new object[] 123.00 , "durnrny text ...", true DemoModelmodel =ηew DemoModel{ Foo = foo }; 在一个基于 Model 类型的强类型 View 中,我们分别调用 HtmlHelper 的 DisplayFor 和 EditorFor 方法将上面创建的 DemoModel 对象的 Foo 属性以显示和编辑的模式 呈现出来。 @model DemoModel @Html.DisplayFor(m=>m.Foo) @Html.EditorFor(m=>m.Foo) Model 对象的 Foo 属性最终呈现出来的 HTML 如下所示,我们可以看到不论是显示模 式还是编辑模式,基本上就是对集合元素呈现的 HTML 的组合而已。 123durnrny text ... ASP. NET MVC 4 在架揭秘156 瓢 第 4 章 Model 元数据的解析 Object 我们说过 ASP.NET 内部采用基于类型的模板匹配策略,如果通过 ModelMetadata 对象 表示的 Model 元数据不能找到一个具体的模板,最终都会落到 0 时 ect 模板上。 Object 模板 对目标对象的呈现方式很简单,它通过 ModelMetadata 的 Properties 属性得到所有基于属性 的 Model 元数据。针对每个表示属性 Model 元数据的 ModelMetadata ,它会根据 DisplayName , 或者属性名称生成一个标签(实际上是一个内部文本为显示名称的
元素),然后根据元 数据将属性值以显示或者编辑的模式呈现出来。 s s e r d d A s s a 14 C C .-14 hu u p{ [DisplayName(" 省"门 public string Province { geti seti } [DisplayName(" 市"川 public string City { geti seti } [DisplayName(" 区") ] public string District { geti seti } [DisplayName(" 街道"门 public string Street { geti seti } 创建一个具体的 Address 对象并将其作为 Model 呈现在一个具有如下定义的 View 中, 这是一个 Model 类型为 Address 的强类型 View ,可以调用 HtmlHelper 的 DisplayForModel 方法将 Model 对象以显示模式呈现出来。 @model Address @Html.DisplayForModel() 从如下所示的 HTML 中可以看出,作为 Model 的 Address 对象的所有属性都以显示模 式呈现出来,而且在前面还具有相应的标签。
江苏省
苏州市
工业因区
街道
星湖街 328 号
值得一提的是, Object 模板在对属性进行遍历的过程中,不论是显示模式还是编辑模式, 只会处理非复杂类型属性成员,也就是如果属性成员是一个复杂类型(不能支持针对字符串 类型的转换),它不会出现在最终生成的 HTML 中。 ←」c a ←」n o 户」s s a --C C .-14 b u p--t [DisplayName(" 姓名") ] public string Name { geti seti } [DisplayName(" 电 t舌") ] ASP. NET MVC 4 在架揭秘4.2 Model 元数据与 Model 模板 穰 157 public string PhoneNo { geti seti } [DisplayName("EmailJ也.ìJl:") 1 public string EmailAddress { geti seti } [DisplayName ("联系地址") ] public Address Address { geti se 乞 i } 通过上面的代码片段我们定义了一个表示联系人的数据类型 Contact ,它具有一个类型 为 Address 的同名属性。现在我们创建一个具体的 Contact 对象,并对包括 Address 属性在 内的所有属性进行初始化,然后通过如下的方式调用 HtmIHelper 的 DisplayForModel 方法将它呈现在以此作为 Model 的 View 中。 吨t­e d O M r o 』LFιCV4 aa +」 14 np os 户 v· 工nυ 1-­e14 唱。 m ot mH QN 」口旧」 从如下所示的 HT1伍中可以看出,由于 Contact 的数据成员 Address 是复杂类型,其内 容并不会呈现出来。
姓名
张三
电 t舌
1234567890
Email 地址
zhangsan@gmail.com
可以有两种方式解决这个问题,一种是为 Address 类型定义相应的模板,另一种就是按 照类似如下的方式手工将复杂类型属性成员呈现出来。 @model Contact @Html.DisplayForModel() @Html.EditorFor(m=>m.Address) 4.2.3 DataTypeName 与模板名称 我们知道,通过 DataTypeAttribute 特性为目标元素设置的数据类型最终会反映在 Mode !M etadata 对象的 DataTypeName 属性上,对于某些的数据类型(如 Date 、 Time 、 Currency 等)还会创建一个相应的 DisplayF ormatAttribute 特性应用到 Mode !M etadata 上,那么 Mode !M etadata 的 DataTypeName 属性对目标元素在 HTML 中的最终呈现具有怎样的影响呢? 实际上在模板匹配的过程中会将 ModelMetadata 的 DataTypeName 属性当作模板名称来 看待,所以下面两种形式的 DemoModel 类型定义可以看成是等效的。通过 UIHintAttribute 特性设置的模板名称和通过 DataTypeAttribute 特性设置的数据类型的唯一不同之处在于前 者具有更高的优先级。换句话说,如果将 UIHintAttribute 和 DataTypeAttribute 同时应用到同 一个数据成员分别将模板名称和数据类型设置为 "ABC" 和 "XYZ" ,自定义模板 XYZ 只有 在模板 ABC 不存在的情况下才会被使用。 14 e d O M o m e D S s a --C C .工14 b u p{ AS P. NET MVC 4 在架揭秘158 …m 第 4 章 Model 元数据的解析 [DataType(DataType.Htrnl)] public string Foo { get; set; } [DataType(DataType.MultilineText)] public string Bar { get; set; } [DataType(DataType.Url)] public string Baz { get; set; } 14 e d O M o m e D S s a 14 C C .工1-b u p{ [UIHint ("Htrnl") ] public string Foo { get; set; } [UIHint("MultilineText")] public string Bar { get; set; } [UIHint("Url")] public string Baz { get; set; } 实例演示:证明 DataTypeName 与模板名称的等奴性 (S410 ) 为了证明通过 DataTypeAttribute 特性设置的数据类型在针对目标元素的可视化呈现过 程中被视为模板名称,我们来做一个简单的实例演示。在这个实例中定义了如下一个表示三 角形的数据类型 Triangle ,其属性 A 、 B 和 C 是一个 Point 对象,表示三个角所在的坐标。 e l qd n a ·工r 巾-s s a 14 C C .-14 b u p{ [DataType("Pointlnfo")] public Point A { get; set; } [DataType("Pointlnfo")] public point B { geti set; } [DataType ("Pointlnfo")] public point C { geti seti } [TypeConverter(typeof(PointTypeConverter))] public class Point public double X { geti set; } public double Y { get; set; } public Point(double x , double y) this.X = x; this.Y = yi public static Point Parse(string point) string[] split = point.Split(' , ')i if (split.Length != 2) throw new ForrnatException("Invalid point expression.")i AS P. NET MVC 4 框架揭秘4.2 Model 元数据与 Model 模板 黯 159 double x; double y; if (!double.TryParse(split[O ], out x) 11 !double.TryParse(split[l] , out y)) throw new FormatException("Invalid point expression."); } return new Point(x , y); public class PointTypeConverter : TypeConverter public override bool CanConvertFrom(ITypeDescriptorContext context , Type sourceType) return sourceType == typeof(string); public override object ConvertFrom(ITypeDescriptorContext context , Culturelnfo culture , object value) if (value is string) return Point.Parse(value as string); } return base.ConvertFrom(context , culture , value); 对于类型 Triangle 和 Point 的定义,有两点值得注意:第一, Triangle 的三个 A 、 B 和 C 属性上应用了 DataTypeAttribute 特性并将自定义数据类型设置为 Poin tI nfo C 不是 Point); 第 二, Point 类型上应用了 TypeConverterAttribute 特性并将 TypeConverter 类型设置为 PointTypeConverter ,后者支持源自字符串的类型转换。通过前面对复杂类型 C Complex Type) 的介绍,这样会将 Triangle 的三个属性从复杂类型成员转换成简单类型成员。根据前面介绍 的关于 Object 模板对数据成员的遍历规则, Triangle 的这三个属性才能被最终呈现出来。 现在我们创建一个 Model 类型为 Point 的强类型 View ,用它作为用于呈现 Point 对象的 模板,并将其命名为 Poin tI nfo C 和前面通过 DataTypeAttribute 特性指定的自定义数据类型一 致)。因为只需要为 Point 定义针对显示模式的模板,所以将具有如下定义的模板文件放在 , -Niews/SharedID isplayTemplates "目录下。如下面的代码片段所示,将一个 Point 对象显 示为 CX , Y) 的形式。 @model Point (@Model.X , @Model.Y) 现在我们创建如下一个 HomeCtronller ,在默认的 Index 操作方法中创建了一个 Triangle 对象将其呈现在默认的 View 中。 public class HomeController : Controller public ActionResult Index() AS P. NET MVC 4 框架揭秘160 -. 第 4 章 Model 元数据的解析 Triangle triangle = new Triangle A = new Point(1 , 2) , B = new Point (2 , 3) , C = new Point(3 , 4) return View(triangle); 下面是 Action 方法 Index 对应 View 的定义,可以看出这是一个 Model 类型为 Triangle 的强类型 View 。在该 View 中,我们仅仅调用了 HtmIHelpe r 的扩展方法 DisplayModel 作为 Model 的 Triangle 对象以显示模式呈现出来。 e -­qJ n a --r mA l> e1 ·dm ot mh G 』< Triangle @Html.DisplayForModel() 运行该 Web 应用后会在浏览器中得到如图 4-13 所示的呈现效果,可以看到 Triangle 对象 的 A 、 B 和 C 属性表示的三个角的坐标是完全按照我们定义的 PO 扭曲曲模板的方式来显示的。 -.二íIIJ:I]!乞æ " 1 口 X g呵 -..叭'H φ 争 。 ; 而、 曹雪Ihostl0些国 磊、 A (1 . 2) B (2.3) C (3.4) 图 4-13 通过 Data TypeAttribute 特性设置模板名称的呈现效果 4.2.4 模板的获取与执行 当我们调用 HtmlHelper 或者 HtmlHelper 的模板方法对整个 Model 或者 Model 的某个数据成员以某种模式(显示模式或者编辑模式)进行 HTML 呈现的时候, ASP.NET MVC 会根据预先创建的用于描述目标数据的 Model 元数据获取相应的模板。如果模板对应着某个 自定义的 View ,那么只需要执行该Vi ew 即可:对于默认模板,则直接可以得到相应的 H Th伍。 根据 Model 元数据对模板的提取是整个模板方法执行流程中最核心的部分,也是本节讨 论的重点。我们以 HtmlHelp 町 的扩展方法 DisplayFor (定义如下〉为例,看看针对 通过表达式 expression 获取的 Model 对象是如何以显示模式呈现出来的。 ASP. NET MVC 4 框架揭翻4.2 Model 元数据与 Model 模板 • 161 public static class DisplayExtensions public static MvcHtmlString DisplayFor( this HtmlHelper html, Expressio 口 > expression, string templateName); 在 DisplayFor 被调用的时候,如果通过参数 expression 表示的 Model 获取表达式是针对 某个属性的,那么属性名会被获取出来,然后执行表达式得到作为 Model 的对象,该对象连 同属性名〈如果有)一起被用于表示 Model 元数据的 ModelMetadata 对象的创建。接下来 ASP.NETMVC 会根据这个 ModelMetadata 对象得到一系列表示分部模板 View 名称的列表, 这些 View 名称按照优先级排列如下。 • 作为参数 templateName 传入的模板名称(如果不为空〉。 • ModelMetadata 的 TemplateHint 属性值(如果不为空)。 • ModelMetadata 的 DataTypeName 属性值(如果不为空〉。 • 如果 Model 对象的真实类型为非空值类型,该类型名作为模板View 名:否则底层 (Underlying) 类型名作为模板View 名(比如,对于 int?类型则将Int3 2 作为模板View 名)。 • 如果 Model 对象的真实类型为非复杂类型,会选择 String 模板(由于非复杂类型能够实 现与 String 类型之间的转换,所以可以转换成 String 进行呈现)。 • 在 Model 的声明类型为接口情况下,如果该接口继承自 IEnumerable 则采用 Collection 模板。 • 在 Model 的声明类型为接口情况下,会选择 Object 模板。 • 如果 Model 声明类型不是接口类型,按照其类型继承关系向上追溯直到 Object 类型, 逐个将类型名称作为模板 View 名称。如果声明类型实现了 IEnumerable 接口,则将最 后的 Object 替换成 Collection 。 对于得到的这个列表, AS丑陋TMVC 会按照先后顺序遍历所有的元素,并将它们作为 模板名称根据呈现模式在指定的路径(显示模式和编辑模式分别为 " lDisplayTemplates/ {TemplateName} "和" ÆditorTemplates/ {TemplateN ame } ")去寻找定义 模板的 Partial View 。如果存在,会直接执行该 View 来对目标元素进行呈现:如果不能找到 自定义模板分部 Partial View ,则根据该模板名称在默认的模板列表中查找。如果存在匹配 的默认模板,则按照对应的默认模板来呈现目标元素:如果默认的模板列表中的名称均与指 定的名称不匹配,会进入下一次迭代。 ASP.阳TMVC 在内部类型 TemplateHelpers 中定义了如下一个 GetViewNames 方法,它 根据 ModelMetadata (metadata 参数〉和作为 TemplateHint 的模板名称列表( templateHints 参数〉返回一个按照选择优先级排列的"候选模板名称"列表。字符串数字参数 templateHints 不仅仅包含 ModelMetadata 的 TemplateHint 属性,还包括在调用模板方法时手工指定的模板 名称〈排在 TemplateHint 属性之前)和 ModelMetadata 表示数据类型名称的 DataTypeName ASP. NET MVC 4 框架揭秘162 • 第 4 章 Model 元数据的解析 属性(排在 TemplateHint 之后),上面我们提到的"数据类型名称作为模板名称"的具体实 现就体现在这里。 internal static class TemplateHelpers //其他成员 internal static IEnumerable GetViewNames(ModelMetadata metadata , params string[] templateHints); 可以通过一个简单的实例来演示模板方法在执行过程中对模板的选择机制。在一个 ASP.NETMVC 应用中定义了如下一个数据类型 DemoModel ,其中属性 Foo 和 B 町是简单类 型 (int 和 int?) ,属性 B 但是复杂类型,而属性 Qux 是一个集合类型。四个属性上均应用了 UIHintA ttribute 和 DataTypeAt位 ibute 特性并作了相同的设置。 『l-e d O M o m e nu s s a --4 C C .l l b u p{ [UIHint("TemplateHint")] [DataType("DataTypeName")] public int Foo { get; set; } [UIHint("TemplateHint")] [DataType("DataTypeName")] public int? Bar { get; set; } [UIHint("TemplateHint")] [DataType("DataTypeName")] public Baz Baz { get; set; } [UIHint("TemplateHint")] [DataType("DataTypeName")] public IEnumerable Qux { get; set; } z a B S s a 14 C C .l l b u13 13P{ 然后我们创建了如下一个 HomeController 。静态方法 GetCandidateTemplates 根据 Mode lM etadata 和显示指定的模板名称返回一个作为候选模板名称的字符串列表。由于我们 需要使用到 TemplateHelpers 这个内部类型的 GetViewNames 方法,所以我们采用反射的方式 来调用它。我们将包含显式指定模板名称、 Mode lM etadata 的 TemplateHint 属性和 DataTypeName 属性的字符串数组作为调用该方法的第二个参数。 public class HomeController : Controller public ActionResult Index() ModelMetadata metadata = new ModelMetadata( ModelMetadataProviders .Current , null , null , typeof (DemoModel) , null); ViewBag.TemplateNamesAccessor = new Func>(GetCandidateTemplates); return View(metadata.Properties); staticIEnumerableGetCandidateTemplates(ModelMetadatamodelMetadata , AS P. NET MVC 4 框架揭秘4.2 Model 元数据与 Model 模板 油 163 string template) Type templateHelpers = Type.GetType ("System.Web.Mvc. Htm l. TemplateHelpers , System.Web.Mvc , Version=4.0.0.0 , Culture=neutral , PublicKeyToken=31bf3856ad364e35"); Methodlnfo getViewNames = templateHelpers.GetMethod("GetViewNames" , BindingFlags.NonPublic I BindingFlags.Static); string[] templates = new string[] { template , modelMetadata.TemplateHint , modelMetadata.DataTypeName }; return (IEnumerable)getV 工 ewNames.lnvoke(null , new object[] { modelMetadata , templates }); 默认的 Action 方法In dex 中,根据类型 DemoModel 创建一个 ModelMetadata 对象,并 将其 Properties 属性(描述 DemoModel 四个属性的 Model 元数据列表〉作为 Model 将默认 的 View 呈现出来。在进行 View 呈现之前,我们设置了 ViewBag 的 Template~amesAccessor 属性,而属性值是一个基于 GetCandidateTemplates 方法的委托对象。 如下所示的是 Action 方法中 Index 对象的 View 的定义,这是一个基于 Mode lM etadata 集合的强类型 View ,在该 View 中,借助从 ViewBag 获取的委托对象将针对 DemoModel 四 个属性的候选模板名称列表呈现出来。 @model IEnumerable 候选棋板列表 @{ Func> templateNamesAccessor = ViewBag.TemplateNamesAccessor;
    @foreach (ModelMetadata metadata in Model)
<工土> @metadata.PropertyName
    @foreach (string templateName in
templateNamesAccessor (metadata , "Mandatory Template") )
  • @templateName
  • 这个程序运行之后会在浏览器中呈现出如图 4-14 所示的输出结果。前面我们介绍的在 模板方法执行过程中针对 Model 元数据的模板选择机制在这里得到了很好的体现。针对 DemoModel 的每一个属性,按照解析出来的模板名称顺序,如果在预定义的目录下存在相 应的模板 View 或者默认模板,它们将被用于该属性值的最终呈现。 (S411 ) ASP. NET MVC 4 框架揭秘164 话 第 4 章 Model 元数据的解析 即盟军 正~~唱现~灌翩面画~ ………… 隅密密遐摇~ 巳 localhost: 104 78 合 E型a • Fo。 。 M扭曲町ITet:即,late o Templalel也t 。 DataTypeName 。Int32 。 s缸iog • Bar 。 Man也101}' T emplale 。 TemplaleHint 。 DataType1吁ame o Int32 。 s位也ε • Baz ., MandatOl}' T enψlate 。 TernplateHint 。 DataTypeName ., Baz 。 Object • Qux 。Mandatory Ternplale o Te:mplalel祖t 。 D血TypeN皿e o IE皿l!Ilef"able'l ., Collection 。 Object 图 4-14 根据 Model 元数据得到的"候选模板名称"列表 4.2.5 实例演示:通过定制 Model 元数据和自定义模板 实现预定义列表的呈现 (8412 ) 根据 Model 元数据,我们不仅可以创建相应的模板来控制某种类型的数据在 UI 界面上 的呈现方式,还可以通过一些扩展来控制 Model 元数据本身。在某些情况下通过这两者的结 合往往可以解决很多特殊数据的呈现问题,接下来演示的实例就是一个典型的例子。 传统的 ASP.NET 具有一组重要的控件类型叫做列表控件 (ListControl) ,它的子类包括 Dr叩DownList 、 ListB ox 、 RadioButtonL ist 和 CheckB oxL ist 等。对于 ASP.NET MVC 来说, 可以通过 HtmIHelperlHtmlHelper 的扩展方法 DropDownL istIDropDownL istF or 和 ListBoxIL istB oxFor 在界面上呈现一个下拉框和列表框,但是需要手工指定包含的所有列表 选工页。在一般的 Web 应用中,尤其是企业应用中,我们会选择将这些列表进行单独地维护。 如果我们在构建"列表控件"的时候能够免去手工提供列表的工作,这无疑会为开发带来极 大的便利,而实际上这很容易实现。 我们先来看看通过该扩展最终实现的效果。我们在 ASP.阳TMVC 应用中定义一个代表 员工的 Employee 类型。如下面的代码片段所示,表示性别、学历、部门和技能的属性分别应用 了RadioButtonListAt国bute 、 DropdownListAttribute 、 ListBoxAt创bute 和 CheckBoxListAttribubte 四个特性。从名称可以看出来,这四个特性分别代表了目标元素呈现在 UI 界面上的形式, 即对应着传统 ASP.NETWeb 应用中的四种类型的列表控件: RadioButtonL ist 、 DropdownL ist 、 ASP. NET MVC 4 在当~~秘4.2 Model 元数据与 Model 模板 踵 165 Lis tB ox 和 Chec kB o xL ist ,特性中指定的字符串表示预定义列表的名称。 e e y 0 14 p m E s s a 14 C C .工14 b u p{ [DisplayName(" 姓名") ] public string Name { geti seti } [RadioButtonList("Gender")] [DisplayName(" 性别"门 public string Gender { geti seti } [DropdownList ("Education")] [DisplayName(" 学历"门 public string Educat 工 on { geti seti } [Lis tBox ("Department") ] [DisplayName(" 所在部门"门 public IEnumerable Departments { geti seti } [CheckB oxList("Skill")] [DisplayName(" 擅长技能") ] public IEnumerable Skills { geti seti } 在创建的默认 HomeController 中,我们定义了如下一个 Index 操作方法,在该方法中, 我们创建了一个具体的 Employee 对象并对它的所有属性进行了相应设置,最终将该对象呈 现在默认的 View 中。 public class HomeController : Controller public ActionResult Index() Employee employee = new Employee Name = "张三", Gender = "M" ,//男 Education = "M" , / /硕士 Departments =口 ew stri 呵[ ] { "HR" , "AD" }, / /人事部,行政部 Skills =口 ew stri 口 g[] { "CSharp" , "AdoNet" }//C# , ADO.NET } i return View(employee)i 如下所示的是 Action 方法中 Index 所对应的 View 的定义,这是一个基于 Employee 的强 类型 View 。在该 View 中,通过调用 HtmIHelper 的模板方法 EditorFor 将作为 Model 的 Employee 对象的所有属性以编辑模式呈现出来。 @model Employee 编辑员工信息 AS P. NET MVC 4 框架揭秘166 L. ÎI 第 4 章 Model 无数据的解析
    @Html.LabelFor(m => m.Name) @Html.EditorFor(m => m.Name)
    @Html.LabelFor(m => m.Gender) @Html.EditorFor(m => m.Gender)
    @Html.LabelFor(m => m.Education) @Html.EditorFor(m => m.Education)
    @Html.LabelFor(m => m.Departments) @Html.EditorFor(m => m.Departments)
    @Html.LabelFor(m => m.Skills) @Html.EditorFor(m => m.Skills)
    图 4-15 体现了该 Web 应用运行时的效果,可以看到,四个属性分别以四种不同的"列 表控件"呈现出来,并与应用在它们上面的四个自定义的列表特性( RadioButtonL is tAttribute 、 Dropdow nL is tA ttribute 、 Lis tB o xA ttribute 和 Chec kB o xL is tA ttribubte) 相匹配。 什 1 - 口~ l 爪陆陆函 …一二\ I --cj 份…阴阳市罚酒 E钮 f78 ~姻 ~, 、. 姓名 军 二二一 二工二 i 性别 ⑨男 。 女 学历 薄云了二 日 脆部门 F;. 擅长技能囹c#巴 ASP 泪 T 团 ADO_NET 图 4-15 应用Li stA ttribute 特性的数据成员的呈现效果 现在我们对上面演示的针对自动化"列表控件"呈现的设计原理进行简单介绍。首先定 义了如下一个表示列表中某个条目〈列表项〉的类型 ListIt em ,简单起见,仅仅定义 Text 和 Value 两个属性,它们分别表示显示的文字和代表的值。对于一组表示国家的列表,列表项 的 Text 属性表示成国家名称(比如"中国勺,具体的值则可能是国家的代码〈比如 "CN 勺。 m e +」TE 品 +L S .工L S s a 14 C C .-1-b U DAg- public string Text { get; set; } public string Value { get; set; } 将提供列表数据的组件称为 Lis tP rovider ,它们实现了 ILis tP rovider 接口。如下面的代码 片段所示, ILis tP rovider 具有唯一的方法 Ge tL ist It ems ,根据指定的列表名称获取所有的列表 项。通过实现 ILis tP rovider ,我们定义了一个默认的 Defaul tL istProvider 。简单起见, DefaultL istP rovider 直接通过一个静态字段模拟列表的存储,在真正的项目中一般会保存在 数据库中。 Defaul tL is tP rovider 维护了四组列表,分别表示"性别"、"学历"、"部门"和"技 能",它们正好对应着 Employee 的四个属性。 ASP. NET MVC 4 在架揭秘4.2 Model 元数据与 Model 模板 • 167 public interface IListProvider IEnurnerable GetListIterns(string listNarne)i public class DefaultListProvider : IListProvider private static Dictionary> listIterns = new Dictionary>()i static DefaultListProvider() { var iterns = new ListItern[] { new ListItern{ Text = "男", Value="M"} , new ListItern{ Text = "女", Value="F"}}i listIterns.Add("Ge 口 der" , iterns)i iterns = new ListItern[]{ 口 ew ListItern{ Text = "高中", Value="H"} , new ListItern{ Text = "大学本科" , Value="B"} , new ListItern{ Text = "硕士" , Value="M"} , new ListItern{ Text = "博士", Value="D"} } i listIterns.Add("Education", iterns)i iterns = new ListItern[]{ 口 ew ListItern{ Text = "人事部", Value="HR"} , new ListItern{ Text = "行政部", Value="AD"} , new ListItern{ Text = "IT 部", Value=" 工 T"} } i listIterns.Add("Departrnent", iterns)i iterns = new ListItern[]{ new ListItern{ Text = "C#" , Value="CSharp"} , new ListItern{ Text = "ASP.NET" , Value="AspNet"} , new ListItern{ Text = "ADO.NET" , Value="AdoNet"}}i listIterns.Add("Skill", iterns); public IEnurnerable GetListIterns(string listNarne) 工 Enurnerable iterns; if (listIterns.TryGetValue(listNarne, out iterns)) return iterns; return new ListItern[O]; 接下来定义如下一个ListProviders 类型,它的静态只读属性 Current 表示当前的 Li stProvider ,而对当前ListProvider 的注册通过静态方法 SetLi stProvider 来实现。如果没有 对当前 ListProvider 进行显式注册,则默认采用 DefaultListProvider 。 public static class ListProviders public static IListprovider Current { get; private set; } static ListProviders() ASP. NET MVC 4 框架揭秘168 ,:. 第 4 章 Model 元数据的解析 Current = new DefaultListProvider()i public static void SetListProvider(Func providerAccessor) Current = providerAccessor()i 基于四种"列表控件"的 H TIvlL的生成是通过定义 HtmIHelper 的扩展方法来实现的, 如下面的代码所示,定义在 ListControlExtensions 中的四个扩展方法实现了针对这四种列表 控件的 UI 呈现。参数 listName 表示使用的预定义列表的名称,而 value 和 values 则表示绑 定的值。 RadioButto nL is tID ropdow nL ist 只允许单项选择,而Li stBo xJ Chec kB o xL ist 允许多项 选择,所以对应的值类型分别是 string 和 IEnumerable 。 public static class ListControlExtensions //其他成员 public static MvcHtmlString RadioButtonList( this HtmlHelper htmlHelper , string name , string listName , string value) return RadioButtonCheckBoxList(htmlHelper , listName , item => htmlHelper.RadioButton(name , item.Value , value == item.Value))i public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper , string name , string listName , IEnumerable values) return RadioButtonCheckBoxList(htmlHelper , listName , item => CheckBoxWithValue(htmlHelper , name , values.Contains(item.Value) , item.Value))i public static MvcHtmlString ListBox(this HtmlHelper htmlHelper , string name , string listName , IEnumerable values) var listltems = ListProviders.Current.GetListltems(listName)i List selectListltems = new List()i foreach (var item in list 工 tems) } selectListltems.Add(new SelectListltem { Value = item.Value , Text =工 tem.Text , Selected = values.Any(value => value == item.Value) })i return htmlHelper.ListBox(name , selectListltems)i public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper , string name , string listName , string value) var listltems = ListProviders.Current.GetListltems(listName)i List selectListltems = new List()i foreach (var item in listltems) AS P. NET MVC 4 在架揭秘4.2 Model 元数据与 Model 模板 黯 169 selectListItems.Add(new SelectListItem { Value = item.Value , Text = item.Text , Selected = value == item.Value}); return htmlHelper.DropDownList(name , selectListItems); 从上面的代码片段可以看到,在 ListBox 和 DropDow nL ist 方法中通过当前的Li stProvider 获取指定列表名称的所有列表项并生成相应的 SelectLi s tI tem 列表,最终通过调用 HtmlHelper 现有的扩展方法Li stBox 和 DropDow nL ist 实现 HTML 的呈现。而 RadioButto nL ist 和 Chec kB o xL ist 最终调用了辅助方法 RadioButtonChec kB o xL ist 显示了最终的 HTML 生成,该 方法定义如下。 public static class ListControlExtensions public static MvcHtmlStri 口 g CheckBoxWithValue( this HtmlHelper htmlHelper , string name , bool isChecked , string value) string fullHtmlFieldName = htmlHelper.ViewContext.ViewData .TemplateInfo.GetFullHtmlF 工 eldName(name); ModelState modelState; //将 ModelState 设直为表示是否勾选布尔值 if (htmlHelper.ViewData.ModelState.TryGetValue(fullHtmlFieldName , out modelState)) htmlHelper.ViewData.ModelState.SetModelValue(fullHtmlFieldName , new ValueProviderResult(isChecked , isChecked.ToString() , CultureInfo.CurrentCulture)); M 飞TcHtmlString html; try html = htmlHelper.CheckBox( 口 ame , isChecked); y 1 14 a n ·工 1IZLr1 //将 ModelState 还原 if (null != modelState) htmlHelper.ViewData.ModelState[fullHtmlFieldName] = modelState; string htmlString = html.ToHtmlString(); var index = htmlString.LastIndexOf('<'); //过滤掉类型为 "hidden" 的 元素 XElement element = XElement.Parse(htmlString.Substring(O , index)); element.SetAttributeValue("value" , value); return new MvcHtmlString(element.ToString()); private static MvcHtmlString RadioButtonCheckBoxList( HtmlHelper htmlHelper , string listName , Func elementHtmlAccessor) var listItems = ListProviders.Current.GetList 工 tems(listName); AS P. NET MVC 4 框架揭秘170 • 第 4 章 Model 元数据的解析 TagBuilder table = new TagBuilder("table"); TagBuilder tr = new TagBuilder("tr"); foreach (var listltem in listltems) TagBuilder td = new TagBuilder("td"); td.lnnerHtml += elementHtmlAccessor( 工工 stltem) .ToHtmlString(); td.lnnerHtml += listltem.Text; tr.lnnerHtrnl += td.ToString(); table.lnnerHtrnl = tr.ToString(); return new MvcHtmlStr 工 ng(table.ToString()); 方法 RadioButtonChec kB o xL ist 在生成 RadioButto nL ist 和 Chec kB o xL ist 的时候使用 进行布局。组成 RadioButto nL ist 的单个 RadioButton 最终是调用 HtmIHelper 现有的扩 展方法 RadioButton 生成的,而 Chec kB o xL ist 中的 Chec kB ox 则是通过调用我们自定义的 CheckB ox With Value 方法生成的。 Chec kB ox With Value 最终还是调用 HtmlHelper 现有的扩展 方法 Chec kB ox 生成单个 Chec kB ox 对应的 HTML 。但是该方法支持布尔值的绑定,并且会 生成一个在这里不需要的 Hidden 元素,所以不得不在调用该方法的前后"作一些手脚"。 现在来介绍应用在 Employee 属性上的四个特性的定义。如下面的代码片段所示,基于 四种"列表控件"的特性均继承自抽象特性 ListAttribute 0 ListAttribute 实现了 IMetadat aAware 接口,在实现的 O nM etadataCreated 方法中将代表列表名称的 ListName 属性添加到 Mode lM etadata 对象的 AdditionalValues 属性中。四个具体的列表特性重写了 O nM etadataCreated 方法,并在此基础上将 ModelMetadata 的 TemplateHint 分别设置为 Dropdow nL ist 、 Li stBox 、 RadioButto nL ist 矛日 Chec kB o xL ist 。 [AttributeUsage(AttributeTargets.Property)] public abstract class ListAttribute : Attribute , IMetadataAware public string ListName { get; private set; } public ListAttribute(string listName) this.ListName = listName; public virtual void OnMetadataCreated(ModelMetadata rnetadata) metadata.AdditionalValues.Add("ListName" , this.ListName); [AttributeUsage(AttributeTargets.Property)] public class DropdowηListAttribute : ListAttribute public DropdownListAttribute(string listName) base(listName) { } public override void OnMetadataCreated(ModelMetadata metadata) base.OnMetadataCreated(metadata); metadata.TemplateHint = "DropdownList"; ASP. NET MVC 4 框架揭秘4.2 Model 元数据与 Model 模板 :自 171 [AttributeUsage(AttributeTargets.Property)] public class ListBoxAttribute : ListAttribute public ListBoxAttribute(string listName) base(listName) { } public override void OnMetadataCreated(ModelMetadata metadata) base.OnMetadataCreated(metadata); metadata.TemplateHint = "ListBox"; [AttributeUsage(AttributeTargets.Property)] public class RadioButtonListAttribute : ListAttribute public RadioButtonListAttribute(string listName) base(listName) { } public override void OnMetadataCreated(ModelMetadata metadata) base.OnMetadataCreated(metadata); metadata.TemplateHint = "RadioButtonList"; [AttributeUsage(AttributeTargets.Property)] public class CheckBoxListAttribute : ListAttribute public CheckBoxListAttribute(string listName) base(listName) { } public override void OnMetadataCreated(ModelMetadata metadata) base.OnMetadataCreated(metadata); metadata.TemplateHint = "CheckBoxList"; 由于四个具体的 ListAttribute 已经对表示模板名称的 ModelMetadata 的 TemplateHint 进 行了设置,如果针对它们定义相应的 Partial View 作为对应的模板,那么在调用 HtmIHelperlHtmlHelper相应模板方法的时候就会按照这些模板对目标元素进行呈现。 实现如图 4-15 所示的效果的四个模板定义如下,它们被保存在"-Niew/SharedÆditorTemplates" 目录下面。 14 m +L h s c -qd tn s' 工 ·工 r -aut ns w 01-电αepd oo rmik nυ 自」 O巴 string listName = (string)ViewData.ModelMetadata.AdditionalValues["ListName"]; @Html.DropDownList("" , listName,Model) ASP. NET MVC 4 在架揭秘172 海 第 4 章 Model 元数据的解析 ListBox. cshtml: @model IEnumerable @{ string listName = (string)ViewData.ModelMetadata.AdditionalValues["ListName"]; @Html.ListBox("" , listName , Model) 14 m •L h s c ←」S -lqd Ln n· 工 or ←」←」ts u RU14 oe --d do amJ ‘ Ree string listName = (string)ViewData.ModelMetadata.AdditionalValues["ListName"]; @Html.RadioButtonList("" , listName , Model) CheckBoxList.cshtml: @model IEnumerable @{ string listName = (string)ViewData.ModelMetadata.AdditionalValues["ListName"]; @Html.CheckBoxList("" , listName , Model) 4.3 Model 元数据的提供机制 表示 Model 元数据的 Mode lMetadata 对象最终是通过一个名为 Mode lM etadataProvider 的组 件提供的,接下来我们着重讨论基于 Mode lMetadataProvider 的 Model 元数据提供机制及其扩展。 4.3.1 再谈 ModelMetadata 我们在前面已经对用于描述 Model 元数据的 ModelMetadata 对象进行了非常详细的介 绍,但是它还具有一些子类值得深入探讨。在之前介绍 ModelMetadata 的章节中,我们主要 讨论了它用于描述 Model 元数据的一些属性,现在来简单看看它的构造函数。 Mode lM etadata 唯一的构造函数如下面的代码片段所示。参数 provider 用于指定提供 Model 元数据的 Mode lM etadat aP rovider 对象,包含在 Properties 属性中针对属性的 Model 元 数据都是通过它来提供的。如果创建针对属性的 Model 元数据,可以通过参数 prope.吗rName 和 containerType 指定属性名称和容器类型。参数 modelType 表示对应数据类型,而 mode lA ccessor 是一个获取作为 Model 对象(数据对象)的委托。 public class ModelMetadata //其他成员 public ModelMetadata(ModelMetadataProvider provider , Type containerType , Func modelAccessor , Type modelType , string propertyName); AS P. NET MVC 4 框架揭秘4.3 Model 元数据的提供机制 滋 173 DataAnnotationsModelMetadata 由于 AS 丑陋 TMVC 采用了基于数据注解特性的声明式定义,所以 ASP.NETMVC 定义 了一个名为 System. Web.Mvc.Dat aAnn otationsModelMetadata 的类型。如下面的代码片段所 示,继承自 Mode lM etadata 的 Dat aAnn otationsModelMetadata 重写了 GetSimpleDisplayText 方法,它的返回值作为 ModelMetadata 的 SimpleDisplayText 属性值,表示简单显示文本。 public class DataAnnotationsModelMetadata : ModelMetadata public DataAnnotationsModelMetadata(DataAnnotationsModelMetadataProvider provider , Type containerType , Func rnodelAccessor , Type rnodelType , string propertyNarne , DisplayColurnnAttr 工 bute displayColurn 口 Attribute)i protected override string GetSirnpleDisplayText(); Dat aAnn otationsMode lM etadata 的构造函数在基类构造函数参数列表上提供了一个具有如下 定义的 DisplayCol umnAttribute 特性对象,它具有一个 DisplayColumn 属性用于指定作为显示文 本的属性名称。如果该特性不为 Null , GetSimpleDisplayText 方法会先获取其 DisplayColumn 属 性值,然后在 Model 类型中找到对应的属性并通过反射得到并返回具体的属性值。 [AttributeUsage(AttributeTargets.Class , Inherited=true , AllowMultiple=false)] public class DisplayColurnnAttribute : Attribute //其他成员 public DisplayColurnnAttribute(string displayColurnn); public string DisplayColurnn { get; } 以前面定义的数据类型 ContactJAddress 为例,我们在 Address 类型上添加一个字符串类型 的属性 DisplayText ,而类型 Address 上应用了 DisplayColumnAt位ibute 特性并将其 DisplayColumn 属性设置为 "DisplayText" 。那么针对一个具体 Contact 对象的 DataAnn otationsMode lM etadata 对 象来说(其 Contact 对象作为其 Model 属性) ,针对属性 Address 的 ModelMetadata 的 SimpleDisplayText 属性就是 Address 的 DisplayText 属性值。 +L c a ←」n o c s s a 14 C C .l l b u PJL //其他成员 public Address Address { get; set; } [DisplayColurnn("DisplayText")] public class Address //其他成员 public string DisplayText { get; set; } CachedDataAnnotationsModelMetadata 很多读者会认为 Dat aAnn otationsMode lM etadata 是 AS 卫 NET MVC 默认使用的描述 ASP. NET MVC 4 在架揭秘174 、 第 4 章 Model 元数据的解析 Model 元数据的类型,实则不然。 System.Web.Mvc.Cache dD at aA nnotationsMode lM etadata 才 是默认使用的 Mode lM etadata 类型,而且 Cache dD at aAnn otationsModelMetadata 和 Dat aAnn otationsMode lM etadata 没有任何关系。在介绍 Cache dD at aAnn otationsModelMetadata 之前,先来了解一下它的基类 System. Web.Mvc.Cache 仙I! odeIMetadata 。 public abstract class CachedModelMetadata : ModelMetadata protectedCachedM odelMetadata(CachedM odelMetadataprototype , Func rnodelAccessor); protected CachedModelMetadata( CachedDataAnnotationsModelMetadataProvider provider , Type containerType , Type rnodelType , string propertyNarne , TPrototypeCache prototypeCache) ; protected virtual string CornputeDataTypeNarne(); 11 其他 CornputeXxx 方法 protected TPrototypeCache public sealed override string 11 其他 Model 元数据属性 PrototypeCache { get; set; } DataTypeNarne { get; set; } Cache dM ode lM etadata采用了类似于"原型"的设计,它的一个构造 函数中会指定另一个作为原型的 Cache 仙I! odeIMetadata对象,而 PrototypeCache 属性就来源这个原型对象的同名属性。另一个构造函数接受一个用于创建 Cache dD at aAnn otationsModelMetadata 的 CachedDat aAnn otationsMode lM etadat aP rovider 对象 (笔者个人觉得这违反了"依赖倒置"的设计原则),通过它指定用于初始化 PrototypeCache 属性的参数 prot。可peCache 。 Cache dM odelMetadata 重写了所有描述 Model 元数据的属性,并且定 义相应的受保护虚方法 ComputeXxx 来最终"计算"它们的属性值。在上面给出的代码片段 中,我们仅仅列出了重写的 DataTypeName 属性和对应的 ComputeDataTypeName 方法。 ComputeDataTypeName 方法仅仅会执行一次,而计算结果会通过字段的形式保存起来,具体 的逻辑反映在如下所示的代码片段中。 public abstract class Cache dM odelMetadata : ModelMetadata 1/ 其他成员 private string dataTypeNarne; private bool _dataTypeNarneCornputed; protected virtual string CornputeDataTypeNarne() return base.DataTypeNarne; public sealed override str 工 ng DataTypeNarne get return Cache dM odelMetadata.CacheOrCornpute( new Func(this.CornputeDataTypeNarne) , ref this. dataTypeNarne , ref this. dataTypeNarneCornputed); ASP. NET MVC 4 在架揭秘4.3 Model 元数据的提供机制 由 175 set this. dataTypeName = value; this._dataTypeNameComputed = true; private static TResult CacheOrCompute(Func computeThunk , ref TResult value , ref bool computed) if (! computed) value = computeThunk()i computed = true; return value; 从上面的代码片段可以看出,用于计算某个 Model 元数据属性值的 Compute Xx x 方法 (ComputeDataTypeName) 直接返回基类的对应的属性值, CachedD a taAnn otationsModelMetadata 会重写这个方法。如下面的代码片段所示, Cache dD at aAnn otationsMode lM etadata 继承自 CachedM odelMetadata ,作为缓存 Model 元数据 信息的类型 System.Web.Mvc.Cached Dat aAnn otationsMetadat aA ttributes 中包含了用于定义 Model 元数据的所有数据注解特性。 public class CachedDataAnnotationsModelMetadata Cache dM odelMetadata public CachedDataAnnotationsModelMetadata( CachedDataAnnotationsModelMetadata prototype , Func modelAccessor); public CachedDataAnnotationsModelMetadata( CachedDat aAn notationsModelMetadataProvider provider , Type contai 口 erType , Type modelType , str 工口 gpropertyName , IEnumerable attributes); protected override string ComputeDataTypeName(); //其他重写的 ComputeXxx 方法 public class CachedDataAnnotationsMetadataAttributes public CachedDataAnnotationsMetadataAttributes(Attribute[] attributes); public DataTypeAttribute public DisplayAttribute public DisplayColumnAttribute public DisplayFormatAttribute public DisplayNameAttribute public EditableAttribute public HiddenInputAttribute public ReadOnlyAttribute public Require dAttribute public ScaffoldColumnAttribute public UIHintAttribute DataType { get; protected set; } Display { geti protected seti } DisplayColumn { get; protected set; DisplayFormat { get; protected seti DisplayName { get; protected set; } Editable { get; protected seti } HiddenInput { get; protected set; } ReadOnly { get; protected set; } Required { get; protected set; } ScaffoldColumn { geti protected seti } UIHint { geti protected set; } AS P. NET MVC 4 框架揭秘176 怡 第 4 章 Model 元数据的解析 实际上用于计算 Model 元数据属性值的 Compute Xx x 方法直接通过包含在这个 Cache dD at aAnn otationsMetadat aA ttributes 对象中对应的特性获取相应的值。下面的代码片段 体现了 ComputeDataTypeName 方法用于计算数据类型名称的逻辑。 public class CachedDataAnnotationsModelMetadata //其他成员 protected override string ComputeDataTypeName() if (base.PrototypeCache.DataType != null) return base.PrototypeCache.DataType.ToDataTypeName(null); if ((base.PrototypeCache.DisplayFormat != null) && !base. PrototypeCache. DisplayFormat.HtmlEncode) return DataType.Html.ToString(); return base.ComputeDataTypeName(); 4.3.2 ModelMetadataProvider 在 ASP.NET MVC 的 Model 元数据相关的应用编程接口中,用于创建 Model 元数据的 Mode lM etadat aP rovider 继承自抽象类 System. Web.Mvc.Mode lM etadat aP rovider 。如下面的代 码片段所示, Mode lM etadataP rovide 具有三个抽象方法: GetM etadat aF orProperties 方法用于 获取表示针对指定容器对象和类型所有属性的 Model 元数据集合; GetMetadat aF orPrope吗r 获取针对指定容器对象和类型某个具体属性对象的 Model 元数据:而 Ge tM etadat aF orType 则直接返回针对容器对象和类型的 Model 元数据。 public abstract class ModelMetadataProvider public abstract IEnumerable GetMetadataForProperties( object container , Type containerType); public abstract ModelMetadata GetMetadataForProperty( Func modelAccessor , Type containerType , string propertyName); public abstract ModelMetadata GetMetadataForType(Func modelAccessor , Type modelType); AssociatedMetadataProvider 不论是用于创建 Dat aAnn otationsMode lM etadata 的 Dat aAnn otationsMode lM etadata Provider ,还是用于创建 Cache dD at aAnn otationsMode lM etadata 的 Cache dD at aA nnotations ModelMetadataP rovider ,它们都是 System. Web.Mvc.AssociatedMetadat aP rovider 的子类。 AssociatedMetadat aP rovider 的主要作用是对应用在 Model 类型或属性上所有"关联"的特性 ASP. NET MVC 4 框架揭秘4.3 Model 元数据的提供机制 • 177 进行解析从而获取定义的 Model 元数据信息,这也是它命名的由来。如下面的代码片段所示, AssociatedMetadat aP rovider 实现了定义在 ModelMetadat aP rovider 的三个方法。 public abstract class AssociatedMetadataProvider : ModelMetadataProvider protected abstract ModelMetadata CreateMetadata( 工 Enumerable attributes , Type containerType , Func modelAccessor , Type modelType , string propertyName); public override IEnumerable GetMetadataForProperties( object container , Type containerType); public override ModelMetadata GetMetadataForProperty( Func modelAccessor , Type co 口 ta 工口 erType , string propertyName) ; public override ModelMetadata GetMetadataForType( Func modelAccessor , Type modelType); 其实针对 ModelMetadata 的创建体现在抽象方法 CreateMetadata 上,它根据作为参数提 供的特性列表得到相应的 Model 元数据信息。对于实现的定义在 ModelMetadat aP rovider 中 的三个方法来说,它们仅仅是通过反射获取应用在 Model 类型和对应属性上的所有特性,并 将这个特性列表作为参数传入抽象方法 CreateMetadata ,以返回创建的 ModelMetadata 对象。 值得一提的是,在调用 CreateMetadata 创建出的 Mode lM etadata 之后, AS 卫 NET MVC 会从特性列表中筛选出实现了 IMetadat aAware 接口的特性,并将该 ModelMetadata 对象作为 参数调用它们的 O nM etadataCreated 方法,所以实现了 IMetadat aAware 接口的特性对 Model 元数据的定制具有最高的优先级。 DataAnnotationsModelMetadataProvider System. Web.Mvc.Dat aAnn otationsMode lM etadat aP rovider 针对于 Dat aAnn otationsModel Metadata 对象的创建。如下面的代码片段所示,它是 AssociatedMetadat aP rovider 的子类。在 实现的 CreateMetadata 方法中,它会先从提供的所有特性列表中提取 DisplayColu mrtA t位 ibute 特性并调用构造函数创建一个 Dat aAnn otationsMode lM etadata 对象,然后获取用于定义 Model 元数据相关的数据注解特性并对其进行初始化。 public class DataAn notationsModelMetadataProvider : Associate dM etadataProvider public DataAnnotationsModelMetadataProvider(); protected override ModelMetadata CreateMetadata( IEnumerable attributes , Type containerType , Func modelAccessor , Type modelType , string propertyName); CachedAssociatedMetadataProvider ASP.NET MVC 用于描述 Model 元数据的 CachedDat aAnn otationsMode lM etadata 是通过 对应的 CachedDat aA nnotationsModelMetadat aP rovider 提供的,而后者直接继承自具有如下定 义的抽象类 System. Web.Mvc.Cache dA ssociate dM etadat aP rovider ,泛型参 ASP. NET MVC 4 框架揭秘178 • 第 4 章 Model 元数据的解析 数 TModelMetadata 继承自 Mode lM etadata 。 public abstract class Cache dAssociatedMetadataProvider AssociatedMetadataProvider where TModelMetadata: ModelMetadata protected sealed override ModelMetadata CreateMetadata( IEnumerable attributes , Type containerType , Func modelAccessor , Type modelType , string propertyName)i protected abstract TModelMetadata CreateMetadataFromPrototype( TModelMetadata prototype , Func modelAccessor)i protected abstract TModelMetadata CreateMetadataPrototype( IEnumerable attributes , Type co 口 ta 工口 erType , Type modelType , string propertyName)i protected CacheltemPolicy protected ObjectCache CacheltemPolicy { geti seti } PrototypeCache { geti seti } 基于对创建的 Model 元数据的缓存实现在 Cache dA ssociatedMetadat aP rovider 中。如上面的代码片段所示,它具有一个类型为 ObjectCache 的受保护属 性 PrototypeCache ,它实现了对针对某个类型或者定义在某个类型中某个属性所创建的 Model 元数据的缓存。 具体来说,在 Cache dA ssociate dM etadataP rovider中重写的 CreateMetadata 方法会根据 Model 类型和属性名称(针对基于属性的 Model 元数据创建)生成一个 Key ,并 借助 0 均 ectCache 对象从缓存中获取预先被缓存的 Mode lM etadata 对象。如果存在着这么一 个被缓存的 Mode lM etadata 对象, ASP.NET MVC 会以此为原型调用抽象方法 CreateMetadat aF ro mP rototype 创建并返回一个新的 ModelMetadata 对象,否则会调用另一个 抽象方法 CreateMetadat aP rot。可 pe 创建作为原型的 Mode lM etadata 对象,该对象在被用于创 建返回的 ModelMetadata 之前会被缓存。 默认情况下缓存的过期时间为 20 分钟,如果我们对包括过期时间在内的缓存策略进行 定制,可以通过其受保护属性 Cache It e mP olicy 来实现。 CachedDataAnnotationsModelMetadataProvider System. Web.Mvc.Cache dD ataAnn otationsMode lM etadataP rovider 用于实现针对默认 Model 元数据类型 CachedDat aAnn otationsModelMetadata 的提供。如下面的代码片段所示,它直接 继承自 CachedAssociate dM etadat aP rovider ,实现的两 个方法 Cre 剖 eMetadat aF ro mP rototype/CreateMetadat aP rototype 直接创建并返回一个 Cache dD at aAnn otationsModelMetadata 对象。 public class CachedDataAnnotationsModelMetadataProvider Cache dAssociatedMetadataProvider protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(CachedDataAn notationsModelMetadata prototype , AS P. NET MVC 4 框架揭秘4.3 Model 元数据的提供机制 黯 179 Func rnodelAccessor) return new CachedDataAn notationsModelMetadata (prototype , rnodelAccessor) i protected override CachedDataAnnotationsModelMetadata CreateMetadataPrototype(IEnurnerable attributes , Type containerType , Type rnodelType , string propertyNarne) return 口 ew CachedDataAnnotationsModelMetadata(this , containerType , rnodelType , propertyNarne , attributes)i 默认使用的 ModelMetadat aP rovider 通过 System. Web.Mvc.Mode lM etadat aP roviders 来获 取。如下面的代码片段所示, Mode lM etadat aP roviders 具有一个 Mode lM etadat aP rovider 类型 的静态可读可写属性 Current 用于获取和设置当前使用的 Mode lM etadat aP rovider 。在默认情 况下, Current 属性返回的就是一个 CachedDat aAnn otationsMode lM etadat aP rovider 对象。 public class ModelMetadataProviders public static ModelMetadataProvider Current { geti seti } 图 4-16 揭示了包括各种 Mode lM etadata 和对应 Mode lM etadat aP rovider 在内的整个 Model 元数据提供系统中各种类型及其相互关系。 ModelM 创adalaProvi ders Modcl 秘的 adata Data An notatlon 甜。由1 始自>t a 邮a , Curren1 _l.._一一---- CaehedModeIMe1ad a1 a CachedDa 妇 AnnotationsModelMeladata AssocÎwl:edMetadataProvider Prov陷e Pn刻地e CachedDat aA nnoiationsModelMet甜 ataProvÎder 图 4-16 Model 元数据提供系统中的 ModelMetadata 和 ModelMetadataProvider ASP. NET MVC 4 框架揭秘180 翻 第 4 章 Model 元数据的解析 ViewData 与 Model 元数据 如下面的代码片段所示,作为 Contro l1 er 基类的 Contro l1 erBase 具有一个类型为 System. Web.Mvc. ViewDataD ictionary 的 ViewData 对象,用于存储向 View 传递的数据。 ViewDataD ictionary 具有一个 ModelMetadata 属性返回相应的 Model 元数据,也就是说,不论 是在 Con位ol1er 还是在 View 中,只要具有一个不为 Nul1的 Model 对象(ViewDataDictionary 的 Model 属性), ASP.NET MVC 就可以根据 ViewData 获取针对该 Model 对象的 Model 元数据。 public class ViewDataDictionary : IDictionary, ICollection> , IEnumerable>, IEnumerable //其他成员 public object Model { get; set; } public virtual ModelMetadata ModelMetadata { get; set; } public abstract class ControllerBase : IController //其他成员 public ViewDataDictionary ViewData { get; set; } 4.3.3 Model 元数据提供系统的扩展 对 Model 元数据提供系统的扩展主要体现在对 ModelMetadataProvider 自定义上。基于 标注特性的元数据定义方式最终是通过 CachedDataAnnotationsModelMetadataProvider 来实 现,通过自定义 ModelMetadataProvider 完全可以提供一种全新的 Model 元数据定义方式。 不过经常使用的方式还是让自定义 ModelMetadataProvider 继承 CachedD ataAnnotationsModel MetadataProviderbing 并在现有元数据提供机制上做一些扩展。 实例演示:通过自定义 ModelMetadataProvider 定制 Model 元数据 (S413 ) 在本章的第 1 节中我们创建了一个用于控制目标元素显示名称的 DisplayTextAttribute 特 性。 408) 。该特性支持基于资源文件的本地化,并且可以省去对资源条目名称和资源类型 的显式指定。该 DisplayTextAt位ibute 特性是通过实现 IMetadataAware 接口的形式实现的,现 在我们将它转换成基于自定义 ModelMetadataProvider 的实现方式。 对于之前定义的 DisplayTextAttribute 特性,只需要对其进行简单的修改。如下面的代码 片段所示,我们删除了它实现的 IMetadataAware 接口,将实现的 OnMetadataCreated 方法名 改成 SetDisplayName 。 [AttributeUsage(AttributeTargets.Class I AttributeTargets.Property)] public class DisplayTextAttribute : Attribute ASP. NET MVC 4 提架揭秘4.3 Model 元数据的提供机制 黯 181 //其他成员 public void SetDisplayName(ModelMetadata rnetadata) this.DisplayNarne = this.DisplayNarne ??(rnetadata.PropertyNarne ?? rnetadata.ModelType.Narne); if (null == this.ResourceType) rnetadata.DisplayNarne = this.DisplayNarne; return; PropertyInfo property = this.ResourceType.GetProperty(this.DisplayNarne, BindingFlags.NonPublic I BindingFlags.Public I BindingFlags.Static); rnetadata.DisplayNarne = property.GetValue(null, null) .ToString(); public static void SetResourceType(Type resourceType) staticResourceType = resourceType; 为了将 DisplayTextAttribute 应用到 Model 元数据的初始化过程中,通过继承 CachedDataAnnotationsModelMetadataProvider 创建了如下一个 ExtendedDataAnnotationsProvider 。 在重写的 CreateMetadataPrototype 方法中,我们调用基类同名方法创建了作为原型的 ModelMetadata 对象,然后根据应用的 DisplayTextA仕ribute 特性对它的 DisplayName 属性进 行了定制。在重写的 Cre剖eMetadataFromPrototype 方法中,我们让最终创建的 ModelMetadata 对象与作为原型的 ModelMetadata 并具有相同的 DisplayName 属性(在默认情况下,总是根 据应用的 DisplayA时ibute/D i 叩layNameA忧ribute 特性进行设置〉。 public class ExtendedDataAnnotationsProvider CachedDataAnnotationsModelMetadataProvider protected override CachedDataAnnotationsModelMetadata CreateMetadataPrototype(IEnurnerable attributes, Type containerType, Type rnodelType, string propertyNarne) CachedDataAnnotationsModelMetadata rnodelMetadata = base.CreateMetadataPrototype(attributes, containerType, rnodelType, propertyNarne); if (string.IsNullOrErnpty(rnodelMetadata.DisplayNarne)) DisplayTextAttribute displayTextAttribute = attributes .OfType() .FirstOrDefault(); if (null != displayTextAttribute) displayTextAttribute.SetDisplayNarne(rnodelMetadata); return rnodelMetadata; protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(CachedDat aA口口otationsModelMetadata prototype, Func rnodelAccessor) ASP. NET MVC 4 在东揭秘182 海 第 4 章 Model 元数据的解析 CachedDataAnnotationsModelMetadata modelMetadata = base.CreateMetadataFromPrototype(prototype , modelAccessor}; modelMetadata.DisplayName = prototype.DisplayName; return modelMetadata; 对于之前创建的演示实例。 408) ,如果我们在 Global. asax 中通过如下的方式对自定义 的 Extende dD at aAnn otationsProvider 进行注册,该实例应用同样可以正常运行。 public class MvcApplication : System.Web.HttpApplication //其他成员 protected void Application_Start(} //其他操作 DisplayTextAttribute.SetResourceType(typeof(Resources}}; ModelMetadataProviders.Current = new ExtendedDataAn notationsProvider(}; 本章小结 Model 元数据是针对数据类型的一种描述信息,它通过提供针对数据类型本身及其成员 属性的描述信息控制数据在界面上的呈现方式。 Model 元数据同时也为 Model 绑定和验证提 供必不可少的元数据信息。 ASP.NETMVC 默认提供基于数据注解特性的 Model 元数据定义 方法,除了利用预定义的数据注解特性对目标元素(数据类型或其属性〉的 Model 元数据进 行定制之外,还可以通过实现 IMetadat aAware 接口定义相应的特性对最终的 Model 元数据 进行灵活控制。 Model 元数据成就了基于模板的数据呈现方式, ASP.NET MVC 提供了一系列默认的模 板用于控制目标数据在显示或者编辑模式下 HTML 的生成。还可以通过定义 View 的方式创 建自定义的模板,使针对某些类型的数据按照我们希望的方式呈现在界面上。 AS 卫 NETMVC 具有一个可扩展的以 Mode lM etadat aP rovider 为核心的 Model 元数据提供 系统,默认采用的 Mode lM etadat aP rovider 类型为 Cache dD ataAnn otationsModelMetadata Provider ,由它提供的 Model 元数据通过一个 Cache dD at aAnn otationsMode lM etadata 表示。除 了通过相应的数据注解特性来定制 Model 元数据之外,还可以通过自定义 Mode lM etadat aP rovider 的方式来从根本上控制最终提供的 Model 元数据。 ASP. NET MVC 4 在~;Vg揭翻第 5 章 Model 的绑定 针对请求的处理和响应最终体现在对激活 Controller 对象中相应 Action 方法的执行上,而 Action 方法执行的前提在于能够根据请求正确地提供相应 的参数列表。为目标 Action 方法提供参数列表是 Model 绑定的使命,所以 Model 绑定在整个 ASP.NETMVC 框架体系中具有重要的地位。 ASP. NET MVC 4 框架揭秘184 回 第 5 章 Model 的绑定 5.1 ControllerDescriptor 、 ActionDescriptor 与 ParameterDescriptor Model 绑定本质上就是为目标 Action 方法生成参数列表的过程。作为 Action 方法参数 的数据存在于当前的 HTTP 请求中,它们可能包含在请求的 URL 中,也可能包含在请求消 息的报头或者主体中。究竟哪个部分应该作为 Action 方法某个参数的数据源,这依赖于用于 描述参数的元数据信息。 Action 方法参数的元数据通过 System. Web.Mvc.ParameterDescriptor 来描述,而另外两个相关的类型 System. Web.Mvc.Con位ollerDescriptor 和 System. Web.Mvc. ActionDescriptor 则用于描述 Controller 和 Action 。 5.1.1 ControllerDescriptor ControllerDescriptor 包含了用于描述某个 Controller 的元数据信息。如下面的代码片段 所示, ControllerDescriptor 具有三个属性,其中 Con位ollerName 和 ControllerType 分别表示 Con位oller 的名称和类型,前者来源于 URL 路由系统针对请求 URL 的解析得到的路由数据。 字符串类型的 Uniqueld 属性表示 ControllerDescriptor 的唯一标识,该标识由 Con位ollerDescriptor 本身的类型、 Controller 的类型以及 Controller 的名称三者派生。 public abstract class ControllerDescriptor : ICustornAttributeProvider public virtual object[] GetCustornAttributes(bool inherit); public virtual object[] GetCustornAttributes(Type attributeType, bool inherit); public virtual bool IsDefined(Type attributeType, bool inherit); public virtual IEnumerable GetFilterAttributes( bool useCache); public abstract ActionDescriptor FindAction( ControllerContext controllerContext, string actionName); public abstract ActionDescriptor[] GetCanonicalActions(); qdqd ne 口 ipi ryr 』L 巾 4 ←」 SS 』L lc1-aaa uru ←L ← L ←」 rsr ·工、 D-l vav Cec --·工·工111 bb' 。 uuu p&PP& 卡」』 L ee gq 1·J {{ ., ee 卡】 mp&e avdqJ NT rrIt ee --d 1414γ4 0oe rru ttq R口 -l oon ccu public interface ICustornAttributeProvider object[] GetCustornAttributes(bool inherit); object[] GetCustornAttributes(Type attributeType, bool inherit); bool IsDefined(Type attributeType, bool inherit); ControllerDescriptor 实现了 System.Reflection.ICustomAttributeProvider 接口,意味着我 们可以通过调用 GetCustomAttributes 方法获取应用在 Controller 类型上相应的特性,也可以 ASP.NET MVC 4 在架揭秘5.1 ControllerDescript。人 ActionDescriptor 与 ParameterDescriptor 事 185 调用 IsDefmed 方法判断指定的自定义特性类型是否应用在对应的 Controller 类型上。另一个 方法 GetFilterAttributes 用于获取应用在 Con位oller 上的所有筛选器特性,我们会在第 7 章 " Action 方法的执行"中对筛选器进行详细地介绍。 Con位ollerDescriptor 类型本身并没有通过对 Controller 类型实施反射来得到相应的特性。 两个 GetCustomAttributes 方法均返回一个空的数组对象, IsDefined 方法则直接返回 False , 而方法 GetFilterAttributes 返回一个元素类型为 FilterAttribute 的空数组对象。 Con位ollerDescriptor 的抽象方法 FindAction 根据指定的 ControllerContext 得到用于描述指 定 Action 的 ActionD escriptor 对象,参数 actionName 表示 Action 的名称, GetCanonicalActions 方法则返回一个 ActionD escriptor 数组,代表描述定义在当前 Controller 中的所有"候选 Action" 。 ReflectedControllerDescriptor ASP.NETMVC 定义了一个 System. Web.Mvc.ReflectedControllerDescriptor 类型,它是抽 象类型 ControllerDescriptor 的唯一继承者。 ReflectedControllerDescriptor 通过对 Controller 类 型的反射得到用于描述 Controller 的元数据。如下面的代码片段所示,表示 Controller 类型 的 ControllerType 属性在构造函数中指定。 public class ReflectedControllerDescriptor : ControllerDescriptor public ReflectedControllerDescriptor(Type controllerType); public override object[] GetCustomAttributes(bool inherit); public override object[] GetCustomAttributes(Type attributeType, bool inherit); public override bool IsDefined(Type attributeType, bool inherit); public override IEnumerable GetFilterAttributes( bool useCache); public override ActionDescriptor FindAction( ControllerContext controllerContext, string actionName); public override ActionDescriptor[] GetCanonicalActions(); public sealed override Type ControllerType { get; } 由于基类 ControllerDescriptor 并没有真正实现定义在接口 ICustomAttributeProvider 中的 三个方法,用于获取筛选器特性列表的 GetFilterAt位ibutes 方法返回的也是一个空的 FilterAttribute 数组,所以 ReflectedControllerDescriptor 重写了这些方法。 GetCanonicalActions 方法返回的 ActionD escI悄or 数组通过对定义在 Controller 类型的 Action 方法进行反射得到。对于定义在当前 Controller 类型中的所有方法成员来说,能够被 视为 Action 方法的仅限于"公有"的"实例"方法,但并不包括从抽象类 Controller 中继承 下来的方法。 在默认情况下,方法的名称被视为 Action 的名称,所以当调用 FindAction 方法的时候, ASP.NET MVC 4 框架揭秘186 • 第 5 章 Model 的绑定 AS钮卫 N陌ETMVC 会从描述 区分大小写)的 Actωion曲De臼scααri年桐i毕桐pμt阳O町r 对象。但是我们并不一定需要将两者 (Action 名称和方法 名称)强行绑定在一起,为了让 Action 名称能够独立于目标方法名称,可以在 Action 方法 上应用具有如下定义的 System. Web.Mvc.Actio nN ameAttribute 特性来指定 Action 的名称。 [AttributeUsage(AttributeTargets.Method , AllowMultiple=false , Inherited=true)] public sealed class ActionNameAttribute : ActionNameSelectorAttribute public ActionNameAttribute(string name)i public override bool IsValidName(ControllerContext controllerContext , string actionName , Methodlnfo methodlnfo)i public string Name { geti } Actio nN ameAttribute 继承自具有如下定义的 System. Web.Mvc.ActionN ameSelector Attribute 抽象类。 Actio nN ameSelector Attribute 通过其抽象方法 IsValidName 判断指定的 Action 名称是否与目标 Action 方法相匹配。定义在 Actio nN ameAttribute 中的 IsValidName 方法的逻 辑很简单,它仅仅判断指定的 Action 名称是否和指定的名称一致(忽略大小写)。 [AttributeUsage(AttributeTargets.Method , AllowMultiple = false , Inherited = true)] public abstract class ActionNameSelectorAttribute : Attribute public abstract bool IsValidName(ControllerContext controllerContext , string actionName , Methodlnfo methodlnfo)i 很多人会将 Actio nN ameSelectorAttribute 与 System. Web.Mvc.ActionMethodSelectorAttribute (定义如下)混淆,虽然两者都具有对候选 Action 进行筛选的作用,不过前者主要针对 Action 名称进行筛选,后者通过抽象的方法 Is ValidF orRequest 判断目标 Action 方法是否与当前的请 求相匹配。 [AttributeUsage(AttributeTargets.Method , AllowMultiple = false , Inherited = true)] public abstract class ActionMethodSelectorAttribute : Attribute public abstract bool IsValidForRequest(ControllerContext controllerContext , Methodlnfo methodlnfo)i ASP.NETMVC 定义了如下 7 个基于相应 HTTP 方法 (GET 、 POST 、 PUT 、 DELETE 、 Head 、 Options 和 Patch) 的 ActionMethodSelectorAttribute 类型。当将它们应用到某个 Action 方法上时,只有在当前请求的 HTTP 方法与之相匹配的情况下目标 Action 方法才 会被选择。 • System. Web.Mvc.HttpGetAttribute • System. Web.Mvc.HttpPos tA忧 ribute • System.Web.Mvc.H忧pPu tA仕 ribute ASPNETMVC4 在架揭秘5.1 ControllerDescriptor 、 ActionDescriptor 与 ParameterDescriptor 黯 187 • System. Web.Mvc.HttpDeleteAttribute • System. Web.Mvc.HttpHeadAttribute • Sy严st臼em • Sy严f③st臼em.We由b.Mvc.Ht忧tpPa创tchA t盯t衍ri池bu川1川te 除了上面 7 个基于某种 HTTP 方法的 Actio nM ethodSelectorAttribute 特性之外, ASP.NET MVC 还定义了另一个名为 System.Web.Mvc.AcceptVerbsAttribute 的特性。 AcceptVerbs Attribute 的不同之处在于可以指定多个匹配的 HTTP 方法。如下面的的代码片段所示, AcceptVerbsAttribute 具有一个 ICollection 类型的只读属'性 Verbs ,表示目标 Action 方 法支持的 HTTP 方法列表 (HTTP Method 又被称为 HTTP Verb) ,该属性在构造函数中被初 始化。实际上述的 7 个 Actio nM ethodSelectorAttribute 在内部使用了 AcceptVerbsAttribute 特 性实现了具体的 Action 方法选择逻辑。 [AttributeUsage(AttributeTargets.Method , AllowMultiple=false , Inherited=true)] public sealed class AcceptVerbsAttribute : ActionMethodSelectorAttribute public AcceptVerbsAttribute(HttpVerbs verbs); public AcceptVerbsAttribute(params string[] verbs); public override bool IsValidForRequest(ControllerContext controllerContext , MethodInfo methodInfo); public ICollection Verbs {get; } s b r e v p ←」←」口um u n e sc qJ· 工 a1 14b FU [p{ Get = 1 , Post = 2 , Put = 4 , Delete = 8 , Head = 16 , 从上面的代码片段可以看出, AcceptVerbsAttribute 具有两个构造函数,其参数类型分别 是 System. Web.Mvc.HtφVe由 s 枚举和字符串数组。由于 AcceptVerbsAttribute 枚举应用了 FlagsAttribute 特性,可以使用操作符 "1" 指定多个 HTTP 方法。如下所示的两种在 Action 方法 UpdateContact 上应用 AcceptVerbsAttribute 特性的方式是等效的。顺便提一下,通过字 符串指定的 HTTP 方法是不区分大小写的。 //使用 HttpVerbs 枚举表示 HTTP 方法 public class ContactController [AcceptVerbs(HttpVerbs.PutIHttpVerbs.PostIHttpVerbs.Delete)] public ActionResult UpdateContact(Contact contact) //省略实现 AS P. NET MVC 4 在架揭秘188 • 第 5 章 Model 的绑定 //使用字符串表示 HTTP 方法 public class ContactController [AcceptVerbs("PUT" , "POST" , "DELETE")] public ActionResult UpdateContact(Contact contact) //省略实现 除了上面 8 个基于 HTTP 方法的 Actio nM ethodSelectorAttribute 特性, ASP.NET MVC 还 定义了另一个具有如下定义的 System. Web.Mvc.No nA ctio nA ttribute 特性。顾名思义,应用了 N o nA ctio nA ttribute 特性的方法将不会被认为是一个 Action 方法。由于其 Is ValidForRequest 方法直接返回 False ,所以在根据请求进行目标 Action 方法选择的时候,这样的方法总是被 排除在候选范围之内。 [AttributeUsage(AttributeTargets.Method , AllowMultiple = false , Inherited = true)] public sealed class NonActionAttribute : ActionMethodSelectorAttribute public override bool IsVal 工 dForRequest(ControllerContext controllerContext , Methodlnfo methodlnfo) return false; 当 Fin dA ction 方法执行的时候,候选 Actio nD escriptor 列表中与请求 Action 名称相匹配 的 Actio nD escriptor 被筛选出来,然后利用应用在对应方法上的 ActionN ameSelector Attribute 和 Actio nM ethodSelectorAttribute 特性做进一步筛选。如果最终没有一个符合条件的 Actio nD escriptor ,该方法会返回 Nu l1 C 最终会抛出一个状态码为 404 的 H忧 pException 异常)。 如果具有多个匹配的 Actio nD escriptor ,该方法会直接抛出 System.Re f1 ection. Am biguous Matc hE xception 异常,也就是说对于每一次请求,要求有且只有一个匹配的 Actio nD escriptor 。 ReflectedAsyncControllerDescriptor System. Web.Mvc.Async.Re f1 ecte dA syncControllerDescriptor 类型为 Re f1 ectedCon仕 o l1 er Descriptor 的异步版本,如下面的代码片段所示,两者具有类似的成员定义。实际上除了 Fin dA ction 和 GetCanonicalActions 两个方法外,其他方法的实现逻辑与 Re f1 ectedController Descriptor 完全一致。 public class Reflecte dAsyncControllerDescriptor : ControllerDescriptor public Reflecte dAsyncControllerDescriptor(Type controllerType); public override object[] GetCustomA ttributes(bool inherit); public override object[] GetCustomA ttributes(Type attributeType , bool inherit); public override IEnumerable GetFilterAttributes( ASP. NET MVC 4 框架揭秘5.1 ControllerDescriptor 、 ActionDescriptor 与 ParameterDescriptor 黯 189 bool useCache); public override bool IsDefined(Type attributeType, bool inherit); public override ActionDescriptor FindAction( ControllerContext controllerContext, string actionName); public override ActionDescriptor[] GetCanonicalActions(); public sealed override Type ControllerType { get; } ReflectedAsyncControllerDescriptor 的 GetCanonicalActions 总是返回一个空的 ActionD escriptor 数组。对于继承自 AsyncContro ller 的 Controller 类型,一个异步 Action 方 法由两个匹配的方法 XxxA sync/XxxCompleted 构成, FindAction 方法在根据方法名称进行匹 配的时候会自动忽略掉方法名称的" Async" 和" Completed" 后缀。 5.1.2 ActionDescriptor 用于描述 Action 的 ActionD escriptor 对象对应着定义在 Controller 类型中的某个 Action 方法。如下面的代码片段所示,它具有 ActionName 和 Con位ollerDescriptor 两个抽象只读属 性,它们分别表示 Action 名称和描述所在 Controller 的 ControllerDescriptor 对象。表示唯一 标识的 UniqueId 属性由自身类型、 Controller 的类型与 Action 名称三者派生。 public abstract class ActionDescriptor : ICustomAttributeProvider public virtual object[] GetCustomAttributes(bool inherit); public virtual object[] GetCustomAttr 工 butes(Type attributeType, bool inherit); public virtual bool IsDef 工 ned(Type attributeType, bool inherit); public virtual IEnumerable GetFilterAttributes( bool useCache); public abstract ParameterDescriptor[] GetParameters(); public abstract object Execute(ControllerContext controllerContext, IDictionary parameters); public virtual ICollection GetSelectors(); public virtual FilterInfo GetFilters(); public abstract string ActionName { get; } public abstract ControllerDescriptor ControllerDescriptor { get; } public virtual string UniqueId { get; } ActionDescriptor 同样实现了 ICustomAttributeProvider 接口,可以通过方法 GetCustomAttributes 得到应用在 Action 方法上的相关特性,或者借助 IsDefmed 方法判断某 种指定的特性类型是否应用在对应的 Action 方法上。 GetF ilterAttributes 方法用于返回应用在 Action 方法上的所有筛选器特性。与 Con衍。llerDescriptor 一样, ActionD escriptor 并没有真正 采用反射去解析应用在 Action 方法上的特性,它的 GetCustomA伽ibutes 和 GetFilterAt位ibutes 方法返回的都是一个空数组,而 IsDefmed 方法直接返回 False 。 ASP. NET MVC 4 框架揭秘190 • 第 5 章 Model 的绑定 Action 方法的每一个参数通过一个 ParameterDescr恫 or 对象来描述, Actio nD escriptor 的 抽象方法 GetParameters 返回一个描述所有参数的 ParameterDescriptor 数组。另一个重要的抽 象 Execute 方法实现了最终对 Action 的执行,该方法的两个参数分别表示当前 Contro l1 erContext 和参数列表。 GetSelectors 返回一组 ActionSelector 对象,实际上是一组 System. Web.Mvc.ActionSelector 类型的委托对象。如下面的代码片段所示, ActionSelector 委托具有一个类型为 Con仕 o l1 erContext 的参数,布尔类型的返回值表示目标 Action 方法是否与指定的 Con位 o l1 erContext 相匹配。该方法默认返回的是一个空的 ActionSelector 集合。 public delegate bool ActionSelector(ControllerContext controllerContext); Actio nD escriptor 的 GetF i1 ters 方法返回的是一个 System. Web.Mvc.F i1 te rI nfo 对象,可以 通过它得到应用在该 Action 方法上所有的筛选器。如下面的代码所示, F i1 te rI nfo 具有四个 只读的集合属性,分别表示四种类型的筛选器( Actio nFi1 ter 、 Authorizatio nFi1 ter 、 Exceptio nFi1 t町和 Resul tFi1 ter) 。在这里返回的是一个空的 F i1 te rI nfo 对象,它的四个集合属 性并不包含任何元素。 。在Ln T4 r e •L 、土.l F s qM a 14 C C .-1 占 'p u p{ public IList ActionFilters { get; } public IList< 工 AuthorizationFilter> AuthorizationFilters { get; } public IList ExceptionFilters { get; } public IList ResultFilters { get; } AsyncActionDescriptor 异步版本的 Actio nD escriptor 通过 System. Web.Mvc.Async.AsyncActio nD escriptor 类型表 示,如下面的代码片段所示, As严lcActio nD escriptor 继承自抽象类 Actio nD escriptor 。除了重写 Execute 方法之外,它还定义了两个用于异步执行 Action 的抽象方法 Beg inExecute/ En dE xecute 。 public abstract class AsyncActionDescriptor : ActionDescriptor public abstract IAsyncResult BeginExecute( ControllerContext controllerContext , IDictionary pararneters , AsyncCallback callback , object state); public abstract object EndExecute(IAsyncResult asyncResult); public override object Execute(ControllerContext controllerContext , IDictionary pararneters); 实际上 AsyncActio nD escriptor 重写的 Execute 方法并没有真正去指定 Action ,而是直接 抛出一个 InvalidOperatio nE xception 异常,意味着异步方法不能通过 Execute 方法以同步方式 执行。 ASP. NET MVC 4 在架锺秘5.1 ControllerDescriptor 、 ActionDescriptor 与 ParameterDescriptor _ 191 ReflectedActionDescriptor Re f1 ectedControllerDescriptor 的 Fin dA ction 和 GetCanonica lA ctions 方法返回的 Actio nD escriptor 对象类型为 System. Web.Mvc.ReflectedActio nD escriptor 对象,它针对 Action 元数据信息的解析同样是通过对目标方法进行反射实现的。如下面的代码片段所示, ReflectedActio nD escriptor 直接继承自 Actio nD escriptor ,分别表示 Action 名称、所在 Controller 的描述以及 Action 方法的只读属性 Actio nN ame 、 ControllerDescriptor 和 MethodInfo 均在构 造函数中初始化。 public class Reflecte dActionDescriptor : ActionDescriptor public Reflecte dActionDescriptor(MethodInfo methodInfo , string actionName , ControllerDescriptor controllerDescriptor)j public override object[] GetCustomA ttributes(bool inherit)j public override object[] GetCustomA ttributes(Type attributeType , bool inherit); public override bool IsDefined(Type attributeType , bool inherit)j public override IEnumerable GetFilterAttributes( bool useCache)j public override ParameterDescriptor[] GetParameters()j public override object Execute(ControllerContext controllerContext , IDictionary parameters)j public override ICollection GetSelectors()j public override string public override ControllerDescriptor public MethodInfo public override string ActionName { getj } ControllerDescriptor { getj } MethodInfo { getj} UniqueId { getj } 由于基类 Actio nD escriptor 并没有真正地实现定义在接口 ICusto mA t位 ibuteProvider 中的 三个方法,获取筛选器特性列表的 GetFilters 方法返回的也只是一个空的 F i1 terlnfo 对象,所 以 Re f1 ecte dA ctio nD escriptor 重写了这些方法。 Re f1 ecte dA ctio nD esc 邱 tor 还重写了 UniqueId 属性,在现有的基础上将表示 Action 方法 的 MethodInfo 对象作为了决定元素之一。也就是说,作为唯一标识的 UniqueId 属性通过自 身的类型、 Controller 类型、 Action 名称和表示目标 Action 方法的 MethodInfo 对象囚者派生。 对于通过方法 GetParameters 返回的用于描述所有参数的 ParameterDescriptor 数组,也是 通过对 Action 方法的参数列表进行反射来创建的。 Execute 方法最终传入参数列表调用 MethodInfo 对象执行 Action 方法。它的 GetSelectors 方法返回一组应用在 Action 方法上的 Actio nM ethodSelectorAttribute 特性列表。 ReflectedAsyncAction Descr怡 tor 异步的 ReflectedControllerDescr怡 tor 由 System ASP. NET MVC 4 框架揭秘192 辛酸 第 5 章 Model 的绑定 Descriptor 类型表示,它用于描述定义在 AsyncCon位 oller 中以 X xxA sync lXx xCompleted 形式 定义的异步 Action 。一个 Reflecte dA syncActionDescr悄 or 对象通过代表这两个方法的 MethodInfo 对象来创建。如下面的代码片段所示, Reflecte dA syncActio nD escriptor 的构造的 参数 asyncMetho dI nfo 和 complete dM e出 odInfo 就代表这两个 MethodInfo 。在构造函数中初始化 的这两个 Method In fo 对象分别通过只读属性 AsyncMethodInfo 和 Complete dM etho dIn fo 返回。 public class Reflecte dAsyncActionDescriptor : AsyncActionDescriptor public Reflecte dAsyncActionDescriptor(MethodInfo asyncMethodInfo , MethodInfo completedMethodInfo , string actionName , ControllerDescriptor controllerDescriptor)i public override IAsyncResult BegiηExecute( ControllerContext controllerContext , IDictionary parameters , AsyncCallback callback , object state)i public override object EndExecute(IAsyncResult asyncResult)i public override object[] GetCustomA ttributes(bool inherit); public override object[] GetCustomA ttributes(Type attributeType , bool inherit)i public override IEnumerable GetFilterAttributes( bool useCache)i public override ParameterDescriptor[] GetParameters()i public override ICollection GetSelectors(); public override bool IsDefined(Type attributeType , bool inherit)i public override string public MethodInfo public MethodInfo public override ControllerDescriptor public override string ActionName { geti } AsyncMethodI 口 fo { geti } CompletedMethodInfo { geti} ControllerDescriptor { get; } UniqueId { geti } ReflectedAsyncActio nD escriptor 用于特性解析(定义在 ICusto mAt 位 ibuteProvider 接口中 的三个方法、用于获取筛选器特性列表的 GetF i1 terAttributes 方法以及获取 ActionMethod SelectorAttribute 特性列表的 GetSelectors 方法〉和参数描述解析( GetParameters 方法)的方 法都是通过对XxxA sync 方法〈对应于 AsyncMethodInfo 属性)的反射实现的。它的 Beg inE xecuteÆndExecute 方法最终利用了 AsyncMethodInfo 和 CompletedMethodInfo 实现了 对 Action 的异步执行。 Tas kA syncActionDescriptor 异步 Action 除了以配对的 X xxA sync lXxx Completed 方法进行定义之外,还可以通过一 个返回类型为 System. Threading. Tasks. Task 的方法来定义。前者必须定义在 AsyncController 中,后者则可以定义在普通的 Controller 中并通过 System. Web.Mvc.Async. TaskAsync Actio nD escriptor 对象来描述。如下面的代码片段所示, Tas kA syncActio nD escriptor 通过一个 类型为 MethodInfo 的只读属性 Tas kM ethodInfo 表示异步 Action 方法,该属性在构造函数中 初始化。 ASP. NET MVC 4 框架揭秘5.1 ControllerDescriptor 、 ActionDescriptor 与 ParameterDescriptor _ 193 public class Tas kA syncActionDescriptor : AsyncActionDescriptor public TaskAsyncActionDescriptor(MethodInfo taskMethodInfo , string actionName , ControllerDescriptor controllerDescriptor)i public override IAsy 口 cResult BeginExecute( ControllerContext controllerContext , IDictionary parameters , AsyncCallback callback , object state)i public override object EndExecute(IAsyncResult asyncResult)i public override object Execute(ControllerContext controllerContext , IDictionary parameters)i public override object[] GetCustomA ttributes(bool inherit)i public override object[] GetCustomA ttributes(Type attributeType , bool inherit)i public override IEnumerable GetFilterAttributes( bool useCache)i public override ParameterDescriptor[] GetParameterS()i public override ICollection GetSelectorS()i public override bool IsDefined(Type attributeType , bool inherit)i public override string public override ControllerDescriptor public MethodInfo public override stri 口 q ActionName { geti } ControllerDescriptor { geti } TaskMethodInfo { geti } UniqueId { geti } Tas kA syncActio nD escriptor 对特性解析和参数描述解析的方法都是通过针对 Tas kM ethodlnfo 的反射来完成的,用于异步执行 Action 操作的 Begi nE xecuteÆndExecute 方 法也是借助于这个 Methodlnfo 对象实现的。 Tas kA syncActio nD escriptor 重写了 Execute 方法 并在其中直接抛出异常。 不论是 Controller 还是 Action ,都具有同步和异步两个版本,至于异步 Controller 和两 种形式的异步 Action 方法如何定义,以及同步和异步 ControllerDescriptor/ActionD escriptor 分别在什么情况下被创建,我们将在第 7 章" Action 的执行"中进行详细介绍。 5.1.3 ParameterDescriptor Model 绑定可以看成是为目标 Action 的方法生成参数列表的过程,所以针对参数的元数据 描述才是 Model 绑定的核心依据。服务于 Model 绑定的参数元数据通过 ParameterDescriptor 类型来表示,而 Actio nD escriptor 的 GetParameters 方法返回的就是一个 ParameterDescriptor 数组。 如下面的代码片段所示, ParameterDescriptor 同样实现了 1 CustomA ttributeProvider 接口 以提供应用在参数上的特性。与抽象类型 ControllerDescriptor 和 Actio nD escriptor 一样,针 对参数的特性解析并没有真正实现在 ParameterDescriptor 相应的方法中,所以 GetCusto mAttributes 方法返回的依旧是一个空数组,而 False 还是直接作为 IsDefmed 方法的 返回值。 ASP. NET MVC 4 框架揭秘194 tÌl 第 5 章 Model 的绑定 public abstract class PararneterDescriptor : ICustornA ttributeProvider public virtual object[] GetCustornA ttributes(bool inherit); public virtual object[] GetCustornA ttributes(Type attributeType , bool inherit); public virtual bool IsDefined(Type attributeType , bool inherit); public abstract ActionDescriptor public abstract string public abstract Type public virtual object ActionDescriptor { geti } PararneterNarne { geti } PararneterType { geti } DefaultValue { geti } public virtual PararneterBindinglnfo Bindinglnfo { geti } ParameterDescriptor 的只读属性 Actio nD escriptor 表示描述所在 Action 的 Actio nD escriptor 对象。属性 ParameterName 、 ParameterType 和 DefaultValue 分别表示参数的名称、类型和默认 值。 ParameterDescriptor 的只读属性 BindingInfo 表示的 System.Web.Mvc. P 町'ameterBindingInfo 对象用于控制请求数据与参数的绑定行为。如下面的代码片段所示,抽象类 Parameter Bindinglnfo 具有四个属性,其中类型为 IMode lB inder 的 Binder 属性返回的 ModelBinder 对 象是整个 Model 绑定的核心,我们将在本章后续部分进行单独介绍。 public abstract class PararneterBindinglnfo { public virtual IModelBinder public virtual ICollection public virtual ICollection public virtual string Binder { geti } Include { geti } Exclude { geti } Prefix { geti } 如果参数类型是一个复杂类型,默认情况下会绑定其所有公共可写属性,而两个 ICollection类型的属性 lnclude 和 Exclude 表示显式设置的参与/不参与绑定的属性名 称列表。在默认情况下,请求数据与参数之间严格按照名称进行绑定,但是有时候请求数据 名称具有相应的前缀,这个前缀体现在 ParameterBindin gI nfo 的 Prefix 属性上。 ReflectedParameterDescriptor 默认使用的 ParameterBindinglnfo 是通过对目标参数对应的 Paramete rI nfo 对象进行反射 获得的,这样的 ParameterDescriptor 通过具有如下定义的 System. Web.Mvc.Reflected ParameterDescriptor 类型表示,而这个 Paramete rI nfo 对象通过只读属性 Paramete rI nfo 表示。 public class ReflectedPararneterDescriptor : PararneterDescriptor public ReflectedPararneterDesc~iptor(Pararneterlnfo pararneterlnfo , ActionDescriptor actionDescriptor)i public override object[] GetCustornA ttributes(bool inherit)i public override object[] GetCustornA ttributes(Type attributeType , bool inherit)i public override bool IsDefined(Type attributeType , bool inherit)i public override ActionDescriptor ActionDescriptor { geti } ASP. NET MVC 4 框架揭秘5.1 ControllerDescript 。人 ActionDescriptor 与 ParameterDescriptor 话 195 public override PararneterBindinglnfo public override object public override string public override Type public Pararneterlnfo Bi 口 dingI 口 fo { get; } DefaultValue { get; } PararneterNarne { get; } PararneterType { get; } Pararneterlnfo { get; } Reflecte dP arameterDescriptor 的 Bindinglnfo 属性返回的是一个 Reflecte dP arameter Bindinglnfo 对象,这是一个内部类型。该 Bindinglnfo 对象的 In c1 ude 、 Exclude 和 Prefix 属 性来源于应用在参数上的 System. Web.Mvc.Bin dA ttribute 特性。如下面的代码片段所示, Bin dA ttribute 中同样定义了这三个属性,其中 In c1 ude 和 Exclude 为通过逗号作为分隔符的属 性名称列表。具有布尔返回类型的 IsPrope即 Allowed 方法用于判断指定的属性是否允许绑 定,只有当指定的属性名在 In c1 ude 列表中(或者 In c1 ude 列表为空)并且不在 Ex c1 ude 列表 中的情况下,该属性才返回 True 。 [AttributeUsage(AttributeTargets.Pararneter I AttributeTargets.Class , AllowMultiple=false , Inherited=true)] public sealed class Bin dAttribute : Attribute public bool IsPropertyAllowed(string propertyName); public string Include { get; set; } public string Exclude { get; set; } public string Prefix { get; set;} 在本节中,我们介绍了三个重要的描述类型,即分别描述 Controller 、 Action 和参数的 ControllerDescriptor 、 Actio nD escriptor 和 ParameterDescriptor 。旨在生成参数的 Model 绑定主 要使用 Parameter Descriptor ,但是其他两个 ControllerDescriptor 和 Actio nD escriptor 在整个 ASP.NET MVC 框架体系中同样具有重要的作用。如图 5-1 所示的 UML 体现了所有抽象和 具体描述类型之间的关系。 Controlle rD escr;p 串or ReflectedAsyncActionDescrlptor 图 5-1 ControllerDescriptor - ActionDescriptor - ParameterDescriptor ASP. NET MVC 4 框架揭秘196 如第 5 章 Model 的绑定 5.2 ValueProvider Model 绑定的数据具有多个来源,可能来源于提交的表单或者 JSON 字符串,也可能来 源于当前的路由数据,或者来源于请求地址的查询字符串。 ASP.NETMVC 将这种基于不同 数据来源的数据提供机制实现在一个叫做 ValueProvider 的组件中。 一般来讲,一个 ValueProvider 采用的数据源容器具有类似于字典类型的数据结构,通 过指定相应的 Key 从字典中获取相应的数据。 ASP.NETMVC 下的 ValueProvider 实现了具有 如下定义的接口 System. Web.Mvc.IValueProvider 0 GetValue 方法根据指定的 Key 从数据源中 获取对应的数据,但是这个 Key 与存在于数据源容器中对应数据条目的 Key 可能并非完全 一致,后者可能在前者基础上添加相应的前缀,而 ContainsPrefix 方法正是用于判断数据源 容器中是否具有包含指定前缀的 Key 0 Controller 使用的 ValueProvider 可以通过定义在 Con位 ollerBase 中的 ValueProvider 属性进行获取和设置。 public interface IValueprovider bool ContainsPrefix(string prefix)i ValueProviderResult GetValue(string keY)i public abstract class ControllerBase : IController //其他成员 public IValueProvider Valueprovider { geti seti } IValueProvider 的 GetValue 方法返回的是一个 System. Web.Mvc. ValueProviderResult 对象, 提供的数据包含在该对象之中。如下面的代码片段所示, ValueProviderResùlt 具有三个只读 属性,其中 RawValue 表示提供的原始数据,而 AttemptedValue 表示数据值的字符串表示。 [Serializable] public class ValueProviderResult public ValueProviderResult(object rawValue , string attemptedValue , Culturelnfo culture)i public object ConvertTo(Type type)i public virtual object ConvertTo(Type type , Culturelnfo culture)i o f4 n TE 占e qdrt nuc ·工 te r---3 tub sco ccc ·工·工·工141414 .bbb uuu ppp AttemptedValue { geti } Culture { geti } RawValue { geti } ValueProviderResult 提供了两个 ConvertTo 方法重载以实现向指定目标类型的转换。某 些类型(比如时间、日期和货币等〉的格式化依赖于相应的语言文化,这个辅助格式化的语 言文化通过 culture 参数来指定。如果调用 ConvertTo 方法时没有显式地指定这个参数, AS 卫 NETMVC 会默认使用 ValueProviderResult 的 Culture 属性值表示的 Culturelnfo 对象。 ASP. NET MVC 4 框架揭秘5.2 ValueProvider 诵 197 5.2.1 NameValueCollectionValueProvider ValueProvider 的数据容器一般具有类似于字典的结构。 NameValueCollection 表示一种 Key 和 Value 均为字符串的字典,并且对 Key 不具有唯一性约束(即两个元素可以共享相同 的 Key) 。具有如下定义的 System. Web.Mvc.Name ValueCollection ValueProvider 是将一个 Name ValueCollection 对象作为数据源容器的 ValueProvider ,作为数据源容器的 Name ValueCollection 对象将在构造函数中指定。 public class NameValueCollectionValueProvider : IUnvalidatedValueProvider, IEnumerableValueProvider, IValueProvider //其他成员 public NameValueCollectionValueProvider(NameValueCollection collectio口, Culturelnfo culture); public virtual bool ContainsPrefix(string prefix); public virtual IDictionary GetKeysFromPrefix (string prefix) ; public virtual ValueProviderResult GetValue(string key); public virtual ValueProviderResult GetValue (string key, bool skipValidation) ; public interface IEnumerableValueprovider : IValueProvider IDictionary GetKeysFromPrefix(string prefix); public interface IUnvalidatedValueProvider : IValueProvider ValueProviderResult GetValue(string key, bool skipValidation); N ame ValueCollection ValueProvider 除了实现 IValueProvider 接口之外,还实现了两个额 外的接口一→EnumerableValueProvider 和 IUnvalidatedValueProvider 。从接口命名也可以猜出 IEnumerable ValueProvider 主要用于针对目标类型为集合的数据提供,方法 GetKeysFromPrefix 以字典的形式返回数据源容器中所有具有指定前缀的 Key 。默认的情况 下数据提供会进行数据验证,而 IUnvalidatedValueProvider 接口提供了一个额外的 GetValue 方法使我们可以忽略对数据的验证。 两种前缀形式 辅助实现 Model 绑定的数据提供机制借助了 Model 元数据对数据类型的描述,通过第 4 章 "Model 元数据的提供"我们知道描述一个复杂数据类型的 ModelMetadata 具有树型层次 化结构,但是作为数据源容器的 NameValueCollection 对象却是一个"扁平"的结构,两者 之间的匹配通过前缀来实现。 举个简单的例子,假设通过 NameValueCollection ValueProvider 提供对象的目标类型为具 有如下定义的 Contact 。表示联系地址的属性是一个复杂类型 Address ,那么针对 Contact 类 ASP. NET MVC 4 框架揭秘198 部第 5 章 Model 的绑定 型的 Model 元数据树具有两个层级。 •L c a 』Ln o c s s a 14 C C .工l b u p{ S αJqdqds nnne ·工·工·工 r rrrd tttd SSSA CCCC ·-·工·工·工1141414 bbbb uuuu p-DLP-P ‘ Name { geti seti } Pho 口 eNo { geti seti } EmailAddress { geti seti } Address { geti seti } s s e r d d A S s a --C C .-14 b u p&It public string Province { geti seti } public string City { geti seti } public string District { geti seti } public string Street { geti seti } 由于组成 N ame ValueCollection 的元素值都是字符串,它不可能单独表示一个复杂对象, 一个复杂对象需要通过多个元素值组装而成。如果通过 Name ValueCollection ValueProvider 来初始化一个完整的 Contact 对象,表示数据源的 Name ValueCollection 至少需要包含 7 个元 素,分别对应 Contact 对象的 Name 、 PhoneNo 和 EmaiIAddress 属性,以及 Address 对象的 Province 、 Ci可、 District 和加 eet 属性。两类元素在 Name ValueCollection 中通过基于属性名 称的前缀来区分,具体的结构如表 5-1 所示。 表 5-1 复杂类型数据在 NameValueCollection 中的表示 Key Value Name 张三 PhoneNo 123456789 EmailAddress zhangsan@gmail.com Address.Province 江苏 Address.City 苏州 Address.District 工业园区 Address. Street 星湖街 328 号 将点号(.)作为分隔符的前缀除了表示基于属性的层级关系之外,还可以用于数据筛选。 如下面的代码片段所示,我们在 ContactCon位o l1 er 中定义了一个用于添加联系人的 Action 方 法 AddContacts ,它具有两个 Contact 类型的参数 foo 和 b 缸,表示添加的两个不同的联系人。 public class ContactController public void AddContacts(Contact foo , Contact bar) //省略实现 ASP. NET MVC 4 在f;Vg揭秘5.2 ValueProvider _ 199 如果我们采用 Name ValueCollection ValueProvider 来提供作为 AddContacts 方法参数的两 个 Contact 对象,保存在 Name ValueCollection 的数据元素必须能够与它们进行合理映射。这 样的映射可以通过针对参数名的前缀来实现,具体数据结构如表 5-2 所示。 表 5-2 具有不同前缀的相同类型数据在 NameValueCollection 中的表示 Key Value foo.Name 张三 foo.PhoneNo 123456789 foo.EmailAddress zhangsan@gmail.com foo.Address.Province 江苏 foo.Address.City 苏州 foo.Address.District 工业园区 foo .Address. Street 星湖街 328 号 bar.Name 李四 bar.PhoneNo 987654321 bar .EmailAddress lisi@gmai l. com bar.Address.Province 江苏 bar .Address. Ci 可 苏州 bar .Address.District 工业园区 bar .Address. Street 机场路 328 号 除了采用基于" "的前缀之外,目标类型为数组或集合的数据源元素可以采用基于"索 引"的前缀,这样的前缀通过方括号"[]"表示。表 5-3 体现了表示目标类型为 Contact 数组 或者集合的数据源容器的结构。 表 5-3 数组/集合对象在 NameValueCollection 中的表示 (1 ) Key Value [O].Name 张三 [O].PhoneNo 123456789 [O].EmailAddress zhangsan@gmail.com [1].Name 李四 [1].PhoneNo 987654321 [1] .EmailAddress lisi@gmai l. com ... ... 除了采用数字作为索引之前,还可以按照如表 5-4 所示的方式通过文字作为索引。两种 不同形式的索引对应着不同的 Model 绑定机制,我们会在本章后续的部分对两者之间的差异 进行详细讲述。 ASP. NET MVC 4 框架揭秘200 也第 5 章 Model 的绑定 表 5 -4 数组/集合对象在 NameValueCollection 中的表示 (2) Key Value [foo].Name 张三 [岛。 ].PhoneNo 123456789 [foo] .EmailAddress zhangsan@gmail.com [b 町 ].Name 李四 [bar] .PhoneN 0 987654321 [bar] .EmailAddress lisi@gmai l. com ... ... 实例演示:返回指定前缀的 Key (S501 、 S502) 在了解了这两种不同类型的前缀之后,我们来关注一下 Name ValueCollection Value Provider 实现的 Ge tK eysFro mP refix 方法。该方法返回的是一个 IDictionary对 象,针对一个作为前缀的字符串,该方法返回的字典会包含怎样的数据元素呢?不妨通过一 个简单的演示实例来找到答案。 在一个 ASP.NETMVC 应用中定义了如下一个 HomeCon位 oller 。在 Action 方法In dex 中 创建了一个 N ame ValueCollection 对象,并在其中添加了 7 个元素。通过前面对 Contact 的定 义我们知道这 7 个元素可以最终组装成一个完整的 Contact 对象,这些元素均以字符串 "foo" 为前缀。最后针对这个 Name ValueCollection 创建了对应的 Name ValueCollection Value Provider ,并将其作为 Model 在默认 View 中呈现出来。 public class HomeController : Controller public ActionResult Index() NameValueCollection datasource = new NameValueCollection(); datasource.Add("foo.Name" , "Foo"); datasource.Add("foo.PhoneNo" , "123456789"); datasource.Add("foo.EmailAddress" , "Foo@gmail.com"); datasource.Add(" 王 oo.Address.Province" , "江苏") ; datasource.Add("foo.Address.City" , "苏州") ; datasource.Add("foo.Address.District" , "工业园区") ; datasource.Add("foo.Address.Street" , "星湖街 328 号") ; NameValueCollectionValueProvider valueprovider = new NameValueCollectionValueProvider(datasource , Culturelnfo.lnvariantCulture); return View(valueProvider); AS P. NET MVC 4 框架锺秘5.2 ValueProvider ~ ÎI 201 如下所示是 Action 方法中In dex 对应 View 的定义,这是一个 Model 类型为 N ame ValueCollection ValueProvider 的强类型 View 。在该 View 中,我们分别将 "foo" 和 " foo.Address "作为前缀调用 Name ValueCollection ValueProvider 对象 GetKeysFro mP refix 方 法,并将返回的字典对象的 Key 和 Value 通过表格的形式呈现出来。 @model NameValueCollectionValueProvider 指定前缀的 Key
    @foreach (var item in Model.GetKeysFromPrefix("foo")) @foreach (var item in Model.GetKeysFromPrefix("foo.Address"))
    foo
    @item.Key@item.Value
    foo.Address
    @item.Key@item.Value
    运行该程序后会在浏览器上呈现出如图 5-2 所示的输出结果。可以看到对于针对指定前 缀返回的字典对象,作为 Key 和 Value 的字符串的不同之处在于前者没有包含指定的前缀而 后者包含。除此之外,字典对象包含的元素全部处于同一级别:将 "foo" 指定为前缀时返 回的元素针对 Contact 的四个属性,但不包含下一级 CAddress 对象的四个属性〉的元素。虽 然 Name ValueCollection 中并不包含一个名为" foo.Address" 的元素,但是 ASP.NETMVC 依 然会将其单独作为以 "foo" 为前缀的 Keyo CS501) Name foo .Name PhoneNo foo.PhoneNo EmailAddress foo.EmailAddress Address fooAddress Province fooAddress.Province Cì ty foo.Add ress. Ci ty Dìstrict fooAddress.District Street fooAdd ress .S treet 图 5-2 GetKeysFromPrefix 方法返回的字典对象的结构(1) 接下来我们采用类似的方式来演示基于索引的前缀,为此我们将 HomeContro l1 er 的 ASP. NET MVC 4 框架揭秘202 白第 5 章 Model 的绑定 Index 方法进行了如下的改写:作为数据源的 N ame ValueCollection 对象针对一个包含两个元 素的 Contact 集合,为这个 Contact 集合对象提供数据的元素以字符 "frrst" 为前缀。 public class HorneController : Controller public ActionResult Index() NarneValueCollection datasource = new NarneValueCollection(); datasource.Add("first[O] . Narne" , "Foo"); datasource.Add("first[O] . PhoneNo" , "123456789"); datasource.Add("first[O] . ErnailAddress" , "Foo@grnail.com"); datasource.Add("first[l] . Narne" , "Bar"); datasource.Add("first[l] . PhoneNo" , "987654321"); datasource.Add("first[l] . ErnailAddress" , "Bar@grnail.com"); NarneValueCollectionValueProvider valueProvider = new NarneValueCollectionValueProvider(datasource , Culturelnfo. 工 nvariantCulture); return View(valueProvider); 我们对 View 进行如下的改动,将三个表示前缀的字符串" first" 、" frrst[O] "和" frrst[ 1] " 作为参数调用 N ame ValueCollection ValueProvider 对象 Ge tK eysFro mP refix 方法,并将获取的 相应字典的 Key 和 Value 呈现出来。 @rnodel NarneValueCollectionValueProvider 指定前级的 Key @foreach (var i tern in Mode l. GetKeysFrornPrefix ("first") ) @foreach (var itern in Model.GetKeysFrornPrefix("first[O]")) @foreach (var itern in Model.GetKeysFrornPrefix("first[l]"))
    first
    @itern.Key@itern.Value
    first[O]
    @itern.Key@itern.Value
    first[1]
    @itern.Key@itern.Value
    AS P. NET MVC 4 在架揭秘5.2 ValueProvider :-IÌI 203 该程序执行之后会在浏览器中产生如图 5-3 所示的输出结果。如果我们将"["和"]" 视为和…一样的分割符,体现在方法 GetKeysFro mP refix 上针对索引作为前缀的规则与基 于" "前缀的规则其实没有本质的区别。 CS502) 噩噩噩噩噩噩 EZ画 些叫叩 叫 2 一一一生E理‘' ..hn 。 日 厄 t[O) 1 fi rst [1) mmÐl Name 日 rst[O ) .Name PhoneNo fi rst[O).PhoneNo EmailAddress 日 rst[O) . EmaiIAddress 1: IH Name 日 rst [ l) . Name PhoneNo fi rst[l] . PhoneN 。 EmailAddress f jrst[l].EmaiIAddress 图 5-3 GetKeysFromPrefix 方法返回的字典对象的结构 (2) FormValueProvider 与 QueryStringValueProvider H 口?请求提交的表单和请求查询字符串是 Model 绑定的两个主要的数据来源,针对它 们的数据提供实现在两个具体的 N ame ValueCollection ValueProvider 中,它们分别是 F orm ValueProvider 和 QueryString ValueProvider ,两个类型均定义在命名空间 System. Web.Mvc 下。如下面的代码片段所示,作为这两个 Name ValueCollection ValueProvider 数据源容器的 Name ValueCollection 对象分别是当前请求的表单 CHttpRequest 的 Form 属性〉和查询字符串 集合 (HtφRequest 的 Q即可 S 位 ing 属性)。 public sealed class FormValueprovider : NameValueCollectionValueProvider public FormValueProvider(ControllerContext controllerContext) { } base(controllerContext.RequestCo 口 text.HttpContext.Request.Form , Culturelnfo.CurrentCulture) public sealed class NameValueCollection : NameValueCollectionValueprovider public NameValueCollection(ControllerContext controllerContext) { } : base(controllerContext.RequestContext.HttpContext.Request.QueryString , Culturelnfo.CurrentCulture) 5.2.2 DictionaryValueProvider 通过 Name ValueCollection ValueProvider 提供的数据源将保存在一个 Name ValueCollection ASP. NET MVC 4 框架揭秘204 旬第 5 章 Model 的绑定 对象中, DictionaryValueProvider 自然将数据源存放在一个真正的字典对象之中。它们之间 的不同之处在于 N ame ValueCollection 中的元素仅限于字符串,并且不对 Key 作唯一性约束: 字典中的 Key 则是唯一的, Value 也不仅仅局限于字符串。 DictionaryValueProvider 的类型全名为 System. Web.Mvc.DictionaryValueProvider , 泛型参数 TValue 表示作为数据容器字典的 Value 类型。如下面的代码片段所示,它实现了 IEnumerable ValueProvider 和 IValueProvider 接口,构造函数接受一个 IDiction缸y 对象作为数据源容器。定义在 Diction at')袖 lueProvider 中所有方法的逻辑与 定义在 Name ValueCollection ValueProvider 中的同名方法基本一致。 public class DictionaryValueProvider : IEnumerableValueProvider , IValueProvider public DictionaryValueProvider(IDictionary dictionary , Culturelnfo culture)i public virtual bool ContainsPrefix(string prefix); public virtual IDictionary GetKeysFromPrefix (string prefix) i public virtual ValueProviderResult GetValue(string keY)i RouteDataValueProvider 通过 URL 路由系统解析请求地址得到的路由数据可以作为 Model 绑定的数据来源,这 样的数据通过类型为 System. Web.Mvc.RouteData ValueProvider 的对象来提供。如下面的代码 片段所示, RouteData ValueProvider 继承自 Dictionary ValueProvider,它在构造函数 中获取保存于当前 ControllerContext 中表示路由数据的 RouteData 对象,并将其 Values 属性 表示的字典作为自身的数据容器。 public sealed class RouteDataValueProvider : DictionaryValueProvider public RouteDataValueProvider(ControllerContext controllerContext) base(controllerContext.RouteData.Values , Culturelnfo.lnvariantCulture) HtlpFileCollectionValueProvider 可以通过类型为 file 的 元素进行文件的上传。在表示 HTTP 请求的 H忧pRequestB 部 e 对象中,上传文件通过只读属性 Files 表示。如下面的代码片段所示,该属 性类型为 H忧:p FileCollectio nB ase ,是一个元素类型为 HttpPoste dF ileBase 的集合。 public abstract class HttpRequestBase public virtual HttpFileCollectionBase Files { geti } public abstract class HttpFileCollectionBase : NameObjectCollectionBase , AS P. NET MVC 4 框架揭翻5.2 ValueProvider 也 205 ICollection, IEnumerable public virtual string[] AIIKeys { geti } public override int Count { geti } public virtual HttpPostedFileBase this[int index] { geti } public virtual HttpPostedFileBase this[string name] { get; } public abstract class HttpPostedFileBase public virtual void SaveAs(string filename)i public virtual int public virtual string public virtual string public virtual Stream Co口 tentLength { get; } ContentType { get; } FileName { geti } InputStream { geti } 对于处理上传文件的 Action 来说,通常定义一个或者多个类型为 HttpPostedFileBase (处 理单个上传文件〉或者 IEnumerable (处理一组上传文件〉的参数来接 收上传的文件。针对参数类型为 H仕pPostedFi1eBase 的 Model 绑定利用一个 System. Web.Mvc.HttpFileCollection ValueProvider 对象来提供所需的数据。如下面的代码片段 所示, H忧pFileCollectionValueProvider 继承自 DictionaryValueProvider , 也就是说作为数据源容器的是一个值类型为 Ht甲PostedFileBase 数组(不是 HttpPostedFileBase )的字典。 public sealed class HttpFileCollectionValueProvider DictionaryValueProvider public HttpFileCollect 工 0口ValueProvider(ControllerContext controllerContext); 当我们根据当前 ControllerContext 构建一个 H即FileCollectionValueProvider 的时候, ASP.NETMVC 会从当前 HTTP 请求的 Files 属性中获取所有的 HttpPostedF ileBase 对象。多 个 Ht甲PostedF i1 eBase 可以共享相同的名称,作为数据源容器的字典将 HttpPostedFileBase 的 名称作为 Key ,具有相同名称的一个或者多个 Ht甲PostedFileBase 对象构成一个数组作为对 应的 Value 。 ChildActionValueProvider 子 Action 和普通 Action 的不同之处在于它不能用于响应来自客户端的请求,只是在某 个 View 中被调用以生成某个部分的 HTML 。如下面的代码片段所示, HtmIHelper 具有一系 列名为 Action 的扩展方法重载用于调用指定 Action 以生成相应的 HTML 。 public static class ChildActionExtensions //其他成员 public static MvcHtmlString Action(this HtmlHelper htmlHelper, string actionName)i ASP. NET MVC 4 在架揭秘206 自第 5 章 Model 的绑定 public static MvcHtmlString Action(this HtmlHelper htmlHelper , string actionName , object routeValues); public static MvcHtmlString Action(this HtmlHelper htmlHelper , string actionName , string controllerName); public static MvcHtmlString Action(this HtmlHelper htmlHelper , string actionName , RouteValueDictionary routeValues); public static MvcHtmlString Action(this HtmlHelper htmlHelper , string actionName , string controllerName , object routeValues); public static MvcHtmlString Action(this HtmlHelper htmlHelper , string actionName , string controllerName , RouteValueDictionary routeValues); 作为子 Action 方法参数的数据来源与普通 Action 方法有所不同, Model 绑定过程中具 体的数据提供由一个类型为 System. Web.Mvc.ChildAction ValueProvider 的对象来完成。如下 面的代码片段所示, ChildA ction ValueProvider 依然是 DictionaryValueProvider 的继承 者,那么在作为数据源容器的字典中,具体的 Key 和 Value 究竟是怎样一个对象呢? public sealed class Chil dA ctionValueprovider DictionaryValueProvider public Chil dActionValueProvider(ControllerContext controllerContext); public override ValueProviderResult GetValue(~tring key); 当我们针对指定的 ControllerContext 创建一个 ChildAction ValueProvider 对象时, Con仕 ollerContext 中表示路由数据的 RouteData 对象会被提取出来,其 Values 属性表示的 Route ValueDictiona.ry对象会作为 Chil dA ctionValueProvider 的数据容器字典,这可以从 ChildA ction ValueProvider 的构造函数的定义看出来。 public sealed class Chil dActionValueProvider DictionaryValueProvider //其他成员 public Chil dActionValueProvider(ControllerContext controllerContext) base(controllerContext.RouteData.Values , Culturelnfo.lnvariantCulture) 但是 Chil dA ctionValueProvider 的 GetValue 方法针对给定的 Key 获取的值却并不是简单 地来源于原始的路由数据,不然 Chil dA ction ValueProvider 就和 RouteDataValueProvider 没有 什么分别了。实际上 Chil dA ctionValueProvider 的 GetValue 方法获取的值来源于调用 HtmH elper 的扩展方法 Action 时,通过参数 route Values 指定的 Route ValueDictionary 对象。 现在来简单介绍一下 Chil dA ction ValueProvider 的 GetValue 方法的实现逻辑。如下面的 代码片段所示, ChildAction ValueProvider 具有一个字符串类型的静态字段 _ childA ction ValuesKey 。当该类型第一次被加载时(静态构造函数被调用),该字段被初始化 成一个 GUID 。 AS P. NET MVC 4 框架揭秘public sealed class Chil dActionValueProvider DictionaryValueProvider //其他成员 5.2 ValueProvider 窜 207 private static string _chil dActionValuesKey = Guid.NewGuid() .ToString(); 当我们通过 HtmIHelper 的扩展方法 Action 调用某个指定的子 Action 时,如果参数 route Values 指定的 RouteValueDictionary 不为空, HtmIHelper 会据此创建一个 Dictionary ValueProvider对象,并将这个对象添加到通过 route Values 参数表示的原始 的 Route ValueDictionary 对象中,对应的 Key 就是 Chil dA ction ValueProvider 的静态属性 _ chil dA ction ValuesKey 所表示的 GUID 。 这个 Route ValueDictionary 被进一步封装成表示请求上下文的 RequestContext 对象,随 后被调子 Action 所在的 Con位 oller 会在该请求上下文中被激活,在 Controller 激活过程中表 示 ControllerContext 的 Con位 ollerContext 被创建出来,毫无疑问它包含了之前创建的 Route ValueDictionary 对象。当我们针对当前 ControllerContext 创建 ChildActionValueProvider 的时候,作为数据源的 Route ValueDictionary 就是这么一个对象。 @Htrn l. Action("XxxChil dAction" , new {Foo=123 , Bar = 456 , Baz=789}) 举个例子,假设我们在某个 View 中采用如上代码调用当前 Controller 的子 Action 方法 "XxxChil dA ction" ,并指定相应的路由变量 (Foo 、 B 町和 B 但〉。最终作为 Chil dA ctionValueProvider 数据源的 Dictionary 将具有如图 5-4 所示的结构,其中左边部分为 Key ,右边 部分为 Value 。 Chil dA ctionValueProvider 图 5 -4 ChildActionValueProvider 数据容器的结构 当调用 ChildAction ValueProvider 的 GetValue 方法获取指定 Key 的值时,实际上它并不 会直接根据指定的 Key 去获取对应的值,而是根据通过其静态字段_chil dAction ValuesKey 值 去获取对应的 Dictionary ValueProvider对象,然后再调用该对象的 GetValue 根据指定 的 Key 去获得相应的值。 AS P. NET MVC 4 框架揭秘208 由第 5 章 Model 的绑定 实例演示: ChildActionValueProvider 的值提供机制 (S503 ) 为了印证上面介绍的关于 Ch i1dA ctionValueProvider 的数据提供机制,我们来演示一个简 单的实例。在进行演示之前需要作一下简单说明:对于 DictionaryValueProvide r< TValu e>对 象来说,作为数据容器的字典通过 values 字段表示,如下面的代码片段所示,这是一个只 读字段,所以我们为 DictionaryValueProvider 定义了如下一个 GetD 砌 Source 扩展方 法将该字段的值提取出来。 public class DictionaryValueProvider : IEnurnerableValueProvider , 工 ValueProvider 11 其他成员 private readonly Dictionary valuesi public static class DictionaryValueProviderExtensions public static Dictionary GetDataSource ( this DictionaryValueProvider valueProvider) Fieldlnfo valu~sField = typeof(DictionaryValueProvider) . GetField ("_ values" , BindingFlags. Instance I BindingFlags. NonPublic) i return (Dictionary)valuesField.GetValue(valueProvider)i 我们在一个 ASP.NETMVC 应用中定义了如下一个 HomeController 。默认的 Action 方法 Index 将对应的 View 呈现出来,而另一个 Action 方法 DataO fC h i1dA ction ValueProvider 则根 据当前 ControllerContext 创建一个 Ch i1dA ctionValueProvider 对象,并将其作为 Model 呈现在 对应的 View 中。 DataO fC h i1dA ctionValueProvider 方法在返回 ViewResult 之前,我们将存在 于当前 ControllerContext 中的三个路由变量 (Foo 、 B 缸和 B 但〉的值进行了相应的修改。 public class HorneController : Controller public ActionResult Index() return View()i public ActionResult DataOfChil dActionValueProvider() { ControllerContext.RouteData.Values["Foo"] = "abc"; ControllerContext.RouteData.Values["Bar"] = "ijk"; ControllerContext.RouteData.Values["Baz"] = "xyz"i Chil dActionValueProvider valueProvider = AS P. NET MVC 4 框架揭翻5.2 ValueProvider 二部 209 new Chil dA ctionValueProvider(ControllerContext)i returηView(valueProvider)i 如下所示的是 Action 方法 Index 对应 View 的定义,在该 View 中我们调用 HtmlHelper 的扩展方法 Action 以子 Action 的方式调用 DataOfCh i1dA ction ValueProvider ,并将生成的内 容作为主体部分的 HTh征。在进行子 Action 调用的同时,还指定了在 DataOfChi1 dAction ValueProvider 方法中被篡改的路由变量。 Chil dA ctionValueProvider 的数据结构 @Htrnl.Action("DataOfChil dActionValueProvider" , 口 ew { Foo = 123 , Bar = 456 , Baz = 789 }) Action 方法 DataOfCh i1dA ctionValueProvider 对应 View 的整个内容定义如下,可以看到 这是一个 Model 类型为 Ch i1dA ctionValueProvider 的强类型 View 。在该 View 中调用 Dictionary ValueProvider 的扩展方法 GetD ataSource 获取作为 Ch i1dA ctionValue Provider 对象数据容器的 Dictionary对象,并将 Key 和 Value 通 过表格的形式呈现出来。如果某个元素的值是一个 DictionaryValueProvider对象,同 样通过调用扩展方法 GetDataSource 得到作为它数据容器的字典对象,并将其结构呈现出来。 @rnodel Chil dActionValueProvider @{ var dictionary1 = this.Model.GetDataSource()i @foreach (var itern1 in dictionaryl) DictionaryValueProvider valueProvider = iternl.Value.RawValue as DictionaryValueProvideri if (null == valueProvider) else var dictionary2 = valueProvider.GetDataSource()i AS P. NET MVC 4 框架揭秘210 也第 5 章 Model 的绑定 foreach(var item2 in dictionary2) '
    KeyValue
    @iternl.Key@itern1.Value.RawValue
    @item 1. Key KeyValue
    @item2.Key@item2.Value.RawValue
    该程序运行之后会在浏览器中呈现出如图 5-5 所示的输出结果,它正好反映了 ChildAction ValueProvider 数据容器具有前面我们介绍过的数据结构。对于这个例子来说,如 果我们调用 Chil dA ction ValueProvider 的 GetValue 方法试图获取 Foo 、 B 缸和 B 缸三个路由变 量,得到的值应该是 123 、 456 和 789 ,而不是 "abc" 、 "ijk" 和 "xyz" 。如果我们将" controller" 或者" action" 作为 GetValue 方法的参数,则会直接返回 Null 。 I ← ~ B .k,Þ mn'.A ttM\9M rm,-; -- ] • 叶 σ 价 1 <:;) localho st: 43二圈 ~ a 、 , :'C勘 ,固立自 1 飞" F∞ abc Bar ij k Baz 可Z controller Home action DataOfChildActionValueProvider b d4ed97c- r. 星坠B 4e1e-4 f9b- Foo 123 afab- Bar 4 56 e52b09b5fe9b Baz 789 图 5-5 ChildActionValueProvider 数据容器的结构 ValueProviderCollection System. Web.Mvc. ValueProviderCollection 表示一个元素类型为 IValueProvider 的集合,除 此之外,它本身也是一个 ValueProvider 。如下面的代码片段所示, ValueProviderCollection 不仅仅实现了 IValueProvider 接口,还实现了 IUnvalidatedValueProvider 和 lenumerable Value Provider 接口。 public class ValueproviderCollection : Collection , IUnvalidatedValueProvider , IEnumerableValueProvider , IValueProvider public ValueProviderCollection(); public ValueProviderCollection(IList list); public virtual bool ContainsPrefix(string prefix); AS P. NET MVC 4 框架揭秘5.2 ValueProvider 海 211 pub 工 ic virtual IDictionary GetKeysFromPrefix (string prefix) ; public virtual ValueProviderResult GetValue(string key); public virtual ValueproviderResult GetValue (string key , bool skipValidation) ; 当 ValueProviderCollection 的 GetValue 方法被调用的时候,它会遍历整个集合并传入指 定的参数调用 ValueProvider 的同名方法,直到返回一个具体的 ValueProviderResult 对象。如 果调用集合中所有的 ValueProvider 的 GetValue 方法均返回 Null ,那么最终的返回值就是 Null 。 用于获取指定前缀的 Key 的 GetKeysFro mP refix 方法具有与 GetValue 类似的实现逻辑, 唯一的不同之处在于,如果调用所有 IEnumerableValueProvider 的 Ge tK eysFro mP refix 方法均 返回一个空的字典对象,那么最终的结果也是一个空的字典。至于另一个方法 ContainsPre 缸, 如果有任何一个 ValueProvider 的 ContainsPrefix 方法返回 True ,则 ValueProviderCollection 的 ContainsPrefix 也返回 Trueo 5.2.3 ValueProviderFactory ValueProviderFactory 是创建 ValueProvider 的工厂,它们继承自具有如下定义的抽象类 System. Web.Mvc. ValueProviderFactory ,唯一的抽象方法 GetValueProvider 会根据当前 ControllerContext 创建相应的 ValueProvider 对象。 public abstract class ValueProviderFactory public abstract IValueprovider GetValueProvider( ControllerContext controllerContext); 在 System. Web.Mvc 命名空间下, ASP.NET MVC 定义了一系列具体的 ValueProviderFactory 类型。前面介绍的两个 NameValueCollectionValueProvider (FormValueProvider 和 QueryString ValueProvider) 通过对应的 F orm ValueProviderFactory 和 QueryStringValueProvider Factory 创建,而三个具体的 DictionaryValueProvider (RouteData ValueProvider 、 H忧pFileCollectionValueProvider 和 Chil dA ction- ValueProvider )对应的 ValueProviderFactory 类型分别是 RouteDataValueProviderFactory 、 H忧:pFileCollectionValueProviderFactory 和 Chil dA ction ValueProviderFactory 。除此之外, ASP.NET MVC 还定义了一个 J son ValueProviderFactory ,它会将 JSON 格式的请求内容转换成字典对象,并据此创建一个 DictionaryValueProvider 对象。 5.2.4 ValueProviderFactories ValueProviderFactory 的注册通过静态类型 System. Web.Mvc. ValueProviderFactories 来实 现气如下面的代码片段所示, ValueProviderFactory 具有一个静态只读属性 Factories ,它返回 ASP. NET MVC 4 在架揭秘212 每每 第 5 章 Model 的绑定 一个表示 ValueProviderFactory 集合的 ValueProviderFactoryCollection 对象。 public static class ValueProviderFactories public static ValueProviderFactoryCollection Factories { get; } public class ValueProviderFactoryCollection : Collection public ValueProviderFactoryCollection(); publicValueProviderFactoryCollection(IListlist); public IValueProvider GetValueProvider(ControllerContext controllerContext); 当执行 ValueProviderFactoryCollection 的 GetValueProvider 方法的时候,集合中每一个 ValueProviderFactory 对象的同名方法会被执行,它们创建的 ValueProvider 包含在最终返回的 ValueProviderF actoryCollection 对象中。 ValueProviderFactory 在 ValueProviderFactoryCo l1 ection 集合中的先后次序决定了创建的 ValueProvider 在返回的 ValueProviderCo l1 ection 对象中的次 序,而这个次序决定了使用的优先级。 如下面的代码片段所示, ValueProviderFactories 类型被加载的时候会对静态属性 Factöries 进行初始化,上面介绍的 6 种 ValueProviderFactory 会被创建并添加到 Factories 集 合中。由于添加的顺序体现了相应 ValueProvider 的选择优先级,意味着如果具有相同的名 称的请求数据同时存在于提交的表单和查询字符串中,前者会被选用。 public static class ValueproviderFactories private static readonly ValueProviderFactoryCollection factories = new ValueProviderFactoryCollection { new Chil dActionValueProviderFactory() , new FormValueProviderFactory() , new JsonValueProviderFactory() , new RouteDataValueProviderFactory() , new QueryStringValueProviderFactory() , new HttpFileCollect 工 onValueProviderFactory() }; public static ValueProviderFactoryCollection Factories get {return factories; } 以 ValueProvider 为核心的数据提供系统中涉及到了三类组件/类型: Va1ueProvider 、 ValueProviderFactotry 和 ValueProviderF actotries 0 ValueProvider 通过 ValueProviderFacto句f 创 建,而 ValueProviderFactotry 通过 ValueProviderF actotries 进行注册。图 5-6 所示的 UML 体 现了三者之间的关系。 ASP. NET MVC 4 框架揭秘5.2 ValueProvider 叫黯 213 ValuePro\liderCollectlon ChlldActlonValueProvlder FormValuoProvldGr QueryStringValulIProvider HttpFlleCollectlonValueProvlder RoutoDataValuoProvldør FormValueProviderFactory JsonValueP冒zyvid町Factory RouteDataValueProviderFactory QooryStrlngValuGProvldorFaetory 图 5-6 ValueProvider-ValueProviderFactory-ValueProviderFactories 实例演示:创建一个自定义 ValueProviderFactory (S504) ASP.NETMVC 提供的 6 种 ValueProviderFactory 基本上可以满足绝大部分 Model 绑定需 求,不过在一些特殊的场景下有可能需要自定义 ValueProviderFactory 。在这个实例演示中, 我们将创建一个以 HTTP 请求报头集合作为数据来源的 ValueProviderFactory 。 将自定义的 ValueProviderFactory 命名为 HtφHeaderValueProviderFactory 。如下面的代码 片段所示, HttpHeaderValueProviderFactory 的定义非常简单,在重写的 GetValueProvider 方 法中,根据指定的 ControllerContext 得到 HTTP 报头集合,并借此创建 NameValueCollection 对象,而最终返回的就是根据它创建的 Name ValueCollection ValueProvider 。为了与参数和属 性命名相匹配,我们剔除了 HTTP 报头名称中包含的" "字符。 public class HttpHeaderValueProviderFactory : ValueProviderFactory public override IValueProvider GetValueprovider( ControllerContext controllerContext) NameValueCollection requestData = new NameValueCollection(); var headers = controllerContext.RequestCo口 text .HttpContext.Request.Headers; ASP. NET MVC 4 框架揭秘214 自 第 5 章 Model 的绑定 foreach (string key in headers.Keys) requestData.Add(key.Replace("-",""),headers[key]); return new NameValueCollectionValueProvider(requestData, Culturelnfo.lnvariantCulture); 在一个 ASP.NET MVCWeb 应用中定义了如下一个名为 Commo时-ItφHeaders 的数据类 型,并在其中定义了 7 个属性,它们对应着 7 个相应的 Hπ? 报头。 public class CommonHttpHeaders public string Connection { get; set; } public string Accept { get; set; } public string AcceptCharset { get; set; } public string AcceptEncoding { get; set; } public string AcceptLanguage { get; set; } public string Host { get; set; } public string UserAgent { get; set; } 然后定义了如下一个 HomeController ,默认的 Action 方法 Index 具有一个类型为 Commo时f忧pHeaders 的参数,我们直接将该参数作为 Model 呈现在默认的 View 中。 public class HomeController : Controller public ActionResult Index(CommonHttpHeaders headers) return View(headers); Action 方法 Index 对应的 View 具有如下的定义,可以看出这是 Model 类型为 CommonHttpHeaders 的强类型 View 。在该 View 中,直接将包含在 Commo世-IttpHeaders 对 象的 7 个 Hπ? 报头的名称和值通过有关表格的形式呈现出来。 @model CommonHttpHeaders HTTP 报头列表
    NameValue
    Accept@Model.Accept
    AcceptCharset@Model.AcceptCharset
    AcceptEncoding@Model.AcceptEncoding
    AcceptLanguage@Model.AcceptLanguag~
    Connection@Model.Connection
    Host@Model.Host
    UserAgent@Model.UserAgent
    为了让我们自定义的 H仕pHeaderValueProviderFactory 能够参与到 Model 绑定过程中以提 ASP.NET MVC 4 框架揭秘5.3 Model8inder !1自 215 供目标 ActÎon 的参数值(这里指 Action 方法 Index 中类型为 Conuno nH ttpHeaders 的参数 headers) ,我们在 Globa l. asax 中采用如下的代码对这个自定义 ValueProviderFactory 进行注册。 public class MvcApplication : System.Web.HttpApplication protected void Application Start() //其他操作 ValueProviderFactories.Factories.Add( 口 ew HttpHeaderValueProviderFactory()); 该程序运行之后会在浏览器中呈现出如图 5-7 所示的输出结果,输出的正是当前请求的 7 个 HTTP 报头。 眶噩噩噩~……… KZ:JI望3 0 1 刷刷 4342 合 E 罩' Accept textlhtml.application/X html+xm l. application/xml;q=O.9.' r:q=0.8 Accep tC harset ISO - 8859-1.utf-8;q=O.7 ,气 q=O .3 AcceptEncoding gzip.de f1 ate.sdch Acceptlanguage en-US , en;q =0.8 Connection Host UserAgent keep-alive localhost4342 Mozilla/S.O (W indows NT 6.1) AppleWebKiν536.11 (KHTM L. like Gecko) ChromeI20.0.1l32.5 7 Safari!S36.11 图 5-7 自定义 HttpHeaderValueProviderFactory 实现的针对 HTTP 报头的值提供机制 5.3 ModelBinder Model 的绑定体现在从当前请求提取相应的数据并生成相应的对象作为调用目标 Action 方法的参数列表, Model 绑定根据用以描述参数的元数据来进行。通过前面的介绍我们知道 Action 方法的参数元数据通过 ParameterDescriptor 来描述,通过它的属性的 Bindinglnfo 表示 的 ParameterBindinglnfo 对象具有一个名为 Mode lB inder 的组件用于真正实现 Model 绑定。 Mode lB inder 是整个 Model 绑定系统的核心,我们先来认识下这个重要的组件。 5.3.1 ModelBinder 与 ModelBinderProvider 真正实现 Model 绑定的 Mode lB inder 实现了接口 System. Web.Mvc.IMode lB inder 。如下 面的代码片段所示, IMode lB inder 接口具有唯一一个名为 BindModel 的方法,针对某个参数 的绑定就实现在这个方法中。 public interface IModelBinder object Bin dM odel(ControllerContext controllerContext , AS P. NET MVC 4 框架揭翻216 组第 5 章 Model 的绑定 ModelBindingContext bindingContext); IModelliinder 的 BindModel 方法具有两个基于上下文的参数,一个是表示当前的 ControllerContext ,另一个则是通过 System. Web.Modelliinding.ModelBindingContext 类型表示 的 Model 绑定上下文。在 Controller 初始化的时候, Con位 ollerContext 已经被创建出来,如 果我们能够创建出针对当前 Model 上下文的 ModelliindingContext 对象就能够调用提供的 Modelliinder 生成相应的参数值。关于 ModelliindingContext 的创建会在后面进行单独介绍, 在这之前先来了解一下 ModelBinder 的提供机制。 CustomModel8inderAttribute 与 Model8inderAUribute 如果某种类型的 Modelliinder 显式绑定到 Action 方法的某个参数上,它会被优先选择用 于针对该参数的 Model 绑定,参数与 Modelliinder 类型之间的绑定可以通过特性 System. Web.Mvc.Custo mM odelBinderAttribute 来完成。如下面的代码片段所示,这是一个抽 象类,它唯一的抽象方法 GetBinder 用于获取相应的 Modelliinder 对象。 [AttributeUsage(AttributeTargets.Pararneter I AttributeTargets.lnterface I AttributeTargets.Enurn I AttributeTargets.struct I AttributeTargets.Class , A 工 lowMultiple=false , Inherited=false)] public abstract class CustornModelBinderAttribute : Attribute public abstract IModelBinder GetBinder(); Custo mM odelBinderAttribute 具有一个唯一的继承者 System. Web.Mvc.ModelBinder At位 ibu饵,可以通过它定制 Model 绑定采用的 Modelliinder 类型。根据如下所示的应用在类 型上的 A伽 ibuteU sageAttribute 特性的定义,该特性不仅可以应用在参数上,也可以应用到 类型(接口、枚举、结构和类〉上,这意味我们既可以将它应用在 Action 方法的某个参数 上,也可以将它应用在参数对应的数据类型上,但是 ParameterDescriptor 只会解析应用在参 数上的特性。 [AttributeUsage(AttributeTargets.Pararneter I AttributeTargets.Interface I AttributeTargets.Enurn I AttributeTargets.struct I AttributeTargets.Class , AllowMultiple=false ,工 nherited=false)] public sealed class ModelBinderAttribute : custornM odelBinderAttribute public ModelBinderAttribute(Type binderType); public override IModelBinder GetBinder(); public Type BinderType { [CornpilerGenerated] get; } 为了演示 ModelBinderAttribute 特性对 ParameterDescriptor 的影响,我们来进行一个简 单的实例演示。在一个 ASP.NET MVC 应用中定义了如下几个类型, FooModelliinder 和 BarModelliinder 是自定义的 Modelliinder ,而 Foo 、 B 町和 B 但是三个作为 Action 方法参数 的数据类型,其中类型 B 缸上应用了 ModelBinderAt位 ibute 特性并将 Modelliinder 类型设置 为 BarModelliinder 。 ASP. NET MVC 4 在身自雹秘5.3 Model8inder • 217 public class FooModelBinder : IModelBinder public object BindModel(ControllerContext controllerContext , ModelBindingContext bindingContext) throw new NotlmplementedException(); public class BarModelBinder : IModelBinder public object BindModel(ControllerContext controllerContext , ModelBindingContext bindingContext) throw new NotlmplementedException(); public class Foo { } [ModelBinder(typeof(BarModelBinder))] public class Bar { } public class Baz { } 我们定义了如下一个 HomeController , DemoAction 是一个空 Action 方法,它具有针对 上面定义的三种数据类型( Foo 、 B 缸和 Baz) 的参数,其中参数 foo 上应用了 ModelBinderAttribute 特性并将 Mode lB inder 类型设置为 F ooMode lB inder 。在 Action 方法In dex 中我们创建了一个用于描述 DemoAction 的 Actio nD escriptor 对象,并将其作为 Model 呈现 在默认的 View 中。 public class HomeController : Controller public ActionResult Index() ControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(typeof(HomeController)); ActionDescriptor actionDescriptor = controllerDescriptor . Fin dAction (ControllerContext , "DemoAction"); return View(actionDescriptor); public void DemoAction( [ModelBinder(typeof(FooModelBinder))] Foo foo , Bar bar , Baz baz) { } 如下所示的是 Action 方法 Index 对应 View 的定义,可以看出这是一个 Model 类型为 Actio nD escriptor 的强类型 View 。在该 View 中,我们调用作为 Model 的 Actio nD escriptor 对 象的 GetParameters 方法得到用于描述所有参数的 ParameterDescriptor 列表,并将参数名称和 对应的 Mode lB inder 类型通过表格的形式呈现出来。 ASP. NET MVC 4 在架揭秘218 南第 5 章 Model 的绑定 @model ActionDescriptor 针对参数的 ModelBinder @foreach (var parameter in Model.GetParameters()) string binderType = "N/A"; IModelBinder binder = parameter.Bindinglnfo.Binder; if(null != binder) binderType = binder.GetType() .Name;
    ParameterModelBinder
    @parameter.ParameterName@binderType
    该实例程序运行之后会在浏览器中呈现出图 5-8 所示的输出结果。可以清楚地看到应用 到数据类型 B盯上的 ModelB inderAttribute 特性设置的 ModelB inder 类型 CßarModelBinder) 并不会体现在用于描述参数的 ParameterDescriptor 对象上。 CS505) -口­/' 0 附帷翻制~阳ω呻叫tru耐川!陌阳E邸i σ d甜- 囡陇~陋回m丽四函跚,咀 ~ 司、、 I | 飞而 | 血血!ir!l:Ii |foo IFooModelBinder |bar INJA lbaz IN/A 图 5-8 针对参数的 ModelBinder (1) ModelBinders l 如果我们不曾利用 ModelB inderAt位ibute 特性将某个 ModelB inder 类型绑定到 Action 方 法的某个参数上,默认采用的 ModelB inder 是通过静态类型 System. Web.Mvc.ModelBinders 来提供的。如下面的代码片段所示, ModelBinders 具有一个静态只读属性 Binders ,它表示 当前注册的 ModelB inder 列表,其类型为 System. Web.Mvc.ModelB inderDictionary 。 public static class ModelBinders public static ModelBinderDictionary Binders { geti } public class ModelBinderDictionary : IDictionary, ICollection>, IEnumerable>, ASP. NET MVC 4 框架揭秘5.3 Model8inder 告 219 IEnurnerable //其他成员 public IModelBinder GetBinder(Type rnodelType); public virtual IModelBinder GetBinder(Type rnodelType , bool fallbackToDefault); Modeill inderDictionary 是一个以数据类型为 Key , Modeillinder 对象为 Value 的字典,它 定义了数据类型与 ModelBinder 之间的匹配关系。 ModeillinderDictionary 的两个 GetBinder 方法重载用于获取针对某个数据类型的 Modeillinder 对象,布尔类型的参数 fallbackToDefault 表示在匹配关系不存在的情况下是否采用默认的 ModelBinder 作为后各,基于默认 Modeillinder 的后备机制会在第一个 GetBinder 方法重载中采用。这里默认 Modeillinder 类型 为 System. Web.Mvc.DefaultModeillinder 。 在针对某个参数进行 Model 绑定的时候,需要先获取相应的 Modeillinder 对象。如果对 应的 ParameterDescriptor 的 Modeillinder 不存在,则通过 ModelBinders 的静态属性 Binders 获取表示当前注册的 ModelBinder 列表的 ModeillinderDictionary 对象,并调用 GetBinder 方 法获取指定参数类型的 ModelBinder 对象。在针对参数类型对 Modeillinder 进行解析的时候, 会提取应用在数据类型上的 ModeillinderAt1万 ibute 特性,通过该特性设置的 Modeillinder 类 型将用于创建绑定该参数的 Modeillinder 对象。 我们根据 Modelliinder 的提供机制对前面演示实例中用于呈现 Action 参数对应 Modeillinder 类型的 View 进行如下的修改,在获取某个参数对应 Mode lB inder 的时候,先判 断作为参数描述对象的 ParameterDescriptor 中是否包含对应的 Modeillinder ,如果不存在则 利用静态 ModelBinders 根据参数类型获取对应 Modeillinder 。 @rnodel ActionDescriptqr 针对参数的 ModelBinder @foreach (var pararneter in Model.GetPararneters()) string binderType = "N/A"; IModelBinder binder = parameter.Bindinglnfo.Binder ?? ModelBinders.Binders. GetBinder(parameter.ParameterType); if(null != binder) binderType = binder.GetType() .Narne;
    PararneterModelBinder
    @pararneter.PararneterNarne@binderType
    再次运行我们的程序会在浏览器中呈现出如图 5-9 所示的输出结果。由于 AS P. NET MVC 4 蓓架揭秘220 组 第 5 章 Model 的绑定 FooMode lB inder 和 BarMode lB inder 通过 Mode lB inderAt位 ibute 特性分别应用到参数和参数类 型上,所以它们将被用于对应参数 (foo 和 b 缸)的 Model 绑定,而另一个参数 b 但则会采用 默认的 DefaultMode lB inder 进行绑定。 (S506) 匮噩噩噩~…­ 匮理.. 温 t:;) localh 。此4 3 42 食 E罩' FooModelBinder BarModelBinder DefaultModelB inder 图 5 - 9 针对参数的 Model8inder (2) 如果我们希望注册另一个 Mode lB inder 来完成针对数据类型 B 但的 Model 绑定,可以直 接通过 Mode lB inders 的静态属性 Binders 进行注册。在上面这个演示实例中,我们定义了如 下一个 B azM ode lB inder ,并在 Globa l. asax 中通过如下的代码将其注册到数据类型 Baz 上。 public class BazModelBinder : IModelBinder public object BindM odel(ControllerContext controllerContext , ModelBindingContext bindingContext) throw new NotlmplementedException(); public class MvcApplication : System.Web.HttpApplication //其他成员 protected void Application_Start() //其他操作 ModelBinders.Binders.Add(typeof(Baz) , new BazModelBinder()); 再次运行我们的程序,在浏览器中会得到如图 5-10 所示的输出结果,从中可以清楚地 看出我们注册的 B azMode lB inder 被用于 b 但参数的 Model 绑定。 (S507) 噩噩噩噩噩~…·圄圄 。 loca .lhost :4 3 42 由 . FooModelBinder BarModelBinder BazModelBinder 图 5-10 针对参数的 Model8inder (3) ASPNETMVC4 在架揭秘5.3 Model8inder _ 221 ModelBinderProvider ASP.NETMVC 的 ModelB inder 提供机制还涉及另一个重要的组件 ModelB inderProvider , 它们实现了具有如下定义的 System. Web.Mvc .lModelBinderProvider 接口。该接口定义了唯一 的 GetBinder 方法用于根据指定的数据类型获取相应的 ModelB inder 对象,不过在 ASP.NET MVC 并没有定义任何一个实现该接口的 ModelB inderProvider 类型。 public interface IModelBinderProvider IModelBinder GetBinder(Type ffiodelType)i 我们可以利用具有如下定义的静态类型 System. Web.Mvc.ModelBinderProviders 进行自 定义 ModelBinderProvider 的注册。 ModelBinderProviders 具有一个静态只读属性 BinderProviders ,它返回一个表示 ModelB inderProvider 集合的 System. Web.Mvc.ModelBinder ProviderCollection 对象,所谓 ModelB inderProvider 的注册体现在将自定义的 ModelB inderProvider 对象添加到该集合之中。 public static class ModelBinderProviders public static ModelBinderProviderCollection BinderProviders { geti } public sealed class ModelBinderProviderCollection Collection //省咯成员 通过 ModelB inderProviders 的静态属性 BinderProviders 表示的 ModelB inderProvider 列表 最终被 ModelB inderDictionary 使用。如下面的代码片段所示, ModelB inderDictionary 除了具 有一个定义数据类型与 ModelB inder 匹配关系的字典 C _ innerDictionary 字段)和一个默认 ModelBinderC defaultB inder) 之外,还具有一个 ModelBinderProvider 列表 C modelBinderProviders 字段)。当 ModelB inderDictionary 被创建的时候,通过 ModelB inderProviders 的静态属性 BinderProviders 表示的 ModelB inderProvider 列表会用于初始化 modelB inderProviders 字段。 public class ModelBinderDictionary //其他成员 private IModelBinder _defaultBinderi private readonly Dictionary innerDictionarYi private ModelBinderProviderCollection _ffiodelBinderProvidersi 以 ModelB inder 为核心的 Model 绑定系统包含的组件,以及它们之间的关系基本上可以 通过图 5-11 所示的 U孔11-来表示。 ASP.NET MVC 4 框架揭秘222 自第 5 章 Model 的绑定 GetBinder GetBinder <> IModelBinder <-m • 4 一 ~---------------­Binders 8inderproviders 图 5-11 ModeI8inder-ModeI8inderProvider-CustomModeI8inderAttribute 当调用 ModelBinderProviderCollection 的 Ge tB inder 方法获取指定数据类型的 Modeillinder 时,一 innerDictionary 字段表示的 Modeillinder 字典会被优先选择。如果数据类 型在该字典中找不到,则利用 modelBinderProviders 字段表示的 ModeillinderProvider 列表来 提供相应的 Modeillinder 。只有在这两种 Modeillinder 提供方式均宣告失败的情况下才会选 择通过 defaultBinder 字段表示的默认 Modeillinder 。也就是说,如果想为某个数据类型定制 某种类型的 Modeillinder ,按照选择优先级具有如下几种方式供选择。 • 将 ModeillinderAt位 ibute 应用在 Action 方法的相应参数上并指定相应的 Modeillinder 类 型,或者在参数上应用一个与之等效的自定义 Custo mM odeillinderAttribute 特性。 • 将 ModeillinderAttribute 应用在数据类型上并指定相应的 Modeillinder 类型,或者在数 据类型上应用一个与之等效的自定义 Custo mM odeillinderAttribute 特性。 • 通过 ModelBinders 的静态属性 Binders 将某个具体的 ModelBinder 对象注册到相应的数 据类型上(通过这种方式注册的 Modeillinder 与通过应用在数据类型上的 ModeillinderAttribute 特性注册的 Modeillinder 保存在相同的集合之中,其优先级取决于 各自存在于集合中的位置)。 • 自定义 ModeillinderProvider 为某种数据类型提供对应的 Modeillinder ,并添加到 ModeillinderProviders 的静态属性 BinderProviders 表示的 ModeillinderProvider 列表中。 前面三种方式的 Modeillinder 提供机制已经通过实例演示过了,现在来演示基于自定义 ModeillinderProvider 的 Modeillinder 提供机制。在前面的例子中我们为 Foo 、 B 缸和 B 但这三 种数据类型创建了相应的 ModeillinderCFooModeillinder 、 BarModeillinder 和 B azM odeillinder) , 现在创建如下一个自定义的 MyModeillinderProvider 将两者(数据类型和 Modeillinder 对象〉 进行关联。在 GlobaLasax 中按照如下的代码利用 ModelBinderProviders 对自定义的 MyModelBinderProvider 进行注册。 public class MyModelBinderProvider : IModelBinderprovider AS P. NET MVC 4 框架揭秘public IModelBinder GetBinder(Type rnodelType) if (rnodelType == typeof(Foo)) return new FooModelBinder()i if (rnodelType == typeof(Bar)) return new BazModelBinder()i if (rnodelType == typeof(Baz)) return new BazModelBinder()i return 口 ulli 5.3 Model8inder 部 223 public class MvcApplication : Systern.Web.HttpApplication //其他成员 protected void Application Start() //其他操作 ModelBinderProviders.BinderProviders.Add(new MyModelBinderProvider())i 由于 MyMode lB inderProvider 实现了针对 Foo 、 B 盯和 B 但三种数据类型的 Mode lB inder 的提供,所以我们需要将应用在 Action 方法参数 C foo) 和数据类型 CB 缸)上的 ModeillinderAttribute 特性删除。再次运行我们的程序,同样会在浏览器中呈现出如图 5-10 所示的输出结果。 CS508) 5.3.2 ModelState 与 Model 绑定 Model 绑定除了利用 ModelBinder 为目标 Action 方法的执行提供参数之外,还会将相关 的数据以 ModelState 的形式存储于当前 Controller 的 ViewData 中。如下面的代码片段所示, Controller 的基类 ControllerBase 中具有一个 System. Web.Mvc. ViewDat aD ictionary 类型的属'性 ViewData 。顾名思义, ViewData 就是 Controller 在进行 View 呈现过程中传递的数据。 public abstract class ControllerBase : IController //其他成员 public ViewDataDictionary ViewData { geti seti } public class ViewDataDictionary : IDictionary , ICollection> , IEnurnerable> , IEnurnerable //其他成员 public ModelStateDictionary ModelState { get; } AS P. NET MVC 4 在架揭秘224 • 第 5 章 Model 的绑定 字典类型的 ViewDataDictionary 具有一个类型为 System. Web.Mvc.ModelStateDictionary 的属性 ModelSta钮,这是一个 Key 和 Value 类型分别为 String 和 System. Web.Mvc.ModelState 的字典。在这里有一点需要引起读者注意: ViewDataDictionary 的 ModelState 属性类型不是 ModelSta饨,而是 ModelStateDictionary 。 [Serializable] public class ModelStateDictionary : IDictionary, ICollection>, IEnurnerable>, IEnurnerable { } ModelState 对象维护的 Model 状态具有两种类型,一类通过 ValueProvider 提供的 ValueProviderResult 对象,对应着属性 Value ,另一类是通过 System. Web.Mvc.Model ErrorCollection 类型表示的错误。 ModellirrorCollection 是一个元素类型为 System. Web.Mvc.Modellirror 的集合,而 Modellirror 通过属性 ErrorMessage 和 Exception 属 性表述错误的消息和抛出的异常。如下面的代码片段所示,所有的这些类型都是可序列化的。 e 卡』a t nb l e d o ]M e 1-s -bs aa z-­-lc 14 ac ---l r1-eb quu [p{ public ModelErrorCollection Errors { geti } public ValueProviderResult Value { geti seti } [Serializable] public class ModelErrorCollection : Collection public void Add(Exception exception)i public void Add(string errorMessage)i r o r r ph > 14 e d o ]M e ls bs aa z­-lc --ac ·工 -l r14 e 、 D QUU [p{ public ModelError(Exception exception)i public ModelError(string errorMessage)i public ModelError(Exception exception, string errorMessage)i public string ErrorMessage { geti private seti } public Exception Exception { geti private seti } 实例演示: Model 绑定过程中对 ModelState 的设定 (S509 ) 在 Model 绑定过程中, ModelBinder 除了为目标 Action 方法的执行提供参数列表之外, 还会对基于当前 Controller 的 ModelState 进行设置,现在通过一个简单的实例来证明这一点。 我们在一个 ASP.NET MVC 应用中定义了如下一个 HomeCon位oller ,在基于 HTTP-GET 的 Action 方法 Index 中,创建了一个 Contact 对象并作为 Model 呈现在默认的 View 中。另一个 基于应用了 H忧pPostAt位ibute 特性的 Index 方法具有一个 Contact 类型的参数,该方法将当前 ModelState 作为 Model 呈现在一个名为 "ModelState" 的 View 中。 ASP. NET MVC 4 症架揭秘5.3 Model8inder _ 225 public class HorneController : Controller public ActionResult Index() Address address = new Address Street = "江苏", - "苏州'1" , = "工业园区", = 11 星湖街 328 号" Province City D 工 str 工 ct Contact contact = new Co 口 tact Narne PhoneNo ErnailAddress Address = "张三", = "123456789" , ,, m o c --牛.工a m qJ 口旧」n a ss qs ne ar -hd zd Ha --一- } ; return View(contact)i [HttpPost] public ActionResult Index(Contact contact) return View("ModelState" , this.ViewData.ModelState)i public class Contact s qJqJαJS nnne ·工 -1· 工 r rrr 、α +L ←」←」 AU SSSA CCCC ·工 -1·l-l 141414 吁i­ bbbb uuuu pppp Narne { geti seti } PhoneNo { get; set; } ErnailAddress { get; set; } Address { geti seti } s s e r d d A s s a 14 C C .工14 b u p{ public string Province { geti seti } public string City { get; set; } public string Distr 工 ct { geti seti } public string Street { geti seti } 下面是 Action 方法 Index 默认 View 的定义,可以看出这是一个 Model 类型为 Contact 的强类型 View 。在该 View 中我们将作为 Model 的 Contact 对象以编辑模式呈现在一个表单 中,并在表单中添加提交按钮。有的读者可能会觉得奇怪:为什么在调用 Html 的 模板方法 EditorF orModel 对 Model 对象进行呈现之外,还需要调用 EditorFor 方法将 Contact 对象的 Address 属性进行基于编辑模式的呈现呢?原因在于 Contact 的 Address 属性是复杂类 型,默认情况下针对 Contact 的 EditorF orModel 方法输出的 HTML 并不会包括其 Address 属 性的内容,相关的内容在第 4 章 "Model 元数据的解析"中具有详细的介绍。 @rnodel Contact 修改 Co 口 tact 信息 AS P. NET MVC 4 框架揭秘226 l. 第 5 章 Model 的绑定 @using (Html.BeginForm()) @Html.EditorForModel() @Html.EditorFor(m=>m.Address) 另一个用于呈现当前 ModelState 的 View 具有如下的定义,这是一个 Model 类型为 ModelStateDictionary 的强类型 View ,在该 View 中我们将作为 Model 的 ModelStateDictionary 对象的 Key 和 Value 以表格的形式呈现出来。 @model ModelStateDictionary ModelState @foreach (var item in Model)
    KeyValue
    @item.Key @item.Value.Value . ConvertTo(typeof(string))
    运行该程序之后,一个用于编辑联系人信息的页面会被呈现出来。直接点击"保存"按 钮提交表单,用于输出当前 ModelState 结构的页面会随之出现。如图 5-12 所示, ModelState 中的值与提交的表单具有相同的结构和数据值。 图 5-12 ModelBinder 针对 ModelState 的设置 ASP. NET MVC 4 框架揭秘5.3 Model8inder _ 227 5.3.3 ModelBindingContext 的创建 通过前面的介绍我们知道最终的 Model 绑定是通过调用相应 ModelB inder 的 BindModel 方法来完成的,该方法具有两个基于上下文的参数,一个是代表当前 Controller 上下文的 ControllerContext 对象,另一个则是针对当前 Model 绑定上下文的 Mode lB indingContext 对象。 前者在 Controller 激活的时候就已经创建了,那么只要我们能够针对当前 Model 绑定创建出 相应的 ModelB indingContext 对象,整个 Model 绑定就迎刃而解。 在正式介绍 ModelB indingContext 的创建过程之前先来看看 ModelB indingContext 的定 义。如下面的代码片段所示, ModelB indingContext 具有一系列属性。由于 Model 绑定是针 对描述 Action 方法的某个参数的元数据进行的,所以这些属性基本上来源于用于描述 Action 方法参数的 ParameterDescriptor 对象。 public class ModelBindingContext public string ModelName { get; set; } public Type ModelType { get; set; } public Model~etadata ModelMetadata { get; set; } public IDictionary PropertyMetadata { get; } public object public ModelStateDictionary public 工 ValueProvider Model { get; set; } ModelState { get; set; } ValueProvider { get; seti } public bool FallbackToEmptyPrefix { geti seti } public Predicate PropertyF 工 lter { get; seti } 整个 Model 绑定在 Action 的执行过程中进行,而 Action 的执行是通过组件 ActionInvoker 来完成的。当 Actionlnvoker 在执行目标 Action 的时候,会得到用于描述 Action 的 ActionD escriptor 对象。然后它遍历 ActionDescriptor 的参数列表,并根据对应的 ParameterDescriptor 对象结合当前 Controller 上下文创建 ModeillindingContext 。最后通过上 面介绍的 Modeillinder 提供机制获取对应的 ModelBinder ,被创建的 ModelB indingContext 对 象作为参数调用 ModelB inder 的 GetModel 方法得到的对象就是相应的参数值。最终数据值 的提供是通过 ValueProvider 完成的,它提供的数据值同时以 ModelState 的形式保存起来。 ModelB indingContext 的 ModelName 和 ModelType 分别表示 Model 的名称和类型,而 Model 的类型自然就是参数类型。如果通过 ParameterDescriptor 的 Prefix 属性表示的前缀不 为空,那么该前缀将会作为 Model 的名称,否则 Model 的名称就是参数名。 ValueProvider 在进行数据提供过程中就是通过 ModelName 属性进行数据匹配的。 ModelB indingContext 的 ModelMetadata 表示针对参数类型的 Model 元数据,而参数类 型属性列表的 Model 元数据被保存在 Prope句rMetadata 属性中。 Prope吗rMetadata 属性类型为 IDictionary,它的 Key 表示属性名称。 Model 元数据信息通过注册的 ModelMetadataProvider 来提供,具体的 Model 元数据提供机制请参见第 4 章 "Model 元数据 ASP. NET MVC 4 在架揭秘228 • 第 5 章 Model 的绑定 的解析"。 ModelB indingContext 的 Model 表示最终绑定的参数值,其 ModelState 属性和 Controller 的 ViewData 的同名属性引用同一个 ModelStateDictionary 对象,所以 Model 绑定过程中对 ModelState 的设置会反映在 Controller 的 ViewData 上,用于提供具体数据值的 ValueProvider 来源于 Controller 的同名属性。 如果没有利用 BindAttribute 特性为参数设置一个前缀,默认情况下会将参数名称作为前 缀。通过前面的介绍我们知道,这个前缀会被 ValueProvider 用于数据的匹配。如果 ValueProvider 通过此前缀找不到匹配的数据,将剔除前缀再次进行数据获取。针对如下定义 的 Action 方法 AddContacts ,在请求数据并不包含基于参数名 ("foo" 和 "b缸")前缀的情 况下,两个参数最终将被绑定上相同的值。 public class ContactController public void AddContacts(Contact foo, Contact bar) //省咯实现 反之,如果我们应用 BindAttribute 特性显式地设置了一个前缀,这种去除前缀再次实施 Model 绑定的后备机制将不会被采用,是否采用后备 Model 绑定策略通过 ModelB indingContext 的 FallbackToEmptyPrefix 属性来控制。 最后一个参数 Prope町F i1ter 返回的是一个 Predicate<耐ing>类型的委托,布尔类型的返 回值表示指定的属性是否会参与 Model 的绑定。具体的逻辑依赖于 ParameterDescriptor 的 Bindinglnfo 属性表示的 ParameterBindinglnfo 对象。具体来说,如果该对象的 Inc1ude 属性(参 与绑定属性集合)为空或者不包含指定的属性,并且指定的属性不包含在 Exc1ude 属性(不 参与绑定的属性集合)中,该委托对象返回 True ,否则返回 False 。 由于数据值的提供最终通过 ValueProvider 来完成,而它只能提供简单类型的数据,所 以对于简单类型参数的 Model 绑定可以直接通过 ValueProvider 来提供。但是对于复杂类型 参数来说,则会遍历属性列表,按照相同的方式提供其对应的属性值,所以 Model 绑定是一 个递归的过程。那么默认情况下采用的 Mode1绑定具有怎样的流程,我们将在下面一节对其 进行详细地讲述。 5.4 Model 绑定的默认实现 总的来说,针对目标 Action 方法参数的 Model 绑定完全由组件 ModelBinder 来完成,在 默认情况下使用的 ModelB inder 类型为 System. Web.Mvc.DefaultModelB inder 。接下来我们以 实例演示的方式逐层深入地讲述 ASP.NETMVC 实现在 DefaultModelB inder 中的默认 Model 绑定机制。 ASP. NET MVC 4 框架揭秘5 .4 Model 绑定的默认实现 混 229 5.4.1 简单类型 对于旨在绑定目标 Action 方法参数值的 Model 来说,最简单的莫过于简单参数类型的 绑定。通过第 4 章 "Model 元数据的提供"的介绍我们知道复杂类型和简单类型之间的区别 仅仅在于它们是否支持针对字符串类型的转换。参数对象的数据源均以字符串形式存在于请 求之中(比如提交的表单、 JSON 字符串以及查询字符串等),而 ValueProvider 也只能实现 基于简单类型的数据提供。对于基于简单类型参数的 Model 绑定,只需要将参数名称或者通 过应用在参数上的 Bin dAttribute 特性指定的前缀作为 Key 调用 ValueProvider 的 GetValue 方 法,并将得到的 ValueProviderResult 对象转换成参数类型即可。 为了演示针对简单数据类型的 Model 绑定,我们自定义了如下一个 DefaultModeffiinder , 它是对 ASP.NETMVC 默认使用的 DefaultModeffiinder 的模拟。随着 Model 绑定类型的复杂 化,我们将对它进行逐渐地完善,最终的 DefaultMode lB inder 将基本能够体现真正的 Model 绑定的实现。 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 modelType , IValueProvider valueProvider , string key) if (!valueProvider.ContainsPrefix(key)) return null; return valueProvider.GetValue(key) .ConvertTo(modelType); 实现在 DefaultModeffiinder 的 Model 绑定体现在 Ge tM odel 方法中,三个参数分别表示 Model 类型、 ValueProvider 对象和被它用于获取数据的 Key 。对于简单类型参数的 Model 绑 定来说, Model 类型为参数类型,而 Key 实际上是参数名称或者是参数上应用的 Bin dA ttribute 特性指定的前缀。这三个对象都可以通过表示 Model 绑定上下文的 ModeffiindingContext 对 象获取。具体参数值最终通过调用 ValueProvider 的 GetValue 来获取,该方法返回的是一个 ValueProviderResult 对象,我们需要调用它的 Conv 创刊方法转换成参数类型。 现在我们通过一个具体的实例来演示 ASP. 阳 TMVC 在执行目标 Action 的时候是如何基 于参数创建 Model 绑定上下文,并最终利用 Modeffiinder 来提供具体参数值的。在一个 ASP.NETMVC 应用中定义了如下一个 DemoController 。 ASP. NET MVC 4 框架揭秘230 .• 第 5 章 Model 的绑定 public abstract class DernoController : Controller public IModelBinder ModelBinder { geti private seti } r e --14 o r ←」n o 户Uo m e nυ C .工l b u p{ this.ModelBinder = new DefaultModelBinder()i this.ValueProvider = this.CreateValueProvider()i protected abstract IValueProvider CreateValueProvider()i protected ActionResult InvokeAction(string actionNarne) ControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(this.GetType())i ActionDescriptor actionDescriptor = controllerDescriptor.Fin dA ction( ControllerContext , actionNarne)i Dictionary pararneters = new Dictionary ( ) i foreach (PararneterDescriptor pararneterDescriptor in actionDescriptor.GetPararneters()) string rnodelNarne = pararneterDescriptor.Bindinglnfo.Prefix ?? pararneterDescriptor.PararneterNarnei ModelBindingContext bindingContext = new ModelBindingContext FallbackToErnptyPrefix = pararneterDescr 工 ptor.Bindinglnfo.Prefix == null , ModelMetadata ModelMetadataProviders.Current.GetMetadataForType(null , pararneterDescr 工 ptor.PararneterType) , ModelNarne = rnodelNarne , ModelState = ModelState , ValueProvider = this.ValueProvider pararneters.Add(pararneterDescr 工 ptor.PararneterNarne , this.ModelBinder.BindM odel(ControllerContext , bindingContext))i return (ActionResult)act 工 onDescriptor.Execute(ControllerContext , pararneters)i 我们在 DemoController 中定义了一个用于完成 Model 绑定的 Mode lB inder 属性,在构造 函数中该属性被初始化成我们自定义的 DefaultMode lB inder ,另一个抽象方法 Create ValueProvider 用于创建提供数据的 ValueProvider 对象。 核心 InvokeAction 方法用于执行指定的 Action (假定 Action 方法返回一个 Actio nResult 对象)。在该方法中我们针对自身类型创建出相应的 Con位 o l1 erDescriptor 对象,并根据 Action 名称得到对应的 Actio nD escriptor 对象。然后根据 Actio nD escriptor 获取用于描述其参数的 ParameterDescriptor 列表,而 Model 绑定就是根据每个 ParameterDescriptor 进行的。 具体来说,根据针对 P 缸缸neterDesαiptor 对象创建表示 Model 绑定上下文的 Mode lB indingContext 对象,并对其 ModelName 、 ModelState 、 ValueProvider 、 ModelMetadata ASP. NET MVC 4 框架揭秘5 .4 Model 绑定的默认实现 • 231 和 FallbackToEmptyPrefix 属性进行了初始化。最后将该 Mode lB indingContext 对象和当前 ControllerContext 作为参数调用 Mode lB inder 的 Bin dM odel 方法得到相应的参数值。所有的 参数最终被添加到一个字典对象中(恨Ke叮y 表示参数名称〉λ,它会作为参数调用 Actωion曲De臼scααr恫i 的 Execute 方法实现对指定 Action 的执行。 我们接下来创建如下一个继承自 DemoController 的 HomeController 0 Action 方法 DemoAction 具有三个简单类型的参数 foo 、 b 缸和 b 缸,其中最后一个参数上应用了 Bin dA ttribute 特性将绑定前缀指定为 "qux" 。我们将三个参数值添加到一个字典对象中 (Key 表示参数名称) ,并将其作为 Model 对象呈现在对应的 View 中。 public class HomeController : DemoController protected override IValueProvider CreateValueProvider() { NameValueCollection dataSource = new NameValueCollection(); dataSource.Add("Foo" , "ABC"); dataSource.Add("Bar" , "123"); dataSource.Add("Baz" , "456.01"); dataSource.Add("Qux" , "789.01"); return new NameValueCollectionValueProvider(dataSource , Culturelnfo.CurrentCulture); public ActionResult Index() return this.lnvokeAction("DemoAction"); public ActionResult DemoAction( string foo , int bar , [Bind(Prefix="qux") ] double baz) Dictionary parameters = new Dictionary () ; parameters.Add("foo" , foo); parameters.Add("bar" , bar); parameters.Add("baz" , baz); return View("DemoAction" , parameters); 实现的抽象方法 Create ValueProvider 返回一个 NameValueCollection ValueProvider 对象, 针对 DemoAction 方法的参数定义,作为数据容器的 Name ValueCollection 包含了对应的数据 项 (foo 、 bar 、 baz 和 qux) 。在默认的 Action 方法In dex 中,我们调用 InvokeAction 方法实 现对 Action 方法 DemoAction 的执行。 如下所示的是 Action 方法 DemoAction 对应 View 的定义,可以看出这是一个 Model 类 型为 IDictionary的强类型 View 。在该 View 中,作为 Model 的字典的 Key 和 Value 通过-个表格呈现出来。 ASP. NET MVC 4 框架揭秘232 '. 第 5 章 Model 的绑定 @model IDictionary 绑定的参数 @foreach (var item in Model)
    NameValue
    @item.Key@item.Value
    该程序运行之后将在浏览器中得到如图 5-13 所示的输出结果,可以看出 Action 方法 DemoAction 得到正常的执行,而呈现出来的就是绑定的参数列表。作为 ValueProvider 数据 容器的 NameValueCollection 中包含 Key 分别为"b缸"和"qux"的两个数据项,由于 DemoAction 的参数 b但上应用了 BindAttribute 特性将绑定前缀设置为 "qux" ,所以对应的 Key 是 "qux" 而不是参数名 "baz"o (S510) 图 5-13 简单类型的 Model 绑定 5.4.2 复杂类型 对于简单类型的参数来说,由于支持与字符串类型之间的转换,相应 ValueProvider 可 以从数据源中提取相应的数据并直接转换成参数类型,所以针对简单类型的 Model 绑定是一 步到位的过程,但是针对复杂类型的 Model 绑定就没有这么简单了。 复杂对象可以表示为一个树形层次化结构,其对象本身和属性代表相应的节点,叶子节 点代表简单数据类型属性。 ValueProvider 采用的数据容器具有一个扁平的数据结构,它通过 采用"属性名称链"为前缀的 Key 实现与这个"对象树"中的叶子节点的映射。 ←」c a t n o c s s a 14 C C .-1-b u p ,t s qdqJqds nnne ·工·工 -lr rrrd tttd SSSA CCCC ·--l-1· 工 1414T 牛『 i b ‘D·D-b uuuu pppp Name { geti seti } PhoneNo { geti seti } EmailAddress { geti seti } Address { geti seti } ASP. NET MVC 4 框架蜀秘• 233 Model 绑定的默认实现5 .4 s s e r d d A S s a 14 C C '工l b u p{ public string Province { get; set; } public string City { get; set; } public string District { get; set; } public string Street { get; set; } 以我们熟悉的 Contact 类型为例,它具有三个简单类型的属性 (Name 、 PhoneNo 和 EmailAddress) 和复杂类型 Address 的属性,而一个 Address 对象又具有四个简单类型的属 性。一个 Contact 对象的数据结构可以通过如图 ι14 所示的树来表示,这棵树的所有叶子节 点均为简单类型的属性。如果需要通过一个 ValueProvider 来构建一个完整的 Contact 对象, 它必须能够提供所有叶子节点的数值,而 ValueProvider 通过基于属性名称前缀的 Key 实现 与对应的叶子节点的映射。 刀『 mwsx· 》且巳「 mmm· ∞叶『 00 月 节『∞ 44}{· 〉且且『 Omm· 口一回叶『-口【 节u32} 〈·〉巳且『 Omm-o-q Province 市『 02}{· 〉且且『 Omm· 屯『 O 〈 -2 口。 复杂对象层次化结构中叶子节点对应的 Key 实际上当我们调用 HtmlHelper 的模板方法 EditorF orÆditorF orModel 的时候就 是按照这样的匹配方式对表单元素进行命名的。假设在一个将 Contact 作为 Model 类型的强 类型 View 中,我们按照如下的方式调用 HtmIHelper 的扩展方法 EditorFor 将 Model 对象的所有信息以编辑的模式呈现出来。 图 5-14 @modelContact @Html.EditorFor(m => m.Name) @Html.EditorFor(m => m.PhoneNo) @Html.EditorFor(m => m.EmailAddress) @Html.EditorFor(m => m.Address.Province) @Html.EditorFor(m => m.Address.City) @Html.EditorFor(m => m.Address.District) @Html.EditorFor(m => m.Address.Street) ASP. NET MVC 4 框架摄秘234 • 第 5 章 Model 的绑定 下面的代码片段代表了作为 Model 对象的 Contact 在最终呈现出来的 View 中的 HTML , 可以清楚地看到这些表单元素完全是根据属性名称和类型层次结构进行命名的。顺便 提一下,对于基于提交表单的 Model 绑定来说,作为匹配的是表单元素的 name 属性而非 id 属性,所以这里的命名指的是 name 属性而非 id 属性。 针对复杂类型的 Model 绑定是一个递归的过程,它先通过反射根据数据类型创建相应的 对象,然后绑定其属性值。每一次递归都会将属性名称附加到现有前缀上作为下一级递归的 前缀。现在我们将针对复杂类型的 Model 绑定实现在自定义的 DefaultModelB inder 之上。 首先在 DefaultModelB inder 中定义如下一个根据指定类型创建相应对象的 CreateModel 方法,默认通过调用 Activator 的 CreateInstance 方法创建给定类型的对象。如果指定的类型 是 IDictionary<,>接口,我们会创建一个 Diction缸严,>对象:如果是 IEnumerable<> 、 ICollection<>或者 IList<>接口,则会创建一个 List<>对象。 public class DefaultModelBinder : IModelBinder //其他成员 private object CreateModel(Type rnodelType) Type type = rnodelTypei if (rnodelType.IsGenericType) Type genericTypeDefiηi tion = rnodel Type. GetGenericTypeDefini tion () ; if (genericTypeDefinition == typeof(IDictionary<,>)) type = typeof(D 工 ctionary< ,>) .MakeGenericType( rnodelType.GetGenericArgurnents()); else if (((genericTypeDefinition == typeof(IEnurnerable<>)) II (genericTypeDefinition == typeof(ICollection<>))) II (genericTypeDefinition == typeof(IList<>))) type = typeof(List<>) .MakeGenericType( rnodelType.GetGenericArgurnents()); return Activator.Createlnstance(type); 然后我们定义了如下一个 GetComplexModel 方法,根据指定的数据类型、 ValueProvider 和前缀来创建一个对应的数据对象。在该方法中,我们先调用上面定义的 CreateModel 方法 根据指定的类型创建一个相应的对象,然后遍历复杂类型的属性列表,将提供的前缀和属性 ASP.NET MVC 4 框架揭秘5.4 Model 绑定的默认实现 醺 235 名组成一个新的 Key ,并以此 Key 调用 GetModel 方法得到对应的属性值,最终通过反射对 属性进行赋值。 public class DefaultModelBinder :工 ModelBinder //其他成员 protected virtual object GetComplexModel (ControllerContext controllerContext, Type modelType, IValueProvider valueProvider, string prefix) object model = CreateModel(modelType); foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(modelType)) y l n o d a e R S T·-vd ←」r e p o r nY SL ·工 JL cont 工 nue; string key = string.IsNullOrEmpty(prefix) ? property.Name : prefix + "." + property.Name; property.SetValue(model, GetModel(controllerContext, property.PropertyType, valueProvider, key)); } return model; 我们将针对 GetComplexModel 方法调用添加到 GetModel 方法中。如下面的代码片段所 示,我们根据数据类型创建出用于描述 Model 元数据的 ModelMetadata 对象,根据它的 IsComplexType 属性判断参数类型是否是复杂类型,如果是则调用 GetComple油lodel 方法来 获取对应的复杂对象。 public class DefaultModelBinder : IModelBinder public object BindModel(ControllerContext controllerContext, ModelBiηdingContext bindingContext) object model = this.GetModel (controllerContext, bindingContext. ModelType, bindingContext.ValueProvider, bindingContext.ModelName); if (bindingContext.FallbackToEmptyPrefix && null == model) model=this.GetModel(controllerContext, bindingContext.ModelType, bindingContext.ValueProvider, ""); } return model; public virtual object GetModel(ControllerContext controllerContext, Type modelType, IValueProvider valueProvider, string key) if (!valueProvider.ContainsPrefix(key)) return null; ModelMetadata modelMetadata = ModelMetadataProviders.Current . GetMetadataForType (null, modelType); ASP. NET MVC 4 框架揭秘236 .• 第 5 章 Model 的绑定 if (!modelMetadata.IsComplexType) return valueProvider.GetValue(key) .ConvertTo(modelType); if (modelMetadata.IsComplexType) return GetComplexModel(controllerContext, modelType, valueProvider, key) ; return null; 前面我们提到在进行 Model 绑定的时候,如果没有在参数上应用 B indAttribute 特性设置 一个绑定前缀, DefaultModeffiinder 会默认采用参数名称作为前缀。如果根据这个前缀获取 不到相应的数据,它会在剔除该前缀的情况下再进行一次 Model 绑定。 ModeffiindingContext 类型的 FallbackToEmptyPrefix 属性表示是否需要采用空字符串作为前缀再次实施 Model 绑 定,这样的机制也体现在前面定义的 BindModel 方法中。 我们使用前面用于演示简单类型 Model 绑定的实例来演示针对复杂类型的 Model 绑定, 为此我们对 HomeController 的 DemoAction 进行了重新定义,让它包含两个 Contact 类型的 参数 (foo 和 b町),并将它们相关的属性添加到一个字典对象中作为 Model 呈现在默认的 View 中。实现的 CreateValueProvider 方法中返回一个 NameValueCollection ValueProvider ,作为数 据容器的 NameValueCollection 提供了一个 Contact 对象的所有属性数据。 public class HomeController : DemoController //其他成员 protected override IValueProvider CreateValueProvider() NameValueCollection dataSource = new NameValueCollection(); dataSource.Add("Name", "张三") ; dataSource.Add("PhoneNo", "123456789"); dataSource.Add("EmailAddress", "zhangsan@gmail.com"); dataSource.Add ("Address. Province", "江苏") ; dataSource.Add("Address.City", "苏州'1") ; dataSource.Add("Address.District", "工业园区") ; dataSource.Add("Address.Street", "星湖街 328 号") ; return new NameValueCollectionValueProvider(dataSource, Culturelnfo.CurrentCulture); public ActionResult DemoAction(Contact foo , Contact bar) Dictionary parameters = new Dictionary ( ) ; parameters.Add("foo.Name", foo.Name); parameters.Add("foo.PhoneNo", foo.PhoneNo); parameters.Add("foo.EmailAddress", foo.EmailAddress); Address address = foo.Address; ASP. NET MVC 4 框架揭秘5.4 Model 绑定的默认实现 L.237 parameters.Add("foo.Address", s t:, ring.Format("{O} 省 {1 }市 {2}{3}" , address.Province, address.City, address.District, address.Street)); parameters.Add("bar.Name", bar.Name); parameters.Add("bar.PhoneNo" , bar.PhoneNo); parameters.Add("bar.EmailAddress", bar.EmailAddress); address = bar.Address; parameters.Add("bar.Address" , string.Format("{O} 省{ 1 }市 {2}{3}" , address.Province, address.City, address.District, address.Street)); return View("DemoAction" , parameters); 运行该程序后会在浏览器中呈现出如图 5-15 所示的输出结果,可以看出两个 DemoAction 方法的两个参数被绑定上了相同的数据。 (S511 ) !黯画也蝇嗣慢--=………… 区怒骂理由 口 oca l host: :H州 地理回 J写 与平,飞 foo.Name 张三 foo.PhoneNo 123456789 foo.EmailAddress zhangsan@gmail.com fooAdd ress 江苏省苏州市工业园区星湖街 328号 bar.Name 张三 bar.PhoneNo 123456789 bar.EmailAddress zhangsan@gmail.com bar .Address 江苏省苏州市工业园区星湖街 328号 图 5-15 复杂类型的 Model 绑定(1) 之所以同一个 Action 方法中两个相同类型的参数会绑定相同的数据,就是源于上面介 绍的"去除前缀的后备 Model 绑定机制"。现在我们对 HomeController 的 CreateValueProvider 方法进行如下修改使 ValueProvider 的数据容器中包含两组 Contact 数组,对应的 Key 采用了 Action 方法的参数名称作为前缀。 public class HomeController : DemoController //其他成员 protected override IValueProvider CreateValueProvider() NameValueCollection dataSource = new NameValueCollection(); dataSource.Add("foo.Name" , "张三") ; dataSource.Add("foo.PhoneNo", "123456789"); dataSource.Add("foo.EmailAddress" , "zhangsan@gmail.com"); dataSource.Add("foo.Address.Province" , "江苏") ; dataSource.Add("foo.Address.City", "苏州 n) ; dataSource.Add("foo.Address.District", "工业园区") ; dataSource.Add("foo.Address.Street", "星湖街 328 号") ; ASP. NET MVC 4 在架揭秘238 诲 第 5 章 Model 的绑定 dataSource.Add("bar.Narne" , "李四") ; dataSource.Add("bar.PhoneNo" , "987654321"); dataSource.Add("bar.ErnailAddress" , "lisi@grnail.com"); dataSource . Add("bar.Address.Province" , "江苏") ; dataSource.Add("bar.Address.City" , "苏州 1") ; dataSource.Add("bar.Address.District" , "工业园区") ; dataSource.Add("bar.Address.Street" , "全鸡湖路 328 号") ; return ηew NarneValueCollectionValueProvider(dataSource , Culturelnfo.CurrentCulture); 再次运行我们的程序后将会发现 DemoAction 方法的两个 Contact 类型的参数分别绑定 了不同的数据,具体的运行结果如图 5-16 所示。 (S512) 匮噩噩噩噩 \国国I'..., 0 localhos t: 10478 公 E温国 ‘ ~lJ t 寸' foo.Name 张三 foo.PhoneNo 123456789 foo.EmailAddress zhangsan@gmai l. com foo .Address 出 工苏省苏州市工业园区星湖街 328 号 bar.Name 学四 ba r. PhoneNo 987654321 bar.EmailAddress lisi@gmai l. com ba r .A ddress 江苏省苏州市工业园区金鸡湖路 328号 图 5-16 复杂类型的 Model 绑定 (2) 5.4.3 数组 基于数组和集合类型的 Model 绑定机制比较类似。作为 N ame ValueCollection ValueProvider 数据容器的 Name ValueCollection 对象并没有对 Key 作唯一性约束,如果目标 对象是一个简单类型的数组或者集合,同一个 Key 对应的多个 Value 将会转换成目标数组或 者集合的元素。此外,针对数组/集合的 Model 绑定还支持索引。 基于罔名数据顶的数组绑定 对于 NameValueConllectio nP rovider 来说,其 GetValue 方法得到的 ValueProviderResult 的 RawValue 属性总是一个字符串数组。当我们调用 ValueProviderResult 的 ConvertTo 方法将 提供的值转换成某种类型时,如果目标类型为一个简单类型,那么第一个字符串将会提取出 来并转换为目标类型。如果目标类型是一个元素为简单类型的数组或者集合,每个字符串都 会被提取出来并转换为目标数组/集合对象的元素。 ASP. NET MVC 4 在架揭秘5 .4 Model 绑定的默认实现 黯 239 N ame ValueCol1 ectionP rovider (包括 F orm ValueProvider 和 QueryString ValueProvider )的 数据提供机制决定了 Model 绑定的默认行为。如果绑定的目标对象是一个数组/集合,匹配 的同名数据项将会作为目标对象的元素。实际上 Ht甲 FileCo l1 ection ValueProvider 也采用了类 似的机制,即如果绑定的目标对象类型是一个 HttpPostedFileBase 数组,那么匹配的同名文 件输入元素都将作为其数据源。 假设我们在某个 Con位 oller 中定义了如下一个 Action 方法 DemoAction ,它具有两个数 组类型的参数 foo 和 b 缸,元素类型分别是字符串和 HttpPostedFileBase 。如果针对该 Action 的请求具有包含如上 元素的表单,那么两组 7G素的值将会分别绑定到这两个 名称匹配的参数上。 Public void DemoAction(string[] foo , HttpPostedFileBase[] bar) 现在我们将针对同名数据项的数组绑定实现在我们自定义的 DefaultModelliinder 中。如 下面的代码片段所示,定义了一个 Get Arr ayModel 来获取目标类型为数组的对象,在该方法 中,我们直接将传入的前缀作为参数调用 ValueProvider 的 GetValue 方法,并将得到的 ValueProviderResult 转换成目标数组类型。在 GetM odel 方法中,如果传入的数据类型为数组, 则调用 Ge tArr ayModel 方法返回绑定的对象。 public class DefaultModelBinder : IModelBinder //其他成员 public virtual object GetModel(ControllerContext controllerContext , Type modelType , IValueprov 工 der valueProvider , string key) if (!valueProvider.ContainsPrefix(key)) return nulli if (modelType.IsArray) return GetArrayModel(controllerContext , modelType , valueProvider , key) i //其他操作 protected virtual object GetArrayModel( ControllerContext controllerContext , Type modelType , IValueProvider valueProvider , string prefix) AS P. NET MVC 4 框架揭秘240 白第 5 章 Model 的绑定 if (valueProvider.ContainsPrefix(prefix) && !string.IsNullOrErnpty(prefix)) } ValueProviderResult result = valueProvider.GetValue(prefix); if (口 ull != result) return result.ConvertTo(rnodelType); return null; 为了演示针对同名数据项的数组绑定,我们对前面演示实例中的 HomeContro l1 er 进行了 如下的修改: Action 方法 DemoAction 具有两个数组类型的参数 foo 和 b町,我们将两个数组 的元素封装到一个字典对象中并作为 Model 呈现在默认的 View 中。对于通过 Create ValueProvider 方法创建的 Name ValueCol1ection ValueProvider ,作为其数据容器的 Name ValueCo l1ection 中具有两组数据,其 Key 分别是 DemoAction 方法的参数名。 public class HorneController : DernoController protected override IValueProvider CreateValueProvider() NarneValueCollection dataSource = new NarneValueCollection(); dataSource .Add ("foo" , "abc"); dataSource.Add("foo", "ijk"); dataSource.Add("foo", "xyz"); dataSource.Add("bar", "123"); dataSource.Add("bar", "456"); dataSource.Add("bar", "789"); return new NarneValueCollectionValueProvider(dataSource, Culturelnfo.CurrentCulture); public ActionResult DernoAction(string[] foo, int[] bar) Dictionary pararneters = new Dictionary ( ) ; for (int i = 0; i < foo.Length; i++) pararneters.Add(string.Forrnat("foo[{O}]", i) , foo[i]); for (int i = 0; i < bar.Length; i++) pararneters.Add(string.Forrnat("bar[{O}]", i) , bar[i]); } return View("DernoAction" , pararneters); 程序运行后会在浏览器中呈现出如图 5-17 所示的输出结果,可以看出 Action 方法 DemoAction 的两个参数得到了正常的绑定。 (S513 ) ASP.NET MVC 4 框架揭秘5 .4 Model 绑定的默认实现 诅 241 M酶声明, 。酶目I明E翩气 x翩I::J腼 I QI 宦、 气 foo[O] abc foo[1J 。 k foo(2) 可1 bar[O) 123 bar[1 ] 456 bar[2 ] 789 图 5-17 数组类型的 Model 绑定(1) 基于索引的数组绑定 ValueProvider 基于索引的匹配策略可以通过 HtmlHelper 的模板方法 EditorFor 来体现。如下面的代码片段所示,在一个 Model 为 Contact 数组的强类型 View 中,我们调 用它的扩展方法 EditorFor 将数组的前两个元素的属性以编辑模式呈现出来。 @modelContact[] @Html.EditorFor(m => m[O].Name) @Html.EditorFor(m => m[O].PhoneNo) @Html.EditorFor(m => m[O] . EmailAddress) @Html.EditorFor(m => m[l] .Name) @Html.EditorFor(m => m[l] .PhoneNo) @Html.EditorFor(m => m[l] . EmailAddress) 上面这段代码最终会生成如下-段 H Th缸, 6 次 EditorFor 方法的调用转换为 6 个文本 框,我们可以清楚地看到它们的名称被添加了 "[0]" 和 "[1] "这样的索引前缀。如果这些 元素存在于一个提交的表单中,并且目标 Action 方法包含一个匹配的 Contact 数组类型的参 数,选用的 Modeillinder 会为之生成一个包含两个元素的 Contact 数组作为其参数。 基于数组的 Model 绑定采用"零基索引",同时要求索引在数值上必须是连续的。举个 简单的例子,假设提交的表单中具有如下 6 个采用索引方式命名的 hidden 元素,但是索引 数值并不是连续的(缺了一个 [3]) 。 AS P. NET MVC 4 在架揭秘242 • 第 5 章 Model 的绑定 如果包含此表单的请求针对如下一个具有一个字符串数组参数的 Action 方法,上述的 ~素值将会绑定到参数 array 上。但是由于索引值不具有连续性,会导致后面的三个 ~ 素值(" 123" 、 "456" 和 "789") 被丢弃,也就是说绑定后的 array 参数值仅仅具 有三个元素(" foo" 、 "b 缸"和 "baz") 。 public ActionResult Index(string[] array); 除了采用零基整数作为数组索引之外,我们还可以采用任意字符串作为其索引,但是作 为索引的字符串需要和数组元素值一样存在于 ValueProvider 的数据源中。索引数据项名称 为 "index" ,并且与数组元素数据项具有相同的前缀。同样以上面这个参数类型为字符串数 组的 Action 方法为例,可以通过提交具有如下内容的表单来调用这个 Action 方法并为之提 供相应的参数值。 被提交表单中三个文本框的值将会绑定到目标 Action 方法的字符串参数 array 。它们通 过基于字符串的索引进行命名(" [frrst]" 、" [second] "和"[出 ird] 勺,而作为索引的字符串通 过 hidden 元素和作为参数绑定的数据一并提交,这些用于定义索引字符串的 hidden 元素统 一命名为 "index" 。 现在我们对自定义的 DefaultMode lB inder 进行进一步的完善,使基于数组的绑定支持两 种索引。我们先定义了如下一个 Ge tIn dexes 方法返回基于指定前缀的索引列表,输出参数 numeric In dex 表示返回的索引是否是"零基索引"。针对文字的索引通过 ValueProvider 的 GetValue 方法获取,而指定的 Key 为" {Prefix} .I ndex" 。如果这样的索引找不到, DefaultModelB inder 才会调用 Ge tZ eroBasedlndexes 方法返回零基索引。 public class DefaultModelBinder : IModelBinder //其他成员 private IEnumerable Getlndexes(string prefix , IValueProvider valueProvider , out bool numericlndex) string key = string.IsNullOrEmpty(prefix) ? "index" : prefix + "." +"index"; ValueProviderResult result = valueProvider.GetValue(key); if (null != result) string[] indexes = result.ConvertTo (typeof (string[] )) as string[]; if (null != indexes) numericlndex = false; AS P. NET MVC 4 框架揭秘5 .4 Model 绑定的默认实现 黯 243 return indexes; numericIndex = true; return GetZeroBasedIndexes(); private static IEnumerable GetZeroBasedIndexes() int iteratorVariable = 0; while (true) yield return iteratorVariable.ToString(); iteratorVariable++; 接下来我们对用于进行数组绑定的 Ge tArr ayModel 方法作进一步的完善。在该方法中, 我们先调用另一个 Get Li stModel 方法将最终需要绑定到数组对象的所有元素保存到一个 List对象中,然后根据目标数组元素类型创建一个具有与该列表具有相同长度的数 组对象,并将列表的对象拷贝到该数组中。至于针对 GetListModel 方法的实现,我们先将前 面介绍的针对同名数据项绑定的数组元素添加到这个列表中,然后遍历通过调用 Getlndexes 获取索引列表,将索引作为 Key 递归地调用 Ge tM odel 方法得到目标数组对象的元素,并将 其添加到列表中。 public class DefaultModelBinder : IModelBinder //其他成员 protected virtual object GetArrayModel(ControllerContext controllerContext , Type modelType , IValueprovider valueProvider , string prefix) List list = GetListModel(controllerContext , modelType , modelType.GetElementType() , valueProvider , prefix); object[] array = (object[])Array.CreateInstance( modelType.GetElementType() , list.Count); list.CopyTo(array); return array; private List GetListModel(ControllerContext controllerContext , Type modelType , Type elementType ,工 Valueprovider valueProvider , string prefix) List list = new List(); if (!string.IsNullOrEmpty(prefix) && valueProvider.ContainsPrefix(prefix)) ValueProviderResult result = valueProvider.GetValue(prefix); if (null != result) IEnumerable enumerable = result.ConvertTo(modelType) as IEnumerable; foreach (var value in enumerable) AS P. NET MVC 4 框架揭秘244 醺 第 5 章 Model 的绑定 list.Add(value); } bool numericlndex; IEnumerable indexes = GetIndexes(prefix , valueProvider , out numericlndex); foreach (var index in indexes) { } string indexPrefix = prefix + "[" + index + "]"; if (!valueProvider.ContainsPrefix(indexPrefix) && numericIndex) { break; list.Add(GetModel(controllerContext , elementType , valueProvider , indexPrefix)); return list; 同样通过我们的实例来演示针对索引的数组绑定,为此我们对 HomeController 进行了如 下的修改: DemoAction 具有一个 Contact 数组类型的参数 contacts ,在该方法中将该数组对 象的相关信息封装到一个字典对象中并作为 Model 呈现在默认的 View 中。通过 Create ValueProvider 方法返回的 Name ValueCollection ValueProvider 以"零基索引"的形式提 供了两个 Contact 对象的数据。提供的数据采用参数名称 contacts 作为前缀,按照前面介绍 的"剔除前缀的后备 Model 绑定机制",这个前缀在这里是可以省掉的。 public class HomeController : DemoController //其他成员 protected override IValueprovider CreateValueProvider() NameValueCollection dataSource = new NameValueCollection(); dataSource.Add("contacts[Q] .Name" , "张三") ; dataSource.Add("contacts[Q] . PhoneNo" , "123456789"); dataSource.Add("contacts[Q] . EmailAddress" , "zhangsan@gmail.com"); dataSource.Add("contacts[O] .Address.Province" , "江苏") ; dataSource.Add("contacts[Q] .Address.City" , "苏州") ; dataSource.Add("contacts[Q] .Address.District" , "工业园区") ; dataSource.Add("contacts[Q] .Address.Street" , "星湖街 328 号") ; dataSource.Add("contacts[l] .Name" , "李四") ; dataSource.Add("co 口 tacts[l] . PhoneNo" , "987654321"); dataSource.Add("contacts[l] . EmailAddress" , "lisi@gmail.com"); dataSource.Add("contacts[l] .Address.Province" , "江苏") ; dataSource.Add("co 口 tacts[l] .Address.City" , "苏州") ; dataSource.Add("contacts[l] .Address.District" , "工业园区") ; dataSource.Add("contacts[l] .Address.Street" , "金鸡湖路 328 号") ; return new NameValueCollectionValueProvider(dataSource , CultureInfo.CurrentCulture); AS P. NET MVC 4 框架揭秘5 .4 Model 绑定的默认实现 ~ . 245 public ActionResult DernoAction(Contact[] contacts) Dictionary pararneters = new Dictionary () ; for (int i = 0; 土< contacts.Length; i++) string narne = contacts[i] .Narne; string phoneNo = contacts[i].PhoneNo; string ernailAddress = contacts[i] . ErnailAddressi string address = 'string. Forrnat (" {O} 省{ 1 }市 {2}{3}" , contacts[i] .Address.Province , contacts[i] .Address.City , contacts[i] .Address.District , contacts[i] .Address.Street); pararneters.Add(string.Forrnat(" [{O}] . Narne " , i) , narne); pararneters.Add(string.Forrnat("[{O}] . PhoneNo" , i) , phoneNo); parameters .Add (string. Forrnat (" [{ O}] . EmailAddress" , i) , ernailAddress); pararneters.Add(string.Format("[{O}].Address" , i) , address); return View("DernoAction" , pararneters); 程序运行之后会在浏览器中得到如图 5-18 所示的输出结果,可以看出 DemoAction 的 Contact 数组参数按照我们希望的方式得到了绑定。 (S514) 睦否有 理 L.....\J.' [O).Name 张三 [O). PhoneNo 1 23456789 [O) .EmaiIAddress zhangsan@gmail.com [ O] Address 在 苏省苏州市工业园区星湖街 328 号 [ l ] .Name 李四 [ 1] .PhoneNo 987654321 ( l ) .EmaiIAddress lisi@gmail.com [ l j Address 江苏省苏州市工业园区金鸡湖路 328号 图 5-18 数组类型的 Model 绑定 (2) 上面这个例子演示了针对基零整数作为索引的数组绑定,基于文字的索引同样是支持 的。如果我们将 HomeController 的 GetValueProvider 方法改写成如下的形式,程序运行后依 然会得到相同的输出结果。 (S515) public class HorneController : DernoController protected override IValueProvider CreateValueProvider() NameValueCollection dataSource = new NameValueCollection(); dataSource.Add("contacts.index" , "first"); dataSource.Add("contacts.index" , "second"); AS P. NET MVC 4 在架揭秘246 • 第 5 章 Model 的绑定 dataSource.Add("contacts[first] . Narne" , "张三") ; dataSource.Add("contacts[first] . PhoneNo" , "123456789"); dataSource.Add("contacts[first] . Ernai1Address" , "zhangsan@grnail.com"); dataSource.Add("contacts[first] .Address.Province" , "江苏") ; dataSource.Add("contacts[first] .Address.City" , "苏州") ; dataSource.Add("contacts[first] .Address.District" , "工业园区") ; dataSource.Add("contacts[first] .Address.Street" , "星湖街 328 号") ; dataSource.Add("contacts[second] . Narne" , "李四") ; dataSource.Add("contacts[second] . PhoneNo" , "987654321"); dataSource.Add("contacts[second] . ErnailAddress" , "lisi@grnail.com"); dataSource.Add("contacts[second] .Address.Province" , "江苏" ) ; dataSource.Add("contacts[second] .Address.City" , "苏州 1") ; dataSource.Add("contacts[second] .Address.District" , "工业园区") ; dataSource.Add("contacts[second] .Address.Street" , "金鸡湖路 328 号") ; return new NarneValueCollectionValueProvider(dataSource , Culturelnfo.CurrentCulture); 5.4.4 集合 这里的集合指的是除数组和字典之外的所有实现 IEnumerable 接口的类型,和基于数 组的 Model 绑定类似, ValueProvider 可以将多个同名的数据项作为集合的元素,基于索引(整 数和字符串〉绑定在这里同样适用。我们对自定义的 DefaultMode lB inder 进一步完善使之支 持集合类型的 Model 绑定,为此我们定义了如下一个 GetCollectio nM odel 方法。 public class DefaultModelBinder : IModelBinder //其他成员 protected virtual object GetCollectionModel( ControllerContext controllerContext , Type rnodelType , IValueProvider valueProvider , string prefix) Type elernentType = rnodelType.GetGenericArgurnents() [0]; List list = GetListModel(controllerContext , rnodelType , elernentType , valueProvider , prefix); object rnodel = CreateModel(rnodelType); ReplaceHelper.ReplaceCollect 工。 n(elernentType , rnodel , list); return rnodel; internal static class ReplaceHelper private static Methodlnfo replaceCollectionMethod = typeof (ReplaceHelper) . GetMethod ("ReplaceCollectionlrnpl" , BindingFlags.Static IBindingFlags.NonPublic); public static void ReplaceCollection(Type elernentType , object rnodel , object list) AS P. NET MVC 4 框架揭秘5.4 Model 绑定的默认实现 麟 247 replaceCollectionMethod.MakeGenericMethod( new Type[] { elementType }) . Invoke (null, new obj ect [] { model, list }); private static void ReplaceCollectionImpl( ICollection model, IEnumerable list) model.Clear(); if (list != null) foreach (object obj2 in list) T item = (obj2 is T) ? ((T) obj2) : default (T); model.Add(item); 在 GetCollectionModel 方法中,我们按照数组绑定的方式调用 GetListModel 方法得到包 含所有集合元素的 List对象,然后根据数据类型调用 CreateModel 方法创建一个空 的集合对象,根据前面给出的对该方法的定义我们知道最终创建出来的是一个 List<>对象, 最后我们调用定义在类型 ReplaceHelper 中的辅助方法 ReplaceCollection 将包含 List 对象中的元素拷贝到这个空 List<>对象中并其返回。 现在需要将针对 GetCollectionModel 方法的调用放到 GetModel 方法中。如下面的代码 片段所示,我们定义了一个 ExtractGenericInterface 方法来辅助判断数据类型是否是一个实现 了 IEnumerable<>接口的集合类型。需要注意的是,由于数组也实现了 IEnumerable接口, 所以需要将下面这段代码放到针对数组绑定的 GetArrayModel 方法调用之后,否则 GetArrayModel 方法将永远得不到执行。 public class DefaultModelBinder : IModelBinder //其他成员 public virtual object GetModel(ControllerContext controllerContext, Type modelType, IValueProvider valueProvider, string key) //其他操作 Type enumerableType = ExtractGener 工 cInterface(modelType , typeof(IEnumerable<>)); if (null != enumerableType) return GetCollectionModel(controllerContext, modelType, valueProvider, key); //其他操作 return valueProvider.GetValue(key) .ConvertTo(modelType); private Type ExtractGenericInterface(Type queryType, Type interfaceType) Func predicate = t => t.IsGenericType && (t.GetGenericTypeDefinition() == interfaceType); ASP. NET MVC 4 在架揭秘248 旬第 5 章 Model 的绑定 e p y m4 y r e u q e 』La c -工d e r P Et­-LIt return queryType.GetInterfaces() .FirstOrDefault(predicate)i return queryTypei 在上面的实例中我们演示了针对数组的绑定 CS515) ,如果需要演示针对集合的绑定,只 需要按照如下的方式将定义在 HomeCon位oller 中的 DemoAction 方法的参数从 Contact 数组类型 改为 IEnumerable ep 可,运行新的程序之后依然可以得到如图 5-18 所示的输出结果。 public class HorneController : DernoController //其他成员 public ActionResult DernoAction(IEnurnerable contacts) Contact[] contactArray = contacts.ToArraY()i Dictionary pararneters = new Dictionary () i for (int i = Oi i < contactArray.Lengthi i++) string narne = contactArray[i].Narnei string phoneNo = contactArray[i] .PhoneNoi string ernailAddress = contactArray[i] .ErnailAddressi string address = string.Forrnat("{O} 省{ 1} 市 {2}{3}" , contactArray[i] .Address.Province, contactArray[i] .Address.City, contactArray[i] .Address.District, contactArray[ 土 ].Address.Street)i pararneters.Add(string.Forrnat(" [{O}] .Narne" , i) , narne)i pararneters.Add(string.Forrnat("[{O}] . PhoneNo" , i) , phoneNo)i pararneters .Add (string. Forrnat (" [{ O}] . EmailAddress" , i) , ernailAddress) i pararneters.Add(str 工 ng.Forrnat("[{O}] .Address", i) , address)i } return View("OernoAction", pararneters)i 5.4.5 字典 这里的字典指的是实现了接口 IDictionary 的类型,在 Model 绑定过程中 基于字典类型的数据映射很好理解。 • 字典是-个 KeyValuePair对象的集合,所以在字典元素这一级可以采用 基于索引的匹配机制。 • KeyValuePair是一个复杂类型,可以按照属性名称 CKey 和 Value) 进 行匹配。 比如说作为某个 ValueProvider 数据源的 NameValueCollection 具有如表 5-5 所示的结构, 它可以映射为→个 IDictionary对象 CContact 对象作为 Value ,其 Name 属性 作为 Key) 。 ASP.NET MVC 4 框架揭翻5.4 Model 绑定的默认实现 磁 249 表 5-5 字典对象在 NameValueCollection 中的表示 Key Value [O].Key 张三 [O].Value.Name 123456789 [0] .Value.EmailAddress zhangsan@gmail.com [l].Key 李四 [l].Value.Name 987654321 [l].Value.EmailAddress lisi@gmail.com 现在我们对用于模拟默认 Model 绑定的自定义 DefaultModeffiinder 作最后的完善,使之 支持针对字典类型的 Model 绑定。如下面的代码片段所示,针对字典类型的 Model 绑定被 定义在 GetDictionaryModel 方法中。 public class DefaultModelBinder : IModelBinder //其他成员 protected virtual object GetDictionaryModel( ControllerContext controllerContext, Type modelType, IValueprovider valueProvider, string prefix) List> list = new List>(); bool numericlndex; IEnumerable indexes out numericlndex); Type[] genericArguments Type keyType Type valueType foreach (var index in indexes) = Getlndexes(prefix, valueProvider, = modelType.GetGenericArguments(); = ge 口 ericArguments[O]; = genericArguments[l]; string indexPrefix = prefix + "[" + index + "]"; if (!valueProvider.ContainsPrefix(indexPrefix) && numeric 工 ndex) { break; } string keyPrefix = indexPrefix + ".Key"; string valulePrefix = indexPrefix + ".Value"; object key = GetModel(controllerContext, keyType, valueProvider, keyPrefix); object value = GetModel(controllerContext, valueType, valueProvider, valulePrefix); list.Add(new KeyValuePair (key, value)); object model = CreateModel(modelType); ReplaceHelper.ReplaceDictionary(keyType, valueType, model, list); 主 eturn model; ASP. NET MVC 4 框架揭秘250 匍第 5 章 Model 的绑定 与在进行数组/集合绑定过程调用 GetListModel 方法获取所有数组/集合元素的 List的逻辑类似,我们将最终绑定到目标字典对象的所有元素添加到一个预先创建 的 List>对象中。对于每个 KeyValuePair对象的 Key 和 Value ,我们递归地调用 GetModel 方法来实现,作为参数的 Key 由指定的前缀添加上 ".Key" 和" .Value" 字符组合而成。接下来通过调用 CreateModel 方法针对数据类型创建一 个空字典对象( Diction町<,>) ,然后通过定义在 ReplaceHelper 中具有如下定义的 ReplaceDictionary 方法将 List >对象中的元素拷贝到字典对象中 并返回。 internal static class ReplaceHelper //其他成员 private static Methodlnfo replaceDictionaryMethod = typeof (ReplaceHelper) . GetMethod ("ReplaceDictionarylmpl", BindingFlags.Static IBindingFlags.NonPublic); public static void ReplaceDictionary(Type keyType, Type valueType, object dictionary, object newContents) replaceDictionaryMethod.MakeGenericMethod( new Type[] { keyType, valueType }) . Invoke (null, new object[] { dictionary, newContents }); private static void ReplaceDictionarylmpl( IDictionary dictionary, IEnumerable> newContents) dictionary.Clear(); foreach (KeyValuePair pair in newContents) TKey key = (TKey)pair.KeYi TValue loca12 = (TValue) ((pair.Value is TValue) ? pair.Value : default(TValue)); dictionary[key] = loca12; 我们通过如下的方式将针对 GetDictionaryModel 方法的调用放到 GetModel 中,值得一 提的是,由于字典类型也实现了 IEnumerable接口,我们必须将下面这段代码放到针对集 合绑定的 GetCollectionModel 之前,否则这段代码将永远得不到执行。 public class DefaultModelBinder : IModelBinder { //其他成员 public virtual object GetModel(ControllerContext controllerContext, Type modelType, IValueProvider valueProvider, string key) //其他操作 Type dictionaryType = ExtractGenericlnterface(modelType, ASPNETMVC4 翻揭秘5.4 Model 绑定的默认实现 回 251 typeof(IDictionary<,>)); if (null != dictionaryType) return GetDictionaryModel(controllerContext, modelType, valueProvider, key); //其他操作 我们照例使用现成的实例来演示针对字典类型的绑定,为此我们对 HomeController 进行 了如下的改写: Action 方法 DemoAction 具有一个 IDictionary类型的参数, 我们将该参数封装成另一个字典对象并作为 Model 对象呈现在默认的 View 中。在 Create ValueProvider 方法中创建 NameValueCollection ValueProvider 对象按照基于字典绑定规 则添加了两个 Contact 对象的数据。 public class HomeController : DemoController //其他成员 protected override IValueProvider CreateValueProvider() NameValueCollection dataSource = new NameValueCollection(); dataSource.Add("contacts.index", "first"); dataSource.Add("contacts.index", "second"); dataSource.Add("contacts[first] .Key", "张三") ; dataSource.Add("contacts[first] .Value.Name", "张三") ; dataSource.Add("contacts[f 工 rst] .Value.PhoneNo", "123456789"); dataSource.Add("contacts[f 工 rst] .Value.EmailAddress", "zhangsan@gmail.com"); dataSource.Add("contacts[first] .Value.Address.Province", "江苏") ; dataSource.Add("contacts[first] .Value.Address.City", "苏州1") ; dataSource.Add("contacts[first] .Value.Address.District", "工业园区") ; dataSource .Add ("contacts [first] . Value .Address. Street", "星湖街 328 号") ; dataSource.Add("contacts[second] .Key", "李四") ; dataSource.Add("co口 tacts[second] .Value.Name" , "李四") ; dataSource.Add("contacts[second] .Value.PhoneNo", "987654321"); dataSource.Add("contacts[second] .Value.EmailAddress", "lisi@gmail.com"); dataSource.Add("contacts[second] .Value.Address.Province", "江苏") ; dataSource.Add("contacts[second] .Value.Address.City", "苏州") ; dataSource.Add("contacts[second] .Value.Address.District", "工业园区") ; dataSource.Add("contacts[second] .Value.Address.Street", "金鸡湖路 328 号") ; return new NameValueCollectionValueProvider(dataSource, CultureInfo.CurrentCulture); public ActionResult DemoAction(IDictionary contacts) var contactArray = contacts.ToArray(); Dictionary par四eters = new Dictionary(); foreach (var item in contacts) ASP. NET MVC 4 框架揭秘252 • 第 5 章 Model 的绑定 string address = string.Format("{O} 省 {1 }市 {2}{3}" , item.Value.Address.Province , item.Value.Address.City , item.Value.Address.District , item.Value.Address.Street); parameters.Add(string.Format("contacts[\"{O}\"] .Name" , item.Key) , item.Value.Name); parameters.Add(string.Format("contacts[\"{O}\"].PhoneN0" , item.Key) , item.Value.PhoneNo); parameters.Add(string.Format("contacts[\"{O}\"] . EmailAddress" , item.Key) , item.Value . EmailAddress); parameters.Add (string. Format ("contacts[\"{O}\"] .Address" , item.Key) , address); return View("DemoAction" , parameters); 该程序运行之后会在浏览器中呈现出如图 5-19 所示的输出结果,可以看到 Action 方法 DemoAction 的参数按照我们期望的方式完成了绑定。 (8517) l摆嚣胃 lC4ï8 曹 l J:. c onta 也("张三丁 . Name 张三 c ontacts[" 张三 "] . PhoneNo 123456789 conta 也["张三 "].EmaiIAddress z hangsan@gmail.com c onta 也["张三"].A ddress 江苏省苏州市工业园区星湖街 328 号 c ontacts["李四 "].Name 李四 contacts[ 罕四 "].PhoneNo 987654321 contacts["李四 " ] .EmaiIAddress conta 出[罕四 "]Address 江 苏省苏州市工业园区金鸡湖路 328 号 图 5-19 字典类型的 Model 绑定 前面我们通过自定义的 DefaultMode lB inder 模拟了 A8P.NETMVC 默认的 Model 绑定实 现机制。由于篇幅所限,自定义的这个 Mode lB inder 不可能涵盖所有的细节(比如通过 Bin dA ttribute 特性的 Exclude 和 Include 属性如何控制数据成员是否参与绑定就没有涉及), 但是它基本上能够反映定义在 ASP.NETMVC 中 DefaultMode lB inder 的实现逻辑。 本章小结 针对某个请求的处理主要体现在对目标 Action 方法的执行,而 Action 方法执行的前提 在于能够预先提供相应的参数列表,所以旨在为目标 Action 方法提供参数列表的 Model 绑 定在整个 AS 丑陋 TMVC 中的地位便可想而知。 在整个 Model 绑定过程中, ValueProvider 完成了针对数据的提供。一个 ValueProvider 具有 ASPNETMVC4 框架揭秘本章小结 盟 253 一个内部数据容器,可以是一个 NameValueCollection 对象,也可能是一个 Dictiomuy对象,它们对应着两种基本的 ValueProvider ,即 NameValueCollection ValueProvider 和 DictionaryValueProvider 0 Form ValueProvider 和 QueryString Val ueProvider 是两种典 型并且常用的 NameValueCollection ValueProvider ,而 Dictionary ValueProvider 则包括 RouteData ValueProvider 、 HttpFileCollectionValueProvider 和 ChildActionValueProvider 。 ValueProvider 通过对应的 ValueProviderFactory 来创建,如果 ASP.NETMVC 默认的提供 机制不能满足需要,可以自定义 ValueProviderFactory 来创建针对某种数据来源(比如 HTTP 报头)的 ValueProvider 。自定义的 ValueProviderFactory 通过静态类型 ValueProviderFactories 进行注册。 Modelliinder 是整个 Model 绑定体系的核心,它通过 ValueProvider 提供的数组构建一个 作为 Action 方法参数的对象。 ASP.NETMVC 会采用 DefaultModelliinder 来完成 Model 绑定 工作,在本章的最后一节我们用了大量的篇幅通过实例演示的方式介绍了 DefaultModelliinder 针对简单类型、复杂类型、数组、集合和字典类型的 Model 绑定。 如果默认的 DefaultModelliinder 不能满足要求,可以自定义 Modelliinder 。自定义的绑 定可以通过 ModelliinderAttribute 或者自定义的 CustomModelliinderAttribute 特性应用到 Action 方法参数或者数据类型上,以实现针对指定参数或者目标数据类型的 Model 绑定的控 制。我们也可以通过自定义 ModelB inderProvider 来作为针对某种数据类型的 ModelB inder 提供者,自定义 ModelliinderProvider 通过静态类型 ModelliinderProviders 进行注册。除此之 外还可以通过静态类型 Modelliinders 将自定义的 Modelliinder 注册到目标数据类型上。 ASP. NET MVC 4 框架揭秘第 6 章 Model 的验证 Action 方法在执行之前需要通过 Model 验证机制确保提供参数的有效性, Model 验证是伴随着 Model 绑定进行的。 ASP.NETMVC 默认采用声明式的验 证规则定义方式,应用在数据类型及其属性上的验证特性最终成为 Model 元 数据的一部分。除了必需的服务端验证之外, ASP.NET MVC 利用 JavaScript 实现客户端验证。 ASP.NET MVC 4 在架揭翻6.1 ModelValidator 与 ModelValidatorProvider 画 255 6.1 'ModelValidator 与 ModelValidatorProvider 很多 ASP.NETMVC 的书籍在介绍 Model 验证这部分的时候都会先为我们列出一系列的 验证特性,但是本章旨在剖析 ASP.NETMVC 的 Model 验证系统的总体设计和运行原理,所 以验证特性不是本质的东西,而用于真正实施验证的 ModelValidator 才是最核心的组件,所 以我们先来了解一下 ModelValidator 及其提供机制。 6.1.1 ModelValidator ASP.NET MVC 中的 ModelValidator 继承自抽象类型 System. Web.Mvc.ModelValidator 。 如下面的代码片段所示,该类型具有一个布尔类型的只读属性 IsRequired ,表示该 ModelValidator 是否对目标数据进行"必需性"验证(即被验证的数据成员具有一个具体的 值) ,该属性默认返回 False 0 GetClientValidationRules 返回一个元素类型为 System. Web. Mvc.ModelClientValidationRule 的集合,而 ModelClientValidationRule 是对客户端验证规则的 封装,我们会在客户端验证部分对其进行详细介绍。 public abstract class ModelValidator //其他成员 public virtual IEnumerable GetClientValidationRules()i public abstract IEnumerable Validate( object container)i public virtual bool IsRequired { geti } 针对目标数据的验证是通过调用 Validate 方法来完成的,该方法的输入参数 container 表示的正是被验证的对象。由于 ASP.NETMVC 下的 Model 验证仅仅针对自定义类型,所以 被验证的对象是一个"容器"对象,这一点可以通过参数名称" container" 体现出来。 Validate 方法表示验证结果的返回值并不是一个简单的布尔值,而是一个元素类型为具有如下定义的 System. Web.Mvc.ModelValidationResult 对象集合。 public class ModelValidationResult public string MemberName { geti seti } public string Message { geti seti } ModelValidationResult 具有两个字符串类型属性 MemberName 和 Message ,前者代表被 验证数据成员的名称,后者表示错误消息。一般来说,如果 ModelValidationResult 对象来源 于针对容器对象本身的验证,它的 MemberName 属性为空字符串;而对于容器对象某个属 性的验证来说,属性名称作为返回的 ModelValidationResult 对象的 MemberName 属性。 ASP. NET MVC 4 框架揭秘256 路第 6 章 Model 的验证 ModelValidatio nR esult 集合只有在验证失败的情况下才会返回。如果被验证数据对象符 合所有的验证规则, Validate 方法会直接返回 Null 或者一个空 ModelValidatio nR esult 集合。 值得一提的是,我们有时候会用 System.ComponentMode l. Dat aAnn otations. Validatio nResult 的 静态只读字段 Success 表示成功通过验证的结果,实际上该字段的值就是 Null 。 public class ValidationResult //其他成员 public static readonly ValidationResult Success; DataAnnotationsModelValidator 对 AS 卫 NETMVC 稍微了解的读者应该知道,我们可以在数据类型及其属性上应用相 应的验证特性(比如 RequiredA ttribute 、 RangeAttribute 和 RegularExpressio nA忧ribute 等) 来定义验证规则。但这仅仅是 Model 验证的其中一种解决方案而己,基于数据注解验证特 性这种声明式验证解决方案最终通过 System. Web.Mvc.Dat aAnn otationsModelValidator 来实 现。由于它代表一种最为常用的 Model 验证方案,在本章后续部分我们将对它进行深入的 介绍。 ClientModelValidator ClientModelValidator 是定义在程序集 System. Web.Mvc.dll 中的内部类型,从其命名就可 以看出它仅仅用于客户端验证。如下面的代码片段所示,我们通过调用构造函数创建一个 ClientModelValidator 的时候,不仅需要指定描述被验证对象类型的 Mode lM etadata 和当前 Con位 ollerContext ,还需要以字符串的形式指定验证类型和错误消息。 internal class ClientModelValidator : ModelValidator public ClientModelValidator(ModelMetadata metadata , ControllerContext controllerContext , string validationType , string errorMessage); public sealed override IEnumerable GetClientValidationRules(); public sealed override IEnumerable Validate( object container); 由于 ClientModelValidator 仅限于客户端验证,用于实现服务端验证的 Validate 方法总是 返回一个空的 ModelValidatio nR esult 集合(表示验证成功),它的 GetClientValidationRules 方法返回的是一个元素类型为 System. Web.Mvc.ModelClientValidatio nRule 的集合。 ModelClientValidatio nRule 表示客户端验证规则,它们最终将会出现在 H Th伍中辅助 JavaScript 验证框架(比如 jQuery )实施客户端验证。 ASP. NET MVC 4 框架揭翻6.1 ModelValidator 与 ModelValidatorProvider 黯 257 ClientModelValidator 具有两个继承者,分别是针对数值类型和日期类型验证的 NurnericModelValidator 和 DateModelValidator 。如下面的代码片段所示,这两个 ClientM odelValidator 用于表示验证类型的字符串分别是 "number" 和 "date" ,而表示错误消息的字符串是从内 部维护的资源文件中获取的。这实际上带来了一个问题,就是我们无法对错误消息进行 定制。 internal sealed class NumericModelValidator :ClientModelValidator public NumericModelValidator(ModelMetadata metadata , ControllerContext controllerContext) : base(metadata , controllerContext , "number" , ClientDataTypeModelValidatorProvider.GetFieldMustBeNumericResource( controllerContext)) internal sealed class DateModelValidator :ClientModelValidator public DateModelValidator(ModelMetadata metadata , ControllerContext controllerContext) base(metadata , controllerContext , "date" , ClientDataTypeModelValidatorProvider.GetFieldMustBeDateResource( controllerContext)) DataErrorlnfoModelValidator 在 System. ComponentM odel 命名空间下定义了一个名为 IDat aE rro rI nfo 的接口,它提供 了一种标准的错误信息定制方式。如下面的代码片段所示, IDat aE rro rI nfo 具有两个成员,只 读属性 Error 用于获取基于自身的错误消息,而只读索引用于返回指定数据成员的错误消息。 public interface IDataErrorInfo string Error { get; } string this[string columnName] { get; } 如果被验证对象的类型实现了 IDat aE rro rI nfo 接口,意味着我们可以通过上述两个成员 获取到相应的错误消息。 ASP.NET MVC 为此专门定义了两个对应的 ModelValidator ,即 Dat aE rrorlnfoClassModelValidator 和 Dat aEπo rI nfoPrope吗 rModelValidator 。不过它们都是内部 类型,我们不能直接使用它们进行 Model 验证。 从名称也可以看出来, Dat aE rro rI nfoClassModelValidator 实现针对容器对象本身的验证, 它通过 Error 属性获取相应的验证错误消息。 Dat aEπo rI nfoPrope吗 rModelValidator 则致力于 ASP. NET MVC 4 在架揭秘258 .翻 第 6 章 Model 的验证 容器对象数据成员(属性)的验证,它根据当前属性名称从索引中获取对应的验证错误消息。 ValidatableObjectA dapter 在 System.ComponentModel.Dat aAnn otations 命名空间下定义了一个 IValidatableObject 接口,它代表另外一种验证的模式,笔者将其称为"自我验证",即数据对象自行实现针对 自身的验证。如下面的代码片段所示,针对自身的验证实现在 Validate 方法中。该方法的参 数不是被验证的对象,而是通过 ValidationContext 类型表示的验证上下文,返回值则是一个 Validatio nR esult 对象的集合。 ValidationContext 和 Validatio nResult 这两个类型均定义在 System.ComponentModel.Dat aA nnotations 命名空间下。 public interface IValidatableObject IEnumerableValidate(ValidationContext validationContext)i ASP.NET MVC 定义了专门的 ModelValidator 对实现了 IValidatableObject 接口的数据类 型实施验证,对应的类型为 System. Web.Mvc. ValidatableObjec tA dapter 。由于被验证对象本身 已经将验证逻辑实现在了. Validate 方法中,所以 ValidatableObjec tA dapter 只需要调用该方法 并将返回的验证结果从 Validatio nResult 类型转换成 ModelValidatio nR esult 类型即可。 public class ValidatableObjectAdapter : ModelValidator public ValidatableObjectAdapter(ModelMetadata metadata , ControllerContext context)i public override IEnumerable Validate( object container)i 6.1.2 ModelValidatorProvider 通过前面章节的介绍我们知道, ASP.阳 TMVC 大都采用 Provider 的模式来提供相应的 组件,比如描述 Model 元数据的 Mode lM etadata 通过对应的 ModelMetadat aP rovider 来提供, 实现 Model 绑定的 Modeillinder 则可以通过对应的 ModeillinderProvider 来提供,用于实现 Model 验证的 ModelValidator 也不例外,它对应的提供者为 ModelValidatorProvider 。 ModelValidatorProvider 继承自具有如下定义的 System. Web.Modeillinding.ModelValidator Provider 抽象类。 public abstract class ModelValidatorProvider protected ModelValidatorProvider()i public abstract IEnumerable GetValidators( ModelMetadata metadata , ControllerContext context)i AS P. NET MVC 4 框架揭秘6.1 ModelValidator 与 ModelValidatorProvider _ 259 如上面的代码片段所示, GetValidators 方法具有两个参数,一个是用于描述被验证类型 或者属性元数据的 ModelMetadata 对象,另一个是当前 ControllerContext ,该方法返回的是 一个元素类型为 ModelValidator 的集合。 OataAnnotationsModelValidatorProvider Dat aAnn otationsModelValidator 提供了我们最常用的基于验证特性的声明式 Model 验证, 它对应的 ModelValidatorProvider 类型为 System. Web.Mvc.Dat aA nnotationsModelValidator Provider 。如下面的代码片段所示, Dat aAnn otationsModelValidatorProvider 并没有直接继承 ModelValidatorProvider ,它的父类是另一个名为 System. Web.Mvc.AssociatedValidatorProvider 的抽象类。 public class DataAnnotationsModelValidatorProvider AssociatedValidatorProvider //其他成员 public DataAnnotationsModelValidatorProvider()i protected override IEnumerable GetValidators( ModelMetadata metadata , ControllerContext context , IEnumerable attributes)i AssociatedValidatorProvider 类型名称中所谓的"关联 (Association) "代表的就是关联的 特性,它利用从 Mode lM etadata 提取的特性来构建相应的 ModelValidator 对象。如下面的代 码片段所示, AssociatedValidatorProvider 定义了一个受保护的虚方法 GetTypeDescriptor ,它 用于获取指定类型的描述对象〈其类型实现了 ICustomTypeDescI梢。 r 接口)。 public abstract class AssociatedValidatorProvider : ModelValidatorProvider public sealed override IEnumerable GetValidators( ModelMetadata metadata , ControllerContext context)i protected virtual ICustomTypeDescriptor GetTypeDescriptor(Type type)i protected abstract IEnumerable GetValidators( ModelMetadata metadata , ControllerContext context , 工 Enumerable attributes)i 如果被验证对象是一个容器对象的某个属性(这可以通过 ModelMetadata 分析出来), AssociatedValidatorProvider 会调用 GetTypeDescriptor 方法得到容器类型描述对象,并进一步 通过 ModelMetadata 提供的属性名称得到应用在属性及其属性类型上的特性列表。对于针对 "根容器对象"的验证来说,它会调用 GetTypeDescriptor 得到被验证类型的描述对象,并据 此得到应用在数据类型上的特性列表。得到的特性列表最终作为参数调用受保护的抽象方法 GetValidators ,该方法根据提供的特性创建相应的 ModelValidator 列表,这个列表就是公有 GetValidators 方法的返回值。 Dat aAnn otationsModelValidatorProvider 正是通过实现这个受保护的抽象方法 ASP. NET MVC 4 框架揭秘260 路 第 6 章 Model 的验证 GetValidators 完成了针对 Dat aAnn otationsModelValidator 的提供。具体来说,它们从提供的 特性列表中筛选出继承自 Validatio nA ttribute 的验证特性,并根据它们创建相应的 Dat aAnn otationsModelValidator 对象。 ClientDataTypeModelValidatorProvider 针对数字/日期类型客户端验证的 NumericModelValidator 和 DateModelValidator 最终是 通过具有如下定义的 System.Web.Mvc.ClientDataTypeModelValidatorProvider 来提供的。在实 现的 Get Validators 方法中,它会根据指定 ModelMetadata 判断被验证类型是否属于数字 /DateTime 类型,如果是则直接返回一个包含单个 NumericModelValidator 或者 DateModelValidator 对象的 ModelValidator 集合。在这里被视为数字的类型包括 byte 、 sbyte 、 short 、 ushort 、 int 、 uint 、 long 、 ulong 、 float 、 double 和 decimal 等。 public class ClientDataTypeModelValidatorProvider : ModelValidatorProvider public ClientDataTypeModelValidatorProvider(); public override IEnumerable GetValidators( ModelMetadata metadata , ControllerContext context); DataErrorlnfoModelValidatorProvider 旨在对实现了 IDataErro rI nfo 接口的数据实施验证的两个 Dat aE rro rI nfoModelValidator (即 Dat aE rro rI nfoClassModelValidator 和 DataErro rI nfoPrope均rModelValidator) ,最终是通过 具有如下定义的 System. Web.Mvc.Dat aE rro rI nfoModelValidatorProvider 来提供的。在实现的 Get Validators 方法中,如果被验证数据类型实现了 IDat aE rro rI nfo 接口,它会基于指定的 Mode lM etadata 和 ControllerContext 创建一个 Dat aE rrorInfoClassModelValidator 对象置于返回 的 ModelValidator 集合中。 如果被验证的对象是容器对象的某个属性,并且容器对象的类型(不是属性类型)实现 了 IDat aE rrorInfo 接口,该方法返回的 ModelValidator 集合中还会包含一个基于指定 Mode lM etadata 和 ControllerContext 创建的 Dat aEηo rI nfoPrope 吗rModelValidator 对象。 public class DataErrorlnfoModelValidatorProvider : ModelValidatorProvider public DataErrorInfoModelValidatorProvider(); public override IEnumerable GetValidators( ModelMetadata metadata , ControllerContext context); 为了让读者更深刻地了解实现在 Dat aE rro rI nfoModelValidator 中的验证机制,以及实现 在 Dat aE rro rI nfoModelValidatorProvider 中对它的提供机制,我们来演示一个简单的实例。在 一个 ASP.NETMVC 应用中定义了如下一个实现了 IDat aE rro rI nfo 接口的 Contact 类型,假设 ASPNETMVC4 在架揭秘6.1 ModelValidator 与 ModelValidatorProvider 黯 261 提供的 Contact 对象总是不能通过验证,并为整个 Contact 容器对象和它的 4 个属性成员定 义了相应的验证错误消息。 public class Contact: 工 DataErrorlnfo r o r r FM qJ n .工r 卡」S C ·工14 ·P u p{ get { return "无效联系人! ";} public string this[string columnName] get switch (columnName) case "Name" case "PhoneNo" case "EmailAdderss" default return "姓名是必需的! "; return "电话号码格式牵制吴! "; retur 口"无效的电子邮箱地址! "; return null; public string Name { get; set; } public string PhoneNo { get; set; } public string EmailAdderss { get; set; } 然后创建了如下一个 HomeContro l1 er ,辅助方法 GetValidators 根据指定的数据类型和 ModelValidatorProvider 对象创建了一个 ModelValidator 集合,该集合同时包括针对数据类型 本身和它所有属性成员的 ModelValidator 。在默认的 Action 方法In dex 中我们将创建的 DataErrorInfoModelValidatorProvider 对象作为参数调用 GetValidators 方法,并将得到的 ModelValidator 集合 Model 呈现在默认的 View 中。 public class HomeController : Controller public ActionResult Index() ModelValidatorProvider validatorProvider = new DataErrorlnfoModelValidðtorProvider(); return View(GetValidators(typeof(Contact) , validatorProvider)); private IEnumerable GetValidators(Type dataType , ModelValidatorProvider validatorProvider) ModelMetadata metadata = ModelMetadataProviders.Current . GetMetadataForType (null , dataType); foreach (var validator in validatorProvider.GetValidators(metadata , ControllerContext)) AS P. NET MVC 4 框架揭秘262 如第 6 章 Model 的验证 yield return validator; foreach (var propertyMetadata in metadata.Properties) foreach (var validator in validatorProvider . GetValidators (propertyMetadata , ControllerContext)) yield return validator; 如下所示的是 Action 方法 Index 对应 View 的定义,可以看出这是一个 Model 类型为 IEnumerable 的强类型 View 。在该 View 中我们使用每一个 ModelValidator 对创建的 Contact 实施验证,并将 ModelValidator 的类型和验证错误消息通过表格的形式呈 现出来。 @model IEnumerable DataErrorInfoModelValidatorProvider @{ Contact contact = new Contact(); @foreach (var validator in Model) ModelValidationResult[] results = validator.Validate(contact) .ToArray(); string firstMessage = (results.Any() ? results.First() .Message : "N/A"); for (int i = 1; i < results.Length; i++)
    ModelValidatorMessage
    @validator.GetType() .Name @firstMessage
    @results[i] .Message 上面的程序运行之后会在浏览器中呈现如图 6-1 所示的输出结果。可以看到通过 Dat aE rrorInfoModelValidatorProvider 创建了 1 个 Dat aE rror In foClassModelValidator 和 4 个 Dat aE rro rI l证。 Prope吗 rModelValidator ,前者针对 Contact 类型本身,后者针对定义在 Contact 中的 4 个属性。 (S60 1) ASP. NET MVC 4 框架揭秘6.1 ModelValidator 与 ModelValidatorProvider ~ . 263 在y牛e二o五命m:主e。俨竖t立~塾哇跑暨r些回白」幽‘\\·圄·嗣…~忠 、 ~- DataE rrorlnfoClassModelValidator N/A Da taErrorlnfoPropertyModelValidator N/A DataErrorlnfoPropertyModelValidato 姓名是必需的! Data E rrorlnfoPrope同yM odelValidato 电话号码格式措误 | Dala E rrorlnfoPropertyModelValidato 无效的电子邮箱地址! 图 6-1 针对 DataErrorl nfoModelValidator 的 Model 验证(1) . 针对属性的 DataEηorInfoPrope吗rModelValidator 能够按照我们希望的方式对指定的 Contact 对象实施验证,但是针对容器类型对象本身的 DataErrorInfoClassModelValidator 则不 能,因为当 DataErrorInfoClassModelValidator 的 Validate 方法被调用的时候,它并不会真正去 验证通过参数指定的容器对象,而是去验证包含在初始化该 DataErrorInfoClassModelValidator 对象时指定的 ModelMetadata 的 Model 属性。由于我们提供的 ModelMetadata 对象的 Model 属性值为 Null ,所以 DataErrorInfoClassModelValidator 会直接返回一个空的 ModelValidationResult 集合。 为了验证这一点,我们可以对定义在 HomeController 中的 GetValidators 方法作如下的改 动:在创建针对 ModelMetadata 时指定了一个创建 Contact 的表达式,该表达式执行后得到 的对象将作为 ModelMetadata 的 Model 属性。 public class HomeController : Controller //其他成员 private IEnumerable GetValidators(Type dataType, ModelValidatorProvider validatorProvider) ModelMetadata metadata = ModelMetadataProviders.Current .GetMetadataForType(()=>new Contact() , dataType); //其他操作 再次运行程序后会在浏览器中得到如图 6-2 所示的输出结果,可以清楚地看到针对 DataErrorInfoClassModelValidator 的验证错误消息("无效联系人 1") 出现了。 (S602) ,~口 X I舍@阳。I刷剑刷~出羽田川R'圃L639 ………唱 ~、 啕" ~寸~气 DataErrorlnfoClassModelValidator 无效联系人 l DataErro~lnfoPrope 内yModelVali d ator N/A DataErrorlnfoPropertyModelValidator 姓名是~嚼的! DataErrorlnfoPropertyModelValidator 电话号码格式错误! DataErrorlnfoPrope 町yModelValidator 无效的电子邮箱地址! 图 6-2 针对 DataErrorl nfoModelValidator 的 Model 验证 (2) ASP. NET MVC 4 在架揭秘264 说第 6 章 Model 的验证 6.1.3 ModelValidatorProviders 通过静态类型 System. Web.Mvc.ModelValidatorProviders 对 ModelValidatorProvider 进行注 册。如下面的代码片段所示, ModelValidatorProviders 具有一个静态只读属性 Providers ,对 应的类型为 ModelValidatorProviderCollection ,表示基于整个 Web 应用范围的全局 ModelValidatorProvider 集合。 public static class ModelValidatorProviders public static ModelValidatorProviderCollection Providers { get; } public class ModelValidatorProviderCollection Collection public ModelValidatorProviderCollection(); publicModelValidatorProviderCollection(IList list); public IEnumerable GetValidators(ModelMetadata metadata, ControllerContext context); public class ModelMetadata //其他成员 public virtual IEnumerable GetValidators( ControllerContext context); ModelValidatorProviderCollection 定义了一个 GetValidators 方法返回一个 ModelValidator 列表,集合中每个 ModelValidatorProvider 创建的 ModelValidator 会包含在该列表中。 ModelMe旬出阔的 GetValidators 方法返回的 ModelValidator 列表正是通过 ModelValidatorProviders 的静态属性 Providers 创建的。 当 ModelValidatorProviders 类型被加载的时候,它会创建三个 ModelValidatorProvider 并添加到通过静态属性 Providers 表示的 ModelValidatorProvider 集合之中,而这三个 ModelValidatorProvider 对应的类型分别为 DataAnnotationsModelValidatorProvider 、 ClientDataTypeModelValidatorProvider 和 DataEηorInfoPrope吗rModelValidator 。 如果我们需要注册一个自定义 ModelValidatorProvider ,可以直接将相应的对象添加到 ModelValidatorProviders 的 Providers 列表中。如果需要采用自定义 ModelValidatorProvider 来 替换掉现有的 ModelValidatorProvider ,比如我们创建了一个扩展的 DataAnnotationsModel ValidatorProvider ,还需要将现有的 ModelValidatorProvider 从该列表中移除。 上面我们介绍了用于进行 Model 验证的 ModelValidator ,用于提供 ModelValidator 的 ModelValidatorProvider ,以及用于注册 ModelValidatorProvider 的 ModelValidatorProviders , 整个 ModelValidator 的提供系统以此三类组件为核心,图 6-3 所示的 UML 体现了它们之间 的关系。 ASP.NET MVC 4 框架揭秘6.1 ModelValidator 与 ModelValidatorProvider 滋 265 1 I ModelValidatorProviderCoUection Pr口说 oors J ?一一-一一一一 GetVelldalors DataErrorlnfoModelValldatorP 阳vlder Clien tD ataTypeModelValidatorProvider l G归刷e时础tω阳汕 厂----------------一 l GBW ModelValidatorProviders 飞、1/ ModelValidator l__ 一「 、…-…-.... DataAnnotatlonsModelValldatol' CllentModelValldator Oat盟主 rrorlnfoC la s. sModelValidator 图 6-3 ModelValidat 町、 ModelValidatorProvider 和 ModelValidatorProviders 之间的关系 CompositeModelValidator 虽然 CompositeModelValidator 仅仅是定义在程序集 System. Web.Mvc.dll 中的一个私有类 型,但是它在 ASP.NETMVC 的 Model 验证系统中具有重要的地位,可以说真正用于 Model 验证的 ModelValidator 就是这么一个对象。 private class CompositeModelValidator : ModelValidator public CompositeModelValidator(ModelMetadata metadata , ControllerContext controllerContext); public override IEnumerable Validate( object container); CompositeModelValidator 实际上并不是一个真正对 Model 对象实施验证的 ModelValidator ,它是一系列 ModelValidator 的组合,它根据基于数据本身类型及其属性的 Mode lM etadata 动态地获取相应的 ModelValidator (通过调用 ModelMetadata 的 GetValidators 方法)对目标数据实施验证。抽象类 ModelValidator 具有一个静态的 GetModelValidator 方法, 它根据指定的 Mode lM etadata 和 ControllerContext 得到相应的 ModelValidator 对象。如下面 的代码片段所示,该方法返回的正是一个 CompositeModelValidator 对象。 ASP. NET MVC 4 在架揭秘266 话第 6 章 Model 的验证 public abstract class ModelValidator //其他成员 public static ModelValidator GetModelValidator(ModelMetadata metadata , ControllerContext context). return new CompositeModelValidator(metadata , context); 当 CompositeModelValidator 被用于验证一个容器对象的时候,会先验证其属性成员。针 对容器对象自身的验证只有在所有属性值都通过验证的情况下才会进行。具体的逻辑是这样 的:它通过调用描述容器类型的 ModelMetadata 对象的 Properties 属性得到所有针对属性的 Mode lM etadata 对象,然后调用它们的 GetValidators 方法得到一组 ModelValidator 对相应的属 性值实施验证,验证得到的 ModelValidatio nResult 被添加到最终返回的 ModelValidatio nResult 列表中。 如果在对所有属性实施验证之后该 ModelValidatio nResult 列表依然为空(所有的属性均 成功通过验证) , CompositeModelValidator 才会获取针对容器类型的 Mode lM etadata 对象, 并采用调用其 GetValidators 方法获取的 ModelValidator 列表对容器对象本身实施验证。表示 验证结果的 ModelValidatio nResult 对象被添加到最终返回的列表中。 实例演示: CompositeModelValidator 采用的验证行为 (8603 , 8604 ) 为了使读者对 CompositeModelValidator 的验证逻辑具有一个深刻的理解,我们来演示一 个具体的实例。在一个 ASP.NET MV 应用中定义了如下一个名称为 AlwaysFailsAttribute 的验 证特性。如下面的代码片段所示,重写的 IsValid 方法总是返回 False ,意味着针对数据的验证 总是会失败。我们还重写了只读属性 Typeld ,让它真正能够唯一标识一个 AlwaysF a i1 sAttribute 特性实例(具体原因我们会在本章后续部分予以介绍)。 [AttributeUsage( AttributeTargets.Classl AttributeTargets.Property)] public class AlwaysFailsAttribute : ValidationAttribute private object typeId; public override bool IsValid(object value) return false; public override object TypeId get { return typeId ?? (typeId = new object()); } 我们将 AlwaysFailsAttribute 应用到表示联系人的 Contact 类型上。如下面的代码片段所 凉,在 Contact 和 Address 的类型和属性都应用了该特性,并且指定了相应的错误消息。 [AlwaysFails(ErrorMessage = "Contact")] public class Contact AS P. NET MVC 4 框架揭秘6.1 ModelValidator 与 ModelValidatorProvider _ 267 [AlwaysFails(ErrorMessage = "Contact.Name")] public string Name { geti seti } [AlwaysFails(ErrorMessage = "Contact.PhoneNo")] public string PhoneNo { geti seti } [AlwaysFails(ErrorMessage = "Contact.EmailAddress")] public string EmailAddress { geti seti } [AlwaysFails(ErrorMessage = "Contact.Address")] public Address Address { geti seti } [AlwaysFails(ErrorMessage = "Address")] public class Address [AlwaysFails(ErrorMessage = "Address.Province")] public string Province { geti seti } [AlwaysFails(ErrorMessage = "Address.City")] public string City { geti seti } [AlwaysFails(ErrorMessage = "Address.District")] public string District { geti seti } [AlwaysFails(ErrorMessage = "Address.Street")] public string Street { geti seti } 我们创建了一个具有如下定义的 HomeController 类,在 Action 方法 Index 中,使用当前 注册的 ModelMetadataProvider 创建了描述 Contact 类型的 ModelMetadata 对象,然后将它和 当前 ControllerContext 作为参数调用抽象类型 ModelValidator 的静态方法 GetValidator 创建一 个 CompositeModelValidator 对象。我们利用该 CompositeModelValidator 来验证创建的 Contact 对象,并将表示验证结果的 ModelValidationResult 列表作为 Model 呈现在默认的 View 中。 public class HomeController : Controller public ActionResult Index() Address address = new Address Province City District Street = "江苏", - "苏州", = 11 工业园区", = "星湖街 328 号" Contact contact = new Contact Name = "张三", PhoneNo = "123456789", EmailAddress="zhangsan@gmail.com". Address = address ModelMetadata metadata = ModelMetadataProviders.Current ASP. NET MVC 4 在架揭秘268 • 第 6 章 Model 的验证 .GetMetadataForType(() => contact , typeof(Contact)); ModelValidator val 工 dator = Model 飞Talidator.GetModelValidator(metadata , ControllerContext); return View(validator.Validate(contact)); 如下所示的是 Action 方法 lndex 对应 View 的定义 , 可以看出这是一个 Model 类型为 IEnumerable的强类型 View 。在该 View 中我们将集合中的每一个 ModelValidatio nResult 对象的成员名称和错误消息通过表格的形式呈现出来。 @model IEnumerable 验证结果 @foreach (ModelValidationResult result in Model) string propertyName = string. 工 sNullOrEmpty(result.MemberName) ? "N/A" : result.MemberName;
    MemberMessage
    @propertyName@result.Message
    该相芋运行后会在浏览器中呈现出如图 6 -4所示的输出结果,可以看出 CompositeModelValidator 对 Contact 对象实施验证得到的 5 个 ModelValidatio nResult 都来源于针对 4 个属性的验证, 应用在 Contact 类型上的 AlwaysFailsAttribute 特性并没有参与验证。 (S603 ) 噩噩需 盟 | Name' ICon 削 N咱阳am |作Pho ne 阳 I c阳阳ζ臼ωoαon阳1 |IEm旧回a剖il旧Add 阳 Icon 阳tEmailAddress |削的 ss IAddress |Address IContact.A ddress 图 6 -4 CompositeModelValidator 的验证规则 (1 ) 按照前面介绍的"针对容器对象本身的验证只有在所有属性通过验证的情况下才会进行" 的原理,为了让 Contact 的四个属性通过验证,我们将应用在四个属性和 Address 类型上的 AlwaysFailsAttribute 特性注释掉,只保留应用在 Contact 类型和 Address 四个属性上的 AlwaysFailsAttribute 特性。再次运行我们的程序将会在浏览器中得到如图 6-5 所示的输出结果, 不难看出输出的 ModelValidationR esult 来源于于应用在 Contact 类型上的 AlwaysFailsAt位ibute 特性。 (S604) ASPNETMVC4 在架揭秘6.2 Model 绑定与验证 咀 269 瞌噩胃 曹-M d 2u t n O F」 ! 也 图 6-5 CompositeModelValidator 的验证规则 (2) 6.2 Model 绑定与验证 Model 绑定解决了针对目标 Action 方法参数的绑定,而 Model 验证的目的在于对绑定 的对象实施验证以确保输入数据的有效性, Model 验证是伴随着 Model 绑定进行的。在上面 -节中我们详细地介绍了真正用于 Model 验证的 ModelValidator 以及相关的提供机制,接下 来讨论在这个以 ModelValidator 为核心的 Model 验证系统中,针对通过 Model 绑定得到的数 据对象的验证是如何实现的。 6.2.1 ModelState 通过第 5 章 "Model 的绑定"的介绍我们知道, Controller 对象的 ViewData 包含一个元 素类型为 ModelState 的集合,用于表示 Model 状态。除了在 Model 绑定过程通过 ValueProvider 提供的数据之外,提供数据的验证结果也保存其中。 e +」a ←」qu 14 e d o ]M e 14s bs aa z­-工 C 14 ac ---l r14 eb quu 『 lp ‘ It public ModelErrorCollection Errors { get; } public ValueProviderResult Value { get; set;} [Serializable] public class ModelErrorCollection : Collection public ModelErrorCollection(); public void Add(Exception exception); public void Add(string errorMessage); r o r r E l e d o ]M e 14s bs aa z14 ·工 C 14 ac --·工r1-e 电 b quu 『 ιD ‘ r1 public ModelError(Exception exception); public ModelError(string errorMessage); public ModelError(Exception exception, string errorMessage); public string ErrorMessage { get; } public Exception Exception { get; } ASPNETMVC4 在架揭秘270 l'. 第 6 章 Model 的验证 通过上面的代码片段所示, ModelState 具有 Value 和 Errors 两个核心属性,前者表示 ValueProvider 提供的 ValueProviderResult 对象,后者表示针对该数据对象的错误集合。 Error 属性的类型为 System. Web.Mvc.ModeærrorCollection ,这是一个元素类型为 System. Web.Mvc. Modeærror 的集合,而一个 Modeærror 对象通过错误消息和异常来描述错误。 实例演示:验证 Model 绑定过程申对 ModelError 的设置 (S605 ) Model 验证可以看成是 Model 绑定过程的一部分,它在参数列表创建过程中会对提供的 数据实施验证,而表示验证结果的 ModelValidatio nR esult 集合会以 Modeærror 的形式写入当 前 ViewData 的 ModelState 中。现在我们通过一个简单的实例来证实这一点,可以直接使用 前面演示实例中创建的 Contact 作为验证类型, Contact 和 Address 类型和属性均应用了上面 定义的 AlwaysFa i1 sAttribute 特性。 我们定义了如下一个 HomeController ,在无参 Action 方法 Index (针对 HπP-GET 方法〉 中我们创建一个 Contact 对象并将其作为 Model 呈现在默认的 View 中。应用了 HtφPostAttribute 特性的In dex 方法具有一个类型为 Contact 的参数,此方法直接呈现默认的 View 。 public class HomeController : Controller public ActionResult Index() Address address = new Address Province ="江苏", City = "苏州", District ="工业园区", Street = "星湖街 328 号" Contact contact = new Contact Name = "张三", PhoneNo = "123456789" , EmailAddress=..zhangsan@gmail.com... Address = address } ; return View(contact); [HttpPost] public ActionResult Index(Contact contact) return View(contact); 如下所示的是 Action 方法 Index 对应 View 的定义,可以看出这是一个 Model 类型为 Contact 的强类型 View 。在该 View 中,如果当前不是针对 Hη'P -POST 的请求,我们会将作 为 Model 的 Contact 对象(连同 Address 属性)以编辑模式呈现在一个表单中,该表单具有 一个提交按钮。对于 Hπ'P -POST 请求来说,我们将保存在当前 ViewData 中所有 ModelState ASP. NET MVC 4 在架揭秘6 .2 Model 绑定与验证 白 271 的 Key 以及该 ModelState 的错误消息以表格的方式呈现出来。 @rnodel Contact Model Error @if (string.Compare(Request.HttpMethod , "POST" , true) != 0) using(Html.BeginForm()) else @Html . EditorForModel() @Html.EditorFor(rn=>rn.Address) @foreach (string key in ViewData.ModelState . Keys) ModelError [] errors = ViewData. ModelState [key] . Errors. ToArray () ; string firstError = errors. An y() ? errors[O] .ErrorMessage : "N/A"; for(int i=l; i
    KeyError
    @key@firstError
    @errors[i] .ErrorMessage
    该程序运行之后会先在浏览器中呈现一个编辑联系人的页面,我们直接点击"保存"按 钮提交表单后会呈现出图 6-6 所示的输出结果,可以看到针对 Contact 和 Address 所有属性 的验证错误信息被成功输出。 Iφ~φ I 份回原网~t:2782 圃-' 口毒 X气 Name Contact.Name PhoneNo Contact. PhoneN 0 EmailAddress ContactEmailAddress Address.Province Add ress .Province Address.City Address.City Address.District Add ress .District Address .s treet Address.Street 图 6-6 Model 验证过程中对 ModelState 的设置 ASP. NET MVC 4 框架揭秘272 辑第 6 章 Model 的验证 对比前面演示 CompositeModelValidator 的实例,我们会发现另一个重要的现象,即 CompositeModelValidator 本身的验证过程不是递归进行的(针对 Contact 的验证过程中不会对 Address 的属性实施验证),但是伴随着 Model 绑定的整个验证过程却是递归进行的 (Address 的属性的验证在针对 Contact 的验证过程中自动完成),我们将在后面详细讨论这个问题。 6.2.2 验证消息的呈现 Model 的验证是伴随着 Model 绑定完成的,当 Mode lB inder 从请求中提取相应的数据为目 标 Action 方法绑定参数值后,验证错误信息已经以 Modeillrror 的形式保存到相应的 ModelState 中。而 ModelState 列表属于当前Vi ewData 的一部分,可以直接在 View 中被使用,这对错误 信息在 View 中的呈现提供了可能。现在我们就来讨论验证错误信息在 View 中如何呈现。 ValidationMessage 八lalidation MessageF or 验证消息在 View 中的呈现可以借助 H恤lH elperlHtm lH elper 来实现。如下面的 代码所示, HtmIHelper 和 HtmlHelper 分别提供了若干 Validatio nM essage 和 ValidationMessageF or 扣展方法重载。 public static class ValidationExtensions { //其他成员 public static MvcHtmlString ValidationMessage (this HtmlHelper htmlHelper , string modelName); public static MvcHtmlString ValidationMessage (this HtmlHelper htmlHelper , string modelName , IDictionary htmlAttributes); public static MvcHtmlString ValidationMessage (this HtmlHelper htmlHelper , string modelName , object htmlAttributes); public static MvcHtmlString ValidationMessage (this HtmlHelper htmlHelper , string modelName , string validationMessage); public static MvcHtmlString ValidationMessage (this HtmlHelper htmlHelper , string modelName , string validationMessage , IDictionary htmlAttributes); public static MvcHtmlStr 工 ng ValidationMessage (this HtmlHelper htmlHelper , string modelName , string validationMessage , object htmlAttributes); public static MvcHtmlString ValidationMessageFor( this HtmlHelper htmlHelper , Expression> expression); public static MvcHtmlString Validat 工。 nMessageFor( this HtmlHelper htmlHelper , Expression> expression , string validationMessage); public static MvcHtmlStr 工 ng ValidationMessageFor( this HtmlHelper htmlHelper , Expression> expression , string validationMessage , IDictionary htmlAttributes); public static MvcHtmlStr 工 ng ValidationMessageFor( this HtmlHelper htmlHelper , Expression> expression , string validationMessage , object htmlAttributes); AS P. NET MVC 4 框架揭秘6.2 Model 绑定与验证 黯 273 ViewData 的 ModelState 属性的类型并不是 ModelState ,而是 ModelStateDictionary 。 HtmlHelper 的扩展方法 ValidationMessage 的参数 modelName 表示对应的 ModelState 在 ModelStateDictionary 中的 Key 。如果针对这个 Key 找不到对应的 ModelState ,或者对应的 ModelState 的 Errors 列表为空,意味着对应的数据成功通过验证,此时不会有任何 HTh在L 生成。 如果对应的 Errors 列表不为空,方法会生成一个 元素来显示验证消息。如果通过 validationMessage 显示指定的验证消息,那么该消息将会直接作为该 元素的内部文本, 否则 Errors 列表中第一个非空消息将会作为验证消息。此外,当我们调用扩展方法 ValidationMessage 的时候还可以通过参数 h缸ùAttributes 为这个元素设置相应的lITML属 性。 ValidationM essageFor 与 ValidationMessage 不同之处在于它会通过指定的表达式来提取 ValidationMessage 方法中的参数 modelName 。 现在我们对上面演示的实例 (S605) 略加改动来演示验证消息的呈现。如下面的代码片 段所示,我们在应用了 H仕pPostA忧ribute 特性的 Index 方法中将作为参数的 Contact 对象作为 Model 呈现在一个名为 "ValidationMessage" 的 View 中。 public class HorneController : Controller //其他成员 [HttpPost] public ActionResult Index(Contact contact) return View("Val 工 dat 工 onMessage" , contact); 如下所示的是这个名为 ValidationMessage 的 View 的定义,这是一个 Model 类型为 Contact 的强类型 View ,在该 View 中我们调用 HtmlHelper 的 ValidationMessage 扩展方法将 所有的验证消息呈现出来。 @rnodel Contact ValidationMessage
    • @Htrnl.Validatio 日Message("Narne")
    • @Htrnl.ValidationMessage("PhoneNo")
    • @Htrnl.ValidationMessage("ErnailAddress")
    • @Htrnl.ValidationMessage("Address.Province")
    • @Htrnl.ValidationMessage("Address.City")
    • @Htrnl.ValidationMessage("Address.District")
    • @Htrnl.ValidationMessage("Address.Street")
    运行该程序后,在联系人编辑页面中直接点击"保存"按钮,这个名为 ValidationMessage ASP. NET MVC 4 在架揭秘274 .. 第 6 章 Model eSJ验证 的 View 会以如图 6-7 所示的效果呈现出来。 (S606) 口!:.. I川/川@如川m叩叩叫叫…[!!:]抽刷阳阳闹嗣刷刷ω叫a皿剧翩[ll加阳Ir;l阳。m…n I ‘φ. 咛φ~创‘ l 酬罔阳胃I隅酣栅t阑嗣E酣耐抽咱百 2暴$ 、 . • Co皿 act. Name • Contact. PhoneNo • C OIltact.E mailA ddress • Address.ProvÍllce • Address. Cíty • AddreS5. Dis tríct • Addr臼 s.Street 图 6-7 验证消息的呈现效果 • 在 ValidationM essage 中针对验证消息的呈现也可以按照如下的方式调用 Htm lH elper 的扩展方法 Validatio nM essageF or 来实现。 (S607)
    • @Html.ValidationMessageFor(c=>c.Name)
    • @Html.ValidationMessageFor(c=>c.PhoneNo)
    • @Html.ValidationMessageFor(c=>c.EmailAddress)
    • @Html.ValidationMessageFor(c=>c.Address.Province)
    • @Html.ValidationMessageFor(c=>c.Address. 巳 ity)
    • @Html.ValidationMessageFor(c=>c.Address.District)
    • @Html.ValidationMessageFor(c=>c.Address.Street)
    通过这两个呈现出来的验证消息具有相同的显示效果,它们会生成如下所示的 HTML 。 可以看出呈现出来的验证显示体现为一个 元素,样式类型 (cl 部 s="field-validation-error" ) 和客户端验证属性 (data-valmsg-fo r= "PhoneNo" data-valmsg- replace="true") 作了相应设置。
    • Contact.Name
    • Contact.PhoneNo
    • Contact.EmailAddress
    • Address.Province
    • Address.City
    • Address.District AS P. NET MVC 4 框架揭秘
    • 6.2 Model 绑定与验证 翻 275 Address.Street
    ValidationSummary 除了通过 Validatio nM essageF or 与 Validatio nM essage 这两个方法显示单条验证消息之外, 我们还可以通过调用 HtmlHelper 的扩展方法 ValidationSummary 将所有的验证消息一并显示 出来。如下面的代码片段所示, HtmlHelper 具有一系列 ValidationSummary 扩展方法重载, 布尔类型的参数 excludePropertyEπors 表示是否需要排除基于属性的错误消息,而通过 message 参数可以为 ValidationSummary 指定一个作为标题的字符串。 public static class ValidationExtensions //其他成员 public static MvcHtmlString ValidationSummary (this HtmlHelper htmlHelper) i public static MvcHtmlString ValidationSummary (this HtmlHelper htmlHelper , bool excludePropertyErrors)i public static MvcHtmlString ValidationSummary (this HtmlHelper htmlHelper , string message)i public static M 飞TcHtmlString ValidationSummary (this HtmlHelper htmlHelper , bool excludePropertyErrors , string message)i public static MvcHtmlString ValidationSummary (this HtmlHelper htmlHelper , string message , IDictionary htmlAttributes)i public static MvcHtmlString ValidationSummary (this HtmlHelper htmlHelper , string.message , object htmlAttributes)i public static MvcHtmlString ValidationSummary (this HtmlHelper htmlHelper , bool excludePropertyErrors , string message , 工 Dictionary htmlAttributes)i public static MvcHtmlString ValidationSummary (this HtmlHelper htmlHelper , bool excludePropertyErrors , string message , object htmlAttributes); ModelStateDictionary 是一个 Key 和 Value 分别为字符串和 ModelState 的字典,并且允许 一个空字符串作为其 Key 0 ValidationSummary 方法通过 Key 是否为空来判断 ModelState 是 否针对一个属性。 ModelStateDictionary 还定义了如下两个 AddModellirror 方法重载使我们很 容易地进行 Modellirror 的设置。在该方法执行过程中,如果具有相同 Key 的 ModelState 对 象存在,那么被添加的 Modellirror 将会直接存放到它的 Errors 集合中,否则会添加到一个 新创建的 ModelState 的 Errors 集合中。 [Serializable] public class ModelStateDictionary : IDictionary , ICollection> , 工 Enumerable> , IEnumerable //其他成员 public void Ad dM odelError(string key , Exception exception)i public void AddModelError(string key , string errorMessage)i AS P. NET MVC 4 在架揭秘276 陆 第 6 章 Model 的验证 我们在一个 ASP.NET MVC 应用中定义了如下一个默认的 HomeController 。在默认的 Action 方法In dex 中添加了四个 Modeærror 到当前的 ModelState 集合中,除了最后一个将一 个空字符串作为 Key 之外,前三个均具有一个明确的 Key ,最后我们直接将默认的 View 呈 现出来。 public class HomeController : Controller public ActionResult Index() ModelState.AddM odelError("Name" , "请输入姓名") ; ModelState.AddModelError("PhoneNo" , "请输入电话号码") ; ModelState.AddM odelError("EmailAddress" , "请输入电子邮箱地址") ; ModelState.AddModelError("" , "系统发生异常,详细信息请与管理员联系") ; return View(); 如下所示的 Action 方法 Index 对应 View 的定义,在该 View 中我们两次调用 HtmlHelper 的 ValidationSummary 方法并且指定了 message 参数, ValidationSummary 方法的参数 excludePrope吗rErrors 在两次调用中分别设置为 False 和 True 。 ValidationSummary @Html.ValidationSummary(false , "excludePropertyErrors: false") @Html.ValidationSummary(true , "excludePropertyErrors: true") 该程序运行之后会在浏览器中呈现如图 6-8 所示的效果,可以看到当 excludePrope吗rErrors 参数被设置为 True 的时候, ValidationSummary 中只会呈现出 Key 为空 字符串的 ModelState 的错误消息。 (S608) -升 ... 二二川- 了二←!二 =­.. ~命辞旧扩 ~ ~W 二二 I 专..和扣动了、科 4二 、 I lIi.安 ~就 I ~ loca~可;t :2782 呵 谷、 I enlud eP ropertyEnors: Calse · 请输入姓名 ·请输入电话号码 • ì奇输人 电子邮箱地址 ·系统 发生异常,详细信 回 d u.dePropertyErrors: 阳 e ·系统发生异常,详细信息请与管理员联系 图 6-8 验证消息在 ValidationSummary 中的呈现效果 ASP. NET MVC 4 框架揭秘6.2 Model 绑定与验证 • 277 EditorForModel 在一个强类型 View 中,当我们调用 HtmlHelper 的扩展方法 EditorForModel 将整个 Model 对象以编辑模式呈现出来时,如果某个属性对应的 ModelState 具有相应的错 误(通过 Errors 属性表示的 Modeærror 集合不为空),错误消息也会一并呈现出来。当然, 如果我们为 Model 类型定义了相应的模板就另当别论了。 我们同样可以通过一个简单的实例来演示错误消息在 EditForModel 方法中的呈现,我们 在一个 AS 卫 NETMVC 应用中定义了如下一个熟悉的 Contact 类型作为 View 的 Model 。 •L c a ←」口。c s s a 14 C C .工14 b u p{ [DisplayName(" 姓名") ] public string Name { geti seti } [DisplayName(" 电话号码") ] public string PhoneNo { geti seti } [DisplayName(" 电子邮箱地址"门 public string EmailAddress { geti seti } 然后创建一个具有如下定义的 HomeController 。在 Action 方法 Index 中,通过调用当前 ModelState 属性的 Ad dM odeærror 方法人为地添加三个错误消息,对应的 ModelState 名称与作 为 Model 的 Contact 类型的属性名称一致,最后将创建的 Contact 对象在默认的 View 中呈现出来。 public class HomeController : Controller public ActionResult Index() ModelState.AddModelError("Name" , "请输入姓名") i ModelState.AddModelError("PhoneNo" , "请输入电话号码") i ModelState.AddModelError("EmailAddress" , "请输入电子邮箱地址") i return View(new Contact())i 下面的代码片段代表了 Action 方法 Index 对应 View 的定义,这是一个 Model 类型为 Contact 的强类型 View 。在该 View 中我们仅仅简单地调用 HtmIHelper 的扩展方法 EditorF orModel 作为 Model 的 Contact 对象以编辑的模式呈现出来。 @model Contact EditorForModel @Html.EditorForModel() ASP. NET MVC 4 框架揭秘278 电第 6 章 Model 的验证 当我们成功运行该程序的时候会在浏览器中呈现出如图 6-9 所示的效果,可以看到错误 消息被显示在对应的文本框后面。 (S609) II!!!!II 口 x M。向阳!|性戈旦旦;即 localhost:l叩副墨、 姓名 电话号码 电子邮箱地址 「一一一-一-一一一甲一 1青输λJ生名 i青辅λ 电话号码 i 剖面λ电子邮箱地址 图 6-9 错误消息在 EditorForModel 方法中的呈现 6.2.3 Model 绑定申的验证 在前面我们不止一次地提到, Model 验证可以看成是 Model 绑定的一个中间环节,默认 的情况下的 Model 绑定实现在 DefaultModeffiinder 中。那么现在有这么一个问题,是 DefaultModeffiinder 得到最终的参数对象后,再递交给 ModelValidator 实施验证呢,还是在实 施 Model 绑定的过程中动态地调用 ModelValidator 对由 ValueProvider 提供的数据值实施验证? 实际上我们上面演示的两个实例己经回答了这个问题。通过上面演示的两个例子我们知 道, CompositeModelValidator 这个默认 ModelValidator 在进行 Model 验证过程中并不是递归 进行的 (S603 ),但是从整个 Model 绑定过程来看, Model 验证却具有递归性,所以 Model 绑定和 Model 验证绝对不可能是先后的过程,唯一的可能是 DefaultModeffiinder 在递归地进 行 Model 绑定的过程中调用 ModelValidator 对提供的数据实施验证。 同样以针对 Contact 类型的 Model 绑定为例,当 DefaultModeffiinder 通过 Model 得到一 个被初始化的空 Contact 对象之后,会将描述 Contact 类型的 ModelMetadata 对象作为参数调 用 ModelValidator 的静态方法 GetModelValidator ,得到的 CompositeModelValidator 被用于对 Contact 对象实施验证。由于 CompositeModelValidator 的 Model 验证不具有递归性,所以只 有应用在 Contact 四个属性 (Name 、 PhoneNo 、 Emai1和 Address) 及其自身类型上的验证规 则在本轮验证中有效。 由于 Contact 的 Address 属性是一个复杂类型,所以 DefaultModeffiinder 在针对 Contact 类型的 Model 绑定过程中会递归地创建一个空 Address 对象作为 Contact 对象的 Address 属 性。在完成对 Address 对象的绑定之后,又会调用 ModelVa1idator 的静态方法 GetModelVa1idator 根据描述 Address 类型的 ModelMetadata 得到一个 CompositeModelValidator ,初始化后的 Address 对象被将交给它验证。 描述 Model 元数据的 ModelMetadata 具有一个树型层次化结构,我们的验证规则可以应 用到每一个节点上。 DefaultModelBinder 就是在递归地绑定复杂对象的过程中对绑定后的对 ASP. NET MVC 4 框架揭秘6.2 Model 绑定与验证 自 279 象实施验证,从而使各个层次上的验证得以实现。不过 CompositeModelValidator 只有在所有 属性值都验证通过的情况下,才会使用应用在类型上的验证规则对数据对象实施验证,所以 验证的结果也不能完全反映所有的验证规则。 实例演示:模拟 Model 绑定申的验证 (S610 ) 在第 5 章 "Model 的绑定"中,我们自定义了一个 DefaultModelB inder 实现了针对简单 类型、复杂类型、数组、集合和字典的 Model 绑定。在本例中,我们在这个自定义的 ModelB inder 中引入 Model 验证部分。 通过前面的介绍我们知道,真正实施 Model 验证的是通过 ModelValidator 的静态方法 GetModelValidator 创建的 CompositeModelValidator 对象,那么我们按照其采用的 Model 验 证逻辑自定义这么一个类型。如下面的代码片段所示,我们自定义的 CompositeModel Validator 直接继承自 ModelValidator 。 public class CornpositeModelValidator: ModelValidator public CornpositeModelValidator(ModelMetadata rnetadata, ControllerContext controllerContext) : base(rnetadata, controllerContext) { } public override IEnurnerable Validate (object container) bool isPropertiesValid = true; foreach (ModelMetadata propertyMetadata in Metadata.Properties) foreach (ModelValidator validator in propertyMetadata.GetValidators(this.ControllerContext)) IEnurnerable results = validator.Validate(propertyMetadata.Model); if (results.Any()) isPropertiesValid = false; foreach (ModelValidationResult result in results) yield return 口 ew ModelValidationResult MernberNarne = DefaultModelBinder.CreateSubPropertyNarne( propertyMetadata.PropertyNarne, result.MernberNarne) , Message = result.Message if (isPropertiesValid) foreach (ModelValidator validator in Metadata.GetValidators(this.ControllerContext)) ASPNETMVC4 框架揭秘280 每第 6 章 Model 的验证 IEnumerable results = validator.Validate(Metadata.Model); foreach (ModelVal 工 dat 工 onResult result in results) yield return result; 定义在 Validate 的 Model 验证逻辑是这样的:先通过 Mode lM etadata 属性获取当前的 Model 元数据,然后遍历所有描述属性的 Mode lM etadata 。对于每一个基于属性的 Mode lM etadata 对象,我们通过调用其 GetModelValidators 方法得到应用在该属性上的 ModelValidator 列表。接下来调用列表中每一个 ModelValidator 的 Validate 方法对属性值进行 验证,并根据返回值创建相应的 ModelValidatio nResult 对象,该对象被添加到最终返回的 ModelValidatio nResult 集合中。 只有在所有的属性通过验证的情况下,我们才根据当前 Mode lM etadata 获取相应的 ModelValidator 列表对容器对象自身实施验证,验证的结果直接添加到最终返回的 ModelValidatio nR esult 集合中。 需要注意一点的是,在进行针对属性的验证过程中,我们并没有直接使用返回 ModelValidatio nResult 对象,而是根据它创建了一个新的 ModelValidatio nResult 对象,该对 象表示成员名称的 MemberName 属性被添加上了对应属性名前缀。用于计算成员名称的静 态方法 CreateSubPrope吗rName 定义在 DefaultModeffiinder 中,具体的定义如下。 public class DefaultModelBinder : IModelBinder //其他成员 internal static string CreateSubPropertyName(string prefix , string propertyName) prefix = prefix ?? ""; propertyName = propertyName ?? ""; return (prefix + "." + propertyName) .Trim('.'); 在自定义的 DefaultModeffiinder 中,我们定义单独的方法来完成针对简单类型、复杂类 型、数组、集合和字典的 Model 绑定,而只需要在进行针对复杂类型的 Model 绑定过程中 利用 CompositeModelValidator 进行 Model 验证。 CompositeModelValidator 能够完成针对容器 对象所有属性及其自身的验证,虽然它不递归地去验证复杂类型属性的数据成员,但是由于 Model 绑定本身是一个递归的过程,所以从整个流程上看 Model 验证是递归进行的。 针对复杂数据类型 Model 验证实现在 DefaultModeffiinder 的 GetComple xM odel 方法中。 如下面的代码片段所示,我们在目标容器对象生成之后利用创建的 CompositeModelValidator 对象对它进行验证。对于返回的每一个 ModelValidatio nR esult ,我们将其错误消息添加到相 应的 ModelState 之中。 ASP. NET MVC 4 框架嚣秘6.2 Model 绑定与验证 白 281 public class DefaultModelBinder : IModelBinder //其他成员 protected virtual object GetComplexModel( ControllerContext controllerContext, Type modelType, IValueProvider valueProvider, string prefix) object model = CreateModel(modelType)i foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(modelType)) if (property.IsReadOnly) continuei string key = string. IsNullOrEmpty (prefix) ? property.Name prefix + "." + property.Namei property.SetValue(model, GetModel(controllerContext, property.PropertyType, valueProvider, keY))i //Model 验证 ModelMetadata metadata = ModelMetadataProviders.Current .GetMetadataForType(() => model, modelType)i CompositeModelValidator validator = new CompositeModelValidator(metadata, controllerContext)i foreach (ModelValidationResult result in validator.Validate(model)) string key = CreateSubPropertyName(prefix, result.MemberName)i controllerContext.Controller.ViewData.ModelState.AddModelError(key, result.Message)i return modeli 在第 5 章 "Model 的绑定"中已经验证过了自定义 DefaultModelB inder 的 Model 绑定功 能,现在通过一个简单的实例来验证刚刚增加的 Model 验证功能。我们直接使用上面实例中 定义的 Contact 类型,并且在 Contact 和 Address 类型和属'性上应用自定义的 AlwaysFailsAttribute 特性。接下来我们定义了如下一个 HomeCon位oller ,针对 Hπ'P-GET 的 Action 方法直接将一 个空 Contact 对象呈现在默认的 View 中,而应用了 H仕pPostA忧ribute 特性的 Index 方法具有 一个 Contact 类型的参数,该参数上应用了 ModeffiinderAttribute 特性将我们自定义的 DefaultModeffiinder 用于该参数的绑定。 public class HomeController : Controller public ActionResult Index() return View(new Contact())i [HttpPost] public ActionResult Index( [ModelBinder(typeof(DefaultModelBinder))] ASP. NET MVC 4 在架揭秘282 组第 6 章 Model 的验证 Contact contact) return View(contact); 如下所示的是 Action 方法Index 对应 View 的定义,这是一个 Model 类型为 Contact 的 强类型View 。在该 View 中我们将作为 Model 的 Contact 对象的所有属性(包括 Address 的 所有属性)以编辑模式呈现在一个表单之中,该表单具有一个用于提交的"保存"按钮。 @model Contact Model 绑定中的验证 @using (Html.BeginForm()) @Html.EditorForModel() @Html.EditorFor(m=>m.Address) 该程序运行之后会现在浏览器中呈现一个"编辑联系人信息"的页面,我们直接点击"保 存"按钮会呈现出如图 6-10 所示的输出结果,文本框右侧显示的文本正是应用在 Contact 和 Address 相应属性上的 AlwaysF ailsAttribute 特性定义的错误消息。 laII隅定中幡迹 ~ f1今 cnf~ ~ . · . ·甲同 墨 、 Name Contact. Nnme NK W 叫 F mnb l tn Address . Pro飞明白 C批y ['=======--=、 Ad齿的 S , C町Dis位ict l 一I Addn: s 二阴阳ct Street I Addre.s s.street 匮主5 图 6-10 实现在自定义 DefaultModelBinder 中的 Model 验证 ASP. NET MVC 4 在架揭秘6.3 基于数据注解特性的 Model 验证 • 283 6.3 基于数据注解特性的 Model 验证 通过前面的介绍我们知道, ModelValidatorProviders 的静态只读属性 Providers 维护着一 个全局的 ModelValidatorProvider 列表,最终用于 Model 验证的 ModelValidator 都是通过这些 ModelValidatorProvider 来提供的。对于该列表默认包含的三种 ModelValidatorProvider 来说, Dat aAnn otationsModelValidatorProvider 无疑是最重要的, ASP.NET MVC 默认提供的基于验 证特性的声明式 Model 验证就是通过它提供的 Dat aAnn otationsModelValidator 来实现的。 6.3.1 ValidationAttribute 特性 与通过数据注解特性定义 Mode lM etadata 类似,我们可以在数据类型及其属性上应用相 应的验证特性来定义相应的验证规则,所有的验证特性都直接或者间接继承自具有如下定义 的抽象类型 System.ComponentMode l. Dat aAnn otations.Validatio nA ttribute 。 public abstract class ValidationAttribute : Attribute qd n -l qJqdr 口 nt ·--les rrp& ttyd ss 巾 4e t cccc ·-·工 -le ll--t bbbo uuur pppp ErrorMessage { get; set; } ErrorMessageResourceName { get; set; ErrorMessageResourceType { get; set; ErrorMessageString {get;} public virtual string FormatErrorMessage(string name); public virtual bool IsValid(object value); protected virtual ValidationResult IsValid(object value , ValidationContext validationContext) public void Validate(object value , string name); public ValidationResult GetValidationResult(object value , ValidationContext validationContext); 如上面的代码片段所示, Validatio nAttribute 具有一个字符串类型的 ErrorMessage 属性用于 指定错误消息。出于对本地化或者对错误消息单独维护的需要,可以采用资源文件的方式来维 护错误消息,在这种情况下只需要通过 ErrorMessageResourceName 和 ErrorMessageResourceType 这两个属性指定错误消息所在资源项的名称和类型即可。如果我们通过 ErrorMessage 属性指定 一个字符串作为验证错误消息,又通过 ErrorMessageResource NameÆrrorMessageResourceType 属性指定了错误消息资源项对应的名称和类型,后者具有更高的优先级。 Validatio nAttribute 具有一个受保护的只读属性 ErrorMessageString 用于返回最终的错误消息文本。 对于错误消息的定义,我们可以指定完整的消息内容,比如"年龄必须在 18 至 25 之间"。 但是对于像资源文件这种对错误消息进行独立维护的情况,为了让定义的资源文本能够最大 限度地被重用,我们倾向于定义一个包含占位符的文本模板,比如" {DisplayName} 必须在 {LowerBound} 和 {UpperBound} 之间",这样消息适用于所有基于数值范围的验证。模板中的 占位符可以在虚方法 F ormatErrorMessage 中进行替换,该方法中的参数 name 实际上代表的 是对应的显示名称,即 ModelMetadata 的 DisplayName 属性。 ASP. NET MVC 4 框架揭秘284 省第 6 章 Model 的验证 F ormatErrorMessage 方法在 ValidationAttribute 中仅仅是按照如下的方式调用 S创吨的静 态方法 Format 将参数 name 作为替换占位符的参数,所以在默认的情况下我们定义错误消息 模板只允许包含唯一一个针对显示名称的占位符 "{O}"。如果具有额外的占位符,或者需要 采用非序号(" {O} ")占位符定义方式(比如采用类似于 "{DisplayName} "这种基于文字的 占位符更具可读性),只需要重写 FormatErrorMessage 方法即可。 public abstract class ValidationAttribute : Attribute //其他成员 public virtual string ForrnatErrorMessage(string narne) return string.Forrnat(CultureInfo.CurrentCulture, ErrorMessageString, new object[] { narne })i 当我们通过继承 ValidationAttribute 创建自己的验证特性的时候,可以通过重写任意一 个 IsValid 方法来定义验证逻辑。之所以能够通过重写任意一个 IsValid 方法来实现自定义验 证,原因在于定义在 ValidationAttribute 的这两个 IsValid 方法之间存在相互调用的关系。很 显然,这种相互调用必然造成"死循环气所以我们需要重写至少其中一个方法来避免"死 循环"的发生。这里的"死循环"被加上引号,是因为 ValidationAttribute 在内部做了处理, 当这种情况出现的时候会抛出一个 NotImplementedException 异常。 //调用公有 IsValid 方法 public class ValidatorAttribute : ValidationAttribute static void Main() ValidatorAttribute validator = new ValidatorAttribute()i validator.IsValid(new object())i //调用受保护 IsValid 方法 public class ValidatorAttribute : ValidationAttribute static void Main() ValidatorAttribute validator = new ValidatorAttribute()i validator.IsValid(new object() , nul 工) i 我们通过一个简单的实例来演示自定义 ValidationAttribute 对两个 IsValid 方法进行重写 的必要性。在一个控制台应用中我们分别编写了如上两段程序,通过继承自 ValidationAttribute 自定义的 ValidatorAttribute 没有重写任何一个 IsValid 方法。当我们在 Debug 模式下分别运行 这两段程序的时候,都会抛出如图 6-11 所示的 NotImplementedException 异常,提示"此类 尚未实现 IsValid(object value) 。首选入口点是 GetValidationResultO ,并且类应重写 IsValid(object value, ValidationContext context)0 " ASP. NET MVC 4 蓓架揭秘6.3 基于数据注解特性的 Model 验证 每 285 图 6-11 没有在自定义 ValidationAttribute 中重写 IsValid 方法导致的异常 受保护的 IsValid 方法中除了包含一个表示被验证对象的参数 value ,还有一个类型为 ValidationContext 的参数。顾名思义, ValidationContext 旨在为当前的验证维护相应的上下文, 这些信息包括通过 Objectlnstance 和 ObjectType 属性表示的验证对象及其类型,以及通过 MemberName 和 DisplayName 属性表示的成员名称(一般指属性名称)和显示名称。 public sealed class ValidationContext //其他成员 public ValidationContext(object instance); public ValidationContext(object instance ,工 Dictionary items); αJGJt nnc ·工·工 ee rr--p ttbvd SSOT CCCC -L-L· 工 -l llll bbbb uuuu pppp DisplayName { get; set; } MemberName { get; set; } ObjectInstance { get; } ObjectType { get; } 该 IsValid 方法返回值类型为 System.ComponentMode l. Dat aAnn otations. Validatio nResult 。 如下面的代码片段所示,它与作为 ModelValidator 验证结果的 ModelValidatio nR esult 类型具 有类似的定义,它依然是错误消息和成员名称的组合。不过 ModelValidatio nR esult 对应某个 单一的成员名称,而 Validatio nResult 包含一组相关成员名称的列表。 public class ValidationResult //其他成员 public ValidationResult(string errorMessage); public ValidationResult(string errorMessage , 工 Enumerable memberNames); public string ErrorMessage { get; set; } public IEnumerable MemberNames { get; } 定义在 Validatio nA ttribute 中的 IsValid 方法在验证失败的情况下会返回一个 Validatio nR esult 对象,如果指定的 ValidationContext 不为 Null ,那么其 MemberName 属性表 示的成员名称将会包含在该 Validatio nR esult 对象的 MemberNames 列表中。 ValidationContext AS P. NET MVC 4 在架揭秘286 [ÌI 第 6 章 Model 的验证 的 DisplayName 属性将会作为调用 FormatErrorMessage 的参数,而得到的格式化错误消息将 会作为 ValidationResult 的 ErrorMessage 属性。如果成功通过验证,则直接返回 Null 。 我们可以通过调用 ValidationAt创bute 的方法 GetValidationResult 对指定的对象实施验证 并得到以 ValidationResult 对象形式返回的验证结果,得到的 ValidationResult 对象实际上就 是调用受保护 IsValid 方法的返回值。也可以调用 Validate 方法验证某个指定的对象,该方法 在验证失败的情况下会直接抛出一个 ValidationException 异常,而通过调用 F ormatErrorMessage 方法(将参数 name 表示的字符串作为参数)格式化后的错误消息将会 作为该异常的消息。 在 System.ComponentModel.DataAnnotations 命名空间下定义了一系列具体的验证特性, 它们大都直接应用在自定义数据类型的某个属性上目标数据成员实施验证。这些预定义验证 特性不是本章论述的重点,所以在这里只是对它们作一个概括性的介绍。 • RequiredAttribute: 用于验证必需数据成员。 • RangeAttribute: 用于验证数据成员的值是否在指定的范围之内。 • StringLengthAttribute: 用于字符串验证数据成员的长度是否在指定的范围之内。 • MaxL engthAttributelMinLengthAt位ibute: 用于验证字符/数组字典的数据成员长度是否小 于/大于指定的上/下限。 • RegularExpressionAttribute: 用于验证字符串数据成员的格式是否与指定的正则表达式 相匹配。 • CompareAttribute: 用于验证数据成员的值是否与另一个成员一致,在用户注册场景中 可以用于确认两次输入密码的一致性。 • Custom ValidationAttribute: 指定一个用于验证目标成员的验证类型和验证方法。 应用 ValidationAttribute 特性的惟一性 对于上面列出的这些预定义 ValidationAttribute ,它们都具有一个相同的特征,那就是 在同一个目标元素中只能应用一次,这可以通过应用在它们上面的 AttributeUsageAttribute 特性的定义看出来。以如下所示的 RequiredAttribute 特性为例,应用在该类型上的 AttributeU sageAttrribute 特性的 AllowMultiple 属性被设置为 False 。 [AttributeUsage(AttributeTargets.Parameter I AttributeTargets.Field I AttributeTargets.Property, A11oWMultiple=false)] public class RequiredAttribute : ValidationAttribute //省略成员 但是是否意味着如果我们在自定义 ValidationAttribute 的时候将 AttributeUsageAttribute 特性的 AllowMultiple 设置为 True ,它们就可以被多次应用到同一个属性或者类型上了呢? ASP. NET MVC 4 在架揭秘6.3 基于数据注解特性的 Model 验证 • 287 我们不妨通过实例演示的方式来说明这个问题。 我们知道 RangeAttribute 可以帮助我们验证目标字段值的范围,但是有时候我们需要进 行"条件性范围验证"。举个例子,我们现在对某个员工的薪水进行验证,但是不同级别的 员工的薪水范围是不同的,为此我们创建了一个名为 RangeI fA ttribute 的验证特性帮助我们 进行针对不同级别的薪水范围验证。如下面的代码片段所示,我们将三个 RangeI fAt位 ibute 特性应用到了表示薪水的 Salary 属性上,分别针对三个级别 CG7 、 G8 和 G9) 的薪水范围 作了相应的设定。 e e V 」 0 14 p m pu s s a 14 C C .-14 hu u p ‘ It public string Name { geti seti } public string Grade { geti seti } [Rangelf("Grade" , "G7" , 2000 , 3000)] [Rangelf("Grade" , "G8" , 3000 , 4000)] [Ra nge 工 f ("Grade" , "G9" , 4000 , 5000)] public decimal Salary { geti seti } 如下面的代码片段所示, RangeI 且即 ibute 直接继承自 RangeAttribute 0 RangeI fAttribute 根据被验证容器对象的另一个属性值来决定是否对当前属性实施验证,属性 Prope均r 和 Value 就分别代表这个属性和与之匹配的值。在重写的 IsValid 方法中,我们通过反射获取到了容 器对象用于匹配的属性值,如果该值与 Value 属性值相匹配,则调用基类同名方法对指定对 象进行验证,否则直接返回 Validatio nResul t. Success C Null) 。应用在 RangeIfAttribute 上的 AttributeU sageAttribute 特性的 AllowMultiple 被设置为 True 。 [AttributeUsage(AttributeTargets.Property , AllowM ultiple = true)] public class RangeIfAttribute: RangeAttribute public string Property { geti seti } public string Value { geti seti } public RangeIfAttribute(string property , string value , double minimum , double maximum) base(minimum , maximum) this.Property = propertYi this.Value = value??""i protected override ValidationResult IsValid(object value , ValidationContext validationContext) PropertyInfo property = validationContext.ObjectType.GetProperty(this.Property )i object propertyValue = property.GetValue(validationContext.ObjectInstance , null)i propertyValue = propertyValue ?? ""i if (propertyValue.ToString()!= this.Value) return ValidationResult.Successi ASP. NET MVC 4 框架揭秘288 由第 6 章 Model 的验证 return base.IsValid(value, validationContext)i 那么这样一个 RangelfAttribute 特性真的能够按照我们期望的方式进行验证吗?为此我 们在创建的空 ASP.NET MVC 应用中定义了如下一个 HomeController 。在 Action 方法 lndex 中创建了用于描述 Employee 的 Salary 属性 ModelMetadata 对象,并通过调用其 GetValidators 方法得到针对该属性的所有 ModelValidator 列表,最终将这个 ModelValidator 列表转化为数 组作为 Model 呈现在对应的 View 中。 public class HomeController : Controller public ActionResult Index() ModelMetadata employeeMetadata = ModelMetadataProviders.Current .GetMetadataForType(() => new Employee() , typeof(Employee))i ModelMetadata salaryMetadata = employeeMetadata.Properties .FirstOrDefault(p => p.PropertyName == "SalarY")i IEnumerable val 工 dators = salaryMetadata .GetValidators(ControllerContext); retur口 View(validators.ToArraY())i 如下所示的 Action 方法 lndex 对应 View 的定义,这是一个 Model 类型为 ModelValidator 数组的强类型 View 。在该 View 中,我们将所有 ModelValidator 对象的类型名称通过表格的 形式呈现出来。 @model ModelValidator[] ModelValidators @for(int i= Oi 工
    @(i+l)@Model[i] .GetType() .Name
    该程序运行之后会在浏览器中呈现出如图 6-12 所示输出结果。由于 Employee 的 Salary 属性类型为非空值类型,所以会自动添加一个 RequiredAttributeAdapter 来进行必要性验证, 另一个用于数值验证的 NumericModelValidator 也是源于 Salary 属性的类型。实际上只有第 一个 DataAnnotationsModelValidator 是针对应用在 Salary 属性上的 RangelfAttribute 特性而创 建的,换句话说,应用在同一属性上的三个 Rangel fAttribute 特性只有一个是有效的,具有 原因何在呢? (S611) ASP. NET MVC 4 框架揭秘DataAnnotationsModelValidator RequiredAttributeAdapter NumericModelValidator 6.3 基于数据注解特性的 Model 验证 电 289 图 6-12 应用了多个同类验证特性的属性具有的 ModelValidator (1) 我们知道 Attribute 具有一个 object 类型的 TypeId 属性,默认返回代表自身类型的 Type 对象。 ASP.NET MVC 在根据 Validatio nA ttribute 特性创建相应的 Dat aAnn otationsModel Validator 对象的时候会根据该 TypeId 属性值进行分组,同一组的 Validatio nA ttribute 只会选 择第一个。这就意味着对于多个应用到相同目标元素的同类 Validatio nA t创 bu钮,有且只有一 个是有效的。 那么如何来解决这个问题呢?其实很简单,既然 Model 验证系统会根据 TypeId 对所有 验证特性进行筛选,我们只需要通过重写 TypeId 属性使每个 Validatio nA ttribute 具有不同的 属性值就可以了,为此我们按照如下的方式在 RangeI fAttribute 中重写了 TypeId 属性。 [AttributeUsage{ AttributeTargets.Fieldl AttributeTargets.Property , AllowMultiple = true)] public class RangelfAttribute: RangeAttribute //其他成员 private object typeid; public override object Typeld get{ return typeid?? (typeid= new object{));} 再次运行程序后将会在浏览器中得到如图 6-13 所示的输出结果,可以看到针对三个 RangeIfAt位 ibute 特性的三个 Dat aAnn otationsModelValidator 被创建出来了。顺便说一下, 通过重写 TypeId 而使多个 Validatio nA ttribute 可以同时应用到相同的属性或者类型的解决 方案并不适合客户端验证,因为这会导致多组相同的验证规则被生成,而这是不允许的。 (S612) 医噩帘 咂里 DataAnno t ationsModelValidator Data An notationsModelValidator DataAnnotationsModelValidator RequiredAttributeAdapter NumericModelValidator 图 6-13 应用了多个同类验证特性的属性具有的 ModelValidator (2) ASP. NET MVC 4 框架锺翻290 .ÌI 第 6 章 Model 的验证 6.3.2 DataAnnotationsModelValidator ModelValidator 是真正用于 Model 验证的组件,应用在数据类型及其属性上的验证特性 最终被转换成相应的 ModelValidator 参与到针对目标数据的验证中,这个 ModelValidator 类 型就是具有如下定义的 DataAnnotationsModelValidator ,它的只读属性 At位ibute 返回的就是 对应的验证特性。 public class DataAnnotationsModelValidator : ModelValidator public DataAnnotationsModelValidator(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute)i public override IEnumerable GetClientValidationRules()i public override IEnumerable Validate (object container) ValidationContext validationContext = new ValidationContext( container ?? this.Metadata.Model, null, null) DisplayName = this.Metadata.GetDisplayName() ValidationResult validationResult = this.Attribute.GetValidationResult( this.Metadata.Model, validationContext)i if (validationResult != ValidationResult.Success) ModelValidationResult iteratorVariable2 = new ModelValidationResult Message = validationResult.ErrorMessage yield return iteratorVariable2i else yield breaki protected ValidationAttribute protected string public override bool Attribute { geti } ErrorMessage { geti } 工 sRequired { geti } 上面的代码片段给出了用于实施验证的核心方法 Validate 的完整定义。该方法首先针对 被验证容器对象创建出表示验证上下文的 ValidationContext 对象,并采用 ModelMetadata 的 DisplayName 属性作为该上下文的显示名称。真正的验证工作通过调用封装的验证特性的 Get ValidationResult 方法来完成,如果该方法返回值不为 Null (ValidationResult. Success) ,则 将返回的 ValidationResult 转换成 ModélValidationResult 对象并添加到最终返回的 ModelValidationResult 集合中。 顺便再说说定义在 DataAnnotationsModelValidator 中的另外两个受保护只读属性的逻 辑。用于返回错误消息的 ErrorMessage 属性来源于对验证特性的 FormatErrorMessage 方法的 ASP. NET MVC 4 在架揭秘6.3 基于数据注解特性的 Model 验证 霞 291 调用,而指定的参数就是当前 ModelMetadata 的 DisplayName 属性。由于只有 RequiredAttribute 特性才会对被验证数据实施必要性验证,所以只有被封装的 ValidationAttribute 为 RequiredAttribute 时其 IsRequired 属性才返回 True 。 除了 DataAnnotationsModelValidator , ASP.NET MVC 还定义了一个具有如下定义的泛型的 System.Web.Mvc.DataAnnotationsModelValidator ,它是 DataAnnotationsModelValidator 的子类,泛型参数表示被封装的验证特性的类型。 public class DataAnnotationsModelValidator DataAnnotationsModelValidator where TAttribute: ValidationAttribute public DataAnnotationsModelValidator(ModelMetadata metadata, ModelBindingExecutionContext context, TAttribute attribute); protected TAttribute Attribute { get; } ASP.NET MVC 为 4 个常用的验证特性( RequiredAttribute 、 RangeAttribute 、 RegularExpressionAttribute 和 StringLengthAttribute) 定义了相应的适配类型。如下面的代码 片段所示,它们都是泛型的 DataAnnotationsModelValidator public RequiredAttributeAdapter(ModelMetadata metadata, ControllerContext context, Requ 工 re dAttr 工 bute attribute); public override IEnumerable GetClientValidationRules(); public class RangeAttributeAdapter DataAnnotationsModelValidator public RangeAttributeAdapter(ModelMetadata metadata, ControllerContext context, RangeAttribute attribute); public override IEnumerable GetClientValidationRules(); public class RegularExpressionAttributeAdapter DataAnnotationsModelValidator public RegularExpressionAttributeAdapter(ModelMetadata metadata, ControllerContext context, RegularExpressionAttribute attribute); public override IEnumerable GetClientValidationRules(); public class StringLengthAttributeAdapter DataAnnotationsModelValidator public StringLengthAttributeAdapter(ModelMetadata metadata, ASP. NET MVC 4 在架揭秘292 自第 6 章 Model 的验证 ControllerContext context , StringLengthAttribute attribute)i public override IEnumerable GetClientValidationRules()i 6.3.3 DataAnnotationsModelValidatorProvider Dat aAnn otationsModelValidator 最终是通过对应的 Dat aAnn otationsModelValidatorProvider 创建的。通过前面的介绍我们知道它是As sociatedValidatorProvider 的子类,后者在用于获取 ModelValidator 的 Get Validators 方法中已经根据指定的 ModelMetadata 将所有特性提取出来, Dat aA nnotationsModelValidatorProvider 只需要从中筛选出继承自 Validatio nA ttribute 的验证特 性并创建对应的 Dat aA nnotationsModelValidator 就可以了。 我们现在结合 Dat aAnn otationsModelValidatorProvider 的相关定义来讨论一下具体的 ModelValidator 提供机制。如下面的代码片段所示,它具有两个静态的字段 AttributeFactories 和 Defaul tA ttributeFactory ,后者是→个类型为 Dat aA nnotationsModelValidatio nF actory 的委托, 前者是以此委托为 Value 以 Type 对象为 Key 的字典。 public class DataAnnotationsModelValidatorProvider AssociatedValidatorProvider //其他成员 internal static readonly Dictionary AttributeFactorieSi internal static DataAnnotationsModelValidationFactory DefaultAttributeFactorYi internal static DataAnnotationsValidatableObjectAdapterFactory DefaultValidatableFactorYi internal static readonly Dictionary ValidatableFactorieSi protected override IEnumerable GetValidators( ModelMetadata metadata , HttpActionContext actionContext , 工 Enumerable attributeS)i public delegate ModelValidator DataAnnotationsModelValidationFactory(ModelMetadata metadata , ControllerContext context , ValidationAttribute attribute)i public delegate ModelValidator DataAn notationsValidatableObjectAdapterFactory(ModelMetadata metadata , ControllerContext context); 委托 Dat aAnn otationsModelValidatio nF actory 根据 Mode lM etadata 、 ControllerContext 和 Validatio nA ttribute 创建一个 ModelValidator 对象。字段 AttributeFactories 表示的字典将验证 特性的类型作为 Key ,换句话说它维护一个 Validatio nAttribute 特性类型和对应 ModelValidator 工厂的匹配关系。 在重写的 GetValidators 方法中,针对提供的每一个 Validatio nA ttribute 特性,它先根据 ASP. NET MVC 4 在架揭秘6.3 基于数据注解特性的 Model 验证 蟹 293 其类型从 AttributeFactories 字典中获取一个对应的 Dat aAnn otationsModelValidatio nF actory 委 托。如果该委托对象存在,则用它来创建相应的 ModelValidator 对象,否则就采用字段 DefaultAttributeFactory 表示的 Dat aA nnotationsModelValidatio nF actory 委托来进行 ModelValidator 的创建。 除了 AttributeFactories 和 DefaultAttributeF actory , Dat aA nnotationsModelValidatorProvider 还具有 DefaultValidatableFactory 和 ValidatableFactories 两个静态字段,它们用于针对可验证 对象(实现了 IValidatableObject 接口)的 ModelValidator 创建。字段 Default ValidatableF actory 的类型是另外一个名为 Dat aA nnotations Validatab leO bj ectAdapterF actory 的委托,该委托根据 ModelMetadata 和 ControllerContext 创建相应的 ModelValidator 。字段 Validatab leF actories 是 一个以此委托为 Value 、以 Type 对象为 Key 的字典。 当 Dat aA nnotationsModelValidatorProvider 完成了针对基于验证特性的 ModelValidator 的 创建之后,如果被验证数据类型实现了 IValidatableObject 接口,它会先从静态字段 ValidatableFactories 中根据此类型获取一个对应的 DataAnn otationsValidatableO bj ec tAdapterF actory 委托。如果匹配的委托对象存在,则用其进行 ModelValidator 的创建,否则采用字段 DefaultValidatableFactory 表示的默认工厂来创建相应的 ModelValidator 对象。 在 DataAnn otationsModelValidatorProvider 类型被加载的时候,上述的四个字段会被初始化。 从如下的代码可以看出,一般的验证特性的 ModelValidator 是一个 Dat aAnn otationsModelValidator 对象(对应 DefaultAttributeFactory 字段) 0 RangeAttribute 、 RegularExpressio nA ttribute 、 Require dA ttribute 和 StringLengt hA ttribute 这四种验证特性,对应的 ModelValidator 是它们对 应的适配器。对于可验证对象来说,默认情况下提供的 ModelValidator 列表中还包含一个 ValidatableObjectAdapter 对象。 public class DataAnnotationsModelValidatorProvider AssociatedValidatorProvider //其他成员 static DataAnnotationsModelValidatorProvider() //l.DefaultAttributeFactory DefaultAttributeFactory = (metadata , context , attribute) =>口 ew DataAnnotat 工 onsModelVal 工 dator(metadata , context , attribute); //2.AttributeFactories Dictionary qictionary = new Dictionary(); dictionary.Add(typeof(RangeAttribute) , (metadata , context , attribute) => new RangeAttributeAdapter(metadata , context ,' (RangeAttribute)attribute)); dictionary.Add(typeof(RegularExpressionAttribute) , (metadata , context , attribute) => new RegularExpressionAttributeAdapter(metadata , context , (RegularExpressionAttribute)attribute)); dictionary.Add(typeof(Require dAttribute) , (metadata , context , attribute) => new Require dA ttributeAdapter(metadata , context , (Require dA ttribute)attribute)); ASPNETMVC4 框架揭秘294 • 第 6 章 Model 的验证 dictionary.Add(typeof(StringLengthAttribute) , (metadata, context, attribute) => new StringLengthAttributeAdapter(metadata, context, (StringLengthAttribute)attribute)); AttributeFactories = dictionary; //3. DefaultValidatableFactory DefaultValidatableFactory = (metadata, context) => ηew ValidatableObjectAdapter(metadata, coηtext)i //4.ValidatableFactories ValidatableFactories = new Dictionary(); DataAnnotationsModelValidatorProvider 四个基于委托的静态字段体现了采用的 ModelValidator 提供机制。由于它们都是内部字段,我们不能直接对其进行操作,但是如下 所示的一系列静态方法定义在 DataAnnotationsModelValidatorProvider 之中,我们可以按照具 体的需要调用它们对默认的 ModelValidator 进行注册。 public class DataAnnotationsModelValidatorProvider AssociatedValidatorProvider //其他成员 public static void RegisterAdapter (Type attributeType, Type adapterType) i public static void RegisterAdapterFactory(Type attributeType, DataAnnotationsModelValidationFactory factory); public static void RegisterDefaultAdapter(Type adapterType); public static void RegisterDefaultAdapterFactory( DataAnnotationsModelValidationFactory factory); public static void RegisterDefaultValidatableObjectAdapter (Type adapterType) ; public static void RegisterDefaultValidatableObjectAdapterFactory( DataAnnotationsValidatableObjectAdapterFactory factorY)i public static void RegisterValidatableObjectAdapter(Type modelType, Type adapterType); public static void RegisterValidatableObj ectAdapterFactory (Type modelType, DataAnnotationsValidatableObjectAdapterFactory factory); 对于上面的 8 个静态方法,除了 RegisterDefaultAdapter 和 RegisterValidatableObjectAdapter 之外,其余的都很好理解。 RegisterDefaultAdapter 用于注册一个默认的针对验证特性的 ModelValidator 类型,该类型必须具有一个参数类型列表为 ModelMetadata 、 ControllerContext 和 At位ibute 的构造函数。如果根据验证特性的类型找到了匹配的 DataAnnotationsModelValidationFactory 委托对象,相应的参数会被传入该构造函数创建一个 我们注册的 ModelValidator 对象。 Register Validatab leObj ectAdapter 和 RegisterDefaultAdapter 比较类似,用于注册一个默认 的针对可验证对象类型的 ModelValidator ,被注册的 ModelValidator 类型必须具有一个参数 类型列表为 ModelMetadata 和 ControllerContext 的构造函数。如果根据验证特性的类型找到 了匹配的 DataAnnotationsValidatableObjectAdapterFactory 委托对象,相应的参数会被传入该 构造函数并最终创建一个我们注册的 ModelValidator 对象。 ASP. NET MVC 4 框架揭秘6.3 基于数据注解特性的 Model 验证 自 295 6.3.4 将 ValidationAtlribute 应用到参数上 如果你够细心应该会发现我们常用的验证特性都可以直接应用到方法的参数上,以下面 的RangeA世ibute 的定义为例,应用在该类型上的 AttributeUsageAttribute 的定义表明可以标 注该特性的目标元素包括参数、字段和属性。 [AttributeUsage( AttributeTargets.Parameter I AttributeTargets.Fie1d I AttributeTargets.Property, A11oWMu1tip1e=fa1se) > public class RangeAttribute : ValidationAttribute //省略成员 但是对于 ASP.阳TMVC 的 Model 验证来说,应用在 Action 方法参数上的验证特性起 不到任何作用,因为用于进行 Model 验证的 ModelValidator 对象是通过基于参数类型的 ModelMetadata 来创建的,它根本不会去解析应用在参数本身上的验证特性。 但是在笔者看来,直接针对 Action 方法参数的 Model 验证具有很高的实用价值。一方 面,目前的 Model 验证仅限于针对容器对象本身及其属性的验证,如果目标 Action 方法参 数为如ing 、 Int32 和 Double 等这样的简单类型,针对它们的验证只能通过手工的方式来完 成:另一方面,具有相同参数类型的多个 Action 方法往往具有不同的验证规则。如果我们 能够将验证特性应用在参数上进行针对性的验证规则定义,这两个问题都将迎刃而解。 到目前为止,我们对 ASP.NETMVC 的 Model 验证系统已经有了一个全面的了解,现在 通过对它进行相应的扩展使直接应用到参数上的验证特性能够生效。我们需要自定义一个 ModelValidatorProvider 来解析应用到参数上的验证特性,并据此生成对应的 ModelValidator 。 但在这之前需要解决的另一个问题是如何将应用于参数的特性提供给我们自定义的 ModelValidatorProvider ,在这里我们将当前 Con位ollerContext 作为它们的载体。 Action 方法的执行是通过 ActionInvoker 来实现的,默认的 ControllerActionInvoker 和 AsyncControllerActionInvoker 都定义了一个受保护的虚方法 GetParameterValue ,它根据描述 参数的 ParameterDescriptor 对象和当前的 ControllerContext 借助于 Model 绑定机制得到对应 的参数值。我们可以通过继承 ControllerActionInvoker/AsyncControllerActionInvoker 以重写 该方法的方式将 ParameterDescriptor 保存当前的 ControllerContext 中。 为此我们自定义了如下两个 ActionInvoker ,其中 ParameterValidationActionInvok,町继承自 Controll町:ActionInvoker ,而 ParameterValidationAsyncActionInvoker 是 AsyncControllerActionInvoker 的子类。在重写的 GetParameterValue 方法中,我们在调用基类的同名方法之前将作为参数 的 ParameterDescriptor 对象保存到当前 Con位ollerContext 中,具体来说是放到了表示当前路 由数据的 RouteDataD ictionary 对象的 DataTokens 集合中。在方法调用之后我们将它从 ControllerContext 中移除。 ASP. NET MVC 4 在架揭秘296 旬第 6 章 Model 的验证 public class PararneterValidationActionlnvoker : ControllerActionlnvoker protected override object GetPararneterValue( ControllerContext controllerContext , PararneterDescriptor pararneterDescriptor) try controllerContext.RouteData.DataTokens.Add("PararneterDescriptor" , pararneterDescriptor); return base.GetPararneterValue(controllerContext , pararneterDescriptor); Y 14 14 a n -- }f{ controllerContext.RouteData.DataTokens.Rernove("PararneterDescriptor"); public class PararneterValidationAsyncActionlnvoker AsyncControllerActionlnvoker protected override object GetPararneterValue(ControllerContext controllerContext , PararneterDescriptor pararneterDescriptor) try controllerContext.RouteData. DataTokens.Add ("PararneterD escriptor" , pararneterDescriptor); return base.GetPararneterValue(controllerContext , pararneterDescriptor); y 14 14 a n ·工 }f{ controllerContext.RouteData.DataTokens.Rernove("PararneterDescriptor"); 被 ParameterValidationActionInvoker 和 ParameterValidationA syncActionInvoker 存放到当前 ControllerC ontext 中的 ParameterDesσiptor 被自定义的 ModelValidatorProvider 提取出来用于创建相 应的 ModelValidator 。如下面的代码所示,我们自定义的 Parame伽ValidationModelValidatorProvider 直接继承自 DataAnn otationsModelValidatorProvider ,在重写的 GetValidators 方法中将 ParameterDescriptor 从 Con位 ollerContext 中提取出来,然后得到应用在参数上的所有特性并 与当前的特性列表进行合并,最后将合并的特性列表作为参数调用基类的 GetValidators 方法。 public class PararneterValidationModelValidatorProvider DataAnnotationsModelValidatorProvider protected override IEnurnerable GetValidators( ModelMetadata rnetadata , ControllerContext context , IEnurnerable attributes) object descriptor; if (rnetadata.ContainerType == null && context.RouteData.DataTokens AS P. NET MVC 4 框架揭秘6.3 基于数据注解特性的 Model 验证 自 297 . TryGetValue ("PararneterDescriptor", out descriptor)) PararneterDescriptor pararneterDescriptor = (PararneterDescriptor)descriptor; DisplayAttribute displayAttribute = pararneterDescriptor . GetCustornAttributes (true) .OfType() .FirstOrDefault() ?? new DisplayAttribute { Narne = pararneterDescriptor.PararneterNarne }; rnetadata.DisplayNarne = displayAttribute.Narne; var addedAttributes = pararneterDescriptor . GetCustornAttributes (true) .OfType(); return base.GetValidators(rnetadata, context, attributes.Union(addedAttributes)); else return base.GetValidators(rnetadata, context, attributes); 值得一提的是,应用在参数上的特性是针对最外层的容器类型,而不是针对容器类型的 属性。比如在类型为 Contact 的参数上应用一个验证特性,该特性应该与应用在 Contact 类 型上的特性具有相同的效果,但是与 Address 属性无关。所以 ParameterDescriptor 的提取以 及特性的合并仅仅在当前 ModelMetadata 的 ContainerType 为 Null 的情况下才会进行。除此 之外,我们还利用了标注在参数的 DisplayAttribute 特性对 ModelMetadata 的 DisplayName 属性进行了相应的设置。 在默认的情况下,只有在针对复杂类型的 Model 绑定过程中才会进行 Model 验证。虽 然我们通过 ParameterValidationModelValidatorProvider 能够根据应用在 Action 方法参数上的 验证特性生成相应的 ModelValidator ,但是如果验证特性是应用在一个简单类型的参数上, 对应的 ModelValidator 也是不会真正用于参数验证的。为了使 Model 验证发生在针对简单类 型的 Model 绑定过程中,我们不得不创建一个自定义的 Modeillinder 。 我们定义了一个具有如下定义的 ParameterValidationModeillinder ,它直接继承自默认使 用的 DefaultModeillinder ,针对简单类型的 Model 验证定义在重写的 BindModel 方法中。 public class PararneterValidationModelBinder : DefaultModelBinder public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) object rnodel = bindingContext.ModelMetadata.Model = base.BindModel(controllerContext, bindingContext); ModelMetadata rnetadata = bindingContext.ModelMetadata; if (rnetadata.IsCornplexType II null == rnodel) return rnodel; //针对简单类型的 Model 验证 ASP. NET MVC 4 在架揭秘298 '. 第 6 章 Model 的验证 Dictionary dictionary = new Dictionary(StringCornparer.OrdinalIgnoreCase); foreach (ModelValidationResult result in ModelValidator . GetModelValidator (rnetadata , controllerContext) .Validate(null)) string key = bindingContext.ModelNarne; if (!dictionary.ContainsKey(key)) dictionary[key] =bindingContext.ModelState.IsValidField(key); if (dictionary[key]) bindingContext.ModelState.AddM odelError(key , result.Message); return rnodel; 到此为止,为了能够将验证特性应用于 Action 方法的参数,我们创建了自定义的 Actio nI nvoker 、 ModelValidatorProvider 和 ModelBinder 。为了验证它们是否能够最终实现期 望的验证效果,我们将它们应用到一个简单的 ASP. 阳 TMVC 应用中。在一个 ASP. 阳 TMVC 应用中创建了一个具有如下定义的 HomeController ,在重写的 CreateActio nI nvoker 方法中, 如果调用基类同名方法返回的 Actio nI nvoker 类型为 ControllerActio nI nvoker ,那么将返回一 个 ParameterValidatio nA ctionlnvoker 对象,否则返回一个 ParameterValidatio nA syncAction Invoker 对象,这样做的目的是与默认的同步/异步 Action 执行方式保持一致。 public class HorneController : Controller protected override IActionlnvoker CreateActionlnvoker() IActionlnvoker actionlnvoker = base.CreateActionlnvoker(); if (actionlnvoker is ControllerActionlnvoker) return new PararneterValidationActionlnvoker(); else return new PararneterValidationAsyncActionlnvoker(); public ActionResult Add( [Display(Narne = "第 1 个操作数") ] [Range(lO , 20 , ErrorMessage = "{O} 必须在{ 1 }和 {2 }之间! ")] [ModelBinder(typeof(PararneterValidationModelBinder))] double operandl , [Display(Narne = "第 2 个操作数") ] [Range(10 , 20 , ErrorMessage = "{O} 必须在{1}和 {2 }之间! ")] [ModelBinder(typeof(PararneterValidationModelBinder))] double operand2) double result = 0.00; if (ModelState.IsValid) AS P. NET MVC 4 框架揭秘6.3 基于数据注解特性的 Model 验证 青 299 result = operandl + operand2; return View (new OperationData { Operandl = operandl, Operand2 = operand2, Operator = "Add" , Result = result }); public class OperationData [DisplayName(" 操作数 1") ] public double Operandl { get; set; } [DisplayName(" 操作数 2" 门 public double Operand2 { get; set; } [DisplayName("操作符") ] public string Operator { get; set; } [DisplayName("运算结果") ] public double Result { get; set; } 我们在 HomeCon位oller 中定义了一个进行加法运算的 Action 方法 Add ,传入的参数代 表两个操作数。我们在两个参数上应用了三个特性,其中 DisplayAttribute 特性用于设置显 示名称,验证特性 RangeAttribute 用于限制数值的范围( 10 到 20 之间) ,而 ModelB inderAttribute 则是为了让针对这两个参数的绑定采用我们自定义的 ParameterValidationModelB inder 来完成。在 Add 方法中我们进行相应的运算,将相关的信息 封装到一个 OperationData 对象中并作为 Model 呈现在默认的 View 中。 如下所示的是 Action 方法 Add 对应 View 的定义,这是一个 Model 类型为 OperationData 的强类型 View 。在该 View 中我们直接调用 HtmIHelper 的扩展方法 EditorForModel 将作为 Model 的 OperationData 对象以编辑模式呈现出来。 a ←」a nμ n o --←」a r e p o l> el dm ot mh a巴/飞 ValidationMessage @Html.EditorForModel() 为了让访问 Action 方法 Add 的请求能够直接将操作数放到请求的 URL 中,我们在默认 生成的 RouteConfig 类型中作了如下所示的路由注册。然后在 Globa1. asax 中通过下面的代码 对我们自定义的 ParameterValidationModelValidatorProvider 进行了注册,在进行注册之前需 要将现有的 ModelValidatorProvider 移除。 ASP. NET MVC 4 框架揭秘300 ‘ 第 6 章 Model 的验证 public class RouteConfig public static void RegisterRoutes(RouteCollection routes) { //其他操作 routes.MapRoute( name: "Add" , url: "{action}/{operandl}/{operand2}" , defaults: new { controller = "Home"} public class MvcApplication : System.Web.HttpApplication protected void Application Start() //其他操作 DataAn notationsModelValidatorProvider validatorProvider = ModelValidatorProviders.providers .OfType() .FirstOrDefault(); if (null != validatorProvider) ModelValidatorProviders.Providers.Remove(validatorProvider); ModelValidatorProviders.Providers.Add( new ParameterValidationModelValidatorProvider()); 现在运行我们的程序并在浏览器中指定相应的 URL 访问定义在 HomeController 的 Action 方法 Add 进行加法运算,如果提供不合法的操作数(比如" / Ad d/ 50/50' 勺,验证消息 将会以如图 6-14 所示的效果显示在相应的文本框旁边。 (8613 ) |? 「 干 口~.: " Valid 翩翩晦wll 唱斟Ilocalhosl丽 L ':l 2lfaàd 阻0/50 四百 ~、 l 操作数 1 医二一二;二…二]第 1 个操作数必须在 1 俯 20 之间! 操作数2 区; 一一一 二] 第 2个操作数必须在 1 昨日 20 之间! 操作符 国豆 ;二二二二二二= J 运算结果 æ=_二二-工 7 丁 图 6-14 通过应用在 Action 方法参数的验证特性进行 Model 验证 6.3.5 一种 Model 类型,多种验证规则 理想的 Model 验证的应该是场景驱动,而不是类型驱动的,因为同一个数据类型在不同 的使用场景中可能具有不同的验证规则。举个简单的例子,对于一个表示应聘者的数据对象 ASPNETMVC4 框架揭秘6.3 基于数据注解特性的 Model 验证 每 301 来说,应聘的岗位不同,肯定对应聘者的年龄、性别、专业技能等方面具有不同的要求。 但是 ASP.NETMVC 的 Model 验证恰恰是类型驱动的,因为验证规则是通过应用在数据 类型及其属性上的验证特性来定义的,这样的验证方式实际上限制了数据类型在基于不同验 证规则的场景中的重用。通过上面的扩展我们将验证特性直接应用在参数上变成了可能,这 在一定程度上解决了这个问题,但是也只能解决部分问题,因为应用到参数的验证特性只能 用于针对参数类型级别的验证,而不能用于针对参数类型属性级别的验证。 现在我们通过利用对 ASP.NET MVC 的扩展来实现一种基于不同验证规则的 Model 验 证。为了让读者对这种认证方式有一个直观的认识,我们先通过一个简单的实例来看看这个 扩展最终实现了怎样的验证效果。我们在一个 ASP.阳TMVC 应用中定义了如下一个 Person 类型。 n o s r e p-s s a 14 C C .工14 b u p{ [DisplayNarne(" 姓名"川 public string Narne { get; set; } [DisplayNarne(" 性别") ] public string Gender { get; set; } [DisplayNarne(" 年龄"门 编辑个人信息 @using (Html.BeginForm()) @Html.EditorForModel() 现在运行我们的程序,并通过在浏览器中指定相应的地址分别访问定义在 HomeController 的三个 Action (Index 、 Rulel 和 Rule2) ,一个用于编辑个人信息的表单会呈 现出来。然后我们根据三个 Action 方法采用的验证规则输入不合法的年龄,然后点击"保存" ASP. NET MVC 4 在架揭秘6.3 墓于数据注解特性的 Model 验证 电 303 按钮,我们会看到输入的年龄按照对应的规则被验证,具体的验证效果如图 6-15 所示。 (S614) 12 0 和 3 0 之间 ! 的制辅 1 0 和 :!07. 间 i ÷年龄必须宦 30和-1 0 之闹! 瑾E 图 6-15 针对不同规则的 Model 验证 我们现在就来具体谈谈上面这个例子所展示的基于不同规则的 Model 验证是如何实现 的。首先需要重建一套新的验证特性体系,因为我们需要指定具体的验证规则。定义了一个 具有如下定义的抽象 ValidatorAttribute 类型,它直接继承自 ValidationA ttribute f 其属性 RuleName 表示采用的验证规则名称。我们重写了 TypeId 属性,因为需要在相同的属性或者 类型上应用多个同类的 ValidatorAt位ibute 特性。 [AttributeUsage( AttributeTargets.Classl AttributeTargets.Property , AllowMultiple = true)] public abstract class ValidatorAttribute: ValidationAttribute private object typeldi public string RuleName { geti seti } public override object Typeld get{return typeld ?? (typeld = new object());} 上面演示实例中采用的Ran geValidatorA伽 ibute 特性定义如下,可以看到它仅仅是对 RangeAttribute 的封装。 Range ValidatorAttribute 具有与 RangeAt位 ibute 一致的构造函数定义, 并直接使用被封装的 RangeAt位 ibute 实施验证。除了能够通过 RuleName 指定具体采用的验 证规则之外,其他的使用方式与 RangeAttribute 完全一致。 [AttributeUsage( AttributeTargets.Property , AllowMultiple = true)] public class RangeValidatorAttribute:ValidatorAttribute private RangeAttribute rangeAttributei public RangeValidatorAttribute(int minimum , int maximum) rangeAttribute = new RangeAttribute(minimum , maximum)i ASP. NET MVC 4 握架揭秘304 • 第 6 章 Model 的验证 public RangeValidatorAttribute(double minimum , double maximum) rangeAttribute = new RangeAttribute(minimum , maximum)i public RangeValidatorAttribute (Type type , string minimum , string maximum) rangeAttribute = new RangeAttribute(type , minimum , maximum)i public override bool IsVal 工 d(object value) return rangeAttribute.IsValid(value)i public override string FormatErrorMessage(string name) return string.Format(Culturelnfo.CurrentCulture , base.ErrorMessageString , new object[] { name , rangeAttribute.Minimum , rangeAttribute.Maximum })i ValidatorAttribute 的 RuleName 属性仅仅指定了验证特性采用的验证规则名称,当前应 该采用的验证规则通过应用在 Action 方法或者 Controller 类型上的 Validatio nRuleAt位 ibute 特 性指定。如下所示的就是这个 Validatio nRuleAttribute 的定义,验证规则名称通过 RuleName 属性表示。 [AttributeUsage( AttributeTargets.Classl AttributeTargets.Method)] public class ValidationRuleAttribute: Attribute public string RuleName { geti private seti } public ValidationRuleAttribute(string ruleName) this.RuleName = ruleNamei 对于这个用于实现针对不同验证规则的扩展来说,其核心是如何将通过 Validatio nRuleAttribute 特性设置的验证规则应用到 ModelValidator 的提供机制中,使之筛选 出与当前验证规则匹配的验证特性。在这里我们依然使用 ControllerContext 来保存这个验证 规则名称。细心的读者应该留意到了上面演示实例中创建的 HomeController 不是继承自 Controller ,而是继承白 RuleBasedController ,这个自定义的 Con位 oller 基类定义如下: public class RuleBasedController: Controller private static Dictionary controllerDescriptors = new Dictionary()i public ControllerDescriptor ControllerDescriptor get ControllerDescriptor controllerDescriptori if (controllerDescriptors.TryGetValue(this.GetType() , out controllerDescriptor)) return controllerDescriptori AS P. NET MVC 4 框架揭秘6.3 基于数据注解特性的 Model 验证 蟹 305 lock (controllerDescriptors) if (!controllerDescriptors.TryGetValue(this.GetType() , out controllerDescriptor)) controllerDescriptor = new ReflectedControllerDescriptor(this.GetType()); controllerDescriptors.Add(this.GetType() , controllerDescriptor); return controllerDescriptor; protected overr 工 de IAsyncResult Beg 工 nExecuteCore(AsyncCallback callback , object state) SetValidationRule(); return base.Beg 工 nExecuteCore(callback , state); protected override void ExecuteCore() SetValidationRule(); base.ExecuteCore(); private void SetValidationRule() string actionName = this.ControllerContext.RouteData .GetRequiredString("action"); ActionDescriptor actionDescriptor = this.ControllerDescriptor . Fin dAction (this.ControllerContext , actionName); if (null != actionDescriptor) ValidationRuleAttr 工 bute val 工 dat 工 onRuleAttribute = actionDescriptor.GetCustomA ttributes(true) .OfType() .FirstOrDefault() ?? this.ControllerDescriptor.GetCustomA ttributes(true) .OfType() .FirstOrDefault() ?? new ValidationRuleAttribute(string.Empty); this.ControllerContext.RouteData.DataTokens.Add("ValidationRuleName" , validationRuleAttribute.RuleName); 在继承自 Controller 的 RuleBasedController 中, ExecuteCore 和 Begi nE xecuteCore 方法被 重写。在调用基类的同名方法之前,我们调用 SetValidatio nRule 方法提取应用在当前 Action 方法 /Controller 类型上的 Validatio nRuleAttribute 特性指定的验证规则名称,并将其保存到当 前 ControllerContext 中。由于针对 Validatio nRuleAttribute 特性的解析需要使用到用于描述 Controller 的 ControllerDescriptor 对象,出于性能考虑,我们对该对象进行了全局缓存。 对于应用在同-个属性或者类型上的多个基于不同验证规则的 ValidatorAttribute 特性, 对应的验证规则名称并没有应用到具体的验证逻辑中。以上面定义的 Range ValidatorA伽 ibute 为例,具体的验证逻辑通过被封装的 RangeAttribute 来实现,如果不做任何处理,基于不同 规则的 Range ValidatorAttribute 都将参与到最终的 Model 验证过程中。我们必须要做的是在 ASP. NET MVC 4 框架揭秘306 但第 6 章 Model 的验证 根据验证特性创建 ModelValidator 的时候只选择那些与当前验证规则一致的 ValidatorAttribute ,这样的操作实现在具有如下定义的 RuleBasedValidatorProvider 中。 public class RuleBasedValidatorProvider : DataAnnotationsModelValidatorProvider protected override IEnumerable GetValidators( ModelMetadata metadata , ControllerContext context , IEnumerable attributes) object validationRuleName = string.Empty; context.RouteData.DataTokens.TryGetValue("ValidationRuleName" , out validationRuleName); string ruleName = validationRuleName.ToString(); attributes = this.FilterAttributes(attr 工 butes , ruleName); return base.GetValidators(metadata , context , attributes); private IEnumerable FilterAttributes( IEnumerable attributes , string validationRule) var validatorAttributes = attributes. OfType () ; var nonValidatorAttributes = attributes. Except (validatorAttributes) ; List validValidatorAttributes = new List(); if (string.IsNullOrEmpty(validationRule)) validValidatorAttributes.AddRange(validatorAttributes.Where( v => string.IsNullOrEmpty(v.RuleName))); else var groups = from validator in validatorAttributes group validator by validator.GetType(); foreach (var group in groups) ValidatorAttribute validatorAttribute = group.Where( v => string.Compare(v.RuleName , validationRule , true) 0) .FirstOrDefault(); if (null != validatorAttribute) validValidatorAttributes.Add(validatorAttribute); else validatorAttribute = group.Where( v => string.IsNullOrErnpty(v.RuleName)) .FirstOrDefault(); if (null != validatorAttribute) validValidatorAttributes.Add(validatorAttribute); return nonValidatorAttr 工 butes.Union(val 工 dValidatorAttributes); AS P. NET MVC 4 框架揭秘6 .4 客户踹验证 • 307 如上面的代码所示, RuleBasedVa1i datorProvider 继承自 DataAnnotationsModelValidatorProvider , 基于当前验证规则(从当前的 ControllerContext 中提取)对 ValidatorAttribute 的筛选以及最 终对 ModelValidator 的创建通过重写的 GetValidators 方法实现。具体的筛选机制是:如果当 前的验证规则存在,则选择与之具有相同规则名称的第一个 Validator Attribute; 如果这样的 ValidatorAttribute 找不到,则选择第一个没有指定验证规则的 Validator Attribute; 如果当前的 验证规则没有指定,那么也选择第一个没有指定验证规则的 Validator A ttribute 。 我们需要在 Globa l. asax 中通过如下的方式对自定义的 RuleBasedValidatorProvider 进行 注册,然后我们的应用就能按照我们期望的方式根据指定的验证规则实施 Model 验证了。 public class MvcApplication : System.Web.HttpApplication //其他成员 protected void Application Start() //其他操作 DataAnnotationsModelValidatorProvider validator = ModelValidatorProviders.Providers .OfType() .FirstOrDefault(); if( 口 ull != validator) ModelValidatorProviders.Providers.Remove(validator)i ModelValidatorProviders.Providers.Add(new RuleBasedValidatorProvider()); 6.4 客户踹验证 之前我们一直讨论的 Model 验证仅限于服务端验证,即在 Web 服务器端根据相应的规 则对请求数据实施验证。如果我们能够在客户端(浏览器〉对用户输入的数据先进行验证, 这样会减少针对服务器请求的频率,从而缓解 Web 服务器访问压力。 ASP.MVC 2.0 及其之前 的版本采用 AS 卫 NETAjax 进行客户端验证,在 ASP.NET MVC 3.0 中引入了 jQuery 验证框架。 6.4.1 jQuery 验证 Unobtrusive JavaScript 已经成为了 JavaScript 编程的一个指导方针,但是到目前为止貌 似还没有一个针对它的确切定义,但是一些 Unobtrusive JavaScript 的基本原则却已经被广泛 地接受。 Unobtrusive JavaScr悄体现了一种被称为"渐进式增强( PE , Progressive Enhancement) "的 Web 设计模式,它采用分层的方式实现了 Web 页面内容与功能的分离。 用于实现某种功能的 JavaScript 不再内嵌于用于展现内容的 HTML 中,而是作为独立的层次 建立在 HTML 之上。 我们就以验证为例,假设一个 Web 页面中具有如下一个表单,需要对针对表单中三个 ASP. NET MVC 4 框架揭秘308 黯 第 6 章 Model 的验证 文本框 (foo 、 bar 和 baz) 的输入进行验证。假设具体的验证操作实现在 validate 函数中,那 么我们可以采用如下的 HTML 使相应的文本框在失去焦点的时候对输入的数据实施验证。
    但这不是一个好的设计,理想的方式是让 HTML 只用于定义内容呈现的结构,让 CSS 控制内容呈现的样式,而所有功能的实现定义在独立的 JavaScript 中,所以用于实现验证对 JavaScript 的调用不应该以内联的方式出现在 HTML 中。按照 Unobtrusive JavaScript 的编程 方式,我们应该将以内联方式实现的事件注册 (onblur="validateO" )替换成如下的形式。
    编辑个人信息
    姓名:
    出生日期: <土口put class="required date" id="birhthDate" name="birhthDate" type="text"/>
    Blog 地,.tJl::
    Email 地址:
    我们需要将两个必要的 .js 文件包含进来,一个是 jQuery 的核心文件 jquery-l 工l.js ,另 一个是实现验证的 j query. validate.j s 。整个 HTML 文件的主体部分是一个表单,可以通过其 ASP. NET MVC 4 在架揭秘310 由第 6 章 Model 的验证 中的文本框输入一些个人信息(姓名、出生日期、 Blog 地址和 Email 地址),最后点击"保 存"按钮对输入的数据进行提交。 对于这四个文本框对应的 元素来说,其 class 属性在这里被用于进行验证规则的 定义。其中 required 表示对应的数据是必需的,而 date 、川和 email 则对输入数据的格式进 行验证以确保是一个合法的日期、 URL 和 Email 地址。真正对输入实施验证体现在如下一段 JavaScr悄调用中,在这里我们仅仅是调用
    元素的 validate 方法而己。 现在运行我们的程序,一个用于提交个人信息的页面会被呈现出来。当我们输入不合法 的数据时(第一次验证发生在提交表单时,之后的验证会在被验证表单元素失去焦点时触 发),对应的验证将会自动被触发,而预定义的错误消息将会显示在被验证表单元素的右侧。 具体的显示效果如图 6-16 所示。 (S615) 攘攘帮 1286~ 堕 姓名: I 出生日期 I [在 Blo g地址 1 23 This field ìs requìred P lea5e en t er a 飞划id date Pl ", ase en 阳 avclid 'C衣上 Emaiti也址 g 应 -51 - l P lea,e e nt er a 飞划id cmail address 理豆 图 6-16 jQue 叩验证及其默认错误消息的呈现 单独指定验证规则和错误消息 验证规则其实可以不用以内联的方式定义在被验证表单元素对应的 HTML 中,可以直 接将它们定义在用于实施验证的 validate 方法中,该方法不仅仅可以指定表单被验证的输入 元素对应的验证规则,还可以指定验证消息,以及控制其他验证行为。 现在我们将上面演示实例中的 View 的 HTML 进行相应的修改,将包含在表单中的四个 文本框通过 class 属性设置的验证规则移除。然后在调用表单的 validate 方法实施验证的时候 按照如下的方式手工地为被验证输入元素指定相应的验证规则和错误消息。验证规则和错误 消息与验证元素之间是通过 name 属性(不是 id 属性)进行关联的。 再次运行我们的程序后发现,定制的错误消息就会按照如图 6-17 所示的效果呈现出 来。 (S616) E雪胃胃-曹 姓名 'L二二二工二二二1请输入姓名 出生日期= 应二;二二二~]请输入一个合法的日期 Blogt也址. @J_ ~请输入二个合法的 U虹 Email地址= 匾育工 一二 l 请输入一个合法的Emaittll!~lI: 瑾蜀 图 6-17 jQuery 验证及其定制错误消息的呈现 6.4.2 基于 jQuery 的 Model 验证 在简单了解了 Unobtrusive JavaScript 形式的验证在 jQuery 中的实现之后,我们来具体讨 论 ASP.NET MVC 是如何利用它实现客户端验证的。服务端验证最终实现在相应的 ModelValidator 中,而最终的验证规则定义在相应的 ValidationAt位ibute 中,而客户端验证规 则通过 Htm旧elper相应的模板方法(比如 TextBoxFor 、 EditorFor 和 EditorForModel 等〉输出到生成的 HTh伍中。服务端验证和客户端验证必须采用相同的验证规则,通过应 用 ValidationAttribute 特性定义的验证规则也同样体现在基于客户端验证规则的 HTML 上。 ValidationAttribute 与 HTML AS卫NETMVC 默认采用基于验证特性的声明式验证,服务端验证最终实现在两个重写 ASP. NET MVC 4 在架揭秘312 黯 第 6 章 Model 的验证 的 IsValid 方法中。对于客户端验证, ASP.NET MVC 对 jQuery 的验证插件进行了扩展,它 将验证规则以表单元素属性的方式输出到最终生成的 HTML 中。为了让客户端和服务端采 用相同的验证规则,应用在 Model 类型某个属性上的 Validatio nA t位ibute 特性最终会反映在 被验证表单元素对应的 HTI\在L 上。 ←」c a 卡」n o c s s a 14 C C .-14 b u p{ [DisplayName(" 姓名"门 [Required(ErrorMessage ="请输入 {O}!")] [StringLength 仰, ErrorMessage="作为 {O} 字符串长皮不能超过 {1}!")] public string Name { get; set; } [DisplayName(" 电子邮箱地址") ] [RegularExpression(@"^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2 , 3}$" , ErrorMessage=" 请输入正确的电子邮箱地址! ")] public string EmailAddress { get; set; } 假设我们具有如上一个数据类型 Contact , Require dA伽 ibute 和 StringL engthAt创 bute 特 性应用到表示姓名的 Name 属性上用于确保用户必须输入一个不超过 128 个字符的字符串, 而表示 Email 地址的 EmaiIAddress 属性应用了一个 RegularExpressio nAt位 ibute 用于确保用户 输入一个合法的 Email 地址。在一个以此 Contact 为 Model 类型的 View 中,如果我们调用 HtmIHelper 的扩展方法 EditorF orModel 最终会生成如下一段 HTML 。
    AS P. NET MVC 4 框架揭秘6 .4客户踹验证 霞 313 通过上面的这段 HTML 我们可以看到, Contact 的两个属性对应的 元素具有一个 "data-val" 属性和一系列以"data-val-"为前缀的属性,前者表示是否需要对用户输入的值 进行验证,后者则代表相应的验证规则。具体来说,去除 "data-val- "前缀后的属性名称就 是 jQuery 验证规则名称。 一般来说,一个 ValidationAttribute 对应着一种验证类型和一系列可选的验证参数。比 如 RequiredAttribute 、 S创ngLengthAttribute 和 RegularExpressionAttribute 对应的验证类型分 别是 "required" 、 "len拼1" 和 "regex" ,而 S位ingLengthA忧ribute 和 RegularExpressionAttribute 各自具有一个验证参数 length-max (表示允许的字符串最大长度)和 regex-pa忧em (正则表 达式)。验证错误消息一般作为验证类型属性的值,而验证参数对应的属性值自然就是相应 的参数值。 对于上面生成的 HTML 还有一点值得一提,对应着被验证属性的 元素会紧跟一 个 元素用于显示验证失败后的错误消息。该 元素的 CSS 类型为 " field-validation-valid" ,当验证失败后被替换为" field-validation-error" ,可以通过它来定制 错误消息的显示样式。 客户踹验证规则的生成 ASP.NETMVC 在利用 jQuery 进行客户端验证的时候,虽然验证规则并没有采用其原生 的方式通过被验证元素的 class 属性来提供,但是却可以通过 "data-val-{rulename} "的命名 模式提取相应的验证规则属性值,并最终得到对应的验证规则, ASP.阳TMVC 只需要对此 作简单的"适配"即可。我们现在关心的是当调用定义在 HtmlHelper 中相应的模 板方法将 Model 对象的某不属'性以表单元素的形式进行呈现的时候, ASP.NET MVC 是如何 生成这些以 "data-val- "为前缀的验证属性的呢? 在这里我们需要涉及到一个表示客户端验证规则的 ModelClientValidationRule 类型。如 下面的代码所示, ModelClientValidationRule 具有三个属性,字符串属性 ErrorMessage 和 ValidationType 表示验证错误消息和验证的类型,类型为 IDictionary的只读属 性 ValidationParameters 表示辅助客户端验证的参数,其中 Key 和 Value 分别表示验证参数名 和参数值。 public class ModelClientValidationRule public string ErrorMessage { get; set; } public string ValidationType { get; set; } public IDictionary ValidationParameters { get; } public abstract class ModelValidator //其他成员 、 public virtual IEnumerable ASP. NET MVC 4 在架揭秘314 • 第 6 章 Model 的验证 GetClientValidationRules(); public abstract IEnumerable Validate( object container); 通过前面的介绍我们知道,抽象类 ModelValidator 中具有一个虚方法 GetClientValidationRules ,它用于创建一个 ModelClientValidationRule 对象的列表,所有支持 客户端验证的 ModelValidator 必须重写该方法以生成相应的客户端验证规则。 以用于进行范围验证的 RangeAttribute 特性对应的 RangeAttributeAdapter 为例,如下面 的代码片段所示,它重写了 GetClientValidationRules ,返回的 ModelClientValidationRule 列表 中包含一个 ModelClientValidationRangeRule 对象。该 ModelClientValidationRangeRule 对象 的验证类型为 "range" ,采用 RangeAttributeAdapter 的 ErrorMessage 属性作为自身的错误消 息。作为验证范围的上、下限的两个属性( Maximum 和 Minimum) 成为了该 Model Client ValidationRule 的两个验证参数,参数名分别为 "max" 和 "min" 。 public class RangeAttributeAdapter : DataAnnotationsModelValidator //其他成员 public override IEnumerable GetClientValidationRules() string errorMessage = base.ErrorMessage; return new ModelCl 工 ent飞TalidationRangeRule[] { new ModelClientValidationRangeRule(errorMessage, base.Attribute.Minimum, base.Attribute.Maximum) }; public class ModelClientValidationRangeRule : ModelClientValidationRule public ModelClientValidationRangeRule(string errorMessage, object minValue, object maxValue) base.ErrorMessage = errorMessage; base.ValidationType = "range"; base.ValidationParameters["min"] = minValue; base.ValidationParameters["max"] = maxValue; 客户端验证在这里还涉及到一个重要的接口 System. Web.Mvc .I ClientValidatable ,它具有 唯一的 GetClientValidationRules 方法来返回一个以 System. Web.Mvc.ModelClientValidationRule 对象表示的客户端验证规则列表。 public interface IClientValidatable IEnumerable GetClientValidationRules( ModelMetadata metadata, ControllerContext context); 所有支持客户端验证的 ValidationAttribute 都需要实现 IClientValidatable 接口并通过实现 ASP. NET MVC 4 在架揭秘6.4 窑户踹验证 ;自 315 GetClientValidationRules 方法提供对应的验证规则,而生成的验证规则需要与通过重写的 IsValid 方法实现的服务端验证逻辑一致。 DataAnnotationsModelValidator 重写了 GetClientValidationRules 方法,如果对应的 ValidationA伽ibute 实现了 IClientValidatable 接口, 它( ValidationAttribute )的 GetClientValidationRules 方法被调用并将返回的 ModelClientValidationRule 列表作为该方法的返回值。 当我们在某个 View 中调用 HtmIHelper 的模板方法将 Model 对象的某个属性以 表单元素呈现出来的时候,它会采用我们前面介绍的 ModelValidator 的提供机制根据目标属 性对应的 ModelMetadata 创建相应的 ModelValidator ,然后调用 GetClientValidationRules 方法 得到一组表示客户端验证规则的 ModelClientValidationRule 列表。如果该列表不为空,它们 将作为验证属性附加到目标属性对应的 :7G素中。 6.4.3 自定义验证 虽然在命名空间 System.ComponentModel.DataAnnotations 中具有一系列继承自 ValidationAttribute 的验证特性可以帮助我们完成基本的数据验证,但是在很多场景下的验证 需要按照我们自定义的方式来进行,接下来以实例演示的方式来指导读者如何以自定义的方 式实现服务端和客户端验证。 假设需要对表示出生日期的输入数据进行验证以确保其年龄在允许的范围内,为此我们 创建了一个自定义的验证特性 AgeRangeAttribute 。如下面的代码片段所示, AgeRangeAt位ibute 应用在表示出生日期的属性上并且指定允许年龄范围 08-25) 的上下限。 n o s r e p-s s a --C C .L 吨 i占 'D U P-{ [DisplayNarne(" 姓名") ] public string Narne { get; set; } [DisplayNarne(" 出生日期") ] [AgeRange(18 , 25 , ErrorMessage=" 年玲必须在 {O }到{ 1 }周岁之间! ")] [DisplayForrnat(ApplyForrnat 工 nEditMode = true, DataForrnatString = "{ü:yyyy-MM-dd}")] publid DateTirne? BirthDate { get; set; } 为简单起见,我们直接让 AgeRangeAttribute 继承自 RangeAttribute 。如下面的代码片段 所示,我们在构造函数中以整数的形式指定表示年龄范围的下限和上限。服务端验证体现在 重写的 IsValid 方法中,而为了让格式化的错误消息是针对年龄而不是针对出生日期,我们 重写了 F ormatErrorMessage 方法。 [AttributeUsage( AttributeTargets.Property)] public class AgeRangeAttribute: RangeAttribute, IClientValidatable public AgeRangeAttribute(int rninirnurn, int rnaxirnurn) base(rninirnurn, rnaxirnurn) ASP. NET MVC 4 在架揭秘316 施第 6 章 Model 的验证 { } public override bool IsValid{object value) DateTime? birthDate = value as DateTime?; if (null == birthDate) return true; DateTime age = new DateTime{DateTime.Today.Ticks _ birthDate.Value.Ticks); return (int) this . Minimum <= age. Year && age. Year <= (int) this . Maximum; public override string FormatErrorMessage{string name) { return string.Format{Culturelnfo.CurrentCulture , this.ErrorMessageString , this.Minimum , this.Maximum); public IEnumerable GetClientValidationRules{ ModelMetadata metadata , ControllerContext context) string errorMessage = FormatErrorMessage{""); ModelClientValidationRule rule = new ModelClientValidationRule {ValidationType = "agerange" , ErrorMessage = errorMessage }; rule.ValidationParameters.Add{ 叫 ninage" , this . Minimum) ; rule.ValidationParameters.Add{"maxage" , this.Maximum); yield return rule; AgeRangeAttribute 实现了 IClientValidatable 接口,在 GetClientValidatio nRules 方法中返 回一个验证类型为" ag 町ange" 的 ModelClientValidatio nRule 对象。它具有两个验证参数 Cminage 和 maxage) 分别表示年龄范围的下限和上限。通过调用 F ormatE rrorMessage 方法 格式化后生成的消息成为了该 ModelClientValidationRule 的错误消息。 ModelClientValidatio nRule 对象的 ValidationType 属性值最终表示 jQuery 验证插件的验证 规则,而该规则通过一个对应的函数来实施验证。对于 AgeRangeAttribute 来说,其对应的 客户端验证类型为" agerange" ,为此我们在一个扣文件中以此命名注册的验证函数,具体 的定义如下所示。 jQuery.validator.addM ethod{"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 =口 ew Date{birthDateArray[O] , birthDateArray[l] , birthDateArray[2]); var currentDate = new Date{); var age = currentDate.getFullYear{) _ birthDate.getFullYear{); return age >= minAge && age <= maxAge; AS P. NET MVC 4 在架揭秘6 .4客户踹验证 • 317 ., ) 1,J jQuery.validator.unobtrusive.adapters.add("agerange" , ["minage" , "maxage"] , function (options) { options.rules["agerange"] = { minage: opt 工 ons.params.mlnage , maxage: options.params.maxage } ; optio 口 s.messages["agerange"] = options.message; } ) ; 如上面的代码片段所示,我们调用全局对象 jQuery 的 validator 属性的 Ad dM ethod 方法 添加了一个用于进行年龄范围验证的函数,该函数对应的验证规则为" agerange" 。验证函数 具有三个参数,其中 value 和 element 分别代表被验证的数据值(表示出生日期的字符串〉 和 HTML 元素,而 p 町ams 参数则表示传入的验证参数〈表示年龄范围的上、下限)。最后 我们调用 jQuery. validator. unobtrusive.adapters 的 add 方法对验证规则 ag町 ange 进行注册,并 指定对应的验证参数名称列表。 我们在一个 ASP.NETMVC 应用中定义了以前面定义的 Person 类型为 Model 的Vi ew 。 如下面的代码片段所示,我们调用 HtmIHelper 的扩展方法 EditorF orModel 将作为 Model 的 Person 对象以编辑模式呈现在一个表单之中,通过 标签引用了四个 .js 文件, 前三个是 ASP.NET MVC 原生脚本,最后一个是我们自定义的脚本文件,上面定义的 "agerange" 相关的扩展就定义在这个文件中。 @model Person 编辑个人信息 @using(Html.BeginForm()) @Html.EditorForModel() 运行程序并在浏览器中将该 View 呈现出来,在输入违反年龄限制的出生日期的时候, 客户端验证将会生效并以图 6-18 所示的形式显示出错误消息。 ASP. NET MVC 4 框架揭秘318 电第 6 章 Model 的验证 ; 于7I 姓名 出生日期 |1981 -Ø8- 24 |年舱必须在 18到 25 周岁之阳! 1.# 1 图 6-18 自定义验证对错误消息的呈现 如果我们查看最终生成出来的 HTML ,会发现表示出生日期的文本框具有如下所示的定 义,高亮显示的三个以 "data-val- "为前缀的属性内容正是根据 AgeRan geAttribute 特性的 GetClientValidatio nRules 方法返回的 ModelClientValidatio nRule 对象生成的。另一个名为 "data-val-date" 的属性是默认添加的针对日期格式的验证规则,由于没有引用相应的本地 化 JavaScript 文件,错误消息被默认设置为英文。 本章小结 Model 验证旨在为通过 Model 绑定生成的参数进行检验以确保用户输入数据的有效 性。 ModelValidator 是整个 Model 验证系统的核心, ASP.NET MVC 定义了 Dat aAnn otationsModelValidator 、 DataE rro rI nfoModelValidator 和 ValidatableObjec tA dapter 三种 ModelValidator 类型,它们体现了实现 Model 验证的三种解决方案。每一种 ModelValidator 均由对应的 ModelValidatorProvider 来提供,而自定义的 ModelValidatorProvider 通过静态类 型 ModelValidatorProviders 进行注册。 Model 验证是伴随着 Model 绑定进行的,而且两者最终都涉及到对 ModelState 的操作, Model 绑定将 ValueProvider 提供的数据写入 ModelState 中,而 Model 验证则将验证后的错 误消息保存到 ModelState 中。正是由于验证的结构最终体现在当前的 ModelState 中,可以利 用 HtmIHelperlHtm lH elper 相应的扩展方法将相应的验证错误消息呈现在 View 中。 基于验证特性的声明式 Model 验证通过 Dat aAnn otationsModelValidator 来实现。在默认 的情况下,只有应用在自定义容器类型及其属性上的验证特性才会生效,不过我们可以通过 扩展将验证特性直接应用到 Action 方法的参数上。同样是利用 Model 验证系统的扩展,我 们实现了利用验证特性在同一个数据类型上定义不同的验证规则。 ASP. NET MVC 4 框架揭秘本章小结 • 319 除了服务端验证, ASP.NET MVC 还支持针对 ASP.NET Aj 缸和 jQuery 的客户端验证。 客户端验证规则通过 ModelClientValidationRule 对象表示, AS卫NET MVC 利用调用 ModelValidator 的 GetClientValidationRules 方法获得的 ModelClientValidationRule 列表辅助 HtmlHelper 的模板方法生成与验证相关的 HTML 。需要进行客户端验证的自定义 ValidationAttribute 可以通过实现 IClientValidatable 接口的方式来提供定义了验证规则的 ModelClientValidationRule 列表。 ASP. NET MVC 4 框架揭秘第 7 章 Action 的执行 对于一个 ASP.NET MVC 应用来说,一个请求的目标通常是定义在某个 Con仕o l1 er 中的 Action 方法,所以对请求的处理最终体现在对目标 Action 方 法的执行。本章讨论的 "Action 的执行"不仅仅包括 Action 方法本身的执行, 还包括相关筛选器的执行。 ASP. NET MVC 4 框架揭秘7.1 异步 Action 的定义 \部 321 7.1 异步 Action 的定义 ASP.NET MVC 3 中的异步 Action 通过两个匹配的方法XxxA sync lX xxCompleted 来定 义,这样的异步 Action 只能定义在继承自 AsyncCon位 oller 的类型中。 AS 卫 NETMVC4 提供 了一种更加简洁的异步 Action 定义方式,不过为了兼容前一版本,传统的定义方式和 AsyncController 类型依然被保留下来。为什么我们需要让 Action 方法异步地执行呢?要回答 这个问题,这就需要了解 ASP.NET 基于线程池 C Thread PooD 的请求处理机制。 7.1.1 基于线程池的请求处理机制 ASP.NET 通过线程池的机制处理并发的 HTTP 请求。一个 Web 应用内部维护着一个线 程池,当探测到抵达的请求后, ASP.NET 会从池中获取一个空闲的线程来处理它。处理完毕 后,线程不会被回收,而是重新释放到池中。线程池具有一个最大容量,如果创建的线程达 到这个上限并且现有的线程均被处于"忙碌"状态,新的 HTTP 请求会被放入一个请求队列 以等待某个线程重新释放到池中。 我们将这些用于处理 HTTP 请求的线程称为工作线程 CWorkerηrread) ,而这个线程池 自然就叫做工作线程池。 ASP. 阳T 这种基于线程池的请求处理机制主要具有如下两个优势。 • 工作线程的重用:创建线程的成本虽然不如进程的激活,却也不是一件一蹦而就的事情, 频繁地创建和释放线程会对性能造成极大的损害。线程池机制避免了总是创建新的工作 线程来处理每一个请求,被创建的工作线程得到了极大地重用,并最终提高了服务器的 吞吐能力。 • 工作线程数量的限制:资源的有限性决定了服务器处理请求的能力具有一个上限,或者 说某台服务器能够处理的请求并发量具有一个临界点,一旦超过这个临界点,整个服务 器将会因不能提供足够的资源而崩溃。由于采用了对工作线程数量具有良好控制的线程 池机制, ASP.阳 T 并发处理的请求数量不可能超过线程池的最大允许的容量,从而避 免了在高并发情况下工作线程的无限制创建而最终导致整个服务器的崩溃。 如果请求处理操作耗时较短,工作线程处理完毕后可以及时地被释放到线程池中以用于 对下一个请求的处理。但是对于比较耗时的操作来说,意味着工作线程将长时间被某个请求 独占,如果这样的操作访问比较频繁,在高并发的情况下在线程池中可能找不到空闲的工作 线程用于及时处理最新抵达的请求。 如果采用异步的方式来处理这样的耗时请求,工作线程可以让后台线程来接手,而自己 可以及时地被释放到线程池中用于进行后续请求的处理,从而提高了整个服务器的吞吐能 力。值得一提的是,异步操作主要用于I1 0 绑定操作(比如数据库访问和远程服务调用等), 而非 CPU 绑定操作,因为异步操作对整体性能的提升来源于当 1/0 设备在处理某个任务的 ASP. NET MVC 4 框架揭秘322 铲寄自 第 7 章 Action 的执行 时候, CPU 可以释放出来去处理另一个任务。如果耗时操作主要依赖于本机 CPU 的运算, 采用异步方法反而会因为线程调度和线程上下文的切换而影响整体的性能。 7.1.2 两种异步 Action 万法的定义 在了解了异步 Action 的必要性之后,我们来简单介绍一下异步 Action 的定义方式。总 的来说,异步 Action 方法具有两种定义方式,一种是将其定义成两个匹配的方法 X xxA synclX xxCompleted ,另一种则是定义一个返回类型为 Task 的方法。 X xxA sync/XxxCompleted 如果使用两个匹配的方法 X xxA synclX xxCompleted 来定义异步 Action ,可以将异步操作 实现在 X xxA sync 方法中,而将最终内容的响应实现在Xx xCompleted 方法中。 XxxCompleted 可以看成是对 X双Async 方法的回调,当定义在 X xxA sync 方法中的操作以异步方式完成执 行后, XxxCompleted 方法会被自动调用。Xx xCompleted 的定义方式和普通的同步 Action 方 法比较类似。 作为演示,笔者在如下所示的 HomeCor由 oller 中定义了一个名为Art icle 的异步操作来 显示指定名称的文章内容。我们将内容的异步读取定义在Art icleAsync 方法中, Art icleCompleted 方法负责将读取的内容以 ContentResult 的形式呈现出来。 CS70 1) public class HorneController : AsyncController public void ArticleAsync(string narne) AsyncManager.OutstandingOperat 工 ons. 工 ncrernent()i Task.Factory.StartNew(() => } ) i string path = ControllerContext.HttpCoηtext.Server .MapPath(string.Forrnat(@"\articles\{O}.htrnl" , narne))i using (StrearnReader reader = new StrearnReader(path)) AsyncManager.Pararneters["content"] = reader.ReadToEnd()i AsyncManager.OutstandingOperations.Decrernent()i public ActionResult ArticleCornpleted(string content) return Content(content)i 对于以XxxA synclX xxCompleted 形式定义的异步 Action 方法来说, ASP.NET MVC 并不 会以异步的方式来调用XxxA sync 方法,所以我们需要在该方法中自行实现异步。在上面定 义的Art icleAsync 方法中,我们是通过基于 Task 的并行编程方式来实现对文章内容的异步 ASP. NET MVC 4 症架揭秘7.1 异步 Action 的定义 霞 323 读取的。 当我们以 Xx xA sync/XxxCompleted 形式定义异步 Action 方法的时候,会频繁地使用到 Controller 的 AsyncManager I属性,该属性返回一个类型为 S句y严S蜘t臼em 对象,将在下面一节对其进行单独讲述。 在上面提供的实例中,我们在异步操作开始和结束的时候调用了 AsyncManager 的 OutstandingOperations 属性的 Increment 和 Decrement 方法对 ASP.NETMVC 发起异步操作开 始和结束的通知。我们还利用 AsyncManager 的 Parameters 属性表示的字典来保存传递给 ArticleCompleted 方法的参数,参数在字典中的对应的 Key (" content " )可以与 Art icleCompleted 的参数名称自动匹配,所以在调用方法 ArticleCompleted 的时候,通过 AsyncManager 的 Parameters 属性指定的对象将自动作为对应的参数值。 Task 返回值 如果采用上面的异步 Action 定义方式,意味着我们不得不为一个 Action 定义两个方法, 实际上可以通过一个方法来完成对异步 Action 的定义,那就是让 Action 方法返回一个代表 异步操作的 Task 对象。除此之外,以 Xx xA sync lX xxCompleted 形式定义的异步 Action 只能 出现在继承自 AsyncController 的类型中,而针对 Task 返回值的异步 Action 则对此没有限制。 实际上保留 AsyncController 这个抽象类主要是为了实现对 AS 卫 NETMVC 3 的向后兼容。上 面通过 XxxA sync lX xxCompleted 形式定义的异步 Action 可以采用如下的定义方式。 (S702) public class HomeController : Controller public Task Article(string name) return Task.Factory.StartNew(() => string path = ControllerContext.HttpContext.Server .MapPath(string;Format(@"\articles\{O}.html" , name)); using (StreamReader reader = new StreamReader(path)) AsyncManager.Parameters["content"] = reader.ReadToEnd(); }) .ContinueWith (task => } ) ; string co 口 tent = (string)AsyncManager.Parameters["co 口 te 口 t"] ; return Content(content); 上面定义的异步 Action 方法Art icle 返回一个 Task 对象,异步文件内容的 读取体现在返回的 Task 对象中。对文件内容呈现的回调操作则通过调用该 Task 对象的 Continue With方法进行注册,该操作会在异步操作完成之后被自动调用。 如上面的代码片段所示,我们依然利用 AsyncManager 的 Parameters 属性实现参数在异 步操作和回调操作之间的传递。其实我们也可以使用 Task 对象的 Result 属性来实现相同的 ASP. NET MVC 4 框架揭秘324 省第 7 章 Action 的执行 功能, Article 方法的定义也改写成如下的形式。 public class HomeController : AsyncController public Task Article(string name) return Task.Factory.StartNew(() => string path = ControllerContext.HttpContext.Server . MapPath(string.Format( 日 "\articles\{O}.html" , name)); using (StreamReader reader = new StreamReader(path)) return reader.ReadToEnd() ; }) .ContinueWith (task => return Content((strinq)task.Result); } ) ; 7.1.3 AsyncManager 在上面演示的异步 Action 的定义中,我们通过 AsyncManager 实现了两个基本的功能, 即在异步操作和回调操作之间传递参数和向 ASP.NET MVC 发送异步操作开始和结束的通 知。由于 AsyncManager 在异步 Action 场景中具有重要的作用,我们有必要对其进行单独介 绍,如下所示是 AsyncManager 的定义。 r e q a n a M c n y S A S s a --C C .工1-b u p{ public AsyncManager(); public AsyncManager(SynchronizationContext syncContext); public EventHandler Finished; public virtual void Finish(); public virtual void Sync(Action action); public OperationCounter OutstandingOperations { get; } public IDictionary Parameters { get; } public int Timeout { get; set; } public sealed class OperationCou口 ter public event EventHandler Completed; public int Increment(); public int Increment(int value); public int Decrement(); public int Decrement(int value); public int Count { get; } ASP. NET MVC 4 在架揭翻7.1 异步 Action 的定义 霞 325 如上面的代码片段所示, AsyncManager 具有两个构造函数重载,非默认构造函数接受 一个表示同步上下文的 SynchronizationContext 对象作为参数。如果指定的同步上下文对象 为 Nu11 ,并且当前的同步上下文(通过 SynchronizationContext 的静态属性 Current 表示)存 在,则使用当前上下文,否则创建一个新的同步上下文。该同步上下文用于 Sync 方法的执 行,也就是说在该方法指定的 Action 委托将会在该同步上下文中以同步的方式执行。 AsyncManager 的核心是通过属性 OutstandingOperations 表示的正在进行的异步操作计 数器,该属性是一个类型为 System. Web.Mvc.Async. OperationCounter 的对象。操作计数通过 只读属性 Count 表示,当我们开始和完成异步操作的时候分别调用 Increment 和 Decrement 方法作增加和减少计数操作。 Increment 和 Decrement 方法各自具有两个重载,作为整数参数 value (该参数值可以是负数〉表示增加或者减少的数值,如果调用无参方法,增加或者减少 的数值为 1 。如果我们需要同时执行多个异步操作,则可以通过如下的方法来操作计数器。 AsyncManager.OutstandingOperations.Increment(3); Task.Factory.StartNew(() => //异步操作 1 AsyncManager.OutstandingOperations.Decrement(); } ) ; Task.Factory.StartNew(() => //异步操作 2 AsyncManager.OutstandingOperations.Decrement(); } ) ; Task.Factory.StartNew(() => //异步操作 3 AsyncManager.OutstandingOperations.Decrement(); } ) ; 对于每次通过 Increment 和 Decrement 方法调用引起的计数数值的改变, OperationCounter 对象都会检验当前计数数值是否为零,为零则表明所有的操作运行完毕,在这种情况下 Completed 事件会被触发。值得一提的是,表明所有操作完成执行的标志是计数器的值等于 零,而不是小于零,如果我们通过调用 Increment 和 Decrement 方法使计数器的值成为一个 负数,注册的 Completed 事件是不会被执行的。 AsyncManager 在初始化时就注册了 OperationCounter 对象(对应 OutstandingOperations 属性)的 Completed 事件,使该事件触发的时候调用自身的 Finish 方法。实现在 AsyncManager 的虚方法 Finish 又会触发自身的 Finished 事件。也就是说, Finished 事件最终会在异步操作 计算器变为零时被触发。 如下面的代码片段所示, CωO∞n附1世往ω011险e町r 类实现了 S句y严S蜘t恒em 接口,而后者定义了一个只读属性 AsyncManager 用于提供辅助执行异步 Action 的 AsyncManager 对象。我们在定义异步 Action 方法时使用的 AsyncManager 对象就是从抽象类 Contro11er 中继承下来的 AsyncManager 属性值。 ASPNETMVC4 在架揭秘326 • 第 7 章 Action 的执行 public abstract class Controller : ControllerBase , IAsy 口 cMa 口 agerContainer , ... { public AsyncManager AsyncManager { get; } public interface IAsyncManagerContainer AsyncManager AsyncManager { get; } XxxCompleted 万法的执行 对于通过 Xx xA sync lX xxCompleted 形式定义的异步 Action ,回调操作 XxxCompleted 会 在定义于 Xx xA sync 方法中的异步操作执行结束之后被自动调用,那么 XxxCompleted 方法 具体是如何被执行的呢? 通过下一节的介绍我们将会知道异步 Action 的执行最终是通过描述它的 AsyncActio nD escriptor 对象的 Begi nE xecute/EndExecute 方法来完成的。通过第 5 章 "Model 的绑定"的介绍我们知道以XxxA synclX xxCompleted 形式定义的异步 Action 是通过一个 Reflecte dA syncActio nD escriptor 对象来表示的,它在执行 Beg inE xecute 方法的时候会注册 Con仕 oller 对象的 AsyncManager 的 Finished 事件,让该事件触发的时候去执行 Completed 方法。 也就是说 AsyncManager 的 Finished 事件的触发标志着异步操作的结束,而此时匹配的 Completed 方法会被执行。由于 AsyncManager 的 Finish 方法会主动触发该事件,所以我们 也可以通过调用该方法促使 Completed 方法立即执行。由于 AsyncManager 的 OperationCounter 对象的 Completed 事件触发的时候会调用 Finish 方法,所以当表示当前正 在执行的异步操作计算器的值为零时, Completed 方法也会自动被执行。 如果我们在 Xx xA sync 方法中通过如下的方式同时执行三个异步操作,并在每个操作完 成之后调用 AsyncManager 的 Finish 方法,意味着最先完成的异步操作会导致Xxx Completed 方法的执行。换句话说,当Xxx Completed 方法执行的时候,可能还有两个异步操作正在执行。 AsyncManager.OutstandingOperations.Increment(3); Task.Factory.StartNew(() => //异步操作 1 Asyn dManager.Finish(); } ) ; Task.Factory.StartNew(() => //异步操作 2 Asyn dManager.Finish(); } ) ; Task.Factory.StartNew(() => //异步操作 3 Asyn dManager.Finish(); } ) ; ASP. NET MVC 4 框架揭秘7.1 异步 Action 的定义 黯 327 如果完全通过异步操作计数机制来控制Xxx Completed 方法的执行,由于计数的检测和 Completed 事件的触发只发生在 OperationCounter 的 lncremenV1) ecrement 方法被执行的时候, 倘若我们在开始和结束异步操作的时候都没有调用这两个方法, Xxx Completed 是否会执行 呢?同样以之前定义的用于读取/显示文章内容的异步 Action 为例,按照如下的方式将定义 在Art icleAsync 方法中针对 AsyncManager 的 OutstandingOperations 属性的 Increment 和 Decrement 方法调用注释掉, Art icleCompleted 方法是否还能正常运行呢? public class HomeController : AsyncController public void ArticleAsync(string name) //Asyn cMa nager. ∞ tstandin gOp erations. 工 ncremen t () ; Task.Factory.StartNew(() => } ) ; string path = ControllerContext.HttpContext.Server .MapPath(string.Format( 白 "\articles\{O}.html" , name)); using (StreamReader reader = new StreamReader(path)) Asy 口 cManager.Parameters["content"] = reader.ReadToEnd(); //AsyncMa nager.OutstandingOp erations.Decrement() ; public ActionResult ArticleCompleted(string content) return Content(content); 实际上在这种情况下Art icleCompleted 依然会被执行,但是如果真的这样做我们就不能 确保正常读取文章内容,因为 ArticleCompleted 方法会在Art icleAsync 方法执行之后被立即 执行。如果文章内容读取是一个相对耗时的操作, Art icleCompleted 方法的 content 参数(表 示读取文章内容的)在执行的时候可能尚未被初始化,那么这种情况下的Art icleCompleted 是如何被执行的呢? 原因很简单, Re f1 ectedAsyncActio nD escriptor 的 Begi nE xecute 方法在执行 X xxA sync 方 法的前后会自行调用 AsyncManager 的 OutstandingOperations 属性的 Increment 和 Decrement 方法。对于我们给出的例子来说,在执行 ArticleAsync 之前 Increment 方法被调用使计算器 的值变成1,随后 ArticleAsync 被执行,由于该方法以异步的方式读取指定的文件内容,所 以会立即返回。最后 Decrement 方法被执行使计数器的值变成 0 , AsyncManager 的 Completed 事件被触发并导致Art icleCompleted 方法的执行。而此时文件内容的读取正在进行之中,表 示文章内容的 content 参数自然尚未被初始化。 Re f1 ectedAsyncActio nD escriptor 这样的执行机制也对我们使用 AsyncManager 提出了要 求,那就是对尚未完成的异步操作计数器的增加操作不应该发生在异步线程中。对于如下所 示的两种编程方式,第一种是不正确的。 ASP. NET MVC 4 框架揭秘328 路 第 7 章 Action 的执行 //辛苦误, public class HomeController : AsyncCo口 troller { //其他成员 public void XxxAsync(string name) { //正确 Task.Factory.StartNew(() => AsyncManager.OutstandingOperations.lncrement(); / / . . . Asy口cMa口ager.OutstandingOperations~Decrement(); } ) ; public class HomeController : AsyncController //其他成员 public void XxxAsync(string 口ame) { AsyncManager.OutstandingOperations. 工 ncrement(); Task.Factory.StartNew(() => / / . . . Asy口 cManager.Outs 仁 andingOperations.Decrement(); } ) ; 最后再强调一点,不论是显式调用 AsyncManager 的 Finish 方法,还是通过调用 AsyncManager 的 OutstandingOperations 属性的 IncrennenU1)ecrennent 方法使计数器的值变成 零,仅仅是让 XxxConnpleted 方法得以执行,并不能真正阻止已经开始的异步操作的执行。 异步操作的超时控制 异步操作虽然适合那些相对耗时的 ν0 绑定型操作,但是也并不是说对异步操作执行的 时间没有限制。异步超时时限通过 AsyncManager 的整型属性 Tinneout 来控制,它表示超时 时限的总毫秒数,其默认值为 45000 (45 秒〉。如果将 Tinneout 属性设置为-1,意味着异步 操作执行不再具有任何时间的限制。 对于以 XxxAsynclXxxConnpleted 形式定义的异步 Action 来说,如果 XxxAsync 执行之后 在规定的超时时限内 XxxConnpleted 没有得到执行,一个 TinneoutException 异常会被抛出来。 如果我们以返回类型为 Task 的形式定义异步 Action ,通过 Task 体现的异步操作的执行时间 不受 AsyncManager 的 Tinneout 属性的限制。我们通过如下的代码定义了一个 Action 方法 Data ,它以异步的方式获取相应的数据并作为 Model 呈现在默认的 View 中。由于异步操作 中具有一个无限循环,当我们访问该 Data 方法时,异步操作将会无限制地执行下去,但是 不会有 TinneoutException 异常发生。 ASP. NET MVC 4 在架揭秘public class HomeController : AsyncController public TaskData() return Task.Factory.StartNew(() => while (true) {} return GetModel()i }) .ContinueWith (task => } ) i object model = task.Resulti return View(task.Result)i 7.1 异步 Action 的定义仨 329 在 ASP.NETMVC 应用编程接口中具有两个特性用于指定异步操作执行的超时时限,它 们是具有如下定义的 S)吼 em. Web.Mvc.AsyncTimeoutAttribute 和 System. Web.Mvc.NoAsync Timeou tA ttribute 。 [AttributeUsage(AttributeTargets.Method I AttributeTargets.Class , Inherited=true , AllowMultiple=false)] public class AsyncTimeoutAttribute : ActionFilterAttribute public AsyncTimeoutAttribute(int duration)i public override void OnActionExecuting (ActionExecutingContext filterContext) i public int Duration { geti } [AttributeUsage(AttributeTargets.Method I AttributeTargets.Class , Inherited=true , AllowMultiple=false)] public sealed class NoAsyncTimeoutAttribute : AsyncTimeoutAttribute // Methods public NoAsyncTimeoutAttribute() : base(-l) { } 从上面给出的定义可以看出这两个特性均是 Actio nFi1 ter 0 AsyncTimeou tA ttribute 的构造 函数接受一个表示超时时限(以毫秒为单位〉的整数作为其参数,它通过重写 O nA ctio nE xecuting 方法将指定的超时时限赋值给 AsyncManager 的 Timeout 属性。 N oAsyncTimeoutAttribute 是 AsyncTimeoutAttribute 的继承者,它将超时时限设置为 -1 ,意味 着它解除了对超时的限制。 从应用在这两个特性的 AttributeUsageA 伽 ibute 定义可以看出它们既可以应用于类也可 以用于方法,意味着我们可以将它们应用到 Controller 类上,也可以应用于异步 Action 方法 (仅对XxxA sync 方法有效,不能应用到Xx xCompleted 方法上〉上。如果将它们同时应用 到 Controller 类和 Action 方法上,针对方法级别的特性无疑具有更高的优先级。 AS P. NET MVC 4 在架揭翻330 • 第 7 章 Action 的执行 7.2 Action 1.]法的执行 Action 方法的执行具有两种基本的形式,即同步执行和异步执行,而在 ASP.NE 1MV C 的整个体系中涉及到很多同步/异步的执行方式。虽然在前面的章节中已经对此作了相关的 介绍,为了让读者对此有一个总体了解,我们在这里来做一个总结性的论述。 7.2.1 MvcHandler 对请求的处理 对于 ASP.NET MVC 应用来说, MvcHandler 是最终用于处理请求的 HtφHandl 町,它是 通过 Ur lR outingModule 这个实现了 URL 路由的 H忧:p Module 被动态映射到当前请求的。 MvcHandler 借助于 ControllerFactory 激活并执行目标 Con位oller ,并在执行结束后负责对 Controller 的释放,相关的内容请参考第 3 章" Controller 的激活"。 如下面的代码片段所示, MvcHandler 同时实现了 IHttpHandler 和 IHt甲 AsyncHandler 接 口,所以它总是调用 Begi nP rocessReques tlE ndProcessRequest 方法以异步的方式来处理请求。 public class MvcHandler IHttpAsyncHandler , IHttpHandler , //其他成员 IAsyncResult IHttpAsyncHandler.BeginProcessRequest(HttpContext context , AsyncCallback cb , object extraData); void IHttpAsyncHandler.EndProcessRequest(IAsyncResult result); void IHttpHandler.ProcessRequest(HttpContext httpContext); 7.2.2 Controller 的执行 通过第 3 章" Controller 的激活"的介绍我们知道 Controller 也具有同步与异步两个版本, 它们分别实现了具有如下定义的两个接口 IController 和 IAsyncController 。激活的 Controller 对象在 MvcHandler 的 Begi nP rocessRequest 方法中是按照这样的方式执行的:如果 Contro l1 er 的类型实现了 IAsyncController 接口,则调用 Begi nE xecuteÆndExecute 方法以异步的方式执 行,否则 Controller 是通过调用 Execute 方法以同步方式执行的。 public interface IController void Execute(RequestContext requestContext); public interface IAsyncController : IController IAsy 口 cResult BeginExecute(RequestContext requestContext , AsyncCallback callback , object state); void EndExecute(IAsyncResult asyncResult); ASP. NET MVC 4 框架揭秘7.2 Action 万法的执行 审 331 默认情况下通过 Visual Studio 的向导创建的 Con仕 oller 类型是抽象类型 Con位 oller 的子 类。如下面的代码片段所示,它同时实现了 IController 和 IAsyncController 这两个接口,所 以 MvcHandler 总是以异步的方式来执行 Controller 。 public abstract class Controller ControllerBase , IController , IAsyncController , //其他成员 protected virtual bool DisableAsyncSupport get{return falsei} 但是 Con位 oller 类型具有一个受保护的只读属性 DisableAsyncSupport ,它用于控制是否 禁用对异步执行的支持。在默认情况下,该属性值为 False ,所以默认情况下是支持 Con位 oller 的异步执行的。如果我们通过重写该属性将值设置为 True ,那么 Controller 将只能以同步的 方式执行。具体的实现逻辑体现在如下的代码片段中。 Begi nE xecute 方法在 DisableAsyncSupport 属性为 True 的情况下通过调用 Execute 方法(该方法会调用一个受保护 的虚方法 ExecuteCore 最终对 Con位 oller 进行同步执行)同步地执行 Controller ,否则通过调 用 Beg inE xecuteCoreÆndExecuteCore 以异步方式执行 Controller 。 public abstract class Controller: //其他成员 protected virtual IAsyncResult BeginExecute(RequestContext requestContext , AsyncCallback callback , object state) if (this.DisableAsyncSupport) //通过调用 Execute 方法同步执行 Controller else //通过调用 BeginExecuteCore/EndExecuteCore 方法异步执行 Controller protected override void ExecuteCore()i protected virtual IAsyncResult BeginExecuteCore(AsyncCallback callback , object state); protected virtual void EndExecuteCore(IAsyncResult asyncResult)i 7.2.3 Actionlnvoker 的执行 包括 Model 绑定与验证在内的整个 Action 的执行是通过一个名为 Actio nI nvoker 的组件 来完成的,它同样具有同步和异步两个版本,分别实现了接口 IActio nIn voker 和 ASP. NET MVC 4 框架揭秘332 位第 7 章 Action 的执行 IAsyncActio nInvoker 。如下面的代码片段所示,这两个接口分别通过InvokeAction 和 Beg inInvokeActio nÆ ndInvokeAction 方法以同步和异步的方式执行 Action 。抽象类 Con仕 oller 中具有一个 Actio nI nvoker 属性用于设置和返回执行自身的 Action 的 Actio nI nvoker ,该对象 最终是通过受保护虚方法 CreateActio nI nvoker 创建的。 public interface IActionlnvoker bool InvokeAction(ControllerContext controllerContext , string actionName); publicinterface IAsyncActionlnvoker : IActionlnvoker IAsyncResult BeginlnvokeAction(ControllerContext controllerContext , string actionName , AsyncCallback callback , object state); bool EndlnvokeAction(IAsy 口 cResult asyncResult); public abstract class Controller 11 其他成员 public IActionlnvoker Actionlnvoker { get; set; } protected virtual IActionlnvoker CreateActionlnvoker() ASP.NET MVC 真正用于 Action 方法同步和异步执行的 Actio nI nvoker 类型分别是 System. Web.Mvc.ControllerActio nInvoker 和 System. Web.Mvc.A 可nc.A 可ncControllerActio nInvoker 。 如下面的代码片段所示, ControllerActio nInvoker 具有一个受保护的方法 GetControllerD白 criptor , 它会根据指定的 ControllerContext 获取用于描述当前 Controller 的 ControllerDescriptor 对象。 它的子类 AsyncCon位 ollerActio nI nvoker 对这个方法进行了重写。 public class ControllerActionlnvoker : IActionlnvoker 11 其他成员 protected virtual ControllerDescriptor GetControllerDescriptor( ControllerContext controllerContext); public class AsyncControllerActionlnvoker : ControllerActionlnvoker , IAsyncActionlnvoker , IAction 工 nvoker 11 其他成员 protected override ControllerDescriptor GetControllerDescriptor( ControllerContext controllerContext); 我们需要着重了解的是在默认情况下 Controller 采用的 Actio nInvoker 类型是哪个。 ASP.NETMVC 对采用的 Actio nI nvoker 类型的选择机制是这样的。 • 步骤 1 :通过当前的 DependencyResolver 以 IAsyncActio nI nvoker 接口去获取注册的 Actio nInvoker ,如果返回对象不为 Null ,则将其作为默认的 Actio nI nvoker ,否则进入步 骤 2 。 ASP. NET MVC 4 在架揭秘7.2 Action 万法的执行 旬 333 • 步骤 2 :通过当前的 DependencyResolver 以 IActio nI nvoker 接口去获取注册的 Actio nI nvoker ,如果返回对象不为 Null ,则将其作为默认的 Actio nI nvoker ,否则进入步 骤 3 。 • 步骤 3: 创建 AsyncControllerActio nInvoker 对象作为默认的 Actio nInvoker 。 在默认的情况下,当前的 De叩pend时denc叮yResωolve町r 直接通过对指定的类型进行反射来提供对 应的实例对象 (De叩pend缸enc叮:y Re臼solve町r 在第 3 章 前面两个步骤均返回 Null ,这意味着 Controller 默认使用的 Actio nI nvoker 类型为 AsyncControllerActio nInvoker ,可以通过如下一个简单的实例来验证这一点。 为了验证 DependencyResolver 对 Actio nI nvoker 的提供机制的影响,我们在创建的 ASP.NET MVC 应用中将在第 3 章" Controller 的激活"中创建的针对 Niniect 的 Ninjec tD ependencyResolver 注册为当前的 DependencyResolv町。然后我们创建了如下两个自定 义 Actio nInvoker ,其中 SyncActio nInvoker 实现了接口 IActio nInvoker ,而 AsyncActio nI nvoker 实现了接口 IAsyncActio nI nvoker 。 public class SyncActionInvoker : IActionInvoker //省咯成员 public class AsyncActionInvoker : IAsyncActionInvoker //省略成员 接下来我们定义了如下一个 HomeController 。方法 GetActio nI nvokers 通过三次调用 CreateActio nI nvoker 方法得到三个具体的 Actio nI nvoker 对象。返回的第一个 Actio nI nvoker 对象代表默认使用的 Actionlnvoker ,其余两个是分别将接口类型 IActio nI nvoker 和 IAsyncActio nI nvoker 注册到当前 DependencyResolver 的情况通过调用 CreateActio nI nvoker 方 法创建的。 public class HomeController : Controller public ActionResult Index() return View(this.GetActionInvokers() .ToArraY())i public IEnumerable GetActionInvokers() NinjectDepe 口 dencyResolver dependencyResolver = (NinjectDependencyResolver) DependencyResolver.Currenti / /1.默认创建的 ActionInvoker yield return this.CreateActionIn 飞 Toker () i //2. 为 Dependency 注册针对 IActionIn 飞Toker 的类型映射 dependencyResolver.Register()i AS P. NET MVC 4 框架揭秘334 毡第 7 章 Action 的执行 yield return this.CreateActionlnvoker(); //3. 为 Dependency 注册针对 IAsyncActionlnvoker 的类型映射 dependencyResolver.Register(); yield return this.CreateActionln飞Toker () ; 在 Action 方法Index 中,通过调用 GetActionInvokers 方法得到 ActionInvoker 列表被转 换为数组作为 Model 呈现在对应的 View 中。如下所示的是该 View 的定义,它是一个 Model 类型为 IActionInvoker 数组的强类型 View 。在该 View 中我们将每一个 ActionInvoker 的类型 通过表格的形式呈现出来。 @model IActionlnvoker[] Actionlnvoker @for(int i=O; i
    @(i+l)@Model[i] .GetType() .Name
    该程序运行后会在浏览器中呈现如图 7-1 所示的输出结果,我们前面介绍的创建 ActionInvoker 的三条规则中只有最后一条在这里得到印证。为什么将注册的 ActionInvoker 类型注册在当前 DepedencyResolver 对 Con位ol1er 的 CreateActionInvoker 方法没有影响呢? (S703 ) 匮噩噩噩 X \_ - t:I X 噩噩圃EEI堕 local 问芋 8758 食E理, AS沪l cControllerJ过tionInvoker AsyncControllerActÎonlnvoker Async汇 ontrollerActionInvoker 图 7-1 Controller 针对 Actionlnvoker 的创建机制 (1) 难道上面介绍的 ActionInvoker 创建机制是错误的吗?实则不然。造成 Con位oller 在任何 时候总是创建相同类型的 ActionInvoker 的原因在于其内部采用的"缓存"机制。实际上 Con位oller 的 CreateActionInvoker 方法并没有直接采用通过 DependencyResolver 的静态属性 ASP.NET MVC 4 在架揭秘7.2 Action 方法的执行 黯 335 Current 代表的当前 DependencyResolver ,而是使用另一个如下所示的内部静态字段 CurrentCache 代表的 DependencyResolver 。 public class DependencyResolver //其他成员 internal static IDepeηdencyResolver CurrentCache { geti } private sealed class CacheDependencyResolver : IDependencyResolver //其他成员 private readonly CbncurrentDictionary cachei 这个 CurrentCache 字段类型为 CacheDependencyResolver ,这是一个私有类型,可以看成 是对通过静态 Current 属性表示的当前 DependencyResolver 的封装。 CacheDependencyResolver 内部使用了被封装的 Dependency Resolver 进行对象的激活,但是它会对激活的对象进行缓 存,而作为缓存容器的就是通过其只读字段 cache 表示的 ConcurrentDictionary 对象,所有 Controller 的 CreateActio nI nvoker 方法返回的都是第一次创建的对象。 为了证实这一点,我们在 HomeController 中定义了如下一个 ClearCachedActio nI nvokers 方法用于清除当前 CacheDependency Resolver 被缓存的所有对象。 Ge tA ctio nI nvokers 方法在 调用 CreateActionInvoler 方法之前按照如下的方式及时地调用 ClearCache dA ctio nI nvokers 方 法将之前缓存的 Actio nI nvoker 进行了清理。 public class HomeController : Controller public ActionResult Index() return View(this.GetActionInvokers() .ToArraY())i public IEnumerable GetActionInvokers() NinjectDependencyResolver depeηdencyResolver = (Ni 口 jectDependencyResolver) Depende 口 cyResolver.Currenti yield return this.CreateActionInvoker()i this.ClearCachedActionlnvokers() i dependencyResolver.Register()i yield return this.CreateActionInvoker()i this.ClearCachedActionlnvokers() i dependencyResolver.Register()i yield return this.CreateActionInvoker()i private void ClearCache dA ctionInvokers() AS P. NET MVC 4 在架揭秘336 电第 7 章 Action 的执行 Propertylnfo property = typeof(DependencyResolver) .GetProperty( "CurrentCache" , BindingFlags.NonPublic I BindingFlags.Static); var cache dActionlnvoker = property.GetValue(null , null); Fieldlnfo field = cache dActionlnvoker.GetType() .GetField(" cache" , BindingFlags.NonPublic I BindingFlags.lnstance); ConcurrentDictionary dictionary = (ConcurrentDictionary)field .GetValue(cachedActionlnvoker); dictionary.Clear(); 再次运行我们的程序后将会得到如图 7-2 所示的 Actio nI nvoker 类型列表,这个列表就和 我们之前介绍的三条 Actio nInvoker 创建规则相匹配了。 (S704) 11ι旦Ds t: 1560E LL| AsyncζontrollerActionlnvoker S于 n cA ctionInvoker Asyn cA ctionInvoker 图 7 -2 Controller 针对 Actionl nvoker 的创建机制 (2) 7.2.4 ControllerDescriptor 的同步与异步 如果 Controller 使用 Con位 ollerActio nI nvoker ,它所有的 Action 总是以同步的方式来执行, 但是当 AsyncControllerActio nInvoker 作为 Controller 的 Actio nInvoker 时,却并不意味着总是 以异步的方式来执行所有的 Action 。 至于这两种类型的 ActionInvoker 具体采用怎样的执行方式,涉及到两个描述对象,即用于 描述 Controller 和 Action 的 c 。由'O llerDescriptor 和 Actio nD escriptor 0 ASP.NET MVC 具有两个具 体的 ControllerDescriptor , ~p ReflectedControllerDesαiptor 和 ReflectedA syncControllerDescriptor , 它们分别代表同步和异步版本的 ControllerDescriptor 。 ReflectedControllerDescriptor 和 Reflecte dA syncCon位'O llerDescriptor 并非对实现了 IContro l1 er 和 IAyncContro l1 er 接口的 Controller 的描述(实际上抽象类 Con位 o l1 er 同时实现这 两个接口)。两者的区别在于创建者的不同,在默认情况下 ReflectedControllerDescriptor 是通 过 ControllerActionInvoker 创建的,而 Reflecte dA syncControllerDescriptor 的创建者则是 AsyncControllerActionI nvoker 0 Actio nI nvoker 和 ControllerDescriptor 之间的关系可以通过如 图 7-3 所示的 UML 来表示。 ASP. NET MVC 4 框主导雹秘<> IActlonlnvoker < <: inlerface> > IAsyncActionlnvoker ControllerActionlnvoker 7.2 Action 方法的执行 雷 337 Controlle rD escrlptor ReflectedConlrollerDescrl ptor AsyncControllerActionlnvoker ReflectedAsyncControllerDescriptor 图 7-3 Actionlnvoker 与 ControllerDescriptor 之间的关系 Actionlnvoker 与 ControllerDescr恫 or 之间的关系可以通过一个简单的实例来验证。在一 个 ASP.NET MVC 应用中,我们创建了如下两个分别继承自 Con仕 ollerActionlnvoker 和 AsyncControllerActionlnvoker 的自定义 Actionlnvoker 时类型。这两个自定义 Actiωonl咀In盯lVO优ke町r . 具有一个公有的 GetCo∞ntωroller白De臼sc创ripμto创r 方法覆盖了基类的同名方法(受保护的虚方法 )λ,并 直接返回通过调用基类的同名方法返回的 Co∞nt仕ro吼ollerDe臼scαr桐 O时r 对象。 public class SyncActionInvoker : ControllerActionIn 飞 Toker public new ControllerDescriptor GetControllerDescriptor( ControllerContext controllerContext) return base.GetControllerDescriptor(controllerContext); public class AsyncActionInvoker : AsyncControllerActionInvoker public new ControllerDescriptor GetControllerDescriptor( ControllerContext controllerContext) return base.GetControllerDescriptor(controllerContext); 接下来我们定义了如下两个名为 F ooController 和 BarController 的 Con位 oller 类型,在重 写的 CreateActionlnvoker 方法中分别返回 SyncActionlnvoker 和 AsyncActionlnvoker 对象。 public class FooController : Controller protected override IActionInvoker CreateActionInvoker() return new SyncActionInvoker(); ASPNETMVC4 框架揭秘338 驾第 7 章 Action 的执行 public class BarController : Controller protected override IActionlnvoker CreateActionlnvoker() return new AsyncActionlnvoker(); 最后我们定义了如下一个 HomeController 0 GetControllerDescriptors 方法返回用于描述 指定 Controller 的 Con位ollerDescr柳or 对象列表。对于每个指定的 Controller 对象,我们先获 取其 ActionInvoker 对象,然后转换成上面定义的 SyncActionlnvoker/AsyncActionInvoker ,并 调用其 GetControllerDescriptor 方法得到对应的 ControllerDescriptor 对象。 public class HomeController : Controller public ActionResult Index() return View(this.GetControllerDescriptors( new FooController() , new BarController())); private 工 Enumerable GetControllerDescriptors( params Controller[] controllers) controllers = controllers?? new Controller[O]; foreach (Controller controller in controllers) ControllerContext.Controller = controller; SyncActionlnvoker syncActionlnvoker = controller.Actionlnvoker as SyncActionlnvoker; AsyncActionlnvoker asyncActionlnvoker = controller.Actionlnvoker as AsyncActionlnvoker; if (null != syncActionlnvoker) yield return syncAction 工 nvoker.GetControllerDescriptor( ControllerContext); if (null != asyncActionlnvoker) yield return asyncActionlnvoker.GetControllerDescriptor( ControllerContext); 在默认的 Action 方法 Index 中,我们调用 GetControllerDescriptors 方法获取用于描述 FooCon位oller 和 BarController 的两个 ControllerDescriptor 对象,然后将返回值作为 Model 呈 现在具有如下定义的 View 中。这是一个 Model 类型为 IEnumerable 的 强类型 View ,在该 View 中我们将每个 ControllerDescriptor 的类型以及对应的 Controller 类 型通过表格的形式呈现出来。 @model IEnumerable ASP. NET MVC 4 框架揭秘7.2 Action 方法的执行 t.339 ControllerDescriptor , @foreach (ControllerDescriptor descriptor in Model)
    ControllerControllerDescriptor
    @descriptor.ControllerType.Name @descriptor.GetType() .Name
    该程序运行之后会在浏览器中呈现出如图 7-4 所示的输出结果 , 可以看到采用 ControllerActionlnvoker 和 AsyncControllerActionlnvoker 作为 Actio nI nvoker 的 F ooController 和 BarController ,对应的 ControllerDescriptor 类型分别为 ReflectedControllerDescriptor 和 ReflectedAsyncControllerDescriptor 0 (S705) 图 7 -4 Actionlnvoker 对 ControllerDescriptor 的创建 7.2.5 ActionDescriptor 的执行 Action 方法可以采用同步和异步执行方式,异步 Action 对应的 Actio nD escriptor 直接或 者间接继承自抽象类 AsyncActio nD escriptor ,后者又是抽象类 Actio nD escriptor 的子类。如下 面的代码片段所示,同步和异步 Action 的执行分别通过调用 Execute 和 Beg inExecute.厄n dExecute 方法来完成。值得一提的是, AsyncActio nD escriptor 重写了 Execute 方法并直接在此方法中 抛出一个 InvalidOperatio nE xception 异常,意味着 AsyncActio nD escriptor 对象只能采用异步 执行方式。 public abstract class ActionDescriptor : ICustomA ttributeProvider //其他成员 public abstract object Execute(ControllerContext controllerContext , IDictionary parameters); publicabstract class AsyncActionDescriptor : ActionDescriptor AS P. NET MVC 4 框架揭秘340 • 第 7 章 Action 的执行 //其他成员 public abstract IAsyncResult BeginExecute( ControllerContext controllerContext , IDictionary parameters , AsyncCallback callback , object state)i public abstract object EndExecute(IAsyncResult asyncResult)i ASP.NETMVC 使用 ReflectedControllerDescriptor 对象来描述同步 Action ,而异步 Action 具有两种不同的定义方式,在 AsyncController 中以 X xxA sync/XxxCompleted 形式定义的异 步 Action 通过 ReflectedAsyncActio nD escriptor 对象来描述,返回类型为 Task 的异步 Action 则通过 Tas kA syncActio nD escriptor 对象描述。 ReflectedControllerDescriptor 包含的所有 Actio nD escriptor 类型均为 Reflect edA ctio nD escriptor 。 Reflecte dA syncControllerDescriptor 描述的 Controller 可以同时包含同步和异步的 Action 方法, Actio nD escriptor 的类型取决于 Action 方法的定义方式。 ControllerDescriptor 与 Actio nD escriptor 之间的关系可以通过如图 7-5 所示的 UML 来表示。 C C> tllroll 曹 rDescnptor ActlonDescrlptor Ta 事kA sytlcActionOc$criptor 图 7 -5 ControllerDescriptor 与 Action Descriptor 之间的关系 实例演示: ReflectedAsyncControllerDescriptor 申的 ActionDescriptor 类型 (S706 ) 通过 ControllerActionlnvoker 创建的 ReflectedControllerDescriptor 包含的所有 Actio nD escriptor 类型均为 ReflectedActio nD escriptor ,而对于通过 AsyncController Actio nI nvoker 创建的 Reflecte dA syncControllerDescriptor 来说,包含其中的某个 Actio nD escri ptor 的类型取决于对应 Action 方法的定义方式。接下来我们来演示定义在 AsyncController 中以不同形式定义的 Action 方法如何决定最终的 Actio nD escriptor 类型。 我们在 ASP.NETMVC 应用中定义了如下一个 HomeCon位oller ,它重写的 CreateActionIn voker 方法返回一个AsyncActionIn voker 对象。 AsyncActionInvoker 继承自 AsyncCon仕ollerActionInvoker , 定义其中的公有 GetControllerDescr悄 or 方法返回描述当前 Controller 的 ControllerDescriptor 对象。 ASP. NET MVC 4 框架揭翻7.2 Action 万法的执行 部 341 public class HorneController : AsyncController protected override IActionInvoker CreateActionIn 飞Toker () return new Asy 口 cActionInvoker(); public ActionResult Index() AsyncActionInvoker actionInvoker = (AsyncActionInvoker)this.ActionInvoker; ControllerDescriptor controllerDescriptor = actionInvoker.GetControllerDescriptor(ControllerContext); List actionDescriptors =ηewList() ; actionDescriptors.Add(controllerDescriptor.Fin dA ction( ControllerContext , "Foo")); actionDescriptors.Add(controllerDescriptor.Fin dA ction( ControllerContext , "Bar")); actionDescriptors.Add(controllerDescriptor.FindA ction( ControllerContext , "Baz")); return View(actionDescriptors); public void Foo() { } public void BarAsync() { } public void BarCornpleted() { } public Task Baz() throw new NotIrnplernentedException(); public class AsyncActionInvoker : AsyncControllerActionInvoker public new ControllerDescriptor GetControllerDescriptor( ControllerContext controllerContext) return base.GetControllerDescriptor(controllerContext); HomeContro l1 er 定义了三个 Action ,其中 Foo 是同步的, B 缸和 Baz 是异步的。两个异 步 Action 采用了不同的定义方式, B 盯是通过两个匹配方法 BarAsynclB arCompleted 定义而 成, B 但则直接返回一个 Task对象。在 Action 方法 Index 中,我们通过 Actio nI nvoker 得到用于描述这三个 Action 的 Actio nD escriptor 对象,并将它们作为 Model 呈 现在具有如下所示的 View 中。这是一个 Model 类型为 IEnumerable 的强 类型 View ,在该 View 中我们将每一个 Actio nD escriptor 的类型和它对应的名称通过表格的 形式呈现出来。 @rnodel IEnurnerable ASPNETMVC4 框架揭秘342 'ÌI 第 7 章 Action 的执行 ActionDescriptor @foreach (ActionDescriptor descriptor in Model)
    ActionNarneActionDescriptor
    @descriptor.ActionNarne @descriptor.GetType() .Narne
    该程序运行之后会在浏览器中呈现出如图 7-6 所示的输出结果,可以看出定义在 HomeControl1er 中的三个 Action 由不同类型的 ActionDescriptor 来描述。 ReflectedAsyn cActionDescriptor TaskAs'归cActionD田criptor 图 7-6 AsyncController 中具有不同定义方式的 Action 对应的 ActionDescriptor 类型 AsyncController 、 AsyncControllerAction I nvoker 与 AsyncActionDescriptor 以XxxAsync/XxxCompleted 形式定义异步 Action 必须定义在继承自 AsyncCor由ol1er 的 Controller 类型中,否则将被认为是同步方法。由于通过 ControllerActionInvoker 只能创建包 含 ReflectedActionD escriptor 的 ReflectedControllerDescriptor ,如果我们在 AsyncController 中 采用 Con仕ollerActionInvoker 对象作为 ActionInvoker ,所有的 Action 方法也将被认为是同步 的。换句话说,真正能够以异步方式执行的 Action 需要同时满足如下两个条件。 • 如果采用XxxAsync/XxxCompleted 定义方式,对应的 Controller 类型必须继承自 AsyncContro l1 er ,否则只能定义成返回类型为 Task 的方法。 • 所在 Contro l1er 采用 AsyncControllerActionInvoker 来执行 Action 。 我们同样可以采用一个简单的实例演示来证实这一点,在 ASP.阳TMVC 应用中定义了 如下两个 Controller ,其中 Controller1继承自抽象类 Co毗oller , Controller2 则继承自 AsyncController 。我们重写了它们的 CreateActionInvoker 方法使 Controller 1 返回一个 ControllerActionInvoker 对象, Controller2 返回的则是一个 AsyncContro l1erActionInvoker 对象 (其实默认返回的就是这么一个对象)。两个 Controller 以不同的方式定义了两个异步 Action , 其中 Foo 以 FooAsynclF ooCompleted 的形式定义,而 B缸则具有一个 Task返 回类型。 ASP. NET MVC 4 在架揭秘7.2 Action 万法的执行 黯 343 public class Controllerl : Controller protected override IActionlnvoker CreateActionlnvoker() retur 口 new AsyncControllerActionlnvoker(); public void FooAsync() { } public void FooCompleted() { } public Task Bar() throw new NotlmplementedException(); public class Controller2 : AsyncController protected override IActionlnvoker CreateActionlnvoker() return new ControllerActionlnvoker(); public void FooAsync() { } public void FooCompleted() { } public Task Bar() throw new Notlmpleme 口 tedException(); 我们定义了如下一个 HomeController 。方法 Ge tA ctio nD escriptors 返回用于描述定义在指 定 Controller 中相关 Action 的 Actio nD escriptor 对象,该方法通过指定 Controller 对象的 Actio nI nvoker 属性获取对应 ControllerActio nI nvoker 或者 AsyncControllerActio nI nvoker 对象, 然后以反射的方式调用受保护的 GetCon位 ollerDescriptor 方法得到用于描述 Controller 的 ControllerDescr悄 or 对象,最终调用 ControllerDescriptor 的 Fin dA ction 方法得到相关的 Actio nD escriptor 对象。 public class HomeController : AsyncController public ActionResult Index() Dictionary> actionDescritors = new Dictionary>(); actionDescritors.Add(typeof(Controllerl) , this.GetActionDescriptors( new Controllerl())); actionDescritors.Add(typeof(Controller2) , this.GetActionDescriptors( new Controller2())); return View(actio 日 Descritors); private IEnumerable GetActionDescriptors( Controller controller) AS P. NET MVC 4 框架揭秘344 也第 7 章 Action 的执行 ControllerContext.Controller = controlleri 工 Actionlnvoker actionlnvoker = controller.Actionlnvokeri Methodlnfo rnethod = actionlnvoker.GetType() . GetMethod ("GetControllerDescriptor" , BindingFlags.lnstance I BindingFlags.NonPublic)i ControllerDescriptorcontrollerDescriptor= (ControllerDescriptor)rnethod . Invoke (actionlnvoker , new object[] { ControllerContext })i striηg[] actionNarnes = new string[] { "Foo" , "FooAsync" , "FooCornpleted" , "Bar" }i foreach (string actionNarne in actionNarnes) ActionDescriptoractionDescriptor = controllerDescriptor.FindAction( ControllerContext , actionNarne)i if (null != actionDescriptor) yield return actionDescriptori 在 Action 方法 Index 中,我们将创建的 Controllerl 和 Controller2 对象作为参数调用 GetActio nD escriptors 方法,并将得到的 Actio nD escriptor 列表保存到一个 Key 为对应 Controller 类型的字典对象中。最后我们将该字典作为 Model 呈现在具有如下定义的 View 中, 在该 View 中我们将 Controller 的类型、 Action 的名称和 Actio nD escriptor 类型以表格的形式 呈现出来。 @rnodel IDictionary> ActionDescriptor @foreach (var itern in Model) ActionDescriptor[] actionDescritors = itern.Value.ToArraY()i for(int i=li i 该程序运行之后会在浏览器中呈现出如图 7-7 所示的输出结果,可以看出在 Controllerl ASP. NET MVC 4 蓓架揭秘7 . 3 筛选器的执行 也 345 和 Con位 oller2 中, FooAsync 和 FooCompleted 方法并未看成是针对一个异步 Action 的定义, 而是作为两个同步 Action 来对待的。对于返回类型为 Task 的 B 缸方法来说,只有在 Controller 1 〈采用 AsyncControllerActio nI nvoker )中被视为异步 Action ,在 Controller2 (采用 ControllerActio nI nvoker) 被视为同步 Actiono (S707) 国普曹EE圃曹 FooAsync ReflectedActìonDescriptor Controllerl FooCompleted ReflectedAction Descripto r Bar Tas kA s y n cA ctìonDescriptor FooAs y nc ReflectedActionDescrìptor Controller2 FooCompleted ReflectedActíonDescriptor Bar ReflectedActíonDescriptor 图 7 -7 AsyncController 和 Action I nvoker 对异步 Action 的影晌 目标 Action 方法的最终执行由被激活 Con 位 oller 的 Actio nI nvoker 决定, Actio nInvoker 通 过调用对应的 Actio nD escriptor 来执行被它描述的 Action 。如果采用 Controller ActionInvoker , 被它创建的 Re f1 ectedControllerDescr悄 or 只包含同步的 Re f1 ecte dA ctio nD escriptor ,所以 Action 方法总是以同步的方式被执行。 对于采用 Xx xA synclXx xCompleted 形式定义的异步 Action ,不论采用怎样的 Actio nInvoker ,其 Controller 类型必须是 AsyncController 的子类,否则被视为两个同步的 Action ,而针对返回类型为 Task 的异步 Action 则无此限制。 7.3 筛选器的执行 在通过 Actio nI nvoker 对 Action 的执行过程中, ASP.NET MVC 除了利用 Actio nD escriptor 执行对应的 Action 方法之外,还需要执行相关筛选器 (Filter) 0 AS 丑陋 TMVC 的筛选器是 一种基于 AOP (面向方面编程)的设计,我们将 一 些非业务的逻辑实现在相应的筛选器, 并以 一 种横切( Crosscutting )的方式应用到对应的 Action 方法上。在 Action 方法执行前后, 这些筛选器会自动执行。 ASP.NETMVC 提供了 Authorizatio nF ilter 、 Actio nF ilter 、 Resul tF ilter 和 Exceptio nF ilter 这四种筛选器,它们对应着四个接口 IAuthorizatio nF ilter 、 IActio nF ilter 、 IResultFilter 和 IExceptio nF ilter 。 7.3.1 Filter 及真提供机制 ASP. 阳 TMVC 中所谓的筛选器具有两个含义,一是实现上述四个筛选器接口之一的类 型,二是指具有如下定义的 System ASPNETMVC4 框主导昌秘346 • 第 7 章 Action 的执行 并以声明的方式应用到目标 Controller 类型或者 Action 方法上,它们最终都需要转换成相应 的 Filter 对象。为了更好地区分这两个概念,本章以下内容提到的"筛选器"表示前者,后 者则用 "Filter" 来表示。 Filter 也可以看成是对筛选器的封装,被封装的筛选器通过Instance 属性返回。 r e •L 丁 4·工Fι s s a 14 C C .工14 b u PJL public const int DefaultOrder = -1; public Filter(object instance, FilterScope scope, int? order); e p o c qu tr ce eL ℃ ·寸」←」 14 嘈 D 口·工 oiF CCC ·工·工·工141414 bbb uuu ppp Instance { get; protected set; } Order { get; protected set; } Scope { get; protected set; } public enum FilterScope Action = 30, Controller = 20 , First = 0, Global = 10 , Last = 100 多个同类(这里主要指四种筛选器类型〉的筛选器可以应用到同一个 Action 方法上, 它们执行的顺序通过 Order 和 Scope 属性来决定。 Order 属性对应数值越小,执行的优先级 越高。该属性的默认值为-1,对应着 Filter 中定义的常量 DefaultOrder 。 如果两个 Filter 具有相同的 Order 属性值,那么 Scope 属性最终决定谁被优先执行。 Filter 的 Scope 属性返回一个类型为 System. Web.Mvc.FilterScope 的枚举,该枚举表示应用 Filter 的范围。枚举项 Action 和 Controller 代表 Action 方法和 Con位oller 类级别, First 和 Last 意味 着希望被作为第一个和最后一个 Filter 来执行,而 Global 代表一个全局的 Filter 。 通过上面的代码片段可以看到, FilterScope 的 5 个枚举选项均被设置了一个对应的数值, 这个值决定了 Filter 的执行顺序,具有更小的枚举值会被优先执行。从 FilterScope 的定义可 以得到这样的结论:对于具有相同 Order 属性值的多个 Filter ,应用在 Controller 上的 Filter 比应用在 Action 方法上的 Filter 具有更高的执行优先级,一个全局的 Filter 的执行优先级又 高于基于 Con仕oller 的 Filter 。 FilterProvider Filter 的提供机制与我们之前介绍的基于 ModelMetadata 、 Modeillinder 和 ModelValidator 的提供机制类似,均是通过相应的 Provider 来提供的。提供筛选器的 FilterProvider 实现了具 有如下定义的接口 System. Web.Mvc .lFilterProvider ,该接口定义了唯一的方法 GetFilters ,它 根据指定的 ControllerContext 和用于描述目标 Action 的 ActionDescriptor 获取所有的 Filter 列表。 ASPNETMVC4 蓓架揭秘7.3 筛选器的执行 霞 347 public interface IFilterProvider IEnumerable GetFilters(ControllerContext controllerContext , ActionDescriptor actionDescriptor)i 可以通过静态类型 System. Web.Mvc.FilterProviders 注册和获取当前使用的 FilterProvider 。如 下面的代码片段所示. FilterProviders 具有一个类型为 System.Web.Mvc.FilterProvide rC ollection 的只读属性 Providers. 表示当前使用的 FilterProvider 列表。 FilterProviderCollection 是元素 类型为 IFilterProvider 的集合,其 GetFilters 方法用于获取集合中所有 FilterProvider 对象提供 的 Filter 。 public static class FilterProviders public static FilterProviderCollection Providers { geti } public class FilterProviderCollection : Collection //其他成员 public IEnumerable GetFilters(ControllerContext controllerContext , ActionDescriptor actionDescriptor)i ASP.NET MVC 提供了三种原生的 FilterProvider. 分别是 Filter AttributeFilterProvider 、 ControllerlnstanceFilterProvider 和 GlobalFilterCollection. 接下来就对它们进行单独的介绍。 FilterAttribute 与 FilterAttributeFilterProvider 我们通常将筛选器定义成特性并以声明的方式应用到 Controller 类型或者 Action 方法 上,而抽象类型 System. Web.Mvc.FilterAttribute 是所有筛选器特性的基类。如下面的代码片 段所示. FilterAttribute 特性实现了 System. Web.M vc .I MvcFilter 接口,实现的两个只读属性 Order 和 AllowMultiple 分别用于控制筛选器的执行顺序以及判断多个相同类型的筛选器特性 能否同时应用到同一个目标元素(类或者方法)。 [AttributeUsage(AttributeTargets.Method I AttributeTargets.Class , Inherited=true , AllowMultiple=false)] public abstract class FilterAttribute : Attribute , IMvcFilter protected FilterAttribute()i public bool AllowMultiple { geti } public int Order { geti seti } public interface IMvcFilter bool AllowMultiple { geti } int Order { geti } ASPNETMVC4 症架揭翻348 .• 第 7 章 Action 的执行 从应用在 FilterAttribute 上的 AttributeU sageAttribute 特性的定义可以看出该特性可以应 用在类型和方法上,这意味着筛选器一般都可以应用在 Controller 类型和 Action 方法上。只 读属性 AllowMultiple 实际上返回的是 Attribute U sageAttribute 特性的同名属性,通过上面的 定义可以看到默认情况下该属性值为 False 。 分别用于描述 Controller 和 Action 的类型 ControllerDescI悄 or 和 Actio nD escriptor 均实现 了 ICusto mA ttributeProvider 接口,可以调用相应的方法获取应用在对应的 Con位。 ller 类型或 者 Action 方法上包括 FilterAttribute 在内的所有特性。实际上这两个类型提供了单独的方法 GetFilterAttributes 获取 FilterAttribute 特性列表。如下面的代码片段所示,该方法具有一个布 尔类型的参数 useCache ,表示是否需要对解析出来的 FilterAt仕 ibute 特性进行缓存以缓解频 繁的反射操作对性能造成的影响。 public abstract class ControllerDescriptor : ICustornA ttributeProvider , IUniquelyIdentifiable //其他成员 public virtual IEnumerable GetF 工 lterAttributes( bool useCache); public abstract class ActionDescriptor : ICustornA ttributeProvider , IUniquelyIdentifiable //其他成员 public virtual IEnumerable GetFilterAttributes( bool useCache); 针对 FilterAttribute 的 Filter 通过具有如下定义的 System. Web.Mvc.FilterAttributeFilterProvider 对象来提供,它直接调用当前 ControllerDescriptor 和 Actio nD escI恫 or 的 Ge tF ilterAttributes 方法获取所有应用在 Controller 类型和当前 Action 方法的 FilterAttribute 特性,并借此创建相 应的 Filter 对象。 FilterAttributeFilterProvider 构造函数的参数 cacheAttributelnstances 表示是 否启用针对 FilterAttribute 的缓存,它将作为调用 GetFilter Attributes 方法的参数。在默认的 情况下(通过调用默认无参的构造函数创建的 FilterAt位 ibuteFilterProvider ) FilterAttributeFilterProvider 是支持缓存的。 public class FilterAttributeFilterProvider :工 FilterProvider public FilterAttributeFilterProvider(); public FilterAttributeFilterProvider(bool cacheAttributeInstances); protected virtual IEnumerable GetActionAttributes( ControllerContextcontrollerContext , ActionDescriptoractionDescriptor); protected virtual IEnumerable GetControllerAttributes( ControllerContextcontrollerContext , ActionDescriptoractionDescriptor); public virtual IEnumerable GetFilters( ControllerContextcontrollerContext , ActionDescriptoractionDescriptor); 对于通过调用 GetFilters 得到的每个 Filter ,对应的 FilterAttribute 特性作为其 Instance 属 性。 Order 属性来源于 FilterAttribute 的同名属性,而 Scope 属性则取决于 FilterAttribute 特性 ASPNETMVC4 框架揭秘7.3 筛选器的执行 黯 349 是应用在 Con位oller 类型上 CScope 属性值为 Controller) 还是当前的 Action 方法上 CScope 属性值为 Action) 。 Controller 与 Controllerl nstanceFi IterProvider 提到 ASP.NET MVC 的筛选器,大部分读者都只会想到各种 FilterAttribute 特性,实际 上 Controller 本身就是一个筛选器。如下面的代码片段所示,抽象类 Controller 实现了 IActionF ilter 、 IAuthorizationFilter 、 IExceptionF ilter 和 IResultFilter 四个接口。 public abstract class Controller : ControllerBase, IActionFilter, IAuthorizationFilter, IExceptionFilter, IResultFilter, //省略成员 ASP.NET MVC 通过具有如下定义的 System. Web.Mvc. ControllerInstanceFilterProvider 类型来表示针对 Controller 对象这种特殊筛选器的 Filter 。它的 GetFilters 方法根据指定的 ControllerContext 获取对应的 Controller 对象,并以此创建一个对应的 Filter C Controller 对象 作为 Filter 对象的 Instance 属性值〉。值得注意的是这个 Filter 的 Scope 不是 Controller 而是 First, Order 的值为 -2147483648 C Int3 2.MinValue) ,毫无疑问这样的 Filter 肯定被第一个执行。 public class ControllerInstanceFilterProvider :工 FilterProvider { publicIEnumerable GetF工 lters(ControllerContextcontrollerContext , ActionDescriptor actionDescriptor); GlobalFilterCollection FilterAttribute 需要显式地应用到某个具体的 Controller 类型或者 Action 方法上,但是在 有些情况下需要一种全局性的 Filter 。所谓全局 Filter ,就是不需要显式与某个 Controller 或 者 Action 进行关联就会默认应用到定义在所有 Controller 中的 Action 方法上,这种全局 Filter 通过具有如下定义的 System. Web.Mvc. GlobalFilterCollection 来提供。 public sealed class GlobalFilterCollection : IEnumerable, IEnumerable, IFilterProvider public GlobalFilterCollection(); public void Add(object filter); public void Add(object filter, int order); private void AddInternal(object filter, int? order); public void Clear(); public bool Contains(object filter); public IEnumerator GetEnumerator(); public void Remove(object filter); ASP. NET MVC 4 框架揭秘350 • 第 7 章 Action 的执行 IEnumerator IEnumerable.GetEnumerator(); IEnumerable IFilterProvider.GetFilters( ControllerContext controllerContext, ActionDescriptor actionDescriptor); public int Count { get; } GlobalFilterCollection 是一个元素类型为 Filter 的集合,同时自身也是实现了 IFilterProvider 接口的 FilterProvider ,它实现的 GetFilters 方法返回的就是它自己。通过 GlobalFilterCollection 提供的方法我们可以实现对全局 Filter 的添加、删除和清除。用于添加 Filter 的 Add 方法的参数 filter 不是一个 Filter 对象而是一个具体筛选器对象。 ASP.NETMVC 根据指定的筛选器创建对应的 Filter (指定的筛选器作为 Filter 的Instance 属性〉并添加到集 合之中,这个 Filter 对象的 Scope 属性被设置成 Global 。 在调用 Add 方法进行全局 Filter 注册的时候,我们可以指定相应的 Order 属性,如果没 有显式指定并且指定的筛选器是一个 FilterAttribute 对象,那么该特性的 Order 属性将会作为 Filter 对象的 Order ,否则添加的 Filter 对象的 Order 属性被设置成默认值-1 。 针对整个 Web 应用的全局 Filter 的注册和获取可以通过静态类型 System.Web.Mvc.GlobalFilters 来实现。如下面的代码片段所示, GlobalFilters 具有-个静态 只读属性 Filters 返回一个 GlobalFilterCollection 对象,它就代表这个全局的 Filter 列表。 public static class GlobalFilters public static GlobalFilterCollection Filters { get; } 到目前为止,我们已经介绍了 ASP.NETMVC 默认提供的三种 FilterProvider ,以及各自 采用的 Filter 提供机制。当用于注册 FilterProvider 的静态类型 FilterProviders 被加载的时候, 它会默认创建这三种类型的对象并添加到通过静态 Providers 属性表示的 FilterProvider 列表 中,具体的逻辑体现在如下所示的代码片段中。也就是说, ASP.阳TMVC 在默认的情况下 会采用这三种 FilterProvider 来提供所有的 Filter 对象。 public static class FilterProviders { static FilterProviders() Providers = new FilterProviderCollection(); Providers.Add(GlobalFilters.Filters); Providers.Add(new FilterAttributeFilterProvider()); Providers.Add(new ControllerlnstanceFilterProvider()); public static FilterProviderCollection Providers{get;} 实例演示:验证 Filter 的提供机制和执行顺序( 8708, 8709 , 8710) 为了让读者对上面介绍的三种 FilterProvider 和各自实现的 Filter 提供机制具有一个更加 深刻的认识,我们来做一个简单的实例演示。在一个 ASP.阳T MVC 应用中定义了 ASP. NET MVC 4 在架揭秘7.3 筛选器的执行 • 351 F ooAttribute 、 BarAttribute 和 B azA ttribute 三个 ActionF i1 ter ,如下面的代码片段所示,它们 都继承自我们自定义的 F i1 terBaseAttribute 。 public abstract class FilterBaseAttribute:FilterAttribute , IActionFilter public void OnActionExecuted(ActionExecutedContext filterContext) { } public void OnActionExecuting(ActionExecutingContext filterContext) { } public class FooAttribute : FilterBaseAttribute { } public class BarAttribute : FilterBaseAttribute { } public class BazAttribute : FilterBaseAttribute { } 首先在默认生成的 F i1 terConfig 类型中通过如下的方式将 Ba zA ttribute 注册为一个全局 Filter 。由于 FilterConfig 默认情况下会注册一个针对 HandleError Attribute 特性的全局 Filter , 在这里我们将这行代码注释掉。 αd -l FTE 口。c r e ←」14 ·工FL s s a 14 C C .工14 b u p 品, 1 public static void RegisterGlobalFilters(GlobalFilterCollection filters) //filters.Add(new HandleErrorAttribute()); GlobalFilters.Filters.Add(new BazAttribute()); 接下来定义了如下一个 HomeController 。我们定义的 FooAt位ibute 应用在 HomeCon住oller 类 型上, Action 方法 DemoAction 上则应用了 B 町A世ibute 。在 Action 方法In dex 中我们创建了一个 描述 Action 方法 DemoAction 的 Actio nD escriptor 对象,并利用静态类型 FilterProviders 得到最终 应用到该 Action 的所有 Filter 对象,最后将这个 Filter 集合作为 Model 呈现在默认的Vi ew 中。 [Foo] public class HomeController : Controller public ActionResult Index() ReflectedControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(typeof(HomeController)); ActionDescriptor actionDescriptor = controllerDescriptor.FindA ction( ControllerContext , "DemoAction"); IEnumerable filters = FilterProviders.Providers.GetFilters( ControllerContext , actionDescriptor); return View(filters); [Bar] public void DemoAction() { } ASP. NET MVC 4 在架揭秘352 • 第 7 章 Action 的执行 如下所示的 Action 方法Index 对应Vi ew 的定义,这是一个 Model 类型为 IEnwnerabl e 的强类型 View ,在该 View 中我们将所有 Filter 对象的 Instance 属性类型、 Order 和 Scope 属 性呈现在一个表格中。 @model IEnumerable Filters
    ControllerActionNarneActionDescriptor
    @itern.Key.Narne @actionDescritors[O] .ActionNarne @actionDescritors[O] .GetType() .Narne
    @actionDescritors[i] .ActionNarne @actionDescritors[i] .GetType() . Narne
    @foreach (Filter filter in Model)
    InstanceOrderScope
    @filter.lnstance.GetType() .Name @filter.Order @filter.Scope
    运行程序之后会在浏览器中呈现如图 7-8 所示的输出结果,可以清楚地看到最终应用到 Action 方法 DemoAction 上的筛选器有 4 个,分别对应着应用到 HomeController 类型上的 F ooAttribute ,应用到 DemoAction 方法上的 BarAttribute ,全局注册的 B azAttribute 和 HomeController 本身。 (S708) 醒苦苦 曹 图 7-8 通过不同方式注册的 Filter 列表 在前面的内容中我们提到如果同一 Action 具有多个相同类型的 Filter ,其 Order 和 Scope 属性最终决定了 Filter 执行的顺序。根据图 7-8 所示的四个 Filter 的 Order/Scope 属性值,它 们执行的先后顺序应该是 HomeControlle r=>B azA t位 ibute=>F ooAttribute=>BarAttribute ,现在 就通过实例来证实这一点,为此我们在 FilterBaseAttribute 的 O nA ctio nE xecuting 方法中将当 前 FilterAttribute 的类型和方法名(" OnActionE xecuting ")写入当前的 HtφResponse 并最终 呈现在浏览器上。 public abstract class FilterBaseAttribute:FilterAttribute , IActionFilter AS P. NET MVC 4 框架揭秘7 . 3 筛选器的执行 电 353 public void OnActionExecuted(ActionExecutedContext filterContext} { } public void OnActionExecuting(ActionExecutingContext filterContext} filterContext.HttpContext.Response.Write( string.Format("{O}.O nA ctio nE xecuting() " ,由 is . Ge t Type () ) ) ; 然后我们按照相同的方式重写了 HomeController 的 O nA ctio nE xecuting 方法,将自身的 类型和当前方法名称呈现出来。 [Foo] public class HomeController : Controller //其他成员 protected override void OnActionExecuting( ActionExecutingContext filterContext} Response.Write("HomeController.OnActionExecuting(}
    "}; [Bar] public void DemoAction(} { } 再次运行我们的程序并在浏览器上指定正确的地址访问定义在 HomeCon位 oller 的 Action 方法 DemoAction. 会在浏览器中呈现如图 7-9 所示的输出结果。这个结果体现了应用到 Action 方法 Data 上的四个 Actio nF ilter 执行的顺序,而这与通过 Order 和 Scope 属性决定的 顺序是一致的。 CS709) Hom eC o由oDer.OnActionE xecutingQ M叹App.B 但Attnbute. OnA ctionE xecutingQ MvcApp.F∞A伽lbute . OnA cti onEx ecutïngQ 协rcApp .B笛Attnbute. OnA c tionExe Cl血.gQ 图 7-9 应用在同一个 Action 方法中的同类筛选器的执行顺序 关于 Filter 的提供机制还有另一个值得深究的问题,即我们在定义 FilterAttribute 的时候 可以将应用的 A伽 ibuteU sageAttribute 特性的 AllowMultiple 属性设置为 False 使它只能在同 一个目标元素上使用一次,但是我们依然可以在 Action 方法和所在的 Controller 类型上应用 它们,甚至可以将它们注册为全局 Filter. 那么这些 FilterAttribute 都将有效吗? 现在就来通过实例来验证这一点。删除现有的所有的 FilterAttribute. 并定义如下一个类 型为 FooAttribute 的 Actio nF ilter. 并将应用在它上面的 Attribute U sageAttribute 特性的 ASP. NET MVC 4 框架揭秘354 由 第 7 章 Action 的执行 AllowMultiple 属性设置为 False 。 [AttributeUsage(AttributeTargets.Class I AttributeTargets.Method , AlloWMultiple = false)] public class FooAttribute : FilterAttribute , IActionFilter public void OnActionExecuted(ActionExecutedContext filterContext) { } public void OnActionExecuting(ActionExecutingContext filterContext) { } 现在我们将该 FooAttribute 特性同时应用在 HomeController 类型和 Action 方法 DemoAction 上,然后在 Globa l. asax 中注册一个针对它的全局 Filter 。 [Foo] public class HomeController : Controller //其他成员 [Foo] public void DemoAction() { } αJ -l ZL n o 户」r e +」14 .l F S s a l c c .l 、4b u p{ public static void RegisterGlobalFilters(GlobalFilterCollection filters) //filters.Add(new HandleErrorAttribute())i GlobalFilters.Filters.Add( 口 ew FooAttribute())i 直接运行我们的程序后浏览器中会呈现出如图 7-10 所示的输出结果,可以清楚地看到 虽然我们在三个地方注册了 FooAttribute ,但是由于该特性的 AllowMultiple 属性为 False , 所以只有其中一个 FooAttribute 最终是有效的。从设计上这也很好理解,虽然我们可以将多 个同类的 Filter 注册为不同的范围 (Action 、 Controller 和 Global) ,但是它们最终还是应用 到具体某个 Action 方法上,我们将应用在它们上的 Attribute U sageAttribute 的 AllowMultiple 属性设置为 False ,就是希望 Action 方法在执行的时候只有一个 Filter 被执行。 (S710) 图 7-10 AllowMultiple 为 False 的 FilterAttribute 的唯一性 如果以不同的 Scope 注册了多个 AllowMultiple 属性为 False 的 FilterAttribute ,最终有效 ASP. NET MVC 4 框架揭秘7.3 筛选器的执行 黯 355 的是哪个呢?从图 7-10 可以看出,貌似应用在 Action 方法 CScope 为 Action) 上的 F ooAttribute 是有效的。其实具体的逻辑是这样的:所有被创建的 Filter 按照 Order 和 Scope 进行排序(即 Filter 执行的顺序) ,最后的那个会被选用。 Filterlnfo ASP.NET MVC 的四种筛选器最终被封装成相应的 Filter 对象,但是它们执行的时机和 方式是不同的,所以在执行之前需要根据被封装的筛选器类型对所有 Filter 进行分组。具体 来说,当 Actionlnvoker 被调用的时候,它会利用静态类型 FilterProviders 得到所有注册的 FilterProvider ,并利用它们根据当前 ControllerContext 和描述目标 Action 的 Actio nD escriptor 对象得到所有的 Filter 对象,然后根据其 Instance 属性表示的筛选器类型将它们分组,最终 得到一个具有如下定义的 System. Web.Mvc.Filte rI nfo 类型的对象。 0 5' 』 n TE-r e t -4 ·工F s s a 14 C C .-14 .b u p{ public Filterlnfo(); public Filterlnfo(IEnumerable filters); public IList ActionFilters { get; } public IListAuthorizationFilters { get; } public IList ExceptionFilters { get; } public IList< 工 ResultFilter> ResultFilters { get; } 如上面的代码片段所示,可以通过指定一个 Filter 列表来创建一个 Filterlnfo 对象,它具 有针对 4 种选器类型的列表属性,如果我们在构造函数中指定了一个 Filter 列表,那么每个 Filter 的 Instance 属性表示筛选器会被提取出来并根据类型添加到某个对应的筛选器列表之中。 7.3.2 AuthorizationFilter 在总体讲述了筛选器及其提供机制之后,我们按照执行的先后顺序对四种不同的筛选器 进行单独介绍,首先来介绍最先执行的 Authorizatio nF ilter 。从命名来看, Authorizatio nF ilter 用于完成授权相关的工作,所以它应该在 Action 方法被调用之前执行才能起到授权的作用。 不仅限于授权,如果我们希望在目标 Action 方法被调用之前"做点什么",都可以通过自定 义的 Authorizatio nF ilter 来实现。 Authorizatio nF ilter 实现了接口 System. Web.Mvc.IAuthorizatio nF ilter 。如下面的代码片段 所示, IAuthorizatio nF ilter 定义了一个 O nA uthorization 方法用于实现授权的操作。该方法的 参数 filterContext 表示一个表示授权上下文的 System.Web.Mvc.AuthorizationContext 对象,它 直接继承自 ControllerContext 。 public interface IAuthorizationFilter void OnAuthorization(AuthorizationContext filterContext); AS P. NET MVC 4 框架渴秘356 • 第 7 章 Action 的执行 public class AuthorizationContext : ControllerContext public AuthorizationContext(); public AuthorizationContext(ControllerContext controllerContext , ActionDescriptor actionDescriptor); public virtual ActionDescriptor ActionDescriptor { get; set; } public ActionResult Result { get; set; } AuthorizationContext 的 Actio nD escriptor 属性表示描述当前执行 Action 的 Actio nD escriptor 对象,而 Result 属性返回一个用于在授权阶段直接对请求进行响应的 ActionR esulto Authorizatio nF ilter 的执行是 Actionlnvoker 进行 Action 执行的第一项工作,因 为后续的工作 CModel 绑定、 Model 验证、 Action 方法执行等)的执行只有在成功授权的情 况下才有意义。 Actio nInvoker 在执行 Authorizatio nF ilter 之前,会先根据当前的 Co∞ntωro刨咄ollerι'Co∞nt臼ex对t 和描述 当前 Action 的 Actωion曲1山De臼scαr桐 O创r 创建一个表示授权上下文的 Au旧I此thor白i泣za州tio∞nCo∞nt臼e侃创x刘t 对象。然后 它将此 AuthorizationContext 对象作为参数,按照 Filter 对象 Order 和 Scope 属性决定的顺序 执行所有 AuthorizationFilter 的 O nA uthorization 方法。 在所有的 AuthorizationFilter 都执行完毕之后,如果 AuthorizationContext 对象的 Result 属性表示的 Actio nR esult 不为 Null ,整个 Action 的执行将会终止,该 Actio nResult 将直接被 执行用于完成对当前请求的响应。一般来说,某个 Authorizatio nF ilter 在对当前请求实施授 权的时候,如果授权失败它可以通过设置 A uthorizationContext 对象的 Result 属性回复一个 " 40 1 , Unauthorized" 响应,或者重定向到一个错误页面。 AuthorizeAtlribute 如果我们要求某个 Action 只能被认证的用户访问,可以在 Con位oller 类型或者 Action 方 法上应用具有如下定义的 System. Web.Mvc.AuthorizeAttribute 特性。 AuthorizeAttribute 还可以 设置目标 Action 可被访问的用户账号或者角色,字符串属性 Users 和 Roles 用于指定被授权的 用户名和角色列表,每个用户名/角色之间用采用逗号作为分隔符。如果没有显式地对 Users 和 Roles 属性进行设置, AuthorizeAt创 bute 在进行授权操作的时候只要求当前用户是经过认证的。 [AttributeUsage(Attr 工 buteTargets.Method I Attr 工 buteTargets.Class , Inherited=true , AllowMultiple=true)] public class AuthorizeAttribute : FilterAttribute , IAuthorizationFilter 11 其他成员 public virtual void OnAuthorization(AuthorizationContext filterContext); protected virtual HttpValidationStatus OnCacheAuthorization( HttpContextBase httpContext); public string Roles { get; seti } public override object Typeld { get; } public stri 口 g Users { get; seti } AS P. NET MVC 4 框架揭秘7.3 筛选器的执行 路 357 如果授权失败(当前访问者是未被认证用户,或者当前用户的用户名或者拥有的角色没 有在指定的授权用户或者角色列表中),一个 System. Web.Mvc.Http UnauthorizedResult 对象会 被创建并赋值给 AuthorizationContext 的 Result 属性,意味着客户端会接收到一个状态为 " 40 1 , Unauthorized" 的响应。 有一些读者会将 AuthorizeAttribute 特性对方法的授权与 PrincipalPermissio nA ttribute 特 性等同起来,实际上不但它们实现授权的机制不一样(后者是通过代同访问安全检验实现对 方法调用的授权) ,其授权策略也不一样。以下面定义的两个方法为例,应用了 PrincipalPermissionAttribute 特性的 PooOrAdmin 方法可以被账号为 "Poo" 或者拥有 "Admin" 角色的用户访问,而应用了 AuthorizeAttribute 特性的方法 PooAndAdmin 方法只能被拥有角 色 "Admin" 的用户 "Poo" 调用。也就是说 PrincipalPermissio nA ttribute 特性对 User 和 Role 的授权逻辑是"逻辑或",而 AuthorizeAttribute 采用的则是"逻辑与"。 [PrincipalPermission( SecurityAction.Demand , Name="Foo" , Role="Admin")] public void FooOrAdmin() { } [Authorize(Users="Foo" , Roles="Admin")] public void FooAndAdmin() { } 除此之外,我们可以将多个 PrincipaIPermissio nAttribute 和 AuthorizeAttribute 特性应用到 同一个类型或者方法上。对于前者,如果当前用户通过了任意一个 Principa lP ermissio nA t位 ibute 特性的授权就有权调用目标方法,对于后者来说,则意味着需要通过所有 AuthorizeAttribute 特性的检验才具有了调用目标方法的权限。以如下两个方法为例,用户 "Poo" 或者 "Bar" 可以有权限调用 PooOrBar 方法,但是没有任何一个用户有权调用 CannotCall 方法。 [PrincipalPermission( SecurityAction.Demand , Name="Foo") [PrincipalPermission( SecurityAction.Demand , Name="Bar")] public void FooOrBar() { } [Authorize(Users="Foo")] [Authorize(Users="Bar")] public void CannotCall() { } RequireHttpsAttribute 从名称也可以看出来, S句y严S如t臼em 求用户总是以 HTTP陀S 请求的方式访问目标 Actio∞n 。如果当前并不是一个 HTTP内S 请求(通过 当前 H 仗即pR肚eqψue创s剖t 的 IsSecωure它eCωonne创ωctio∞n 属性判断 )λ, Re叫q甲ψu山lÎ血山ir让r诅e GET 的情况下会创建一个 S句y严f咯吼S剑侃t饨em.We由b.Mvc.Red副ire它ectRe臼su山Il山l让t 对象并作为 Aut此thor白i泣zationContext 的 Result 属性。 Redirec tResult 用于客户端的重定向,原请求地址的网络协议前缀 CScheme) 替换成 "h忧ps://" ASP. NET MVC 4 在架揭秘358 .• 第 7 章 Action 的执行 后得到的 URL 作为重定向的目标地址。比如当前请求地址为 h句 :llwww.artech. ∞m/home/index , RequireHtφsA忧 ribute 将其重定向到 https:llwww.artech.co m/ home/index 。我们将在第 8 章 "View 的呈现"中对 RedirectResult 进行单独介绍。 [AttributeUsage(AttributeTargets.Method I AttributeTargets.Class , Inherited=true , AllowMultiple=false)] public class RequireHttpsAttribute : FilterAttribute , IAuthorizationFilter protected virtual void HandleNonHttpsRequest( AuthorizationContext filterContext); public virtual void OnAuthorization(AuthorizationContext filterContext); RequireHttpsAttribute 特性针对 HTTPS 的重定向仅限于 HTTP-GET 请求,如果当前请求 的 HTTP 方法并不是 GET , RequireHttpsAttribute 会直接抛出一个 InvalidOperatio nE xception 异常。如上面的代码片段所示,针对非 HTTPS 请求的处理通过调用受保护的方法 HandleN o nH t甲 sRequest 来完成,如果我们需要采用不同的处理方法,可以继承 RequireHttpsAt位 ibute 并重写该方法。 Validatel npu tAttribute 为了避免用户在请求中嵌入一些不合法的内容对网站进行恶意攻击(比如 XSS 攻击), ASP.NET 需要对请求的输入进行验证。如下面的代码片段所示,表示 Hπ? 请求的抽象类 型 HtφRequestBase 具有一个 Validatelnput 方法用于验证请求的输入。实际上这个方法仅仅 是在请求上作一下标记而己,在读取相应的请求输入时才根据这些标记决定是否需要进行相 应的验证,不过为了便于表达,我们还是将针对 Validatelnput 方法的调用说成是对请求输入 的验证。 public abstract class HttpRequestBase //其他成员 public virtual void Validatelnput(); 所有 Con位 oller 的基类 ControllerBase 具有如下一个布尔类型的属性 ValidateRequest ,它 表示是否需要对请求输入进行验证,在默认情况下该属性的默认值为 True ,意味着针对请求 输入的验证在默认情况下是开启的。当 Actio nI nvoker 在完成了对所有 Authorizatio nF ilter 的 执行之后,会根据该属性决定是否需要通过调用当前 HttpRequest 的 Validatelnput 方法进行 请求输入的验证。 public abstract class ControllerBase : IController //其他成员 public bool ValidateRequest { get; set; } 也正是由于 Actio nI nvoker 针对请求输入验证是在所有 Authorizatio nF ilter 执行之后进行 ASP. NET MVC 4 框架揭秘7.3 筛选器的执行 旬 359 的,所以我们可以通过自定义 Au也 orizationF ilter 的方式来设置当前 Con位 oller 的 ValidateRequest 属性进而开启或者关闭针对请求输入的验证。 System. Web.Mvc. Validate Inpu tA ttribute 就是这么 做的,这可以从如下表示 ValidateIn putAt 位ibute 的定义看出来(构造函数的参数 enableValidation 表示是否启动针对请求的输入验证)。 [AttributeUsage(AttributeTargets.Method I AttributeTargets.Class , Inherited=t 口le , AllowMultiple=false)] public class ValidatelnputAttribute : FilterAttribute , IAuthorizationFilter public ValidatelnputAttribute(bool enableValidation) this.EnableValidation = enableValidation; public virtual void OnAuthorization(AuthorizationContext filterContext) if (filterContext == null) throw new ArgumentNullException("filterContext"); filterContext.Controller.ValidateRequest = this.EnableValidation; public bool EnableValidation { get; private set; } 为了让读者对 ValidatelnputAttribute 针对开启和关闭输入验证的作用有一个深刻的认 识,我们来进行一个简单的实例演示。在一个 AS 丑陋 T MVC 应用中定义了如下一个 HomeController ,包含在该 Con位oller 中的两个 Action 方法 CActionl 和 Actio n2)具有两个 字符串类型的参数 foo 和 b 町,其中 Actionl 方法上应用了 Validatelnpu tAttribute 特性并将参 数设置为 False 。 public class HomeController : Controller [Validatelnput(false)) public void Actionl(string foo , string bar) Response.Write(string.Format("{O}: (l}
    " , "foo" , HttpUtility.HtmlEncode(foo))); Response.Write(string.Format("{O}: (l}
    " , "bar" , HttpUtility.HtmlEncode(bar))); pub 工 ic void Action2(string foo , string bar) Response.Write(string.Format("{O}: (l}
    " , "foo" , HttpUtility.HtmlEncode(foo))); Response.Write(string.Format("{O}: (l}
    " , "bar" , HttpUtility.HtmlEncode(bar))); 直接运行该程序并在浏览器中通过输入相应的地址来访问这两个 Action ,我们以查询字 ASP. NET MVC 4 在架揭秘360 喝 第 7 章 Action 的执行 符串的形式指定它们的两个参数。为了检验 ASP.NETMVC 对请求输入的验证,我们将表示 参数 foo 的查询字符串的值设置为 "
    "。如图 7-11 所示, Actionl 能够正常地 被调用,而 Actio n2在调用过程中抛出异常,并提示请求中包含危险的查询字符串。 (S711) Server Error in 11' Applìcation. A potentíally dangerous Reques t. Query5tring vaJue was detected from the c1 ient (foo=" < scrip t> "). Description: ASP .NET h... detected dala in 伽 e re制 esl th .. 1 is potentiω Iy dangerous be臼 use 画 mighl include Hn~l markup or script. Th e dala mig h.t represent an attempt to ∞ mpromise Ih e secu 咐。 f your applicaiion , such as a cmS8-sile scripting attack. 1I Ih 国 type 01 input is app r<> pria 恒 in your appficalion , you can inc lU元素提供属于攻击者自身的 Email 地址,由于注册了 window 的 onload 事件,该表单会在页面加载完成之后自动提交。 假设攻击者部署该页面的地址为 http://bar/maliciouspage.html ,攻击者在某篇博文中 添加一个包含如下 HTML (一张不能正常显示的图片〉的评论,作为博主的我们在登录情 况下打开这篇博文之后就会对这张图片的源地址发起请求,而定义在上面的这个表单被自 动提交。 ... ASPNETMVC4 框架揭秘362 黯 第 7 章 Action 的执行 由于登录用户的安全令牌一般以 Cookie 形式存在,而该 Cookie 会存在于发送给针对 Action 方法 UpdateEmailAddress 的调用请求中〈针对等标签在跨 域访问中是否会发送非 Session Cookie 决于我们采用的浏览器),服务器会认为该请求来自 被认证用户,所以最终造成了我们的 Email 地址被恶意修改而不自知。 这个例子充分说明了 CSRF 是一种比较隐蔽并且具有很大危害的网络攻击,促成攻击的 原因在于服务器在执行目标操作的时候并没有验证请求的真正来源。对于 ASP.NETMVC 来 说,如果我们在执行某个 Action 方法之前能够确认当前的请求来源的有效性,就能从根本 上解决 CSRF 攻击。 ValidateAntiForgeryTokenAttribute 结合 HtmIHelper 的AntiForgeryToken 方法有效地解决了这个问题。 r e p --e H 14 m +」H S s a 14 C C .工l b u p{ //其他成员 public MvcHtmlString AntiForgeryToken(); public MvcHtmlString AntiForgeryToken(string salt); public MvcHtmlString AntiForgeryToken(string salt, string domain, string path); 如上面的代码片段所示, HtmlHelper 具有三个 AntiForgeryToken 方法(这里的方法是 HtmlHelper 的实例方法,不是扩展方法)。当我们在一个 View 中调用这些方法时,它们会为 我们创建一个所谓"防伪令牌 C Anti-Forgery Token)" 的字符串,并以此生成一个类型为 hidden 的 7G素。除此之外,该方法的调用还会根据这个防伪令牌设置一个具有 HttpOnly 标 记的 Cookie 。接下来就来详细讨论这个过程。 上述防伪令牌是通过一个AntiForgeryData 对象生成的,如下面的代码片段所示, AntiF orgery Data 是一个具有四个属性的内部类型,其核心是通过属性 Value 表示的"值"。 属性 UserName 和 CreationD ate 表示访问令牌授权的用户名和创建时间。字符串属性 Salt 是 为了增强防伪令牌的安全系数,不同的 Salt 值会生成具有不同内容的防伪令牌,不同的防伪 令牌在不同的地方被使用可以避免攻击者对一个防伪令牌的破解而使整个应用受到全面的 攻击。 ValidateAntiForgeryTokenAttribute 也具有一个同名的属性。 internal sealed class AntiForgeryData e m qdα71qd nn 巾ιn ·工·工 e· 工 rrtr ttat ssDS CCCC ·工 -1·l· 工 -14141 bbbb uuuu pppp Value { get; set; } Salt { get; set; } CreationDate { get; set; } Username { get; set; } 当AntiF orgeryToken 方法被调用时,它会先根据当前的请求的应用路径(对应 HttpRequest 的 ApplicationPath 属性)计算出表示防伪令牌 Cookie 的名称,该名称会在通过 对应用路径进行 Base64 编码(编码之前需要进行一些特殊字符的替换工作)生成的字符串 前添加"_ RequestVerificationToken" 前缀。 ASP. NET MVC 4 在架揭秘7.3 筛选器的执行 白 363 如果当前请求具有一个同名的 Cookie ,则直接通过对 Cookie 的值进行反序列化得到一 个AntiForgeryData 对象。需要注意的是,这里针对AntiForgeryData 进行序列化和反序列化 并不是一个简单的在对象到字符串之间进行转换的过程,还包含采用 MachineKey 对 AntiFo电eryData 的四个属性进行加密/解密的过程。 如果这样的 Cookie 不存在, HtmlHelper 会随机生成一个长度为 16 的字节数组,并对它 进行 Base64 编码,然后创建一个AntiF orgeryData 对象并将这个编码后的字符串作为它的值 (Value 属性值〉。系统当前时间 (UTC) 作为该AntiForgeryData 对象的创建时间,但是该 AntiF orgeryData 对象的 UserName 和 Salt 属性为空。 接下来 HtmIHelper 会根据之前计算出来的 Cookie 名称创建一个 HtφCookie 对象,新创 建出来的AntiF orgeryData 对象被序列化后生成的字符串作为该 H仕pCookie 的值。如果我们 在AntiF orgeryToken 方法调用时设置了表示域和路径的 domain 和 path 参数,它们将作为该 HttpCookie 对象的 Domain 和 Path 属性。 HtmlHelper 最后将 HttpCookie 对象写入当前的 HTTP 响应 (Set-Cookie) 。 AntiF orgeryToken 方法返回的是一个类型为 hidden 的 元素对应的 HTML ,该 Hidden 元素的名称为 "_RequestVerificationToken" (即代码访问令牌 Cookie 名称的前缀〉。 为了生成该 Hidden 元素的值, HtmlHelper 会根据现有的AntiForgeryData 对象(从当前请求 获取的或者新创建的)创建一个新的AntiF orgeryData 对象,新旧两个对象具有相同的 CreationD ate 和 Value 属性,而当前用户名和指定的 Salt 参数将会赋值给新AntiForgeryData 对象的 UserName 和 Salt 属性(原AntiF orgeryData 不具有两个属性)。 @using (Html.BeginForm()) @Html.AntiForgeryToken("647B8734-EFCA-4F51-9D98-36502D13E4E7") 假如我们在一个 View 中通过如上所示的代码在一个表单中调用 HtmlHelper 的方法 AntiForgeryToken (将一个 GUID 作为 Salt) ,在最终生成的 HTML 中将会具有如下一个名为, 6 二RequestVerificationToken" 的 Hidden 元素。
    对于该 View 的首次访问或者对应的 Cookie 不存在,如下所示的一个名称为 " _ RequestVerificationToken _ LO 12YOFwcDEx" 的 -Cookie (代表防伪令牌)将会出现在响 应中。由于设置的 Cookie 具有 HttpOnly 标记,客户端(浏览器〉是不能通过脚本获取到 ASP. NET MVC 4 在架揭秘364 11 第 7 章 Action 的执行 Cookie 的值的。 e 卡 La v --r p k 0·· 14 00 or 。ι+ 」口 10 ·c 寸4- /e p 嘈 n TC Ta HC Set-Cookie: ___ RequestVerificationToken_L012YOFwcDEx=EYPOofprbBOog8vI+PzrlunYOYe5BihYJg OIYBqzvZDZ+hcT5QUu+fj2hvFUVTTCFAZdjgCPzxwIGsoNdEyD8nSUbgapk8Xp3+ZD8cxguUrg 101AdFd4ZGWEYzzOIN5815saPJpuaChVR4QaMNbilNG4y7xiN2/UCrBF80LmP04=; pa世1=/ ; HttpOnly 如果 ASP.NETMVC 确保其请求提交的表单具有一个名为"一RequestVerificationToken" 的 Hidden 元素,并且该元素的值与对应的防伪令牌的 Cookie 值相匹配,就能够确保请求并 不是由第三方恶意站点发送的,进而防止 CS盯攻击。原因很简单,由于 Cookie 值是经过 加密的,供给者可以得到整个 Cookie 的内容,但是不能解密获得具体的值(AntiForgeryData 的 Value 属性),所以不可能在提供的表单中也包含一个具有匹配值的 Hidden 元素。针对防 伪令牌的验证就实现在 ValidateAntiForgeryTokenAttribute 的 OnAuthorization 方法中。 我们来具体介绍一下实现在 ValidateAntiF orgeryTokenAttribute 中针对防伪令牌的验证逻 辑。首先它根据当前请求的应用路径采用与生成防伪令牌 Cookie 相同的逻辑计算出 Cookie 名称。如果对应的 Cookie 不存在于当前请求中,则直接抛出 System. Web.Mvc.Http AntiF orgery Exception 异常,否则获取 Cookie 值,并反序列化生成一个 AntiForgery Data 对象。 ValidateAntiForgeryTokenAt创bute 从提交的表单中提取一个名为" RI叫uestVerificationToken " 的输入元素,如果这样的元素不存在,同样抛出 Ht甲AntiFo皂eryException 异常,否则直接 对具体的值进行反序列化生成一个 AntiForgery Data 对象。最后它对这两个AntiForgeryData 的 Value 属性,以及将后者的 UserName 和 Salt 属性与当前用户名和自身的 Salt 属性进行比 较,任何一个不匹配都会抛出 HttpAntiForgery Exception 异常。 ChildActionOnlyAttribute 如果我们希望定义在 Controller 中的方法能以子 Action 的形式在某个 View 中被调用(这 样的调用一般用于生成组成页面的某个部分的 HTML) ,可以在方法上应用具有如下定义的 System.Web.Mvc.ChildAction臼llyAttribute 特 d性。 ChildActionOnlyAttribute 是一个 AuthorizationFilter , 它在重写的 OnAuthorization 方法中对当前请求进行验证,对于非子 Action 调用它会直接抛 出一个 InvalidOperationException 异常。 [AttributeUsage(AttributeTargets.Method I AttributeTargets.Class, AllowMultiple=false, Inherited=true)] public sealed class ChildActionOnlyAttribute : FilterAttribute, IAuthorizationFilter public void OnAuthorization(AuthorizationContext filterContext); ASP. NET MVC 4 框架揭秘7.3 筛选器的执行 蝇 365 有的读者可能会问, Authorizatio nF ilter 如何区分当前的请求是基于子 Action 的调用, 而不是一般的来自客户端的请求呢?其实很简单,当我们在调用 HtmlHelper 的扩展方法 Actio nIR enderAction 的时候会将当前的 ViewContext 作为 "p 盯 ent ViewContext" 保存到表示 当前路由数据的 RouteData 的 DataTokens 属性中,对应的 Key 为 "ParentActionViewContext" 。 如下面的代码片段所示, ControllerContext 的 IsCh i1dA ction 属性正是通过该路由信息来判断 当前请求是否是针对子 Action 的调用。 public class ControllerContext //其他成员 public virtual bool IsChil dAction get RouteData routeData = this.RouteDatai if (routeData == null) return falsei return routeData. DataTokens.ContainsKey ("ParentActionViewCont ext")i 7.3.3 ActionFilter 在前面介绍 AsyncManager 的时候我们己经涉及到了两个 Actio nF ilter 的使用,它们是用 于控制超时时限的 AsyncTimeoutA ttribute 和 NoAsyncTimeou tA伽 ibuteo Actio nF ilter 允许我们 在 Action 方法执行前后对调用进行拦截以执行一些额外的操作, Actio nF ilter 实现了具有如 下定义的接口 System. Web.Mvc .I Actio nF ilter 。 public interface IActionFilter void OnActionExecuting(ActionExecutingContext filterContext)i void OnActionExecuted(ActionExecutedContext filterContext)i public class ActionExecutingContext : ControllerContext public ActionExecutingContext()i public ActionExecutingContext(ControllerContext controllerContext , ActionDescriptor actionDescriptor , IDictionary actionParameterS)i public virtual ActionDescriptor ActionDescriptor { geti seti } public virtual IDictionary ActionParameters { geti seti } public ActionResult Result { geti seti } public class ActionExecutedContext : ControllerContext ASPNETMVC4 蓓架揭秘366 省第 7 章 Action 的执行 public ActionExecutedContext()i public ActionExecutedContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor, bool canceled, Exception exception)i public virtual ActionDescriptor public virtual bool public virtual Exception public bool public ActionResult ActionDescriptor { geti seti } Canceled { geti seti } Exception { geti seti } ExceptionHandled { geti seti } Result { geti seti } 如上面的代码片段所示, IActionFilter 接口具有 OnActionExecuting 和 OnActionExecuted 两个方法,它们分别在目标 Action 方法执行前后被调用。这两个方法分别具有两个基于上 下文的参数,类型分别是 System. Web.Mvc.ActionExecutingContext 和 System. Web.Mvc.Action ExecutedContext ,它们均是 Contro l1erContext 的子类。 我们可以从 ActionExecutingContext 对象中获取到用于描述当前 Action 的 ActionDescriptor 和参数列表。 ActionF ilter 可以在 OnActionExecuting 方法中对 ActionExecutingContext 对象的 Result 属性进行赋值来对当前的请求实施响应。一旦 ActionExecutingContext 的 Result 属性被成功赋值,将会终止后续 ActionF ilter 和最终目标方 法的执行。 ActionExecutedContext 具有额外的三个属性,其中属性 Exception 表示执行 Action 方法 过程中抛出的异常:属性 ExceptionHandled 是一个表示是否对异常己经做出处理的标记: Canceled 属性表示没有完成整个 ActionF ilter 链和目标 Action 方法的执行而中途被终止。 ActionFilter 的执行机制 当 ActionInvoker 在执行目标 Action 方法之前,会根据 Order 和 Scope 属性对 ActionF ilter 进行排序,然后根据当前 Contro l1erContext 和 ActionDescriptor 创建一个 ActionExecuting Context 对象,最后将其作为参数依次调用所有 ActionF ilter 的 OnActionExecuting 方法。 ActionF ilter 链中每一个 ActionF ilter 的 OnActionExecuting 方法执行完毕之后, ActionInvoker 在执行目标 Action 方法之后会根据当前 ControllerContext 、 ActionDescriptor 以及 Action 方法执行过程中抛出的异常创建一个 ActionExecutedContext 对象。该 ActionExecutedContext 的 Cancel 属性被设置为 False 。如果 Action 方法返回一个 ActionResult 对象,该对象将会作为该 ActionExecutedContext 的 Result 属性。 接下来 ActionInvoker 按照相反顺序的调用 ActionF ilter 链中每个 ActionFilter 的 OnActionExecuted 方法,执行过程中的 ActionF ilter 可以修改 ActionExecutedConte刻的 Result 属性。当整个 ActionF ilter 链执行结束之后, ActionExecutedContext 的 Result 属性返回的 ActionResult 将会用于针对请求的响应。图 7-12 基本上反映了整个 ActionFilter 链的执行 过程。 ASP.NET MVC 4 框主导雹秘• 367 筛选器的执行7.3 QSESEREa 连同目标 Action 在内的整个 ActionFilter 链的执行图 7-12 ActionFilter 对 ActionResult 的设置 上面我们已经提到过,在调用 Actio nF ilter 的 O nA ctio nE xecuting 方法的过程中,一旦某个 ActionF ilter 为 Actio nE xecutingContext 的 Result 属性设置了一个 Actio nR esult 对象,后续 Actio nF ilter 和目标 Action 将不会被执行。此时 Actio nInvoker 会创建一个 Actio nExecutedContext 对象,设置的 Actio nResult 直接作为其 Result 属性,而 Cancel 属性被设置为 True 。现在要 考虑的问题是之前的 Actio nF ilter 的 O nA ctio nE xecuted 是否还会执行呢? 我们不妨使用一个简单的演示实例来寻求问题的答案。在一个 ASP. 阳 TMVC 应用中定 义了如下三个 ActionF ilter (FooAt位 ibute 、 BarAttribute 和 B 缸At位 ibute) ,它们都继承自自定义 的 FilterBaseAttribute 。在 FilterBaseAttribute 中实现的 O nA ctio nE xecuting 和 O nA ctio nE xecuted 方法中,我们将 Actio nF ilter 自身的类型和执行方法名写入当前 HttpResponse 并最终呈现在 浏览器中。 BarAttribute 重写了 O nA ctio nE xecuting 方法,在调用基类同名方法之后为 Actio nE xecutingContext 的 Result 设置了一个 ErnptyResult 对象。 IActionFilter public abstract class FilterBaseAttribute : FilterAttribute , public virtual void OnActionExecuted (ActionExecutedContext fil terContext) filterContext.HttpContext.Response.Write( string.Format("{O}.OnActionExecuted()
    " , this.GetType() .Name))i public virtual void OnActionExecuting(ActionExecutingContext filterContext) filterContext.HttpContext.Response.Write( string.Format("{O}.OnActionExecuting()
    " , this.GetType() .Name))i ASPNETMVC4 框架揭秘368 • 第 7 章 Action 的执行 public class FooAttribute : FilterBaseAttribute { } public class BarAttribute : FilterBaseAttribute public override void OnActionExecuting (ActionExecutingContext filterContext) base.OnActionExecuting(filterContext)i filterContext.Result = new EmptyResult()i public class BazAttribute : FilterBaseAttribute { } 然后我们定义了如下一个 HomeController ,上面三个 Actio nF ilter 特性同时被应用到了 Action 方法 Index 上。我们对三个 Actio nF ilter 特性的 Order 属性作了相应地设置使它们可以 按照我们希望的顺序 (FooAttribute =>BarAttribute =>B 但 Attribute) 执行。 public class HomeController : Controller [Foo(Order = 1)] [Bar(Order = 2)] [Baz(Order = 3)] public void Index() Response.Write(~Index...
    ")i 运行该程序后会在浏览器中呈现出如图 7-13 所示的输出结果。根据这个输出结果可以 看出应用到同一个 Action 方法上的三个 Actio nF ilter 按照 Order 属性构建的 Actio nF ilter 链。 在执行 O nA ctio nE xecuting 方法的过程中,处于中间位置的 BarAttribute 将 Actio nE xecutingContext 的 Result 属性进行了相应设置后,位于它之前的 FooAttribute 的 O nA ctio nE xecuted 方法依然会执行。 (S712) F ooAttribute. OnActio nExecu世ngO BarA伯也 ute . OnA ctionE xe Cl血 19O FooAt缸司bute . OnActio nE xecutedO 图 7-13 ActionFilter 链执行流程 这个简单的实例揭示了应用到同一个 Action 方法上的 Actio nF ilter 链的执行机制:如果 某个 Actio nF ilter 在执行 O nA ctio nE xecuting 方法过程中对 Actio nE xecutingContext 的 Result 属性进行了设置,后续的 Actio nF ilter 和目标 Action 方法将不会再执行。此时 Actio nE xecutedContext 对象被创建, Actio nE xecutingContext 的 Result 属性表示的 Actio nResulut 对象将会作为它的 Result 属性。 ASP. NET MVC 4 蓓架揭秘握 369 接下来 ASP.NET MVC 会以前一个 ActionF ilter 作为起点逆向执行 ActionF ilter 链的 OnA ctionExecuted 方法,而作为参数的自然就是这个 ActionExecutedContext 对象。图 7-14 基本上揭示了整个 ActionF ilter 链执行的流程。 筛选器的执行7.3 U坐j 。至旦 3mznc 江揭 02hFnHgzmuhA 阳会每 @hM 03 》 953mvhAMQ 注仰 a 03kynHF03mxmncmmg 03 〉 953mxonc 【-3 白 03 >982mxoncnma 在 Action 方法执行前对 Result 的设置对整个 ActionFilter 链执行的影晌 如果在逆向执行 ActionF ilter 链的 OnActionExecuted 方法过程中某个 ActionF ilter 对 ActionExecutedContext 的 Result 属性作了相应的设置,后续的 Actio nF ilter 依然按照相应的 图 7-14 次序正常执行。 ActionFilter 中的异常处理 前面我们讨论了 ActionF ilter 链在执行过程中某个 ActionF ilter 分别在执行 和 OnA ctionExecuted 方法时分别对 ActionExecutingContext 和 ActionExecutedContext 的 Result 进行相应设置具有怎样的影响,现在我们讨论如果某个 ActionF ilter 在执行 OnA ctionExecuting 和 OnA ctio nExecuted 方法过程中抛出异常后,后续的 工作又将如何进行。 OnActionExecuting 如果 ActionF ilter 链的第一个 ActionF ilter 在执行 OnActionExecuting 或者 OnActionExecuted 方法的过程中出现异常,那么这个异常会被直接抛出。如果抛出异常的并不是第一个 ActionF ilter ,抛出异常会被捕捉。 Actionlnvoker 会创建一个 ActionExecuted Context 对象, 抛出的异常直接作为它的 Exception 属性。随后这个 ActionExecutedContext 对象被作为参数 调用前一个 ActionF ilter 的 OnA ctionExecuted 方法,如果在执行过程中这个 ActionF ilter 将 ActionExecutedContext 的 ExceptionHandled 属性设置为 True ,表明抛出的异常已经经过处理, ASPNETMVC4 框架揭秘Action 的执行 那么 Actio nI nvoker 会按照正常的方式逆向调用后续 Actio nF ilter 链(整个 Actio nF ilter 链中位 于当前 Actio nF ilter 之前的部分)。 第 7 章• 370 如果 Actio nF ilter 在执行 O nA ctionE xecuted 之后 ActionE xecutedContext 的 Exceptio nH andled 属性依然是 False ,之前捕获的异常会再次抛出来。 Actio nI nvoker 又会捕获这个异常创建一 个 Actio nE xecutedContext 对象作为参数调用前面的 Actio nF ilter 的 O nA ctio nE xecuted 方法 。 如果异常是在非链头的 Actio nF ilter 的 O nA ctio nE xecuted 方法中抛出,处理流程与此类似。 我们不妨举例说明 Action 链在执行过程中对异常的处理。假设具有如图 7-15 所示的包 含四个节点的 Actio nF ilter 链,其 Filterl 、 Filter2 和 Filter3 的 O nA ctio nE xecuting 方法先后正 常执行,但是 Filter4 在执行 O nA ctio nE xecuting 方法的时候抛出一个异常。这个异常会被 Actio nI nvoker 捕获并据此创建一个 Actio nE xecutedContext 对象, Actio nI nvoker 随后会将它 作为参数调用 Filter3 的 O nA ctio nE xecuted 方法(步骤 1) 。 〉白白一 03窍。问『 dohH ③ 03 〉口 S 。3m}(onc-oa ActionFilter 链执行过程中对异常的处理 如果 Filter3 在执行 O nA ctio nE xecuted 方法后 Actio nE xecutedContext 的 Exceptio nH andled 属性为 False ,之前捕获的异常会再次抛出来。 Actio nI nvoker 又会按照相同的方式根据抛出 的异常创建一个 Actio nE xecutedContext 对象,并作为参数调用 Filter2 的 O nA ctio nE xecuted 方法(步骤 2) 。 图 7-15 ASP. NET MVC 4 在架揭秘7.3 筛选器的执行 磁 371 如果 Filter2 在执行 OnActionExecuted 方法的过程中将 ActionExecutedContext 的 Exceptio吐Iandled 属性设置为 True ,表明抛出的异常已经经过合理的处理, ActionInvoker 会 按照正常的方式调用 Filterl 的 OnActionExecuted 方法(步骤 3) 。如果 Filterl 在执行 OnA ctionExecuted 过程中不出现异常,最终是不会有异常抛出的。 7.3.4 ExceptionFilter ExceptionFilter 是一个用于进行异常处理的筛选器。 ExceptionF ilter 不仅仅用于处理执行 整个 ActionF ilter 链(包括目标 Action 方法的执行)最终抛出的异常,还用于处理执行整个 ResultFilter 链(包括对 Action 方法返回的 ResultFilter 的执行〉最终抛出的异常。 ExceptionF ilter 实现了具有如下定义的接口 System. Web.Mvc .lExceptionF ilter 。 public interface IExceptionFilter void OnExceptio口 (ExceptionCo 口 text filterContext); public class ExceptionContext : ControllerContext public ExceptionContext(ControllerContext controllerContext, Exceptioηexception); n 0 ·工•L p et c­XU PU-q > e 14DA a 口 uo t--i rOLt ·工 OC vbA CCC ·工·工 -l 11414 bbb uuu ppp Exception { get; set; } Exception 日 andled { get; set; } Result { get; set; } 如上面的代码片段所示, IExceptionF ilter 具有唯一的方法 OnException 用于进行异常处 理,该方法的参数是一个类型为 System. Web.Mvc.ExceptionContext 的上下文对象。 ExceptionContext 同样是 ControllerContext 的子类,它的 Exception 表示抛出的异常,而 ExceptionHandled 属性表示是否己经完成了对异常的处理。如果需要对请求作出响应,需要 为 ExceptionContext 的 Result 属性设置一个 ActionResult 对象。我们可以设置一个 ViewResult 显示一个错误页面:对于 Ajax 请求,可以返回一个包含异常信息的 JsonResult 对象。 最终应用到某个 Action 方法上的多个 ExceptionF ilter 根据 Order 和 Scope 属性排列成一 个 ExceptionF ilter 链,对于 ExceptionF ilter 的执行,有如下三点需要着重强调。 • ExceptionF ilter 链是反向执行的。对于根据 Order 和 Scope 属性排好序的 ExceptionFilter 链,排在后面的具有更高的执行优先级。 • 将 ExceptionContext 的 ExceptionHandled 设置为 True 并不能阻止后续 ExceptionF ilter 的 执行。 • 如果 ExceptionF ilter 在执行 OnException 过程中出现异常,整个 ExceptionF ilter 链的执 行将立即终止,并且该异常会被直接抛出来。 ASP. NET MVC 4 在架揭秘372 寄自 第 7 章 Action 的执行 HandleErrorAtlribute ASP.NET MVC 提供了一个 System. Web.Mvc.HandleErrorAttribute 使我们可以针对具体 的异常类型来呈现对应的错误页面。如下面的代码片段所示, HandleErrorAttribute 具有一个 表示被处理异常类型的 ExceptionType 属性,只有在被处理异常与该类型匹配的情况下通过 HandleErrorAttribute 指定的作为错误页面的 View 才会呈现出来。该属性的默认值为 System.Exception 类型,意味着默认情况下 HandleErrorAttribute 可以处理所有类型的异常。 [AttributeUsage(AttributeTargets.Method I AttributeTargets.Class, Inherited=true, AllowMultiple=true)] public class HandleErrorAttribute : FilterAttribute, IExceptionFilter public virtual void OnException(ExceptionContext filterContext); •L c e .「」b o e d qqi nnr e· 工·工 r prre yttv TSSO CCCC ---l-L-1 14141 占 1- bbbb uuuu pppp Exceptio 口 Type { get; set; } Master { get; set; } View { get; set; } TypeId { get; } HandleErrorAttribute 对异常的处理策略就是基于异常类型的错误页面的呈现,它的属性 View 和 Master 表示作为错误页面的 View 名称和对应的布局文件名,默认值分别为 "E町or" 和空字符串。 HandleErrorAttribute 在进行 View 呈现的时候会根据当前 Controller 和 Action 的名称以及抛出的异常创建一个具有如下定义的 System. Web.Mvc.HandleErrorInfo 对象作为 其 Model ,所以我们定义一个 Model 类型为 HandleErrorInfo 的 View 来显示相应的错误和上 下文信息。 public class HandleErrorInfo public HandleErrorInfo(Exception exception, string controllerName, string actionName); 口。·工 qqt nnp ·工·工 e rrc 卡」卡』 X SSE CCC ·-·工·工lll bbb uuu ppp ActionName { get; private set; } ControllerName { get; private set; } Exceptio口{ get; private set; } 由于应用在 HandleErrorAttribute 上面的 AttributeUsageAttribute 的 AllowMultiple 属性被 设置为 True ,所以 ASP.NET MVC 允许我们按照如下的方式在同一个 Action 方法或者 Con位oller 类型上应用多个 HandleErrorAttribute 特性,以实现对不同类型异常的针对性处理 (根据抛出异常类型的不同,显示不同的错误页面)。 public class FooController : Controller [HãndleError(ExceptionType = typeof(Exception1) ,View="ErrorView1" , 。rder=3)] [HandleError(ExceptionType = typeof(Exception2) , View = "ErrorView2" , 。rder=2)] [HandleError(Order = 1)] public ActionResult Bar() ASP. NET MVC 4 框架揭秘7.3 筛选器的执行 穰 373 //省略操作 针对 ExceptionFilter 链反向执行的特性,我们需要通过设置 Order 属性让针对具体异常 类型的 HandleErrorA ttribute 优先执行。以上面的代码片段为例,如果 Exceptionl 继承白 Exception2 ,我们必须让针对 Exceptionl 的 HandleErrorAttribute 先于针对 Exception2 的 HandleErrorAttribute 执行,而针对 System.Exception (默认值)的 HandleErrorAttribute 最后执行。 如果反过来,针对 Exceptionl 和 Exception2 的 HandleAttribute 特性将变得毫无意义。因 为 HandleAttribute 的异常处理工作只有在当前 ExceptionContext 的 ExceptionHandled 属性为 False 的时候才会进行,而它在进行异常处理的时候总会将该属性设置为 Tme 。 当我们通过 Visual Studio 的 ASP.NET MVC 项目模板创建一个 Web 应用的时候,针对 HandleErrorAttribute 的注册代码默认出现在生成的 FilterConfigC 该类型所在文件在 App_Start 目录下)类型的 RegisterGlobalFilters 方法中,具体的注册代码如下所示。 内3 ·工ZL n o 户」r e •L 14 .l F s s a ---C C .工14 b u p{ public static void RegisterGlobalFilters(GlobalFilterCollection filters) filters.Add(new HandleErrorAttribute()); public class MvcApplicatioη: System.Web.HttpApplication protected void Application_Start() //其他操作 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); 关于 HandleErrorAttribute 还有一点需要着重强调的是,只有在当前 HttpContext 用于表 示是否可以对错误进行定制的 IsCustomErrorEnabled 属性为 Tme 的情况下, HandleErrorAttribute 才会真正被用于处理抛出的异常,可以通过如下的配置来控制是否允许 对错误的定制。 7.3.5 实例演示:集成 EntLib 实现自动化异常处理 (8713, 8714, 8715) 个人觉得异常处理对于程序员来说是最为熟悉的同时也可能是最难掌握的。说它熟悉, ASP. NET MVC 4 在架揭秘374 :自 第 7 章 Action 的执行 是因为异常处理的编程模式仅仅是 try/ catch/finally 而己:说它难以掌握,则是因为很多开发 人员往往说不清楚 try/catch/fmally 应该置于何处?什么情况下需要对异常进行日志记录?什 么情况下需要对异常进行封装?什么情况下需要对异常进行替换?对于捕获的异常,在什么 情况下需要将其再次抛出?什么情况下又不需要再次抛出。 合理的异常处理应该是场景驱动的,在不同的场景下采用的异常处理策略往往是不同 的。异常处理的策略最好是可配置的,因为应用程序出现怎样的异常往往是不可预测的,现 有异常策略的不足往往需要在真正出现某种异常的时候才会体现出来,所以我们需要一种动 态可配置的异常处理策略维护方式。目前有一些开源的异常处理框架提供了这种可配置的、 场景驱动的异常处理方式,微软企业库 (EntLib) 的 Exception Handling Application Block (以 下简称 EHAB) 就是一个不错的选择。 Ent Li b 申的 EHAB 微软企业库异常处理应用块 (EHAB) 采用基于"策略"的异常处理机制,异常处理策 略通过配置定义。 EHAB 中的异常处理策略大致可以通过下面的公式表示。 异常处理策略 (Exc叩tion Handling Policy) =异常类型 (Exception Type) +异常处 理器( Exception Handler) +异常后续处理方式( Post Handling Action ) EHAB 中的异常处理策略表达的意思是,当出现某种类型的异常时,应该采用怎样的方 式处理,以及在处理之后是否抛出原始异常或者处理后异常。 EHAB 的异常处理机制是基于 "类型"的,而异常处理逻辑则实现在一个个异常处理器中 (Exception Handler) 。异常后续 处理方式主要分为三种情形:抛出原始异常、抛出处理后的异常和不做任何操作。这三种处 理方式定义在 Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.PostHandling Action 枚 举中。 public enum PostHandlingAction None , NotifyRethrow, ThrowNewException EHAB 异常处理策略可以通过配置的方式定义。下面的配置中演示了针对 Sqffixception 的处理,我们通过 LoggingExceptionHandler 和 ReplaceHandler 这两个异常处理器先后对抛出 的 Sqffixception 异常进行处理,前者对进行抛出的异常进行日志记录,后者将其替换成自定 义的 DbException 。当抛出的异常先后被这两个异常处理器处理之后,替换后的 DbException 会被抛出来 (postHandlingAction="ThrowNewException" )。 ASP. NET MVC 4 框架揭秘7.3 筛选器的执行 审 375 由于异常处理策略完全定义在配置中,在编程的时候,我们仅仅需要按照下面的方式指 定相应的策略名称即可( data access policy) 。关于 EHAB ,由于篇幅的问题,只能点到为止, 有兴趣的读者可以参阅微软企业库开发文档。 try return this.MembershipProxy.ValidateUser(username , password)i catch (Exception ex) if (ExceptionPolicy.HandleException(ex , "data access policy")) throwi "自动化"异常处理 在正式介绍如何通过扩展 EntL ib 的 EHAB 进行集成以实现自动化异常处理之前,我们 不妨先来体验一下异常处理到底怎么个"自动化"法。以用户登录场景为例,我们在一个 ASP.阳 T MVC 应用中定义了如下一个简单的数据类型 Logi nI nfo ,两个属性 UserName 和 Password 表示登录输入的用户名和密码。 o fι n T-n -1 9 0 L S s a 14 C C .l y4 b u p{ [DisplayName(" 用户名") ] [Required(ErrorMessage=" 请输入 {O} "门 public string UserName { geti seti } [DisplayName(" 密码") ] [Required(ErrorMessage = "请输入 {O}") ] [DataType(DataType.Password)] public string Password { geti set; } AS P. NET MVC 4 框架揭秘376 泪第 7 章 Action 的执行 然后定义了如下一个 HomeController 。基于 HTTP-GET 的 Action 方法In dex 将会呈现一 个用户登录 View ,而真正的用户验证逻辑定义在另一个应用了 HttpPostAttribute 特'性的 Index 方法中。具体的认证逻辑很简单,如果用户名不为 "Foo" ,抛出 InvalidUserNameException 异常:如果密码不是" password " ,则抛出 Invali dP asswordException 异常。 InvalidU serName Exception 和Invali dP asswordException 是我们自定义的两种异常类型。 [ExceptionPolicy (" defaul tPolicy") ] public class HorneController : ExtendedController public ActionResult Index() return View(new Loginlnfo()); [HttpPost] [HandleErrorAction("OnlndexE rror")] public ActionResult Index(Log 工 nInfo loginlnfo) if (string.Cornpare(loginlnfo.UserNarne , "foo" , true) != 0) throw new InvalidUserNarneException(); if (loginInfo.Password != "password") { throw new InvalidPasswordException(); return View(loginlnfo); [HttpPost] public ActionResult OnlndexError(LoginInfo loginInfo) return View(loginlnfo); 上面定义的 HomeCon位 oller 具有三点与自动化异常处理相关的地方。 • HomeController 继承自自定义的基类 ExtendedController ,后者完成了对异常的自动化 处理。 • HomeController 类型上应用了自定义的 Exceptio nP olicy A仕ribute 特性用于指定默认采用 的异常处理策略名称(" defaultPolicy 勺。 • 应用了 HttpPostAttribute 特性的 Index 方法上标注了一个 HandleErrorActio nA ttribute 特 性用于指定一个 Handle- Error-Action 名称。在目标 Action 执行过程中抛出的异常被 EHAB 处理后,指定的 Action 会被执行以实现对请求的响应。对于我们的例子来说, 从 Index 方法抛出的异常被处理后会调用 O nI nde xE rror 方法并利用返回的 Actio nResult 来响应当前请求。 下面是代表登录页面的 View 的定义,这是一个 Model 类型为 Log inI nfo 的强类型 View 。 ASP. NET MVC 4 框架揭秘7.3 筛选器的执行 霞 377 在该 View 中,作为 Model 的 LoginInfo 对象以编辑模式呈现在一个表单中,表单中提供了 一个"登录"按钮提交表单。除此之外,这个 View 中还具有一个 ValidationSummary 。 @rnodelLoginInfo 用户登录 @using (Htrnl.BeginForrn()) @Htrnl.ValidationSurnrnary(true) @Htrnl.EditorForModel() 通过 HomeController 的定义知道,两种不同类型的异常 (InvalidUserNameException 和 InvalidPasswordException) 分别在输入无效用户名和密码时被抛出来,而我们需要处理的就 是这两种类型的异常。针对这两种类型异常的处理策略定义在如下的配置中,策略名称就是 通过应用在 HomeController 上的 ExceptionPolicyAttribute 特性指定的" defaultPolicy" 。
    ASP. NET MVC 4 框架揭秘378 电第 7 章 Action 的执行 如上面的配置片段所示,我们使用一个自定义的 ErrorMessageHandler 来处理抛出来的 lnvalidUserNameException 和 Invali dP asswordE xception 异常,而 ErrorMessageHandler 仅仅是 指定一个友好的错误消息而已。 运行该程序后一个登录页面会被呈现出来,当我们输入错误的用户名和密码的时候,相 应的错误消息(在配置中通过 ErrorMessageHandler 设置的错误消息)会以如图 7-16 所示的 效果显示在当前Vi ew 的 ValidationSummary 中。其实整个 View 是通过执行 Action 方法 O nI nde xE rror 返回的 ViewResult 呈现出来的。 (S713) 瑾雪D '回国匹 1 j 干Ar -­ 名 ' 肿 一 与 F­ E dv EMW 密 名 ·户 m 码 用 R 密 〔翠 〕 图 7-16 通过执行 Handle-Error -Action 呈现异常处理结果 除了通过执行对应的 Handle-Error-Action 来呈现异常处理后的最终结果之外,还支持错 误页面的错误呈现方法。简单起见,我们直接将作为错误页面的 View 名称设置为 "Error" 。 为了演示基于错误页面的呈现方式,我们按照如下的方式在"飞 Views'飞 Sh 缸ed飞"目录下定义了 如下一个名为 Error 的 View 。 @modelExtendedHandleErrorlnfo @{ Layout = nulli Error AS P. NET MVC 4 框架揭秘

    @Html.DisplayFor(m=>m.ErrorMessage)

      7.3 筛选器的执行 毡 379
    • Controller: @Html.DisplayFor(m => m.ControllerName)
    • Action: @Html.DisplayFor(m => m.ActionName)
    • Exception:
      • Message: @Html.DisplayFor(m => m.Exception.Message)
      • Type: @Model.Exception.GetType() . FullName
      • StackTrace: @Html.DisplayFor(
    m => m.Exception.StackTrace) 上面这个作为错误页面的 View 使用具有如下定义的 Extende dH andleErro rI nfo 类型作为 其 Modelo Extende dH andleErro rI nfo 继承自 HandleErrorInfo ,它只额外定义了一个表示错误 消息的 ErrorMessage 属性。在上面的这个Vi ew 中,我们将错误消息、异常类型、 StackTrace 、 当前 Contro l1 er 和 Action 的名称呈现出来。 public class ExtendedHandleErrorlnfo : HandleErrorlnfo public string ErrorMessage { get; private set; } public ExtendedHandleErrorlnfo(Exception exception , string controllerName , str 工 ng actionName , str 工 ng errorMessage) : base(exception , controllerName , actionName) this.ErrorMessage = errorMessage; 如果我们采用错误页面的方式来响应请求,需要按照如下的方式将应用在 Action 方法 lndex 上的 HandleErrorActio nAttribute 特性注释掉。 [ExceptionPolicy("defaultPolicy")] public class HomeController : ExtendedController //其他成员 [HttpPost] //[HandleErrorAction("OnlndexE rror")] public ActionResult Index(Loginlnfo loginlnfo) //省略实现 再次运行该程序并在用户登录页面上分别输入错误的用户名和密码后,默认的错误页面 (Eηor.cshtml) 将会以如图 7-17 所示的效果把处理后的异常信息呈现出来。 (S714) AS P. NET MVC 4 擅架揭秘380 电第 7 章 Action 的执行 • cm血olb: Home • A c1ÌoD: ludcx • Ex cCJ插曲二 。 M 巳组吕R萨eι:E弘zαP 岛 aαoft句}pe o T'贸捂Mv町cApp Jnv温dP缸回v田dE~饵调咀 。 S 恒ckT=:at Microsoft.防配 tices .En甜φ由'Linary .&α同OIIHaodIiøgEx cep伽nP olicyEn町Intð由l3IRe也o w(Exceptï∞ C恒温 xce讪锢. Exception 由单aIEX回回00)" D:1iC侃酣ct. N町、\ SZ-IC-R2 -'πSIσ沃油mect'-iC但回ctlEnterprise library S()\Blocks lExc eptionH且也哥.Sr c\Exce同onHandIiDg'Ex臼同侃PolicyEz血y.cs :line 102 at 图 7-17 通过执行错误 View 呈现异常处理结果 用于实施认证的 Action 方法In dex 也可以通过 Aj 缸请求的方式来调用。对于 Ajax 请求 来说,我们会将通过 EntLib 处理后的异常封装成如下一个类型为 ExceptionD etail 的对象, 它具有与 Exception 对应的属性设置,最终根据这个 Exceptio nD etail 对象创建一个 Jso nResult 来响应当前的请求。 public class ExceptionDetail public ExceptionDetail(Exception exception , string errorMessage=null) { this.HelpLink = exception.HelpLinki this.Message = string.IsNullOrErnpty(errorMessage) ? exception.Message : errorMessagei this.StackTrace = exception.StackTrace; this.ExceptionType = exception.GetType() .ToString(); if (exception.lnnerException != null) this.lnnerException = new ExceptionDetail(exception.lnnerException); l -l a t e D n 0 .l qdtqdqdqd np ‘ nnn -le---1·l rcrrr txttt sESSS CCCCC ·工 -1·l-1·l lllll bbbbb uuuuu ppppp HelpLink { get; set; } InnerException { get; set; } Message { geti set; } StackTrace { get; set; } ExceptionType { geti seti } 当客户端接收到响应的 Json 对象后,可以通过检测其是否具有一个 Exc 叩 tionType 属性 (对于一个 Exceptio nD etail 对象来说,该属性不可能为 Null) 来判断是否发生异常。作为演 示,我们对 Action 方法In dex 对应的 View 进行了如下改动。 @rnodel Loginlnfo 用户登录 AS P. NET MVC 4 框架揭秘7.3 筛选器的执行 审 381 @{ AjaxOptions options = new AjaxOptions{OnSuccess = "login"}; @using (Ajax.BeginForm(options)) @Html.EditorForModel() 如上面的代码片段所示,通过调用 Aj 缸Help 町的 Beg inF orm 生成了一个以 Ajax 形式提 交的表单。表单成功提交(服务端因对抛出的异常进行处理而响应一个封装了异常信息的 Json 对象,对于提交表单的 Ajax 请求来说依然属于成功提交)后会调用我们定义的回调函 数 login 。在该 JavaScript 函数中,我们通过得到的对象是否具有一个 ExceptionType 属性来 判断服务端是否抛出异常。如果抛出异常,则通过调用 alert 方法将错误消息显示出来,否 则显示"认证成功"。 再次运行我们的程序并分别输入不合法的用户名和密码,相应的错误消息会以对话框的 形式显示出来,具体的显示效果如图 7-18 所示。 (S715) 用户名 ~τ事可 Foo 一一 I'! h 问 ~atloah耐 U 呵s: 踊 11 附归不四 ~ ~L__",… 图 7-18 针对 Ajax 请求的错误消息的呈现 ASP. NET MVC 4 在架揭秘382 喝第 7 章 Action 的执行 ExtendedController 通过上面的实例演示可以看出我们的扩展能够利用 EntL ib 的 EHAB 根据指定的异常处 理策略对抛出的异常进行处理。而对于处理后的结果,它会按照如下的策略来响应请求。 • 对于人j 缸请求,直接创建一个用于封装被处理后异常的数据对象,并据此创建一个 Jso nResult 对象来响应当前请求。 • 对于非 Aj 缸请求,如果当前 Action 方法上通过应用的 HandleErrorActio nAttribute 特性 设置了匹配的 Action 方法,这个 Action 方法会自动被执行并采用返回的 Actio nResult 对象响应当前请求。 • 如果 HandleErrorActio nAttribute 特性不曾应用在当前 Action 方法上,或者通过该特性指 定的 Action 根本不存在,则将作为错误页面的 View 呈现出来作为对请求的响应。 所有的这些都是通过一个自定义的 Exceptio nF ilter 来实现的。不过我们并没有定义任何 的 Exceptio nF ilter 特性,而是将异常处理实现在一个自定义的 ExtendedController 基类中,对 异常的自动处理实现在重写的 O nE xception 方法中。在介绍定义在该方法中具体的异常处理 逻辑之前,我们先来看看定义在 ExtendedController 中的其他辅助成员。 public class ExtendedController: Controller private static Dictionary controllerDescriptors = new Dictionary(); private static object syncHelper = new object(); protected override void OnException(ExceptionContext filterContext) 11 省略成员 //描述当前 Controller 的 ControllerDescriptor public ControllerDescriptor Descriptor get ControllerDescriptor descriptor; if(controllerDescriptors.TryGetValue(this.GetType() , out descriptor)) return descriptor; lock (syncHelper) if (controllerDescriptors.TryGetValue(this.GetType() , out descriptor)) return descriptor; else descriptor = new ReflectedControllerDescriptor(this.GetType()); controllerDescriptors.Add(this.GetType() , descriptor); return descriptor; AS P. NET MVC 4 框架揭翻7.3 筛选器的执行 白 383 11 获取异常处理策略名称 public string GetExceptionPolicyName() string actionName = ControllerContext.RouteData.GetRequiredString("action"); ActionDescriptor act 工 onDescriptor = this. Descriptor. FindAction (ControllerContext, actionName); if (null == actionDescriptor) return string.Empty; ExceptionPolicyAttribute exceptionPolicyAttribute = actionDescriptor.GetCustomAttributes(true) .OfType() .F工 rstOrDefault()?? Descriptor.GetCustomAttributes(true) .OfType() .FirstOrDefault()?? 口 ew ExceptionPolicyAttribute(""); return exceptionPolicyAttribute.ExceptionPolicyName; 11 获取 Handle-Error-Action 名称 public string GetHandleErrorActionName() string actionName = ControllerContext.RouteData.GetRequiredString("action"); ActionDescriptor actionDescriptor = this.Descriptor.FindAction(ControllerContext, actionName); if (null == actionDescriptor) return string.Empty; HandleErrorActionAttribute handleErrorActionAttribute = actionDescriptor.GetCustomAttributes(true) .OfType() .FirstOrDefault()?? Descriptor.GetCustomAttributes(true) .OfType() .FirstOrDefault()?? new HandleErrorActionAttribute(""); return handleErrorActionAttribute.HandleErrorAction; 11 用于执行 Handle-Error-Action 的 Actionlnvoker public HandleErrorActionlnvoker HandleErrorActionlnvoker { get; private set; } public ExtendedController() this.HandleErrorActionlnvoker = new HandleErrorActionlnvoker(); ExtendedController 的 Descriptor 属性返回描述自身的 ControllerDescriptor 对象,实际上 是一个 ReflectedControllerDescriptor 对象。为了避免频繁的反射操作造成对性能的影响,我 们将解析出来的 ReflectedControllerDescr悄or 对象针对 Controller 类型进行了全局性缓存。 GetExceptionPolicy N ame 方法用于返回当前采用的异常处理策略名称。异常处理策略名 ASP.NET MVC 4 框架揭秘384 '. 第 7 章 Action 的执行 称是通过具有如下定义的 Exceptio nP olicy Attribute 特性来指定的,该特性既可以应用在 Con位 oller 类型上,也可以应用在 Action 方法上,换句话说,我们可以采用不同的策略来处 理从不同 Action 执行过程中抛出的异常。 GetExceptio nP olicyName 方法利用 ControllerDescriptor 和 Actio nD escriptor 可以很容易地得到应用的 Exceptio nP olicy Attribute 特 性,进而得到相应的异常处理策略名称。 [AttributeUsage( AttributeTargets.Classl Attr 工 buteTargets.Method , AllowMultiple = false , Inherited = true)] public class ExceptionPolicyAttribute: Attribute public string ExceptionPolicyName { geti private seti } public ExceptionPolicyAttribute(string exceptionPolicyName) this.ExceptionPolicyName = exceptionPolicyNamei 另一个方法 GetHandleErrorActionName 用于获取通过应用在 Action 方法上的特性 HandleErrorActio nA ttribute 设置的 Handle-Error-Action 的名称。该特性定义如下,它既可以 应用于某个 Action 方法,也可以应用于 Controller 类。 GetHan叫d创leEr町ror爪Acωtio∞nN ame 方法同样 利用 Co∞n仕ωollerDe臼scααri句i怡pt伽O创町r 和 Actωion曲DeωS臼Cαri悄i 终得至到u 对应自的9 异常处理.A ction 名称。 [AttributeUsage( AttributeTargets.Classl AttributeTargets.Method , AllowMultiple = false)] public class HandleErrorActionAttribute: Attribute public string HandleErrorAction { geti private set; } public HandleErrorActionAttribute(string handleErrorAction = "") this.HandleErrorAction = handleErrorAction; 通过 HandleErrorActio nAttribute 特性设置的 Handle-Error-Action 需要手工执行以实现对 当前请求的响应,为此我们创建了一个具有如下定义的 HandleErrorActionlnvoker 。它是 ControllerActio nI nvoker 的子类, Handle-Error-Action 的执行以及对当前请求的响应实现在虚 方法 InvokeActio nM ethod 中。 ExtendedController 的 HandleErrorActio nI nvoker 返回的就是这 样一个对象。 public class HandleErrorActionlnvoker: ControllerActionlnvoker public virtual ActionResult InvokeActionMethod( ControllerContext controllerContext , ActionDescriptor actionDescriptor) IDictionary parameterValues = this.GetParameterValues( controllerContext , actionDescriptor); return base.lnvokeAçtionMethod(controllerContext , actionDescriptor , parameterValues)i AS P. NET MVC 4 框架揭秘7.3 筛选器的执行 • 385 整个异常处理和最终对请求的响应实现在如下所示的 O nE xception 方法中,流程并不复 杂,在这里就不再赘述了。 public class ExtendedController: Controller //其他成员 protected override void OnException(ExceptionContext filterContext) //或者当前的 ExceptionPolicy ,如果不存在,则直接调用基类 OnException 方法 string exceptionPolicyName = this.GetExceptionPolicyName(); if (string.IsNullOrEmpty(exceptionPolicyName)) base.O 口 Exception(filterContext); return; //利用 EntLib 的 EHAB 进行异常处理,并获取错误消息和最后抛出的异常 filterContext.ExceptionHandled = true; Exceptio 口 exceptionToThrow; string errorMessagei try ExceptionPolicy.HandleException(filterContext.Exception , exceptionPolicyName , out exceptionToThrow); errorMessage = System.Web.HttpContext.Current.GetErrorMessage(); y 14 14 a n .工 1I 在牛 f1 System.Web.HttpContext.Current.ClearErrorMessage(); exceptionToThrow = exceptio 口 ToThrow ?? filterContext.Exception; //对于 Ajax 请求,直接返回一个用于封装异常的 JsonResult if (Request.IsAjaxRequest()) filterContext.Result = Json(ηew ExceptionDetail( exceptionToThrow , errorMessage)); return; //如果设直了匹配的 HandleErrorAction ,则调用之; //否则将 Error view 呈现出来 string handleErrorAction = this.GetHandleErrorActionName(); string controllerName = ControllerContext.RouteData.GetRequiredString("control ler")i string actionName = ControllerContext. RouteData. GetRequiredString ("action" ); errorMessage = string. IsNullOrEmpty (errorMessage) ? exceptionToThrow.Message : errorMessagei if (string.IsNullOrEmpty(handleErrorAction)) filterContext.Result = View("Error" , ηew ExtendedHandleErrorlnfo(exceptionToThrow , controllerName , actionName , errorMessage))i else AS P. NET MVC 4 框架揭秘386 骂自 第 7 章 Action 的执行 ActionDescriptor actionDescriptor = Descriptor.FindA ction( ControllerContext , handleErrorAction); Mode l. State. AddModel.Error("" , errorMessage); filterContext.Result = this.HandleErrorActionlnvoker . InvokeActionMethod (ControllerContext , actionDescriptor); 在调用 EntLib 的 EHAB 对异常处理过程中,允许相应的 Exceptio nH andler 设置-个友 好的错误消息,而这个消息被保存在当前 H即 Context 的It ems 中。在调用异常处理方法之前, 我们将错误消息添加到当前的 ModelState 中,这也是为什么在上面的实例演示中错误消息会 自动出现在 ValidationSummary 中的根本原因。 EHAB 完成了对异常的处理之后从调用 HttpContext 具有如下定义的扩展方法 Ge tErrorMessage 提取错误消息,另一个扩展方法 ClearErrorMessage 方法实现对错误消息的 清除。除了这两个扩展方法我们还定义了另一个用于设置错误消息的 SetErrorMessage 方法。 public static class HttpContextExtensions public static string keyOfErrorMessage = Guid.NewGuid() .ToString(); public static void SetErrorMessage(this HttpContext context , string errorMessage) context.ltems[keyOfErrorMessage]=errorMessage; public static string GetErrorMessage(this HttpContext context) return context.ltems[keyOfErrorMessage] as string; public static void ClearErrorMessage(this HttpContext context) if (context.ltems.Contains{keyOfErrorMessage)) { context.ltems.Remove{keyOfErrorMessage); 用于设置错误信息的 ErrorMessageHandler 以及对应配置元素类型 ErrorMessageHandlerData 定义如下。 ErrorMessageHandler 表示错误消息的 ErrorMessage 属性在构造函数中被初始化, 而在实现的 HandleException 方法中直接通过调用当前 HttpContext 的扩展方法 SetErrorMessage 进行错误消息的设置。 [ConfigurationElementType {typeof (ErroíMessageHandlerDa ta))] public class ErrorMessageHandler: IExceptionHandler public string ErrorMessage { get; private set; } public ErrorMessageHandler{string errorMessage) this.ErrorMessage = errorMessage; AS P. NET MVC 4 在架揭秘7.3 筛选器的执行 二窜 387 public Exception HandleException(Exception exception , Guid handling 工 nstanceld) if (null != HttpContext.Current) HttpContext.Current.SetErrorMessage(this.ErrorMessage); ., n 0 ·工+L nr e c x e n r u 卡」e }r public class ErrorMessageHandlerData : ExceptionHandlerData [ConfigurationProperty("errorMessage" , IsRequired=true)] public string ErrorMessage get { return (string)this["errorMessage"]; ) set { this["errorMessage"] = value; } public override IEnumerable GetRegistrations( string namePrefix) yield return new TypeRegistration( () => new ErrorMessageHandler(this.ErrorMessage)) Name = this.BuildName(namePrefix) , Lifetime = TypeRegistrationLifetime.Transient 7.3.6 ResultFilter 如果 Action 方法返回一个 Actio nResult 对象, Actio nInvoker 在完成了 Action 方法的执 行后会调用返回的 Actio nResult 对象的 ExecuteResult 方法以实现对请求的响应。通过自定义 Actio nF ilter 我们不仅可以在 Action 方法执行前后完成一些额外的操作,甚至还可以通过 Action 方法执行前设置当前 Actio nE xecutingContext 的 Result 属性直接对请求作出响应。如 果我们需要对 Actio nR esult 的执行进行类似的控制,可以自定义相应的 Resul tF ilter 。 Resul tF ilter 实现了接口 System. Web.Mvc .l Resul tF ilter 。如下面的代码片段所示, IResul tF ilter 定义了两个方法 O nResul tE xecuting 和 O nR esultExecuted ,它们将在 Actio nR esult 执行前后被执行。两个方法各自具有一个基于上下文类型的参数,类型名称分别为 Resul tExecutingContext 和 Resul tE xecutedContext 。两个上下文类型均定义在 System. Web.Mvc 命名空间下,具体定义如下所示。 public interface IResultFilter void OnResultExecuted(ResultExecutedContext filterContext); void OnResultExecuting(ResultExecutingContext filterContext); AS P. NET MVC 4 在架揭秘388 黯 第 7 章 Action 的执行 public class ResultExecutingContext : ControllerContext { public ResultExecutingContext()i public ResultExecutingContext(ControllerContext controllerContext , ActionResult result)i public bool Cancel { geti seti public virtual ActionResult Result { geti seti public class ResultExecutedContext : ControllerContext public ResultExecutedContext(); public ResultExecutedContext(ControllerContext controllerContext , ActionResult result , bool canceled , Exception exception)i public virtual bool public virtual Exception public bool public virtual ActionResult Canceled { geti seti } Exception { get; set; } Exceptio 口 Handled { geti seti } Result { get; set; } 包含 Actio nR esult 的执行的整个 ResultFilter 链的执行流程与 Actio nF ilter 链的执行流程 比较类似。在正常情况下 ResultFilter 链会按照类似于图 7-9 所示的流程执行(将 Actio nF ilter 和 Action 方法的执行分别看成是 ResultFilter 和 Actio nResult 的执行)。 如果某个 ResultFilter 在执行 OnResultExecuting 过程将 Resul tE xecutingContext 的 Cancel 属性设置为 True ,后续 ResultFilter 和最终的 Actio nR esult 都不会被执行,但是之前 Resul tF ilter 的 O nR esultExecuted 方法会照常执行。 本章小结 为了改善 Web 应用的吞吐量、可用性和响应能力, ASP.NET 采用了线程池的机制来进 行请求的处理。对于一些相对耗时的 1/0 绑定型操作,我们倾向于采用异步的方式来定义对 应的 Actiono AS 丑陋 T MVC 4.0 保留了之前版本的异步 Action 定义方式(在继承自 AsyncController 的类型中以两个匹配的方法 X xxA synclX xxCompleted 来定义),同时采用并 行编程模式提供了一种全新的异步 Action 定义方式〈将异步 Action 定义成一个返回类型为 Task 的方法)。在定义异步 Action 的时候,我们在异步操作完成之后需要利用 AsyncManager 向 AS 丑陋 TMVC 发送通知。除此之外,异步操作向回调操作传递参数以及超时管理也可以 通过它来完成。 就整个 AS 卫 NET MVC 进行请求处理的流程来看,很多处理环节都体现了同步和异 步之间的差别,比如 MvcHandler 对请求的处理, Controller 和 Action 的执行。很多核心 的组件都具有同步和异步两个版本,比如定义的 Controller 类型可以继承 IController 和 IAsyncController 接口:用于执行 Action 的 Actionlnvoker 具有 ControllerActio nI nvoker 和 AsyncCon位ollerActio nI nvok町两种类型:它们创建的用于描述 Controller 的 ControllerDescriptor ASP. NET MVC 4 框架揭秘本章小结 黯 389 的具体类型分别是 ReflectedControllerDescriptor 和 ReflectedAsync ControllerDescriptor; 具体 用于描述同步 Action 的 ActionDescriptor 类型是 ReflectedAction Descriptor ,以不同方式定义 的两种异步 Action 分别通过类型 ReflectedAsyncActionD escriptor 和 TaskAsyncActionD escriptor 来描述。 ActionInvoker 直接调用 ActionD escriptor 的 Execute 或者 BeginExecute/EndExecute 方法 来执行目标 Action 方法,具体的 Action 方法执行通过反射来实现。伴随着目标 Action 方法执 行的还有一系列筛选器的执行, ASP.NET MVC 定义了四种类型的筛选器( AuthorizationF ilter 、 ActionF ilter 、 ExceptionF ilter 和 ResultF ilter) 0 ASP.NET MVC 框架本身提供的一些功能是通过 相应的筛选器来实现的,我们也可以根据需要白定义筛选器。 ASPNETMVC4 框架揭秘第 8 章 View 的呈现 定义在 Con衍 o l1 er 中的 Action 方法一般会返回一个 Actio nResult 对象对请 求予以响应, ASP.NET MVC 定义了一系列 ActionResult 类型,合理地使用它 们可以使请求响应变得非常容易。 ReviewResult 是最为常用毡,是最为重要的 ActionResult ,它利用可扩展的 View 引擎获取对应的 View 并最终将其呈现 出来。 ASP. NET MVC 4 在架揭秘8.1 ActionResult ~ 391 8.1 ActionResult H口?是一个单纯采用请求/回复消息交换模式的网络协议, Web 服务器在接收并处理来 自客户端的请求后会根据处理结果对请求予以响应。一般来说针对请求的处理最终体现在对 目标 Action 方法的执行上,我们可以在定义 Action 方法中人为地控制对请求的响应。如下 面的代码片段所示,抽象类 Con位o l1er 具有一个只读的 Response 属性表示当前的 HttpResponse ,我们也可以间接地通过当前 H仕pContext 或者 Con位o l1 erC ontext 来获取用于响 应请求的当前 H忧pRe叩onse 对象。 public abstract class Controller : ControllerBase, //其他成员 public HttpResponseBase Response { geti } public HttpContextBase HttpContext { geti } public abstract class ControllerBase : IController //其他成员 public ControllerContext ControllerContext { geti seti } 原则上任何类型的响应都可以利用当前 HtφResponse 来实现,但是我们一般并不这么 做,而是将针对请求的响应实现在一个 System. Web.Mvc.ActionResult 对象中。如下面的代码 片段所示, ActionResult 是一个抽象类型,请求响应实现在抽象 ExecuteResult 方法中。 public abstract class ActionResult //其他成员 public abstract void ExecuteResult(ControllerContext context)i 顾名思义, ActionResult 就是 Action 执行的结果。 ActionInvoker 在完成对 Action 方法的 执行后,如果返回一个 ActionResult 对象, ActionInvoker 会将当前 ControllerContext 作为参 数调用其 ExecuteResult 方法。 View 的最终呈现是通过 ActionResult 的子类 ViewResult 来完 成的,除了 ViewResult , AS卫NETMVC 还为我们定义了一些额外的 ActionResult 。 8.1.1 EmptyResult 上面我们谈到 Action 方法返回的 ActionResult 对象被 ActionInvoker 调用以实现对当前 请求的响应,其实这种说法不够准确。不论 Action 方法是否具有返回值,也不论它的返回 值是什么类型, ActionInvoker 最终都会创建相应的 ActionResult 对象。如果 Action 方法返回 类型为 void ,或者返回值为 Null ,最终生成的就是一个 System. Web.Mvc.EmptyResult 对象。 如下面的代码片段所示,在重写的 ExecuteResult 方法中 EmptyResult 其实什么都没有做,所 ASP. NET MVC 4 框架揭秘392 • 第 8 章 View 的呈现 以 EmptyResult 是一个"空"的 Actio nResult 。 public class EmptyResult : ActionResult public override void ExecuteResult(ControllerContext context) EmptyResult 其实体现了一种设计思想。用于处理请求的 ASP.NET MVC 框架采用管道 式设计,整个处理流程具有三个基本的环节,即" Action 方法的执行"、"生成 ActionR esult" 和"执行 Actio nR esult" 。可能这个流程不适合某些特殊的请求,比如 Action 方法不具有返 回值或者返回值为 Null ,那么后面的两个环节可以忽略。为了让处理管道对所有的请求"一 视同仁",我们可以做一些适配工作,虽然 Empty Result 什么都没有做,但是它却在上述这 两种场景中起到了适配器的作用。 8.1.2 ContentResult System. Web.Mvc.ContentResult 使 ASP.NET MVC 采用我们提供的内容来响应请求。如 下面的代码片段所示,可以利用 ContentResult 的 Content 属性以字符串的形式指定响应的内 容,另外两个属性 ContentEncoding 和 ContentType 则控制采用的字符编码方式和媒体类型 (MTh伍类型)。抽象类 Controller 定义了如下三个受保护的 Content 方法重载,可以调用它 们根据指定的内容、编码和媒体类型创建相应的 ContentResult 。 public class ContentResult : ActionResult public override void ExecuteResult(ControllerContext context)i qJ n αflGJ ndn ·工 0· 工 rcr tnμt sES Cec ---1· 工 141414 bbb uuu ppp Co 口 tent { geti seti } ContentEncoding { geti seti } Co 口 tentType { geti seti } public abstract class Controller : ControllerBase , 11 其他成员 protected ContentResult Content(string content)i protected ContentResult Content(string content , string contentType)i protected virtual ContentResult Content(string content , string contentType , Encoding contentEncoding)i 在重写的 ExecuteResult 方法中, ContentResult 利用作为参数的 ControllerContext 对象得 到当前的 HttpResponse 对象,并借助它将提供的内容按照希望的编码方式和媒体类型对请求 予以响应,具体的实现如下面的代码片段所示。 public class ContentResult : ActionResult //其他成员 ASP. NET MVC 4 信架揭秘8.1 ActionResult _ 393 public override void ExecuteResult(ControllerContext context) HttpResponseBase response = context.HttpContext.Response; if (!string.IsNullOrEmpty(this.ContentType)) response.ContentType = this.ContentType; if (this.ContentEncoding != null) response.ContentEncoding = this.ContentEncoding; if (this.Content != null) response.Write(this.Content); 上面我们说过, ASP.阳TMVC 为了能够采用相同的流程来处理所有的请求,不论 Action 是否具有返回值,具有怎样的返回值, ActionInvoker 都会创建相应的 ActionResult 。对于不 具有返回值或者返回 Null 的 Action 方法调用来说,最终创建的是一个 EmptyResult 对象, 那么如果返回值不是一个 ActionResult 对象, Actionlnvoker 最终会创建怎样一个 ActionResult 对象呢? 如果 Action 方法执行后的返回值是一个 ActionResult , ActionInvoker 会直接利用它来进 行请求响应,否则它会将对象转换成字符串并以此创建一个 ContentResult 对象。 ControllerActionInvoker 根据 Action 方法的返回值生成相应 ActionResult 对象的逻辑体现在 它的 CreateActionResult 方法上。如下面的代码片段所示,这是一个受保护的虚方法,最后 一个参数( actionRetum Value) 表示执行 Action 方法得到的返回值。 public class ControllerActionInvoker : IActionInvoker 11 其他成员 protected virtual ActionResult InvokeActionMethod( ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary parameters); protected virtual ActionResult CreateActionResult( ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue); 另一个受保护的 InvokeActionMethod 负责执行 Action 方法并返回相应的 ActionResult 对象,它在执行 Action 方法后将得到的返回值作为参数调用 CreateActionResult 方法返回相 应的 ActionResult 对象。 我们可以通过一个简单的实例来验证 ActionInvoker 根据 Action 方法返回值对 ActionResult 的创建逻辑。在一个 AS卫NET MVC 应用中我们定义了如下一个 HomeController ,其中定义了四个无参数的 Action 方法。 Foo 返回一个具体的 ActionResult ( RedirectResul t) 对象, B盯的返回类型为 viod , Baz 返回值为 NuU ,而 Qux 则返回一个 double 类型的数字。 ASP. NET MVC 4 在架揭秘394 iiI 第 8 章 View 的呈现 public class HomeController : Controller //其他成员 public ActionResult Foo() return new RedirectResult(..http://www.asp.net..); public void Bar() { } public ActionResult Baz() return null; public double Qux() return 1. 00; 然后我们在 HomeCon位oller 中定义如下一个 Action 方法In dex 。在该方法中,我们通过 Actio nI nvoker 属性得到当前的 Actionlnvokere 对象,并以反射的方式调用其 GetControllerDescriptor 方法得到描述当前 Controller 的 Con仕 ollerDescriptor 对象。接下来我 们调用这个 ControllerDescriptor 的 Fin dA ction 方法得到描述定义在 HomeCon位 oller 中的四个 Action (Foo 、 B 缸、 Baz 和 Qux) 的 ActionD escriptor 对象。针对每个具体的 ActionD escript町, 我们采用反射的方式调用其InvokeActio nM ethod 方法得到最为 Action 方法返回值的 Actio nResult 对象。我们将它们连同对应的 Actio nD escriptor 对象构建一个 Dictionary对象,并作为 Model 呈现在默认的Vi ew 中。 public class HomeCont~oller : Controller //其他成员 public ActionResult Index() Dictionary actionResults = new Dictionary()i Methodlnfo getControllerDescriptor = this.Actionlnvoker.GetType() . GetMethod("GetControllerDescriptor" , BindingFlags.lnstance I BindingFlags.NonPublic)i ControllerDescriptor controllerDescriptor = (ControllerDescriptor)getControllerDescriptor . Invoke (this.Actionlnvoker , new object[] { ControllerContext })i Methodlnfo invokeAct 工 onMethod = this.Actionlnvoker.GetType() . GetMethod ("InvokeActionMethod" , BindingFlags.lnstance I BindingFlags.NonPublic); string[] actions = new string[] { "Foo" , "Bar" , "Baz" , "Qux" }i Array. ForEach (actions , action => { ActionDescriptor actionDescriptor = controllerDescriptor . FindAction (ControllerContext , action)i ActionResult actionResult = (ActionResult)invokeActionMethod AS P. NET MVC 4 在架揭秘8.1 ActionResult r. 395 .Invoke(this.ActionInvoker , new object[] { ControllerContext , actionDescriptor , new Dictionary() }); actionResults.Add(actionDescriptor , actionResult); } ) ; return View(actionResults); 如下所示的是 Action 方法In dex 对应 View 的定义, IDictionary作为该 View 的 Model 类型。在该 View 中我们将存在于字典中的 Actio nR esult 对象的类型和对应的 Action 名称以表格的形式呈现出来。 @model IDictionary ActionResults @foreach (var item in Model)
    ActionNameActionResult
    @item.Key.ActionName@item.Value.GetType() .Name
    运行该程序后会在浏览器中得到如图 8-1 所示的输出结果,我们可以看到返回类型为 void 的 Action 方法 B 町和返回值为 Nu l1的 Action 方法 Baz 执行后得到的都是一个 EmptyResult 对象,而返回非 Actio nR esult (double 类型〉类型的 Action 方法 Qux 执行之后 返回的是一个 ContentResulto (S80 1 ) -口 X 挝 、、 l ~ 。rJ… l ocalhos1翩翩………酣 ~、 :-.~ 11- '~I 市飞 F o。 RedirectResult Bar EmptyResult Baz EmptyResult Qux ContentResult 图 8-1 执行具有不同返回值的 Action 方法得到的 ActionResult 类型 实例演示:通过 ContentResult 实现主题定制 (8802 ) 由于可以通过 ContentResult 的 ContentType 属性指定响应的媒体类型,所以我们不仅可 以利用它来返回最终会在浏览器中显示的文本,还可以返回其他一些类型的内容,比如 JavaScript 脚本(" text/j avascript" 或者" applicationljavascript") 和 CSS 样式 ("text/ css 勺等。 ASP. NET MVC 4 在架揭秘396 • 第 8 章 View 的呈现 我们可以利用 ContentResult 实现"静态文本的动态响应",也就是说我们可以在某个 Action 中根据当前的请求动态地生成一些文本(比如 CSS 样式),而这些文本内容在大部分情况下 定义在静态文本文件中。 在接下来的这个实例演示中,我们将利用 ContentResult 实现对界面主题的定制。实现 的机制非常简单,我们让一个返回类型为 ContentResult 的 Action 方法返回基于当前主题的 CSS 样式,而当前的主题通过一个可持久化的 Cookie 保存下来。我们在一个 ASP.NETMVC 应用中定义了如下一个 HomeController ,其 Action 方法 Css 返回一个封装了 CSS 样式内容 的 ContentResult 。在该 Action 方法中,我们从请求中提取表示主题的 Cookie ,并根据它生 成基于当前主题的 CSS 样式(这里仅仅设置了字体类型和大小)。 public class HomeController : Controller 11 其他成员 s s c t 14 u s e R n 0 .工t C A C --14 b u p{ HttpCookie cookie = Request.Cookies["theme"] ?? new HttpCookie("theme" , "default"); switch (cookie.Value) case "Themel": return Content("body{font-family: SimHei; font-size: 1. 2em} ", "textl css") ; case "Theme2": return Content("body{font-family: KaiTi; font-size:1.2em}" , "text/css"); default: return Content("body{font-family: SimSong; font-size:1.2em}" , "text/css"); 我们在 HomeController 中定义了如下两个 lndex 方法。无参的 lndex 方法(针对 HTTP-GET 请求)从预定义 Cookie 中提取当前的主题(如果没有则采用默认的主题 " default") 并以 ViewBag 的形式传递给 View; 另一个应用 HttpPostAt位 ibute 特性的 lndex 方法将通过参数指定的主题名称设置为响应的 Cookie 后,同样以 ViewBag 的形式保存当前 的主题名称。两个 Index 方法最终都将默认的 View 呈现出来。 public class HomeController : Controller 11 其他成员 public ActionResult Index() HttpCookie cookie = Request.Cookies["theme"] ?? new HttpCookie("theme" , "default"); ViewBag.Theme = cookie.Value; return View(); [HttpPost] public ActionResult Index(string theme) HttpCookie cookie = new HttpCookie("theme" , theme); ASP. NET MVC 4 框架揭秘cookie.Expires = DateTime.MaxValuei Response.SetCookie(cookie)i ViewBag.Theme = themei return VieW()i 8.1 ActionResult 话 397 通过 Css 方法的定义可以看出我们定义了三个主题 C Th emel 、 ηleme2 和 De臼ult) ,它 们采用不同的中文字体(黑体、楷体和宋体 )0 Action 方法 Index 对应 View 具有如下一个表 单,该表单中为这三个主题添加了相应的 RadioButton 使用户可以及时切换当前的主题。这 个 View 最核心的部分是用于引用 CSS 文件的 元素,可以看到它的 href 属性指向的地 址正对应着定义在 HomeCon位 oller 中的 Action 方法 Css ,也就是说最终用于控制页面样式的 css 是通过调用该 Action 获得的。 主题设直 @using(Html.BeginForm()) string theme = ViewBag.Theme.ToString()i @Html.RadioButton("theme" , "Default" , theme == "Default") 默认主题(宋体)
    @Html.RadioButton("theme" , "Themel" , theme == "Themel") 主题 1 (黑体)
    @Html.RadioButton("theme" , "Theme2 " , theme == "Theme2") 主题 2 (楷体)
    现在直接运行我们的程序并在出现的"主题设置"界面中设置不同的主题,界面的样式 (字体)将会根据选择的主题而及时改变,具体的显示效果如图 8-2 所示。 图 8-2 通过 ContentResult 实现的主题定制 ASP. NET MVC 4 框架揭秘398 仨第 8 章 View 的呈现 8.1.3 FileResult System. Web.Mvc.FileResult 是一个基于文件的 Actio nR esult ,利用 FileResult 我们可以很 容易地将某个物理文件的内容响应给客户端。如下面的代码片段所示, FileResult 具有一个 表示媒体类型的只读属性 ContentType ,该属性在构造函数中被初始化。当我们基于某个物 理文件创建相应的 FileResult 对象的时候应该根据文件的类型指定媒体类型,比如说目标文 件是一个 .jpg 图片,那么对应的媒体类型为" image/jpeg " ;对于一个 .pdf 文件,则采用 " applicationlpdf" 。 public abstract class FileResult : ActionResult protected FileResult(string contentType); public override void ExecuteResult(ControllerContext context); protected abstract vo 工 d Wr 工 teFile(HttpResponseBase response); public string ContentType { get; } public string FileDownloadName { get; set; } 针对文件的响应具有两种形式,即内联 (Inline) 和附件 (A位 achment) 。一般来说,前 者会利用浏览器直接打开响应的文件,而后者会以独立的文件下载到客户端。对于后者,我 们一般会为下载的文件指定一个文件名,这个文件名可以通过 FileResult 的 FileDownloadName 属性来指定。文件响应在默认情况下采用内联的方式,如果需要采用附 件的形式,需要为响应创建一个名称为 "Content-Disposition" 的报头,该报头值的格式为 "a忧 achment; filename={FileDownloadName} "。 FileResult 仅仅是一个抽象类,文件内容的输出实现在抽象方法 WriteFile 中,该方法会 在重写的 ExecuteResult 方法中调用。如果 FileDownloadN ame 属性不为空,意味着会采用附 件的形式进行文件响应, FileResult 会在重写的 ExecuteResult 方法中进行 "Content-Disposition" 响应报头的设置。下面的代码片段基本上体现了 ExecuteResult 方法 在 FileResult 中的实现。 public abstract class FileResult : ActionResult //其他成员 public override void ExecuteResult(ControllerContext context) HttpResponseBase response = context.HttpContext.Response; response.ContentType = this.ContentType: if (!string.IsNullOrEmpty(this.FileDownloadName)) { //生成 Content-Disposition 响应报头值 string headerValue = ContentDispositionUtil.GetHeaderValue(this.FileDownloadName); context.HttpContext.Response.AddHeader("Content-Disposition" , headerValue); this.WriteFile(response); ASP. NET MVC 4 信架揭秘8.1 ActionResult : IÌI 399 ASP.NET MVC 定义了三个具体的 F i1 eResult ,分别是 F i1 eContentResult 、 FilePat hR esult 和 F i1 eS 悦amR esult ,接下来对它们进行单独介绍。 FileContentResult System. Web.Mvc.F i1 eContentResult 是针对文件内容创建的 FileResult 。如下面的代码片 段所示, F i1 eContentResult 具有一个字节数组类型的只读属性 F i1 eContents 表示响应文件的内 容,该属性在构造函数中指定。 FileContentResult 针对文件内容的响应实现也很简单,从如 下所示的 WriteF i1 e 方法定义可以看出,它先获得当前 HttpResponse 的 OutputStream 属性表 示的输出流,然后调用其 Write 方法直接将表示文件内容的字节数组进行输出。 public class FileContentResult : FileResult public byte[] FileContents { geti } public FileContentResult(byte[] fileContents , string contentType) protected override void WriteFile(HttpResponseBase response) response.OutputStream.Write(this.FileContents , 0 , this.FileContents.Length)i public abstract class Controller : ControllerBase , { //其他成员 protected FileContentResult File(byte[] fileContents , string contentType)i protected virtual F 工 leContentResult F 工 le(byte[] fileContents , string contentType , string fileDownloadName)i 抽象类 Con位 oller 中定义了如上两个 F i1 e 重载,它们根据指定的字节数组、媒体类型和 下载文件名〈可选〉生成相应的 FileContentResult 。由于 FileContentResult 是根据字节数组 创建的,当我们需要动态生成响应文件内容〈而不是从物理文件中读取〉时, FileContentResult 是一个不错的选择。 FilePath Result 从名称可以看出, System. Web.Mvc.F i1 ePat hResult 是一个根据物理文件路径创建的 F i1 eResult 。如下面的代码片段所示,表示响应文件的路径通过只读属性 FileName 表示,该 属性在构造函数中被初始化。在实现的 WriteF i1 e 方法中,它直接将文件路径作为参数调用 当前 H忧pResponse 的 Transmi tF ile 方法实现了目标文件内容的输出。抽象类 Controller 同样 定义了两个 File 方法重载来根据文件路径创建相应的 F i1 ePat hR esult 。 public class FilePathResult : FileResult public string FileName { geti } ASP. NET MVC 4 框架揭秘400 翻 第 8 章 View 的呈现 public FilePathResult(string fileName , string contentType); protected override void WriteFile(HttpResponseBase response) response.TransmitFile(this.FileName); public abstract class Controller : ControllerBase , //其他成员 protected FilePathResult F 工 le(string fileName , str 工 ng contentType); protected virtual FilePathResult File(string fileName , string contentType , string fileDownloadName); FileStreamResult System. Web.Mvc.FileStrea mR esult 允许我们通过一个用于读取文件内容的 Stream 对象来 创建 FileResult 。如下面的代码片段所示,读取文件的 Stream 对象通过只读属性 FileStream 表示,该属性在构造函数中被初始化。在实现的 WriteFile 方法中,它通过指定的文件流读 取文件内容,并最终调用当前 HttpResponse 的 OutputStream 属性的 Write 方法将读取的内容 写入当前 HTTP 响应的输出流中。抽象类 Controller 中同样定义了两个 File 方法,它们重载 根据文件读取的 Stream 对象创建相应的 FileStrea mResult 。 public class FileStreamResult : FileResult public Stream FileStream { get; } public FileStreamResult(Stream fileStream , string contentType); protected override void WriteFile(HttpResponseBase response) Stream outputStream = respoηse.OutputStream; using (this.FileStream) byte[] buffer = new byte[Oxl000]; while (true) int count = this.FileStream.Read(buffer , 0 , Oxl000); if (count == 0) return; outputStream.Write(buffer , 0 , count); public abstract class Controller : ControllerBase , AS P. NET MVC 4 在架揭秘8.1 ActionResult 11 401 //其他成员 protected FileStreamResult File(Stream fileStream , string contentType)i protected virtual FileStreamResult File(Stream fileStream , string contentType , string fileDownloadName)i 实例演示:通过 FileResult 发布图片 (S803 ) 为了让读者对 FileResult 具有更加深刻的认识,我们通过一个实例来演示如何通过 FileResult 来对外发布图片。在→个 AS 卫 NETMVC 应用的根目录下添加一个名为 images 的 子目录来存放发布的 .jpg 图片,然后定义如下一个 HomeController 。 public class HomeController : Controller public ActionResult Index() return View()i public ActionResult Image(string id) string path = Server.MapPath("/images/" + id + ".jpg")i return File(path , "image/jpeg"); 图片的发布体现在 Action 方法 Image 上,表示图片白的参数同时作为图片的文件名(不 含扩展名)。在该方法中,我们根据图片 ID 解析出对应文件的路径后,直接调用 File 方法创 建一个媒体类型为" image/jpeg" 的 FilePat hR esult 。 如下所示的是 Action 方法 Index 对应 View 的定义,在该 View 中我们通过一个列表显 示 6 张图片。对于显示图片的 元素的 src 属性来说,它正是指向定义在 HomeController 的 Action 方法 Image ,而指定的表示图片 ID 的参数分别是 001 、 002 、…、 006 。 Gallery
    • OOl
    • 002
    • 003
    • ASP. NET MVC 4 在架揭秘402 电第 8 章 View 的呈现
    • 004
    • 005
    • 006
    我们将 6 张 .jpg 图片存放到" /images" 目录下,并分别命名为 001 、 002 、…、 006 。直 接运行程序之后这 6 张图片会以如图 8-3 所示的效果显示在浏览器上。 |MEGlJm -l ~+φ 甲 { } public class ShoppingCartItem qg nn -1· 工 rrt ttn ss·-CCC ·工 -1· 工 141414 bbb uuu ppp Id { geti seti } Name { geti seti } Quantity { geti seti } ASPNETMVC4 在架垣秘404 自 第 8 章 View 的呈现 然后我们创建如下一个 HomeController ,在默认的 Action 方法 Index 中创建一个包含三 个商品的 ShoppingCart 对象,并将其作为 Model 呈现在对应的 View 中。 Action 方法 ProcessOrder 用于处理提交的购买订单,如果订购商品的数量没有超过库存量(通过一个静态字典字段 stock 表示),则调用 alert 函数提示"购物订单成功处理",否则提示"库存不足",并将相应 商品当前库存量显示出来。 public class HomeController : Controller pri vate static Dictionary stock = new Dictionary () ; static HomeController() stock.Add("OOl" , 20); stock.Add("002" , 30); stock.Add("003" , 40); public ActionResult Index() ShoppingCart cart =口 ew ShoppingCart(); cart.Add(new ShoppingCartItem { Id = "001" , Quantity=l , Name = "商品 A" }); cart.Add(new ShoppingCartItem { Id = "002" , Quantity = 1 , Name = "商品 B" }); cart.Add( 口 ew ShoppingCartItem { Id = "003" , Quantity = 1 , Name = "商品 C" }); return View(cart); public ActionResult ProcessOrder(ShoppingCart cart) StringBuilder sb = new StringBuilder(); foreach (var cartItem in cart) if (!CheckStock(cartItem.Id , cartItem.Quantity)) sb.Append(string.Format("{O}: {1};" , cartItem.Name , stock[cartItem.Id])); if(string.IsNullOrEmpty(sb.ToString())) return Content("alert(' 购物订单成功处理! ');", "text/j avascript") ; string script = string.Format("alert(' 库存不足! ({ O} ) , ) ; " , sb.ToString() .TrimEnd(';')); retur 口 JavaScript(script); private bool CheckStock(stri 口 g id , int quantity) return stock[id] >= quantity; ASPNETMVC4 框架揭秘8.1 ActionResult f. 405 如下所示的是 Action 方法 Index 对应 View 的定义,这是一个 Model 类型为 ShoppingCart 的强类型 View 。在一个以 Ajax 请求提交的表单(表单的 Action 属性对应着上面定义的 Action 方法 ProcessOrder) 中显示了购物车中的商品和数量,用户可以修改订购数量并通过点击"提 交订单"按钮以 Ajax 请求的方式提交订单。 @model ShoppingCart 用户登录 @using (Ajax.BeginForm("ProcessOrder" , new AjaxOptions())) for (int i = 0; i < Model.Count; i++)
    @Html.HiddenFor(m=>m[i] .Id) @Html.HiddenFor(m => m[i] .Name) @Html.DisplayFor(m => m[i].Name): @Html.EditorFor(m => m[i] .Quantity)
    运行程序后,一个包含三个商品的购物车信息会被呈现出来,当我们输入相应的订购数 量并点击"提交订单"后,订单处理结果消息会弹出来。图 8-4 所示的就是库存不足的情况 下显示的消息。 (S804) 商品A:. 100 ~汽 商品 B: 1 叫f' The 阿 e at localh 出 tln3 均气 商品 C : 100 南军用 图 8-4 通过 JavaScriptResult 显示的提示消息 8.1.5 JsonResult JavaScript 己经在 Web 应用中得到广泛的应用,而 JSON 则成了标准的数据格式。但是 ASP. NET MVC 4 在架揭秘406 部第 8 章 View 的呈现 对于后台程序来说,数据却是通过二个基于某种 CLR 类型的对象来承载的,当客户端调用 某个 Action 方法并希望以 JSON 的格式返回请求的数据时, ASP.NET MVC 需要有一种机 制将 CLR 对象转换成 JSON 格式予以响应,而这可以通过 System. Web.Mvc.J sonResult 来 解决。 如下面的代码片段所示, J sonResult 具有一个 object 类型的属性 Data 表示需要被转换成 JSON 格式的数据对象。属性 ContentEncoding 和 ContentType 表示为当前响应设置的编码方 式和媒体类型,默认采用的媒体类型为"叩plic剖ionljson" 。 public class JsonResult : ActionResult public override void ExecuteResult(ControllerContext context)i public object Data { geti seti } public Encoding ContentEncoding { geti seti } public string ContentType { get; set; } public JsonRequestBehav工 or JsonRequestBehavior { geti seti } public int? MaxJsonLeηgth { get; set; } public int? RecursionLimit { get; set; } public enum JsonRequestBehavior { +L et Ge wG OY --n 14e AD 出于对安全的考虑, JsonResult 在默认的情况下不能作为对 HTTP-GET 请求的响应,在 这种情况下并会直接抛出一个 InvalidOperationException 异常。我们可以通过它的 J sonRequestBehavior 属性开启对 HTTP-GET 请求的支持。该属性类型为 System. Web.Mvc. J sonRequestBehavior 枚举,两个枚举项 AllowGet 和 DenyGet 分别表示允许/拒绝支持对 HTTP-GET 请求的响应。 JsonResult 的 JsonRequestBehavior 属性在初始化的时候被设置为 DenyGet ,如果我们需要用创建的 JsonResult 来响应 HTTP-GET 请求,需要显式地将它的 J sonRequestBehavior 属性设置为 AllowGet 。 CLR 对象到 JSON 格式字符串的序列化过程通过具有如下定义的序列化器 System.Web.Scrip t. Serialization.JavaScriptSerializer 来完成。 JavaScriptSerializer 的 Serialize 和 Deserialize 方法实现了 CLR 对象的序列化和对 JSON 字符串的反序列化。 public class JavaScriptSerializer //其他成员 public string Serialize(object obj); public object Deserialize(string input, Type targetType)i public int MaxJso 口 Length { get; set; } public int RecursionLimit { geti seti } ASP. NET MVC 4 在架揭秘8.1 ActionResult Ell自 407 J avaScriptSerializer 具有两个整型的属性 MaxJ so nL ength 和 Recursio nL imit ,它们对应着 Jso nResult 的同名属'性。 MaxJso nL ength 限制了被反序列化和序列化生成的 JSON 字符串的长 度,默认值为 2097152 (Ox200000 ,等同于 4MB 的 Unicode 字符串数据) 0 RecursionL imit 用于设置被序列化对象和反序列化生成对象结构的允许的层级数,默认值为 100 。 定义在 Jso nResult 的 ExecuteResult 方法通过 J avaScriptSerializer 对数据对象的序列化, 并将序列化生成的 JSON 字符串作为内容对请求进行响应,具体的逻辑基本上可以通过下面 的代码片段来体现。 public class JsonResult : ActionResult //其他成员 public override void ExecuteResult(ControllerContext context) //确认是否用于响应 HTTP-GET 请求 if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && string.Cornpare(context.HttpContext.Request.HttpMethod, "GET" , true) == 0) throw new InvalidOperationException(); HttpResponseBase response = context.HttpContext.Response //设直媒体类型和编码方式 response.ContentType = string.IsNullOrErnpty(this.ContentType) ? "application/json" : this.ContentType; if (this.ContentEncoding != null) response.ContentEncoding = this.ContentEncoding; //创建 JavaScriptSerializer 将数据对象序列化成 JSON 字符串并写入当前 HttpResponse if (null == this.Data)return; JavaScriptSerializer serializer = new JavaScriptSerializer() MaxJsonLength = this.MaxJsonLength.HasValue ? this.MaxJsonLength.Value : Ox200000 , RecursionLirnit = this.RecursionLirnit.HasValue ? this.RecursionLirnit.Value : 100 response.Write(serializer.Serialize(this.Data)); 抽象类 Con仕 oller 同样定义了如下一系列的 Json 方法用于根据指定的数据对象、编码方 式以及 Jso nR eques tl3 ehavior 来创建相应的 Jso nResult 。 public abstract class Controller : ControllerBase ,. //其他成员 protected internal JsonResult Json(object data); protected internal JsonResult Json(object data , string conteηtType) ; protected internal JsonResult Json(object data , AS P. NET MVC 4 在架揭秘408 组第 8 章 View 的呈现 JsonRequestBehavior behavior); protected internal virtual JsonResult Json (object data, string contentType, Encoding contentEncoding); protected internal JsonResult Json(object data, string contentType, JsonRequestBehavior behavior); protected internal virtual JsonResult Json(object data, string conteηtType , Encoding contentEncoding, JsonRequestBehavior behavior); 8.1.6 HttpStatusCodeResult 每一个 Hηp 响应均具有一个表示响应状态的代码和一个可选的状态描述,正常情况下 返回 "200 OK" 0 System. Web.Mvc.HttpStatusCodeResult 使我们很容易地响应一个指定状态的 回复。如下面的代码片段所示, H仕pSta阳sCodeResult 具有 StatusCode 和 StatusDescription 两 个只读的属性,它们分别表示响应状态码和状态描述信息。 HttpStatusCodeResult 的 HTTP 状态既可以在构造函数中通过一个整数来指定,也可以通过 System.Net.HttpStatusCode 枚举 形式来指定状态码。 public class HttpStatusCodeResult : ActionResult public HttpStatusCodeResult(int statusCode); public HttpStatusCodeResult(HttpStatusCode statusCode); public HttpStatusCodeResult(int statusCode, string statusDescription); public HttpStatusCodeResult(HttpStatusCode statusCode, string statusDescription); public override void ExecuteResult(ControllerContext context); qd n ·工 tr n 』L ·工 s ec --·工ll bb uu pp StatusCode { get; } StatusDescription { get; } HttpStatusCodeResult 实现在 ExecuteResult 方法中的请求响应逻辑很简单。如下面的代 码片段所示,它仅仅是设置了当前 HtψResponse 的 StatusCode 和 StatusDescription 而己。值 得一提的是,如果我们采用 Visual StudioDevelopment Server 作为 Web 应用的宿主,通过 H扰pSta阳sCodeResult 的 StatusDescription 属性设置的状态描述信息不会反映在 HTTP 响应中, 只有采用 IIS 作为宿主才会真正将此信息写入响应消息。 public class HttpStatusCodeResult : ActionResult //其他成员 public override void ExecuteResult(ControllerContext context) context.HttpContext.Response.StatusCode = this.StatusCode; if (this.StatusDescription !=ηull) context.HttpContext.Response.StatusDescription = this.StatusDescription; ASP. NET MVC 4 框架揭秘8.1 ActionResult [ÎI 409 HttpSta阳sCodeResult 具有两个子类,一个是基于响应状态 "404 , Not Found" 的 System. Web.Mvc.H即NotF oundResult ,另一个是基于响应状态 "401 , Not Authorized" 的 System. Web.Mvc.HttpUnau由orizedResult ,第 7 章" Action 的执行"中筛选器 AuthorizeAt位ibute 在授权检验失败的情况下返回的就是一个 HttpUnauthorizedResult 对象。 8.1.7 RedirectResult/RedirectToRouteResult System.Web.Mvc.RedirectResult 帮助我们实现针对某个地址的重定向,其作用与调用 HttpResponse 的 Redirect!RedirectPermanent 方法完全一致。如下面的代码片段所示, RedirectResult 具有两个只读属性 Permanent 和 Url ,前者表示采用永久重定向还是暂时重定 向,默认值为 False ,后者表示重定向的目标地址,既可以采用绝对地址〈比如 h即 :/1飞lVWW.asp.net) ,也可以采用相对地址(比如 ~/accountlregister )。 public class RedirectResult : ActionResult public RedirectResult(string url)i public RedirectResult(string url, bool permanent)i public override void ExecuteResult(ControllerContext context)i qd n 14· 工 or ot bs cc ·-·工1414 bb uu pp Permanent { geti } Url { geti } 暂时重定向和永久重定向可以分别通过调用 H忧pResponse 的 Redirect 和 RedirectPermanent 方法来实现,实际上 RedirectResult 基于重定向的实现就是通过调用这两 个方法来完成的,这可以通过如下所示的 ExecuteResult 方法的定义看出来。 public class RedirectResult : ActionResult //其他成员 public override void ExecuteResult(ControllerContext context) //其他操作 string url = UrlHelper.GenerateContentUrl(this.Url, context.HttpContext)i if (this.Permanent) bool endResponse = falsei context.HttpContext.Response.RedirectPermanent(url, false)i else bool flag2 = falsei context.HttpContext.Response.Red工 rect(url , false)i ASP. NET MVC 4 框架揭秘410 • 第 8 章 View 的呈现 RedirectResult 使我们可以直接重定向到指定的目标地址,另一个类似的 System. Web.Mvc.RedirectToRouteResult 帮助我们根据注册的路由进行重定向。如下面的代码 片段所示, RedirectToRouteResult 没有了表示重定向目标地址的 Url 属性,取而代之的是表 示路由注册名称和路由参数的 RouteName 和 RouteValues 属性, ASP.NET MVC 根据这两个 属性利用注册的路由解析出具体的重定向地址。 public class RedirectToRouteResult : ActionResult public RedirectToRouteResult(RouteValueDictionary routeValues); public RedirectToRouteResult(string routeName, RouteValueDictionary routeValues); public RedirectToRouteResult(string routeName, RouteValueDictionary routeValues, bool permanent); public override void ExecuteResult(ControllerContext context); public bool public string public RouteValueDictionary Permanent { get; } RouteName { get; } RouteValues { get; } 抽象类 Controller 中定义了一系列创建 RedirectResultIRedirectToRouteResult 的方法,比 如 RedirectIRedirectPermanent 方法用于创建重定向到指定 URL 的 RedirectResult , RedirectToActionlRedirectToActionPermanent 用于创建重定向到指定的目标 Action 的 RedirectResultIRedirectToRouteResult ,而 RedirectToRoutelRedirectToRoutePermanent 创建的 RedirectResultIRedirectToRouteResult 对象是针对注册的某个路由的。 暂时重定向和永久重定向有时又被称为 "302 重定向"和 "301 重定向", 302 和 301 表 示响应的状态码。当我们调用 HttpResponse 的 RedirectIRedirectPermanent 方法时,除了会设 置相应的响应状态码之外,还会将重定向的目标地址写入响应报头 (Location) ,浏览器在接 收到响应之后自动发起针对重定向目标地址的访问。 public class HomeController : Controller public ActionResult Redirect() return Redirect(..http://www.asp.net..); public ActionResult RedirectPermanent() return RedirectPermanent(..http://www.asp.net..); 在上面的代码片段中,我们定义了采用暂时重定向和永久重定向的 Action 方法 Redirect 和 RedirectPermanent ,如果我们通过浏览器分别对它们发起访问,会得到具有如下内容的两 个响应。两种重定向的不同作用主要体现在 SEO (Search engine optimization) 上,搜索引擎 会使用永久重定向目标地址更新自己的索引,对于暂时重定向则不会。 ASP. NET MVC 4 框架揭秘8.2 ViewResult 与 ViewEngine 士自 411 //1. Redirect HTTP/1.1 302 Found Server: ASP.NET Development Server/10.0.0.0 Date: Wed , 13 Jun 2012 09:34:15 GMT X-AspNet-Version: 4.0.30319 X-AspNetMvc-Version: 4.0 Location: http://www.asp.net Cache-Control: private Content-Type: text/html; charset=utf-8 Content-Length: 135 Connection: Close Object moved

    Object moved to here.

    //2. RedirectPermanent HTTP/1.1 301 Moved Per.ma nently Server: ASP.NET Development Server/10.0.0.0 Date: Wed , 13 Jun 2012 09:34:40 GMT X-AspNet-Version: 4.0.30319 X-AspNetMvc-Version: 4.0 Location: http://www.asp.net Cache-Control: private Content-Type: text/html; charset=utf-8 Content-Length: 135 Connection: Close Object moved

    Object moved to here.

    8.2 ViewResult 与 ViewEngine 前面我们讨论了各种 Actio nR esult ,与这些采用简单而直接的请求响应机制的 Actio nR esult 相比, ViewResult 基于 View 呈现的请求响应机制要复杂得多,它内部借助了 AS 卫 NETMVC 提供的 View 引擎实现了对 View 获取、激活和呈现。 8.2.1 . View 51 擎申的 View ASP.NETMVC 为我们提供了两种 View 引擎,一种是传统的 WebForm 引擎(由于该引 擎下 View 的设计与我们定义 .aspx 页面一致,又称为 ASPX 引擎),另外一种则是本书默认 采用同时也是推荐使用的 Razor 引擎。在讨论两种 View 引擎的运行机制之前,我们有必要 回答这么一个问题: "View 如何表示? " 提到 View ,很多开发人员可能首先想到的是定义 U 界面的 .aspx (Web Form 引擎〉或 者 .cshtm l/. vbhtml 文件 (R缸 or 引擎),其实对于 View 引擎来说, View 通过 System. Web.Mvc. IView 接口来表示。如下面的代码片段所示, IView 仅仅具有唯一的 Render 方法实现对 View 的呈现。 ASP. NET MVC 4 在架揭秘412 跚 第 8 章 View 的呈现 w e ·工V T'-e c a fι r e +L n ·工C -1 14 b u PJt void Render(ViewContext viewContext , TextWriter writer); public class ViewContext : ControllerContext //其他成员 public virtual bool ClientValidationEnabled { get; set; } public virtual bool Unobtrus 工 veJavaScriptEnabled { get; set; } public virtual TempDataDictionary TempData { get; set; } [Dynamic] public object ViewBag { [return: Dynamic] get; } public virtual ViewDataDictionary ViewData { get; set; } public virtual IView View { get; set; } public virtual TextWriter Writer { get; set; } public abstract class HttpResponseBase //其他成员 public virtual TextWriter Output { get; set; } IView 用于呈现 View 的 Render 方法具有两个参数,其中之一就是表示 View 上下文的 System. Web.Mvc.ViewContext 对象。通过上面的代码片段可以看出 ViewContext 是 Con位 ollerContext 的子类,用于表示状态数据的 ViewData 、 ViewBag 和 TempData 对应着 Con位 ollerBase 的同名属性。 ViewConte且具有两个布尔类型属性 ClientValidationEnabled 和 UnobtrusiveJav aS crip但nabled , 它们分别表示是否支持客户端验证和 UnobtrusiveJavaScript 。这两个属性可以通过如下所示 的同名的 AppSettings 配置项进行设置。如果不具有对应的配置,两个属性默认值为 False 。 配置的范围是针对整个 Web 应用而言的,这个全局属性还可以通过 HtmlHelper 的同名 静态属性进行设置。值得一提的是, ASP.NET MVC 允许我们针对某个 View 开启或者关闭 对客户端验证和 UnobtrusiveJ avaScript 的支持,这可以通过 Htm 旧 elper 的实例方法 EnableClientValidation 和 EnableUnobtrusiveJavaScript 来实现。 r e p 叮t-e 口μ14 m 卡」口μs s a 14 C C .l l b u PJ ‘ //其他成员 public void EnableClientValidation(); public void EnableClientValidation(bool enabled); public void EnableUnobtrusiveJavaScript(); public void EnableUnobtrus 工 veJavaScript(bool enabled); ASP. NET MVC 4 框架揭秘8.2 ViewResult 与 ViewEngine _ 413 public static bool ClientValidationEnabled { get; set; } public static bool UnobtrusiveJavaScr 工 ptEnabled { get; set; } 接口 IView 的 Render 方法的第二个参数是一个 TextWriter 对象。 View 内容的输出可以 通过针对 TextWriter 对象的写操作来实现,因为在 View 引擎在调用 Render 方法的时候,作 为该参数的是当前 H 忧 pResponse 的 Output 属性表示的 TextWriter 。 8.2.2 ViewEngine View 引擎的核心是 ViewEngine 对象,它实现了 System. Web.Mvc .I ViewEngine 接口。如 下面的代码片段所示, IViewEngine 定义了两个 FindView 和 FindPartialView 方法,它们根据 指定的 Con位 ollerContext 、 View 名称和布局文件名称获取对应的 View 和 Partial View 。这两 个方法均具有一个表示是否启用缓存的参数 useCache 。另一个方法 Release View 用于释放 View 对象。 public interface IViewEngine ViewEngineResult FindPartialView(ControllerContext controllerContext , string partialV 工 ewName , bool useCache); ViewEngineResult FindView(ControllerContext controllerContext , string viewName , string masterName , bool useCache); void ReleaseView(ControllerContext controllerContext , IView view); FindView 和 FindPartialView 方法的返回类型并非 IView ,而是一个用于封装 View 的类 型 System.Web.Mvc.ViewEngineResult 。如下面的代码片段所示, ViewEngineResult 的只读属 性 View 和 ViewEngine 属性表示找到的 View 对象和作为调用者的 ViewEngine 对象。在成功 获取到对应 View 的情况下这两个属性会通过构造函数进行初始化。如果没有找到相应的 View ,则将表示搜寻位置的字符串列表传入另一个构造函数来创建返回的 View EngineResult ,只读属性 Searche dL ocations 表示的就是这么一个搜寻位置列表。 public class ViewEngineResult public ViewEngineResult(IEnumerable searchedLocations); public ViewEngineResult(IView view , IViewEngine viewEngine); public IEnumerableSearchedLocations { get; } public IView View { get; } public IViewEngine ViewEngine { get; } 如果返回的 ViewEngineResult 包含一个具体的 View ,那么这个 View 将会最终被呈现出 来。反之,如果 ViewEngineResult 仅仅包含一个通过 Searche dL ocations 属性表示的搜索位置 列表,那么最终呈现出来的就是如图 8-5 所示的包含该列表的错误页面。 AS P. NET MVC 4 框架揭秘414 电第 8 章 View 的呈现 E噩噩噩噩国 . E __ iioI4iCu, I. @localhos t: BG54 食.. 画 Server Error in 'j' Application. .-Bage-- BF The vìew 'NonεxÎstentVìew' or its master was not found or no view engine support宫的 e searched 10臼 tions. The following 10臼 tions were searched: ",/V,尼ws/Hom句INonExistentVìew.aspx "'jViews/Hom句INonExistentView.ascx "'jViewsjSharedjNonExistentView. aspx "'jViewsjSharedjNonExistentView.asα "'jViews/HomejNonExistentView. 臼html "'jVìewsjHomejNonExistentVìew.vbhtml "'jView写IShared/Non ExistentView. cshtml ^'/ViewsjSharedjNonExistentview. vbhtml ~ C~正在 ι 旦旦卫工"_ , - , - . _-一 … 丁 图 8-5 包含搜索位置列表的错误页面 我们可以通过一个简单的实例来验证这一点。在一个 ASP.NETMVC 应用中定义了如下 一个 HomeController ,在默认的 Action 方法 Index 中,我们通过 System. Web.Mvc.ViewEngines 的静态只读属性 Engines 得到一个全局 ViewEngine 列表,并调用其 FindView 方法试图去获 取一个根本不存在的 View ("NonExistentView 勺,最后将得到的 ViewEngineResult 对象的 SearchedLocations 属性表示的搜寻位置列表呈现出来。 public class HomeController : Controller public void Index() ViewEngineResult result = ViewEngines.Engines.FindView( ControllerContext, "NonExistentView", null); foreach (string location in result.SearchedLocations) { Response.Write(location + "
    "); 运行程序后,表示在获取目标 View 过程中采用的搜寻位置列表会以如图 8-6 所示的效 果呈现出来,这个列表的内容与图 8-5 是完全一致的。 .……l阳M叫…ωω圳…III叫…h叫耐σ 钳阳血回血E町百每、 I -凡r~咀创neJNonE冠军tent飞lìew.a句px -NiewS!但OmeJNonExistentVtew.asα -fV陀ws!Shared!'NonExistentVìew.aspx -N"rews!SharedNonE剧创:Vìew.asα -lVìewSl但.ome明白口E且stentVìew.csl阻1 -:凡rtew址f哑巴INω证~:cistet茸:Vtew.由html -lV但wslShared!刷onExistentvÎew .cshtml -Nrews!Shared'NonE副:entVrew:、也hbnl 图 8-6 在获取目标 View 未果情况下采用的搜索位置列表 上面实例演示涉及到了一个重要的静态类型 ViewEngines ,它通过如下定义的只读属性 Engines 维护一个全局 ViewEngine 列表。从给出的定义可以看出,两个原生的 ViewEngine ASP. NET MVC 4 框架揭秘8.2 ViewResult 与 ViewEngine • 415 在初始化的时候就被添加到了该列表中,它们的类型就是分别代表 Web Form 和 Razor 引擎 的 System. Web.Mvc. WebForm ViewEngine 和 System. Web.Mvc.RazorViewEngine 。如果我们创 建了一个自定义 View 引擎,相应的 ViewEngine 也可以通过 ViewEngines 进行注册。 public static class ViewEngines private static readonly ViewEngineCollection engines = 口 ew ViewEngineCollection { new WebFormViewEngine() , new RazorViewEngi 口 e () }; public static ViewEngineCollection Engines get { return _eηgines;} public class ViewEngineCollection : Collection //其他成员 public virtual ViewEngineResult FindPartialView( ControllerContext controllerContext , string partialViewName); public virtual ViewEngineResult FindView( ControllerContext controllerContext , string viewName , string masterName) ; ViewEngines 的静态只读属性 Engines 的类型是 System. Web.Mvc. ViewEngineCollection , 它是一个元素类型为 IViewEngine 的集合。 ViewEngineCollection 同样定义了 FindViewlF indPartialView 方法用于获取指定名称的 View 和分部 View ,这两个方法利用包含 的 ViewEngine 对象进行 View 和 Partial View 的获取。由于 WebFormViewEngine 排在 Raz orViewEngine 之前,所以前者会被优先使用,这可以从图 8-5/8-6 所示的搜寻位置列表看 出来(先搜索 .aspx 和 .ascx ,再搜索 .cshtml 和 .vbhtml) 。 对于 ViewEngineCollection 的 FindView lF indPartialView 方法来说,不知道读者是否注意 到了它们没有一个表示是否采用缓存的 useCache 参数。实际上当这两个方法被调用的时候, ASP.NETMVC 会先采用缓存的方式调用相应的 ViewEngine ,如果返回值为 Null ,再以不采 用缓存的方式再次调用它们。 8.2.3 ViewResult 的执行 View 引擎对 View 的获取以及对 View 的呈现最初是通过 ViewResult 触发的,那么两者 是如何衔接的呢?在正式讨论这个问题之前不妨先来看看 ViewResult 的定义。如下面的代码 片段所示,表示 ViewResult 的类型 System. Web.Mvc. View Result 是抽象类 System. Web.Mvc. ViewResultBase 的子类。 public class ViewResult : ViewResultBase protected override ViewEngineResult FindView(ControllerContext context); public string MasterName { get; set; } ASP. NET MVC 4 在架揭秘416 • 第 8 章 View 的呈现 public abstract class ViewResultBase : ActionResult public override void ExecuteResult(ControllerContext context)i protected abstract ViewEngineResult FindView(ControllerContext context)i public object public TempDataDictionary [Dynamic] public object public ViewDataDictionary public string public ViewEngineCollection public IView Model { get i } TempData { geti seti } ViewBag { [return: Dynamic] geti } ViewData { geti seti } ViewName { geti seti } ViewEngineCollection { geti seti } View { geti seti } ViewResul tB ase 的只读属性 Model 表示作为 View 的 Model 对象,三个表示数据状态的 属性 CViewData 、 ViewBag 和 TempData) 来源于 Controller 的同名属性。 View 和 ViewName 属性则代表具体的 View 对象和 View 的名称。 ViewEngineCollection 属性值默认来源于 ViewEngines 的静态属性 Engines 代表的全局 ViewEngine 列表。 ViewResultBase 用于获取具体 View 的抽象方法 FindView 在 ViewResult 中被实现,后者 提供了额外的属性 MasterName 表示布局文件名称。 FindView 方法在内部会直接调用 View EngineCollection 属性的 FindView 方法,如果返回的 ViewEngineResult 包含一个具体的 View CView 属性不为空),则直接将它返回,否则抛出一个 InvalidOperation 异常,并将通 过 ViewEngineResult 的 SearchedLocations 属性表示的搜寻位置列表格式化成一个字符串作为 该异常的消息,所以图 8-5 所示的搜寻位置列表实际上是抛出的 InvalidOperation 异常的消息。 ASP.NET MVC 的 View 引擎涉及到的相关的类型/接口以及它们之间的关系可以通过如 图 8-7 所示的 UML 来表示。 ViewResult 通过静态类型 ViewEngines 利用 View 引擎激活对应 的 View 对象并最终将 View 的内容呈现出来。 View 主 ngine q l Fi r、dViswfFindPar 甘 aMs咄 <> IView u s e R e n ns n 岖e v ViewEngines En 仰的 < ViewResuJt 图 8-7 View 引擎与 ViewResult ASP. NET MVC 4 在架揭秘8.2 ViewResult 与 ViewEngine _ 417 与除 Empty Result 以外的所有 Actio nR esult 类型一样,抽象类 Controller 中提供了相应的 方法辅助创建 ViewResult 。如下面的代码片段所示, Controller 具有如下一系列 View 方法重 载帮助我们根据指定的 View 名称、 View 对象、布局文件名称和 Model 对象创建相应的 ViewResult 。 public abstract class Controller : ControllerBase , //其他成员 protected ViewResult View()i protected viewResult View(object model); protected ViewResult View(string viewName)i protected ViewResult View(IView view)i protected ViewResult View(string viewName , object model)i protected ViewResult View(string viewName , string masterName)i protected virtual ViewResult View( 工 View view , object model); protected virtual ViewResult View(string viewName , string masterName , object model)i ViewResult 与 View 引擎的交互体现在实现的 ExecuteResult 方法上。如下面的代码片段 所示,如果 View 属性为 Null , ASP.NET MVC 会调用 FindView 方法根据指定的 View 名称 (如果没有执行则采用当前的 Action 名称作为 View 名称)得到一个 ViewEngineResult 对象, 并将其 View 属性作为自身的 View 。接下来一个 ViewContext 被创建出来,它和当前 H 仕pResponse 的 Output 属性代表的 TextWriter 对象被作为参数调用 View 对象的 Render 方法 实现对 View 的呈现。 View 呈现完成之后, ViewEngineResult 对应的 ViewEngine 被提取出来, 并调用其 Release 方法释放 View 对象。 public abstract class ViewResultBase : ActionResult //其他成员 public override void ExecuteResult(ControllerContext context) //其他操作 if (string.IsNullOrEmpty(this.ViewName)) this.ViewName = context.RouteData.GetRequiredString("action")i ViewEngineResult result = null; if (this.View == null) result = this.FindView(context); this.View = result.Viewi TextWriter output = co 口 text.HttpCo 口 text.Response.Output; ViewContext viewContext = new ViewContext(context , this.View , this.ViewData , this.TempData , output)i this.View.Render(viewContext , output); if (result != null) result.ViewEngine.ReleaseView(context , this.View); ASP. NET MVC 4 框架揭翻418 旬第 8 章 View 的呈现 ViewResult 为我们提供了一种与 View 引擎交互的捷径,其实我们在进行 View 的获取和 呈现的时候完全可以抛开 ViewResult ,直接利用 View 引擎来完成。如下所示的两种 Action 方法的定义是完全等效的。 //Action 方法直接返回 ViewResult public class HomeController : Controller public ActionResult Index() return View(); //Action 方法直接调用 View 引擎 public class HomeController : Controller public void Index() string viewName = ControllerContext.RouteData .GetRequiredString("action"); ViewEngineResult result = ViewEngines.Engines.FindView(ControllerContext, viewName, null); if (null == result.View) { throw new InvalidOperationException(FormatErrorMessage( viewName, result.SearchedLocations)); try ViewContext viewContext =口 ew ViewContext(ControllerContext, result.View, this.ViewData, this.TempData, Response.Output); result.View.Render(viewContext, viewContext.Writer); V 」 14 14 a n --在牛,,‘ result.ViewEngine.ReleaseView(ControllerContext, result.View); private string FormatErrorMessage(string viewName, IEnumerablesearchedLocations) string format = "The view '{ O}' or i ts master was not found or no view engine supports the searched loc~t~ons. The following locations were searched:{l}"; StringBuilder builder = new StringBuilder(); foreach (string str in searchedLocations) builder.AppendLine(); builder.Append(str); ASP. NET MVC 4 在架揭秘8.2 ViewResult 与 ViewEngine 额 419 return string.Format(Culturelnfo.CurrentCulture, format, viewName, builder); 前面仅仅介绍了 ViewResult 利用 View 引擎进行 View 的获取和呈现,其实当我们调用 HtmlHelper 的扩展方法 Partial 进行 Partial View 呈现时,内部调用 View 引擎的方式与之类似。 实例演示:创建自定义 View (S805) 为了让读者对 View 引擎及其 View 呈现机制具有一个深刻的认识,我们白定义一个简 单的用于呈现静态 HTML 的 StaticFileViewEngine 。在一个 ASP.NET MVC 应用中定义了如 下一个实现了 IView 接口的类型 StaticFileView ,它表示一个用于呈现指定文件内容的自定义 View ,在实现的 Render 方法中指定文件的内容被读取出来写入作为参数的 TextWriter 对象中。 public class StaticFileView:IView public string FileName { get; private set; } public StaticFileView(string fileName) this.FileName = fileName; public void Render(ViewContext viewContext, TextWriter writer) byte [] buffer; usi 口 9 (FileStream fs = new FileStream(this.FileName, FileMode.Ope 口) ) buffer = new byte[fs.Length]; fs.Read(buffer, 0 , buffer.Length)i writer.Write(Encod工 ng.UTF8.GetString(buffer))i 由于 StaticFileView 中定义的内容完全是静态的,所以缓存显得很有必要。我们只需要 基于 Controller 和 View 名称来实施缓存,为此定义了如下一个作为 Key 的数据类型 ViewEngineResultCacheKey 。 internal class ViewEngineResultCacheKey public string ControllerName { geti private seti } public string ViewName { geti private seti } public ViewEngineResultCacheKey(string controllerName, string viewName) this.ControllerName = controllerName ?? string.EmptYi this.ViewName = viewName ?? string.EmptYi public override int GetHashCode() return this.ControllerName.ToLower() .GetHashCode() ASPNETMVC4 框架揭秘420 省第 8 章 View 的呈现 ^ this.ViewName.ToLower() .GetHashCode(); public override bool Equals(object obj) ViewEngineResultCacheKey key = obj as ViewEngineResultCacheKey; if (null == key) return false; return key.GetHashCode() == this.GetHashCode(); 具有如下定义的 StaticFile View Engine 代表 StaticFile View 对应的 ViewEngine 。我们通过 一个字典类型的字段 viewEngineResults 来全局缓存创建的 ViewEngineResult ,而 View 的获 取实现在 IntemalFindView 方法中。通过 StaticFile View 表示的 View 定义在一个以 View 名称 作为文件名的文本文件中,该文件的扩展名为 .shtml CStatic HTML) 。 public class StaticFileViewEngine : IViewEngine private Dictionary viewEngineResults = new Dictionary()i private object syncHelper = new object(); public ViewEngineResult F 工 ndPartialView(ControllerContext controllerContext , string partialViewName , bool useCache) return this. FindView (controllerContext , partialViewName , null , useCache); public ViewEngineResult FindView(ControllerContext controllerContext , string viewName , string masterName , bool useCache) string controllerName = controllerContext.RouteData.GetRequiredString("controller"); ViewEngineResultCacheKey key = new ViewEngineResultCacheKey(controllerName , viewName)i ViewEngineResult result; if (! useCache) result = InternalFindView(controllerContext , viewName , controllerName)i viewEngineResults[key] = result; return result; if(viewEngineResults.TryGetValue(key , out result)) return result; lock (syncHelper) if (viewEngineResults.TryGetValue(key , out result)) AS P. NET MVC 4 在架揭秘8.2 ViewResult 与 ViewEngine 黯 421 return result; result = InternalFindView(controllerContext, viewName, controllerName); viewEngineResults[key] = result; return result; private ViewEngineResult InternalFindView(ControllerContext controllerContext, str 工 ng viewName, str 工 ng controllerName) string[] searchLocations = new string[] string.Format( "-/views/{O}/{l} .shtml", controllerName, viewName) , string.Format( "-/views/Shared/{O}.shtml", viewName) string fileName = controllerContext.HttpCo口text.Request.MapPath(searchLocations[O]); if (File.Exists(fileName)) return new ViewEngineResult(new StaticFileView(fileName) , this); fileName = string.Format(@"\views\Shared\{O}.shtml", viewName); if (File.Exists(fileName)) return new ViewEngineResult( 口 ew StaticFileView(fileName) , this); return new ViewEngineResult(searchLocations); public void ReleaseView(ControllerContext controllerContext, IView view) {飞} 在 IntemalFindView 方法中,我们先在 "~Niews/{ControllerName }/"目录下寻找 View 文件,如果不存在则在目录 "~Niews/Sh缸ed/"中寻找。如果对应 View 文件被找到,则以 此创建一个 StaticFileView 对象,并将其封装在返回的 ViewEngineResult 对象中。如果目标 View 文件找不到,则根据基于这两个目录的搜寻地址列表创建并返回对应的 ViewEngineResult 。 现在我们在 Globa1. asax 中通过如下的代码对自定义的 StaticFileView Engine 进行注册, 我们将创建的 StaticF i1eViewEngine 作为第一个使用的 ViewEngine 。 public class MvcApplication : System.Web.HttpApplication protected void Application Start() //其他操作 ViewEngines.Engines.Insert(O, new StaticFileViewEngine()); ASP. NET MVC 4 在架揭秘422 'iI 第 8 章 View <<.y呈现 接下来定义了如下一个简单的 HomeController , Action 方法 ShowNo nE xistentView 中通 过调用 View 方法呈现一个不存在的 View (View 名称为" NonE xistentVi ew 勺,而 ShowStaticFile View 方法则将对应的 StaticFileView 呈现出来。 public class HomeController : Controller public ActionResult ShowNonExistentView() return View("NonExistentView"); public ActionResult ShowStaticFileView() return View(); 我们为 Action 方法 ShowStaticFile View 定义了一个具有如下所示 HTML 的 View 文件 ShowStaticFi1 e View.shtml (扩展名不是 .cshtml ,而是 .shtm l) ,并将其保存在 "~Niews 庄fome" 目录下。 Static File View 这是一个自定义的 StaticFileView! 运行程序并在浏览器中输入相应的地址访问 Action 方法 ShowNo nExistentView ,会得到 如图 8-8 所示的输出结果。图中列出的 View 搜寻位置列表中的前两项正是我们自定义的 StaticFile ViewEngine 寻找对应 . shtml 文件的两个路径。 醒曹帘 615/ t1 omeíS t1 owN o I1 Exi , ten目『盟 Server Error i 门'j' App l ication. 们 I '! I 了he view 'NonEx;stentVÎew' or its mas 恒 r was not found 1=11 or no view 叫 ine suppo 由 the sea 时 ed Jocations. 了he U following locations were searched: 1"'/ Vlews/ nomel NOn l=XISrenCVlew. snrm "'jviewsjSharedjNonExistentView.shtml ""jViewsjhomejNonExÎstentView. aspx "'jViewsjhomejNonExistentView.ascx "'jViewsjSharedjNonExistentV;ew.aspx "'jViewsjSharedjNonExistentView.ascx "'jViewsjhomejNonExistentView.cshtml "'jViewsjhomejNonExistentView. vbhtmJ tvjViewsjSharedjNon ExistentV;ew.cshtml "'jViewsjSharedjNonExistentView. vbhtml 图 8-8 自定义 ViewEngine 在获取 View 失败情况下对搜寻地址列表的显示 ASP. NET MVC 4 框架揭秘8.3 Razorsl 擎... 423 如果我们改变浏览器的地址来访问另一个 Action 方法 ShowStaticFileView ,会呈现出如 图 8-9 所示的输出结果,不难看出呈现出来的正是定义在 ShowStaticFile Vi ew.shtml 中的 HTML 。 图 8-9 St aticFileView 针对静态文件内容的呈现 8.3 Razor 51 擎 WebForm 引擎〈或者 ASPX 引擎)和Raz or 引擎是 ASP.NETMVC 原生支持的两种 View 引擎,但从编程便捷性来看,后者无疑是更好的选择,所以我们选择对 Razor 引擎的运行机 制作一个详细介绍。Vi ew 引擎实现了Vi ew 对象的创建与呈现,对于Raz or 引擎来说,具体的 Vi ew 的内容被定义在相应的 .cshtml 或者 .vb.html 文件中,表面来看Vi ew 引擎帮助我们根据指 定的Vi ew 名称按照预定义的目录列表来搜寻相应的 View 文件,但是不要忘了 ASP. 阳 TMVC 不能"解释" View 文件, View 文件必须经过编译。 8.3.1 View 的编译原理 通过 .cshtml 或者 .vbhtml 文件定义的 View 能够被执行,必须先被编译成存在于某个程序 集的类型, ASP.NET MVC 采用动态编译的方式对 View 文件实施编译。当我们在对 ASP.NET MVC 进行部署的时候,需要对 .cshtml 或者 .vbhtml 文件进行打包。针对某个 View 的第 1 次 访问会触发针对它的编译,一个 View 对应着一个类型。 我们可以对 .cshtml 或者 .vbhtml 进行修改, View 文件修改后的第 1 次访问将会导致 View 的再一次编译。和 ASP.阳 T 传统的编译方式一样,针对 View 的编译默认是基于目录的, 也就是说同一个目录下的多个 View 文件被编译到同一个程序集中。 实例演示:探测基于目录的 View 编译机制 (5806 ) 为了让读者对 ASP.NET MVC 中 View 文件的编译机制有一个深刻的认识,我们通过一 个简单的实例来确定 View 文件最终都被编译成什么类型,所在的程序集又是哪一个。我们 在一个 ASP.NETMVC 应用中为 HtmlHelper 定义了如下一个扩展方法 ListView Assemblies , 该方法用于获取当前被加载的包含Vi ew 类型的程序集(程序集名称以 "App_Web_" 为前缀)。 public static class HtmlHelperExtensions AS P. NET MVC 4 框架揭秘424 恒第 8 章 View 的呈现 public static MvcHtrnlString ListViewAssernblies(this HtrnlHelper helper) TagBuilder ul = new TagBuilder("ul"); foreach(var assernbly in AppDornain.CurrentDornain.GetAssernblies() .Where(a=>a.FullNarne.StartsW 工 th ( "App _ Web _") ) ) TagBuilder li = new TagBuilder("li"); li.lnnerHtrnl = assernbly.FullNarne; ul.lnnerHtrnl+= li.ToString(); return new MvcHtrnlStr 工 ng(ul.ToString()); 然后我们定义了如下两个 Controller 类型 (F ooController 和 BarController) ,它们之中各 自定义了两个 Action 方法 Action1 和 Actio n2。 public class FooController : Controller public ActionResult Actionl() return View(); public ActionResult Action2() return View(); public class BarController : Controller public ActionResult Actionl() return View(); public ActionResult Action2() return View(); 接下来我们为定义在 F ooController 和 BarController 的四个 Action 创建对应的 View (对应文件路径为: "~/Views/Foo/Action l. cshtml" 、" ~/Views/Foo/Action2.cshtml "、 '~/Views/Bar/ Action1.cshtml" 和" ~Niews lB ar/ Actio n2 .cshtml 勺。它们具有如下相同的定 义,我们在 View 中显示自身的类型和当前加载的基于 View 的程序集。
    当前 View 类型 :@this.GetType() .AssernblyQualifiedNarne
    当前加载的 View 程序集:
    @Htrnl.ListViewAssernblies() 现在运行我们的程序并在浏览器中通过输入相应的地址"依次" ("Foo/Action1" 、 "F 00/ Action2 "、 "B 缸/Action1 "和 "Bar/Action2 ")访问定义在 FooCon位 oller 和 BarController 的四个 Action ,四次访问得到的输出结果如图 8-10 所示。 AS P. NET MVC 4 框架揭秘当前View类型ASP__ Pag<飞yiews_bar_Ac出n2_cs脑也 App_Web_,也!in lo3 a, Version=O_O_O_O, C由ure=唱E由司, PublicKeyToken司UD 当前加载的 Vìew.程序集 • App_Web_j04xtjs)'飞 Version=O_O_O_O , Cu1ture=司1回lIral , PublK这eyToken=null • App_ Web_dajulo3a, Version=O_O_O_O, Cu1ture=电回国L Pub 1icKeyTok臼月un 图 8-10 当前 View 的类型和加载的 View 程序集 图 8-10 所示的输出结果至少可以反映三个问题。 8.3 Razorsl擎毡 425 • ASP.NET MVC 对 View 文件进行动态编译生成的类型名称基于 View 文件的虚拟路径 (比如文件路径为" ~NiewslF 00/ Actionl.cshtml" 的View 对应的类型为"ASP._Page_ Views foo Action 1 cshtml 勺。 • AS卫NET MVC 是按照目录进行编译的(" ~NiewslF 00/" 下的两个 View 文件最终都被 编译到程序集 "App_Webj04xtjsy" 中)。 • 程序集是按需加载的。第一次访问 "~NiewlFoo/ "目录下的View 并不会加载针对 "~Niew/B缸j" 目录的程序集(实际上此时该程序集尚未生成〉。 我们可以调用 BuildManager 类型的静态方法 GetCompiledType 和 GetCompiledAssembly (如下面的代码片段所示〉根据 View 文件的虚拟路径分别得到编译后的类型和程序集。 public sealed class BuildManager //其他成员 public static Type GetCompiledType(string virtualPath); public static Assembly GetCompiledAssembly(string virtualPath); 在现有演示实例的基础上我们创建了如下一个 HomeCon仕oller ,默认的 Action 方法Index ASP. NET MVC 4 在架揭秘426 电第 8 章 View 的呈现 中通过调用 BuildManager 的静态方法 GetCompiledType 得到并呈现出四个 View 文件对应的 类型名称。 public class HomeController : Controller public void Index() Response.Write(Buil dM anager.GetCompiledType( "-/Views/Foo/Actionl.cshtml") + "
    "); Response.Write(Buil dM anager.GetCompiledType( "-/Views/Foo/Action2.cshtml") + "
    "); Response.Write(Buil dM anager.GetCompiledType( "- /Views/Bar /Action 1. cshtml") + "
    ") ; Response.Write(Buil dM anager.GetCompiledType( "-/Views/Bar/Action2.cshtml") + "
    "); 直接运行程序后会在浏览器中得到代表四个 View 文件编译类型名称的字符串,具体显 示效果如图 8-11 所示。与图 8-10 显示的 View 类型名称相比较,我们会发现它们是一致的。 -口 x 醺霸 p/ 合 l ~、 ASP __Page_ Vi ews_Foo_Acti∞ l_cshtml ASP __Page_ Vi ews_F ∞_Acti0n2_c 也国l ASP __Page_ Views_Bar_Actionl_cshtml ASP _ _Pag哇二,Views_Bar_Actio n2 _csh恒温 图 8-11 View 文件编译后的类型名称 编译 View 文件会生成怎样的类型 前面我们简单地介绍了 ASP. 阳 TMVC 以目录为单位的动态 View 编译,有人可能会问 一个问题:编译生成的程序集存放在哪里?在默认情况下, View 文件被动态编译后生成的 程序集被临时存放在 ASP.NET 的临时目录"% W inDit'lo \Microsoft.NE1\Framework\ {Version No}\Temporary AS 卫 NET Files飞"下,不过我们可以通过如下所示的配置节 / 的 tempDirectory 属性来改变动态编译的临时目录。如果我们改变了这个临时 目录,需要确保工作进程运行账号具有访问该目录的权限。 一个寄宿于 IIS 的 Web 应用会在上述的临时目录下创建一个与 Web 应用同名的子目录,所 以我们可以很容易地找到应用对应的编译目录。但是对于将Vi sual Studio Development Server 作为宿主的 Web 应用都会编译到名称为 Root 的子目录下。如果这样的应用太多,我们往往 不太容易准确地找到基于某个应用的编译目录。有时候可以根据目录最后的修改时间来找到 它,但是笔者个人倾向于直接删除整个 Root 目录,然后运行我们的程序后会重新生成一个 ASP. NET MVC 4 框架揭翻8.3 Razor 51 擎. 427 只包含该应用编译目录的 Root 目录。 对于上面演示的实例,笔者将 Web 应用寄宿于 IIS 下并且命名为 MvcApp ,在本机的目 录" C:\ Windows\Microsoft.NET\Framework\v4.0.30319\ Temporary ASP.NET Files\mvcapp\ c4eaOafa\a83bd407 "下可以找到动态编译的生成的文件。如图 8-12 所示,两个 View 目录 ("~Niews lF oo" 和 "~Niews lB缸勺编译生成的程序集就在这个目录下面。 @己守 Organi" ...一 Open New folder 弓... t1l 'íi • r) amε !!!" as 咒 mbly hash u 坦rC ache d'c App_global.ðsax.ogqhmgb2.dll 毛 App_Web_lofnog13.dll I Datξmo di!, ed Typ 是 导 15 :201':: S: f) S ;::t'A F!te f ,::!:j~r 6..15:2 (j l~ .s :3~1 Pt'~l Fdefc!dtr 6ílS;201 、 .j:ll?M Hk:fckiel 6l1 S;2QL). 9:17 P~./f t~P! ,~lic;;!.iori t.'xt 、川, 6/1S 旷 2 古 L) 9:17 Pt .. l ;''\t;p l!.-:曰 lon f..%tξnτ :是 IZ e: 4 K:, 5 ,3 ,。 App_Web_o3k忘 kdso.dll I 6/15i2012 S:l 7 Pt ... l "ypli( 民 10 门缸t t: n:.. 5 i'-- B isuj5v 町r.cmdline 6/15/20129:17 P 以 ζM Cl llNE File 6 K8 isuj5vey.err Date mcdifiedι'15/2012 9:17 P~A Dl 坦白 eated: 6/1 5/20129:17 PM [F~ F e Size: 0 b}'tes 图 8-12 ASP.NET 临时目录中编译生成的程序集 读者一定很好奇一个 View 文件通过动态编译最终会生成一个怎样的类型?对应前面演示 的实例我们已经知道了四个 View 文件编译生成的类型名称和所在的程序集,我们只需要通过 Reflector 打开对应的程序集就能得到 View 文件编译类型的定义。如下所示的是 View 文件 " ~NiewslF 00/ Action.cshtrr址"编译后生成的 ASP. _P age_ Views _F 00 _ Action 1_ cshtml 类型的定义。 [Dynamic (new bool [] { false , true })] public class _Page_Views_Foo_Actionl_cshtml : WebViewPage public override void Execute() this.WriteLiteral("
    当前 View 类型:
    \r\n
    "); this.Write(base.GetType() .AssemblyQualifiedName); this.WriteLiteral("

    \r\n
    当前加载的 View 程序集:
    \r\n"); this.Write(base.Html.ListViewAssemblies()); protected global_asax ApplicationInstance get return (global asax) this.Context.ApplicationInstance; 8.3.2 WebViewPage 与 WebViewPage 从上面的代码可以看出 View 文件编译生成的类型是 System. Web.Mvc. Web ViewPage ASP. NET MVC 4 框架揭秘428 旬第 8 章 View 的呈现 的子类,泛型参数 TModel 代表 View 的 Model 类型。 WebViewPage是 System. Web.Mvc. Web ViewPage 的子类,不论强类型还是弱类型 View 文件,最终编译生成的 都是 WebViewPage子类,弱类型 View 类型继承自 WebViewPage (比如上 面的_Page_ Views _Foo _ Actionl_ cshtml) 。 WebViewPage 定义和继承了很多成员,由于 View 文件编译的目标类型是 WebViewPage 的子类,意味着这些成员可以直接在定义 View 的时候使用,所以很有必要对它们有一个基 本的了解。受篇幅所限,在这里我们只对一些常用的属性和方法作一个大概的介绍。 WebPageExecutingBase Web ViewPage 可以简单地视为一个 WebPage ,它最终继承自具有如下定义的抽象类 System. Web. WebPages. WebPageExecutingB ase 。我们通过调用抽象方法 Execute 将页面内容呈 现出来, View 文件编译生成的类型正是通过重写这个方法将自身定义的内容呈现出来的。 具体来说, View 文件的内容大体可以分为静态和动态两个部分,静态内容被原样呈现,而 动态内容通过执行一段代码来生成。静态内容和动态内容的输出分别通过调用 WriteLiteral 和 Write 方法来完成。除了这三个重要的抽象方法之外, WebViewPage 还具有一个表示虚拟 路径的 VirtualPath 属性。 public abstract class WebPageExecutingBase //其他成员 public abstract void Execute(}i public abstract void Write(object value}i public abstract void WriteLiteral(object value}i public virtual string VirtualPath { geti seti } WebPageRenderingBase 另一个抽象类 System. Web. WebPages. WebPageRenderingBase 是 WebPageExecutingB ase 的子类。我们知道一个 View 文件可以具有一个外部的布局文件(或者母版页),布局文件的 虚拟路径通过抽象属性 Layout 表示,在定义 View 的时候对 Layout 文件的设置实际上就是 对该属性赋值。 View 自身的内容通过重写的 Execute 方法输出,整个页面(包括布局文件和 View 文件)的内容输出通过调用抽象方法 ExecutePageHierarchy 来完成。 public abstract class WebPageRenderingBase : WebPageExecutingBase, //其他成员 public abstract void ExecutePageHierarchY(}i public abstract string Layout { geti seti } public virtual bool IsAjax { get; } ASP. NET MVC 4 框架揭秘8.3 Razorsl擎也 429 public virtual bool IsPost { geti } public string Culture { get; seti } public string UICulture { geti seti } public virtual HttpRequestBase public virtual HttpResponseBase public virtual HttpServerUtilityBase public virtual HttpSessionStateBase public virtual IPrincipal public ProfileBase public virtual Cache Request { geti } Respo口 se { get i } Server { geti } Session { geti } User { geti } Profile { geti } Cache { geti } WebPageRenderingBase 还定义了一些辅助的属性帮助我们判断当前的请求是 Aj 缸请求 CIsAjax) 还是 Post 请求(lsPost) 。通过属性 CulturelUlCulture 可以获取和设置当前线程的 语言文化,通过其他相应的属性还可以获取基于当前 H忧pContext 的一些属性 CRequest 、 Response 、 Se凹町、 Session 、 Profile 和 Cache 等)。 WebPageBase System. Web. WebPages.WebPageBase 是 WebPageRenderingB ase 的子类,也是 WebViewPage 的直接基类。除了实现定义在 WebPageRende血gBase 中的抽象方法 ExecutePageHierarchy 方法 之外, WebPageBase 定义了额外两个 ExecutePageHierarchy 方法重载。在调用这两个方法的时 候,我们需要将执行的上下文信息封装在类型为 System. Web. WebPages. WebPageContext 的参 数 pageContext 中,而另一个参数 st町tPage 表示定义在一ViewStart-cshtmLViewStart-vbhml 中的开始页面。最终呈现出来的页面的内容来源于三个部分:布局文件、开始页面和 View 本身的内容。完整页面内容的呈现是通过调用这两个 ExecutePageHierarchy 方法来完成的。 public abstract class WebPageBase : WebPageRenderingBase //其他成员 public override void ExecutePageHierarchy(); public void ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer); public void ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer, WebPageRenderingBase startPage)i public override void Write(object value)i public override void WriteLiteral(object value); public override string Layout { geti seti } public HelperResult RenderSection(string name)i public HelperResult RenderSection(string name, bool required); public HelperResult RenderBodY()i public bool IsSectionDefined(string name)i public void DefineSection(string 口 ame , SectionWriter action)i WebPageBase 实现了定义在 WebPageExecutingB ase 上的抽象方法 Write/WriteLiteral 和抽 ASP.NET MVC 4 在架揭秘430 诲第 8 章 View 的呈现 象属性 Layouto 除此之外, WebPageBase 还定义了一些帮助我们在布局文件上输出 Section 的方法,其中 RenderSection 用于输出指定名称的 Section ,参数 required 表示输出的 Section 是否是必需的。如果该参数值为 True ,在相应的 Section 尚未定义的情况下会直接抛出 HtψExc 叩 tion 异常。 RenderBody 用于输出主体部分〈名称为 "Body" 的 Section) ,而 IsSectio nD efmed 用于判断指定名称的 Section 是否被定义。方法 DefmeSection 用于定义相应 的 Section ,在 View 文件中定义 Section 的 @section sectio nN ame{...} 语句最终都转换为针对 该方法的调用。 比如说我们通过如下一个布局文件将页面分为 Header 、 Body 和 Footer 三个 Section ,如 果 View 中没有显式定义 Header 和 Footer ,则采用默认定义的内容进行填充。使用该布局文 件的 View 定义了 Body 的内容 (View 中没有显式指定 Section 名称的内容被默认作为 Body 的内容),并且定义了 Header 这个 Section 。 //布局文件定义 @ViewBag.Title @(if(IsSectionDefined("Header")) {@RenderSection("Header")i} else{

    @ViewBag.Title

    } @RenderBody ( ) @(if (IsSectionDefined("Footer")) {@RenderSection("Footer")i} else{

    @2012 Artech. All rights reserved.

    } //View 的定义

    dummy text...dummy text...dummy text...dummy text...dummy text... dummy text...

    dummy text...dummy text...dummy text...dummy text...dummy text... dummy text...

    @section Header

    Article Title

    上面定义的 View 最终呈现在浏览器中的效果如图 8-13 所示,我们可以看到 Header 、 Body 和 Footer 的内容得到了合理的显示,其中 Header 和 Footer 的内容分别来源于 View 和 布局文件。 ASP. NET MVC 4 框架揭秘. 飞 - E J X []j仪 alh ost:15 S5. ‘ ~: φ .. c: 甜哩哩!些些 5 'cl ti 、 Article Title dummy texL chm:皿 y text .. .d田nm. y t ext.. .dummy t四… dummy text .. .dmmny t ext. . dummy text... dmmny text ... dummy texL. dummy text._ .dummy text. .. dummv text … ~2012 Art民h_ An ri政臼 reserved 图 8-13 采用了布局文件的 View 的呈现效果 WebViewPage 8.3 Razorsl 擎电 431 View 文件最终编译生成的 Web ViewPage 是 System. Web.Mvc. Web ViewPage 的 子类,而后者继承自 WebPageBase 。我们在定义 View 的时候使用的三个帮助对象 (Htm 旧 elp 町、 UrlHelper 和 Aj axH elper) 所对应的属性就定义在这里,它们通过调用 InitHelpers 方法进行初始化。 public abstract class WebViewPage : WebPageBase , IViewDataContainer , IViewStartPageChild //其他成员 public HtrnlHelper Htrnl { get; set; } public UrlHelper Url { get; set; } public AjaxHelper Ajax { get; set; } public virtual void InitHelpers(); public ViewContext ViewContext { get; set; } public ViewDataDictionary ViewData { get; set; } [Dynarnicl public object ViewBag { get; } public TernpDataDictionary TernpData { get; } public object Model { get; } protected virtual void SetViewData(ViewDataDictionary viewData); 我们在定义 View 的时候可以通过只读属性 Model 、 ViewData 、 ViewBag 和 TempData 得到在 Controller 中设置的状态数据,也可以通过调用 SetViewData 设置 ViewData , ViewContext 表示 View 当前上下文。 WebViewPage 虽然 View 具有强弱类型之分,但是 View 文件生成的类型都是"强类型的",它们都是 Web ViewPage 的子类。对于弱类型 View 来说,生成的类型继承自 Web Vi ewPage 。如下面的代码片段所示, Web ViewPage 的属'性 Ajax 、 Html 、 ASP. NET MVC 4 在架揭秘432 二黯 第 8 章 View 的呈现 Model 和 ViewData 属性都是针对泛型参数 TModel 的,而初始化它们的 InitHelpers 和 SetViewData 方法被重写。 public abstract class WebViewPage : WebViewPage public override void InitHelpers(); protected override void SetV 工 ewData(ViewDataD 工 ct 工 onary viewData); public AjaxHelper public HtrnlHelper public TModel public ViewDataDictionary 8.3.3 RazorView Ajax { get; set; } Htrnl { get; set; } Model { get; } ViewData { get; set; } Razor 引擎下的 View 通过类型 System. Web.Mvc.RazorView 表示,它与表示 Web Form 引擎 View 的类型 System. Web.Mvc. WebForm View 都是 System. Web.Mvc.BuildManager CompiledView 的子类。 BuildManagerCompiledView 为了能够清楚地说明实现在 BuildManagerCompiledView 中的 View 激活与呈现机制,我 们列出了 BuildManagerCompiledView 中与此相关的内部和受保护的成员。 public abstract class Buil dM anagerCornpiledView : IView internal IViewPageActivator ViewPageActivator; protected BuildManagerCornpiledView(ControllerContext controllerContext , string viewPath); protected BuildManagerCornpiledView(ControllerContext controllerContext , string viewPath , IViewPageActivator viewPageActivator); internal BuildManagerCornpiledView(ControllerContext controllerContext , string viewPath , IViewPageActivator viewPageActivator , IDepe 口 dencyResolver dependencyResolver); public void Render(ViewContext viewContext , TextWriter writer); protected abstract void RenderV 工 ew(ViewContext viewContext , TextWriter writer , object instance); internal IBuil dM anager BuildManager { get; set; } public string ViewPath { get; protected set; } 采用 Razor 引擎的 View 文件( .cshtml 或者 .vbhtm l)最终都会编译成一个 Web ViewPage 类型,所以通过 RazorView/WebFormView 体现的 View 的呈现机制最终体现在对 Web ViewPage 对象的激活上。根据上面介绍的 ASP.NET MVC 编译原理,可以利用 BuildManager 根据 View 文件的虚拟路径得到编译后的类型。从名称也可以看出来, Buil dM anagerCompiledView 内部就是利用了 Buil dM anager 根据指定的 View 文件虚拟路径完 ASP. NET MVC 4 框架揭翻8.3 Razor 51 擎霞 433 成对 WebViewPage 对象激活的。 BuildManagerCompiledView 的属性 ViewPath 表示的就是 View 文件的虚拟路径,该属性 在构造函数中被初始化。 BuildManagerCompiledView 具有三个构造函数,对象本身的构造逻 辑体现在内部构造函数上。如上面的代码片段所示,除了将当前 ControllerContext' 和 View 文件虚拟路径作为构造函数的参数之外,该构造函数还具有额外两个参数,其类型分别是 System 川 Teb.M vc.IView PageActivator 和 IDependency Resolver 。 public interface IViewPageActivator object Create(ControllerContext controllerContext , Type type)i 上面的代码片段体现了接口 IViewPageActivator 的定义。顾名思义,该接口旨在实现对 Web ViewPage 对象的激活,基于类型的对象激活机制实现在 Create 方法中。 BuildManagerCompiledView 的构造函数中指定的 View PageActivator 被用于初始化内部字段 ViewPageActivator ,默认采用的是一个 DefaultViewPageActivator 对象。 DefaultViewPageActivator 是一个具有如下定义的内部类型,可以看到它实际上依赖于一 个 Dependency Resolver 对象完成针对 Web ViewPage 对象的激活。这个 Dependency Resolver 对象可以通过构造函数进行显式设置,而默认使用的 Dependency Resolver 对象来源于 Dependency Resolver 类型的静态属性 Current 。 internal class DefaultViewPageActivator : IViewPageActivator private Func resolverThunki public DefaultViewPageActivator() : this(null) { } public DefaultViewPageActivator(IDependencyResolver resolver) Func< 工 DependencyResolver> func = nulli if (resolver == null) this. resolverThunk = () => DependencyResolver.Currenti else if (func == null) func = () => resolveri } this. resolverThunk = funCi public object Create(ControllerContext controllerContext , Type type) retur 口 (this. resolverThunk() .GetService(type) ?? Activator.Createlnstance(type))i AS P. NET MVC 4 蓓架揭秘434 • 第 8 章 View 的呈现 如果我们在构造 BuildManagerCompiledView 的时候没有指定具体的 ViewPageActivator ,那 么 AS卫NETMVC 会根据指定的 DependencyResolv町来创建默认的 DefaultViewPageActivator 。 如果我们只是根据 Con位ollerContext 和 View 文件虚拟路径来构建 BuildManagerCompiledView ,最终用于激活 WebPageView 的实际上就是当前的 DependencyResolver 。换句话说,我们可以通过注册白定义 DependencyResolver 的方法以 IoC 的方式来实现对 WebPageView 的激活,接下来我们会演示相关的实例。 BuildManagerCompiledView 对 View 的呈现机制其实很简单。它调用 BuildManager 的静 态方法 GetCompiledType 根据指定的 View 文件虚拟路径得到编译后的 WebPageView 类型, 然后将该类型交给 ViewPageActivator 激活一个具体的 WebPageView 对象,并调用其 Render 方法完成对 View 的最终呈现。 BuildManagerCompiledView 利用激活的 WebPageView 对 View 的呈现实现在抽象方法 RenderView 中,而 Render 方法仅仅实现了根据 View 文件虚拟路径 对 WebPageView 的激活,具体的实现可以通过如下的代码片段来体现。 public abstract class BuildManagerCompiledView : IView //其他成员 public void Render(ViewContext viewContext, TextWriter writer) Type viewType = BuildManager.GetCompiledType(ViewPath); object instance = null; if (null != viewType) } //controllerContext 字段表示在构造函数中指定的 ControllerContext instance = this.ViewPageActivator.Create( controllerContext, viewType); this.RenderView(viewContext, writer, instance); } protected abstract vo 工 d Render飞Tiew(ViewContext viewContext, TextWriter writer, object instance); RazorView 表示 Razor 引擎下的 View 的类型 RazorView 直接继承 BuildManagerCompiledView 。如 下面的代码片段所示,它具有额外的三个只读属性属性。 LayoutPath 表示 View 使用的布局 文件的虚拟路径,而 RunViewStartPages 和 ViewStartFileExtensions 属性与通过 , ViewStart.cshtml" 或" ViewStart.vbhtml" 文件定义的开始页面有关,前者表示是否需要 执行开始页面,后者表示开始页面文件的扩展名。对于 Razor 引擎默认创建的 RazorView , Run ViewStartPages 属性为 TrueC 意味着总是会执行开始页面) 0 ViewStartFileExtensions 属性 表示的字符串集合包含两个元素 "cshtml" 和 "vbhtml" 。 public class RazorView : BuildManagerCompiledView public RazorView(ControllerContext controllerContext, string viewPath, string layoutPath, bool runViewStartPages, ASP. NET MVC 4 框架揭翻8.3 Razor 5 隆德 435 IEnumerable viewStartFileExtensions); public RazorView(ControllerContext controllerContext , string viewPath , string layoutPath , bool runViewStartPages , IEnumerable viewStartFileExtensions , IViewPageActivator viewPageActivator); protected override void RenderView(ViewContext viewContext , TextWriter writer , object instance); public string LayoutPath { get; } public bool RunViewStartPages { get; } public IEnumerable ViewStartFileExtensions { get; } RazorView 通过实现 RenderView 方法最终完成了对 View 的呈现。方法传入参数 instance 是通过 BuildManagerCompiledView 激活的 View 对象,通过上面的介绍我们知道这是一个空 的 Web ViewPage 对象(默认情况下是通过默认构造函数创建的) 0 RazorView 在 RenderView 方法中对其进行初始化后调用 ExecutePageHierarchy 方法将整个页面内容呈现出 来。 RazorView 实现 RenderView 方法的逻辑基本上可以通过如下的代码片段来表示。 public class RazorView : BuildManagerCompiledView //其他成员 protected override void RenderView(ViewContext viewContext , TextWriter writer , object instance) WebViewpage page = instance as WebViewPage; //初始化 WebViewPage Initialize(page) ; //得到表示开启页面的 WebPageRenderingBase 对象 WebPageRenderingBase startPage; if (this.RunViewStartPages) startpage = StartPage.GetStartPage(page , " ViewStart" , this.ViewStartFileExtensions); HttpContextBase httpContext = v 工 ewContext.HttpContext; page.ExecutePageHierarchy( new WebPageContext(viewContext.HttpContext , null , null) , writer , startPage); 实例演示:创建一个简单的 RazorView (S807) 为了让读者了解 RazorView 实现Vi ew 呈现的本质,我们按照其实现原理自定义一个简 单的 RazorView 类型,在一个 ASP.NETMVC 应用中定义了如下一个表示自定义 RazorView 的 SimpleRazorView 类型, SimpleRazorView 直接实现了 IView 接口,在构造函数中初始化 的属性 ViewPath 表示 View 文件的虚拟路径。 public class SimpleRazorView: IView ASP. NET MVC 4 在架揭秘436 诵第 8 章 View 的呈现 public stri 口 g ViewPath { get; private set; } public SimpleRazorView(string viewPath) this.ViewPath = viewPath; public void Render(ViewContext viewContext, TextWriter writer) Type viewType = BuildManager.GetCompiledType(this.ViewPath); object instance = Activator.CreateInstance(viewType); WebViewpage page = (WebViewPage)instance as WebViewPage; page.VirtualPath = this.ViewPath; page.ViewContext = v 工 ewContext; page.ViewData = viewContext.ViewData; page.InitHelpers()i WebPageContext pageContext = new WebPageContext(viewContext.HttpContext ,口ull , null)i WebPageRenderingBase startpage = StartPage.GetStartPage( page, " ViewStart", new string[] {"cshtml", "vbhtml"}); page.ExecutePageHierarchy(pageContext, writer, startPage); 在用于呈现 View 的 Render 方法中,我们利用 BuildManager 根据当前 View 文件的虚拟 路径得到动态编译后的类型,然后利用该类型以反射的方式创建一个 WebViewPage 对象。 接下来我们初始化该 WebViewPage 对象的 VirtuaIPath 、 ViewContext 和 ViewData 属性,并调 用 InitHelpers 方法对 HtmIHelper 、 UrlHelper 和 Aja温句er 进行初始化。 SimpleRazorView 总是会执行开始页面,所以我们通过调用 System. Web.Mvc. ViewStartPage 的静态方法 GetStartPage 根据指定的开始页面文件名 C ViewStart) 和扩展名 列表 Ccshtml 和 vbhtml) 得到表示开始页面的 WebPageRenderingBase 对象。最后我们创建 WebPageContext 对象,并将它和表示开始页面的 WebPageRenderingB ase 对象作为参数调用 Web ViewPage 的 ExecutePageHierarchy 方法实现对整个页面的呈现。 为了验证 SimpleRazorView 能够正常地完成对 View 内容的呈现,我们定义了如下一个 HomeController 。在默认的 Action 方法 Index 中,创建一个 Contact 对象作为当前 ViewData 的 Model ,然后通过指定 View 文件的虚拟路径 C"~Niews庄IomelIndex.cshtml ")创建我们自 定义的 SimpleRazorView 对象,最后创建 ViewContext 并将其作为参数调用 SimpleRazorView 的 Render 方法将默认的 View 呈现出来。 public class HomeController : Controller public void Index() ViewData.Model = new Co口 tact { Name = "张三", PhoneNo = "123456789", EmailAddress=..zhangsan@gmai1.com.. }; SimpleRazorView view =口 ew SimpleRazorView ("-/Views/Home/ 工 ndex.cshtml"); ASP. NET MVC 4 在架揭秘8.3 Razor 51 擎醺 437 ViewContext viewContext = new ViewContext(ControllerContext , view , ViewData , TempData , Response.Output)i view.Render(viewContext , viewContext.Writer)i +」c a •L n o 户」s s a 14 C C .工14 b u nyfI [DisplayName(" 姓名"门 public string Name { geti seti } [DisplayName(" 电话号码") ] public string PhoneNo { geti seti } [DisplayName(" 电子邮箱地址") ] public striηg EmailAddress { geti seti } 我们的 View 很简单。如下面的代码片段所示,这是一个 Model 类型为 Contact 的强类 型 View ,在该 View 中直接调用 HtmlHelper 的扩展方法 EditorForModel 将作为 Model 的 Contact 对象以编辑模式呈现在一个表单之中。 @modelContact @{ ViewBag.Title = Model.Namei @using (Html.BeginForm()) @Html.EditorForModel() 为了验证我们白定义的 SimpleRazorView 对布局文件和 ViewStart 页面的支持,在 "-Niews/Sh 町 ed/" 目录下定义了如下一个名为"一Layou t. cshtml" 的布局文件,布局文件的 设置通过定义在 "-Niews/" 目录下具有如下定义的" ViewStart.cshtml" 文件来指定。 Layout. cshtml: @ViewBag.Title

    编辑联系人信息

    @RenderBody ( ) ViewStart.cshtml: @{ Layout = "-/Views/Sharedl Layout.cshtml"i 运行程序后会在浏览器中呈现如图 8-14 所示的输出结果,可以看出这和我们直接在 Action 方法 Index 中返回一个 ViewResult 对象没有什么不同。 ASPNETMVC4 框架揭秘438 白第 8 章 View 的呈现 IT二 -LL L I 编辑 联系人 信息 姓名 电 话号码 :旦到 673 9 电子邮箱 地址 I ~h a n gsan@gma i l. com f 配 图 8-14 通过自定义 RazorView 呈现出的 View 实例演示:以 loC 的 15 式激活 View (S808) 在前面介绍 BuildManagerCompiledView 的时候我们谈到,默认使用的 ViewPageActivator 使用当前注册的 Dependency Resolver 来完成对目标 View 的激活,这意味着可以通过注册自 定义 Dependency Resolver 的方式实现基于 IoC 的 View 激活。在第 3 章" Controller 的激活" 中我们创建了一个针对 Ninject 的 Dependency Resolver 实现了基于 IoC 的 Controller 激活方式, 现在通过这个自定义的 NinjectDependencyResolver 来激活 View 。 我们演示的是一个针对多语言支持的场景,为了让 View 上输出的一些内容随着当前线 程的 UICulture 而动态地变化,我们在-个 ASP.NET MVC 应用中定义如下一个读取资源内 容抽象类 ResourceReader 。这里资源是一个宽泛的概念,并不对存储方式作强制的约束,我 们可以使用资源文件也可以使用数据库来存储资源内容,简单起见, ResourceReader 仅仅定 义了唯一的 GetString 方法获取指定名称的字符串。 public abstract class ResourceReader public abstract string GetString(string name)i 我们默认采用资源文件来定义数据源,为此在项目中添加了两个资源文件 Resoures.resx (语言文化中性)和 Resources.zh.resx (中文),并在资源文件中添加了如图 8-15 所示的资 源项 (Hello World) 。 . ox 图 8-15 基于不同语言文化的资源文件定义 ASP. NET MVC 4 框架揭秘8.3 Razor 51 擎回 439 然后创建如下一个默认的 DefaultResourceReader ,它默认读取我们添加的资源文件来获 取 GetString 方法返回的字符串(静态类型 Resources 是添加资源文件自动创建的类型)。 public class DefaultResourceReader : ResourceReader public override string GetString(string name) return Resources.ResourceManager.GetString(name); 为了让 ResourceManager 能够应用到所有的 View 中,我们为整个应用的 View 创建了如 下一个基类 LocalizableViewPage ,该类型是 WebViewPage 的子类,它具 有一个类型为 ResourceManager 的属性 ResourceManager 。由于该属性上应用了 Niniect. lniectAttribute 特性,意味着该属性会以"属性注入"的方式被自动初始化。 public abstract class LocalizableViewPage: WebViewPage [Inject] public ResourceReader ResourceReader {get; set; } 接下来我们定义了如下一个简单的 HomeController ,其默认的 Action 方法 Index 中直接 将对应的 View 呈现出来。 public class HomeController : Controller public ActionResult Index() return View(); 如下所示的是 Action 方法 Index 对应 View 的定义,使用 @inherits 指令让动态编译生成的 View 类型继承自我们自定义的基类 LocalizableViewPage,直接调用 ResourceReader 属性的 GetString 方法提取名称为 "HelloWorld" 的字符串资源内容并将其显示出来。 @inheritsLocalizableViewPage

    @ResourceReader.GetString("HelloWorld")

    我们采用基于 U虹的语言文化决定机制,即将语言文化的代码置于请求 URL 中来决定 希望采用的语言。为此我们在自动生成的 RouteConfig 类型中注册了如下一个 URL 模板为 " {culture} / { controller} / { action} "的路由对象。 内Hd --俨 ιn o c e ←」u O R s s a 14 C C .工14 b u p{ ASP. NET MVC 4 框架揭秘440 '. 第 8 章 View 的呈现 public static void RegisterRoutes(RouteCollection routes) jj 其他操作 routes.MapRoute( name url defaults culture "Default" , "{culture}j{controller}j{action}" , new{ = "zh-CN" , controller = "Home" , action = "Index"} 我们自定义的 DefaultResourceReader 能够根据当前线程的 UICulture 选择对应的资源文 件,那么我们只需要根据请求地址指示的语言文件对当前线程的语言文件进行相应的设置即 可。于是在 Globa 1. asax 中定义了如下一个 Application二Beg inRequest 方法,它会在 H忧pApplication 的 Beg inRequest 事件触发的时候被执行并从请求地址中提取语言文化代码, 然后对当前线程的语言文化进行相应的设置。除此之外,针对 NinjectDependencyResolver 的注册和 ResourceReader 与 Default ResourceReader 之间的映射关系定义在 Application_ Start 方法中。 public class MvcApplication : System.Web.HttpApplication protected void Application Start() jj 其他操作 NinjectDependencyResolver dependencyResovler = new NinjectDependencyResolver(); dependencyResovler.Register(); DependencyResolver.SetResolver(dependencyResovler); protected void Application_BeginRequest() HttpCo 口 textBase contextWrapper = new HttpContextWrapper(HttpContext.Current); string culture = RouteTable.Routes.GetRouteData(contextWrapper) .Values["culture"] as string; if (!string.IsNullOrEmpty(culture)) try Culturelnfo culturelnfo = new Culturelnfo(culture); Thread.CurrentThread.CurrentCulture = culturelnfo; Thread.CurrentThread.CurrentUICulture= cultureInfo; catch{ } 现在运行程序,并通过地址指定采用的语言文化,可以发现呈选出来的内容与我们指定 的语言文化是一致的,具体的输出效果如图 8~16 所示。 AS P. NET MVC 4 框架揭秘8.3 Razorsl 擎电 441 图 8-16 通过请求 URL 决定语言的选择 8.3.4 RazorViewEngine 基于 Web Form 引擎的 System. Web.Mvc. WebForm Vi ewEngine 和针对Raz or 引擎的 System. Web.Mvc. Raz orViewEngine 都是抽象类型 System. Web.Mvc.Buil dManagerViewEngine 的子类,而后者又继承自 System. Web.Mvc . VirtuaIPathP roviderViewEngine ,在这里我们仅仅 对实现在 RazorViewEngine 中 View 获取的逻辑进行简单介绍。 由于 Razor 引擎下的 View 通过 R缸,o rView 对象来表示,而 RazorView 通过 View 文件的 虚拟路径来构建,所以 RazorViewEngine 的 View 获取机制在于根据当前上下文找到与指定 View 名称相匹配的 View 文件 C.cshtml 或者 .vbhtml 文件),然后根据该 View 文件的虚拟路 径创建一个 RazorView 对象并最终封装成 ViewEngineResult 对象返回。 实现在 RazorViewEngine 中的目标 View 文件的搜索是根据一个预定义顺序进行的,如 果当前请求不是针对某个Ar ea 的,下面的列表代表了 View 的搜索顺序。 • ~Niews/ {ControllerName} / {Vi ewName } .cshtml • -/Views/ {ControllerName} / {ViewName}. vbhtml • -/Views/Shared/ {ViewName} .cshtml • ~Niews/Share d/ {ViewName }. vbh恤 l 对于针对某个Ar ea 的请求, Raz orViewEngine 会先按照如下的顺序对目标 View 进行搜 索。如果在这个列表中没有成功找到目标 View 文件,会继续按照上面的属性进行搜索。 • -/ Areas/ {Are aN ame} /Views/ {ControllerN 缸ne}/ {ViewNamë} .cshtml • ~/ Areas/ {Are aN ame} Niews/ {ControllerName } / {ViewName }. vbhtrr让 • -/ Areas/ {Ar e aN缸 ne } Niews/Sh 町 e d/ {ViewN 缸 ne}.cshtml • -/Ar eas/{ Ar e aN ame}Niews/Sh缸 ed /{ViewN 缸ne}.vbhtml 如果按照上面的搜索顺序依然找不到目标 View 文件, RazorViewEngine 会根据这个列表 创建并返回一个 ViewEngineResult 对象。这里介绍的 View 搜索机制不仅仅应用于普通的 View 文件,还应用于 Partial View 和布局文件的搜索。 ASP. NET MVC 4 在架揭秘442 • 第 8 章 View 的呈现 ViewEngine 不仅仅通过 FindViewIFindPartialView 根据当前上下文获取指定的 View ,还 通过 Release View 对指定的 View 进行释放回收操作。 Release View 方法在 RazorViewEngine 的实现很简单,如果指定的 View 对象的类型实现 IDispose 接口,它会直接调用其 Dispose 方 法。图 8-17 所示的 UML 体现了 Razor 引擎涉及的相关类型/接口以及它们之间的相互关系。 BuUdManage rC ompiledVi ew Razo rV ie wE ngine BuildManagerViewEngine L 一一 图 8-17 Razor 引擎涉及的相关类型/接口 实例演示:创建一个简单的 RazorViewEngine (S809) <> IVlew <> IVle wE nglne 在前面的实例 (S807) 演示中我们创建了一个用于模拟 RazorView 的 SimpleRazorView , 现在为它创建一个对应的 RazorViewEngine ,直接在该实例项目中添加如下一个 SimpleRazorViewEngine 。 public class SimpleRazorViewEngine: IViewEngine private string[] viewLocationFormats = new string[] { "-/Views/{l}/{O}.cshtml" , "-/Views/{l}/{O}.vbhtml" , "-/Views/Shared/{O}.cshtml" , "-/Views/Shared/{O}.vbhtml" }; private string[] areaViewLocationFormats =口 ew string [] { "-/Areas/{2}/Views/{1}/{O}.cshtml" , "-/Areas/{2}/Views/{1}/{O}.vbhtml" , "-/Areas/{2}/Views/Shared/{O}.cshtml" , "-/Areas/{2}/Views/Shared/{O}.vbhtml" }; AS P. NET MVC 4 框架揭秘8.3 Razor 引擎仨 443 public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache) retur口 FindView(controllerContext , partialViewName, null, useCache); public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) string controllerName = controllerContext.RouteData . GetRequiredStr 工 ng("controller"); object areaName; List viewLocations = new List(); Array. ForEach (viewLocationFormats, format => viewLocations.Add(string.Format(fo口nat , v工 ewName , controllerName))); if (controllerContext.RouteData.Values.TryGetValue("area", out areaName)) { } Array. ForEach (areaViewLocationFormats, format=>viewLocations.Add(string.Format(format, viewName, controllerName, areaName))); foreach (string viewLocation in viewLocations) string filePath = controllerContext.HttpContext.Request .MapPath(viewLocation); if (File.Exists(filePath)) return new ViewEngineResul t (new Simp 工 eRazorView(viewLocation) , this); } return new ViewEngineResult(viewLocations); public void ReleaseView(ControllerContext controllerContext, IView view) IDisposable disposable = view as IDisposable; if (ηull != disposable) disposable.Dispose(); 我们完全按照上面介绍的路径顺序搜索指定的目标 Viewo 简单起见,在对目标 View 进行搜索时忽略了指定的布局文件名和对 ViewEngineResult 的缓存。这个自定义的 SimpleRazorViewEngine 在 Global. asax 中通过如下的代码对进行注册。 public class MvcApplication : System.Web.HttpApplication protected void Application_Start() //其他操作 ViewEngines.Engines.Clear(); ViewEngines.Engines.Add(new SimpleRazorViewEngine()); ASP. NET MVC 4 框架揭秘444 • 第 8 章 View 的呈现 然后我们按照正常的方式来定义 HomeController 的 Action 方法 Index ,运行程序之后依 然会得到如图 8-14 所示的输出结果。 public class HomeController : Controller public ActionResult Index() Contact contact = new Contact Name = "张三", PhoneNo = "123456789", EmailAddress="zhangsan@gmail.com" } ; return View(contact); 本章小结 ASP.NETMVC 针对请求的处理最终体现在对目标 Action 的执行,并就处理的结果对请 求予以响应,而 ActionResult 为我们提供了一种响应请求的快捷方式。 ASP.NET MVC 定义 了一系列的原生的 ActionResult 类型,其中包括 EmptyResult 、 ContentResult 、 FileResult 、 JavaScriptResult 、 JsonResult 、 H仕pSta阳sCodeResult 、 RedirectResult 矛口 RedirectToRouteResult 等,它们或者帮助我们将指定的内容按照相应的媒体类型响应给客户端,或者回复一个指定 状态码的响应,又或者实现客户端的重定向。 ViewResult 是重要也是最为常用的 ActionResult ,我们可以利用它将指定的 View 呈现在 客户端的浏览器上。针对 ViewResult 的 View 呈现最终是利用 View 引擎实现的。 View 引擎 中的 View 实现了 IView 接口,对应着某个 View 文件,而核心组件 ViewEngine 实现针对 View 的获取、激活、呈现以及最终的释放。 ASP.NET MVC 提供了 Web Form 和Razor 两种 View 引擎。 Razor 引擎中的 View 和 ViewEngine 对应的类型为RazorView 和RazorViewEngineo RazorView 对应一个以 .csh阳ù/.vbhtrr咀 文件定义的View 文件,这样的文件通过 ASP.NET 的动态编译生成一个 WebViewPage 类型。 RazorView 通过激活的 WebViewPage对象实现了对 View 的最终呈现。 ASP.NET MVC 4 框架揭秘第 9 章 ASP.NET Web API 不知道大家是否注意到一个现象,传统 Web 服务其实并没有直接建立在 Web 上而是建立在 SOAP 上,以至于我们提到 Web 服务就会想到 SOAP。随 着在 Web 服务越来越多地采用 REST 架构, SOAP 在整个 Web 服务体系中的 垄断地位正在发生改变,或者已经发生了改变。 REST 提倡一种面向资源的架 构,直接在 Web 上建立一种轻量级的 Web 服务。虽然 WCF 自 3.5 之后提供 了针对 REST 的支持,但是这种在"重量级"通信平台上实现的"轻量级" 消息通信给我们一种"牛刀杀鸡"之感,所以直接建立在 ASP.NET 平台上的 ASP.NET Web API 是更好的选择。 ASP.NET MVC 4 在架揭秘446 /ÌI 第 9 章 ASP.NET Web API 9.1 Web 、 REST 与 WebAPI 如果说在过去半个世纪中哪种信息技术对人类影响最为深远,我想很多人的答案是 WWW C World Wild Web 、 W3 或者万维网),它完全改变了我们的生活和思维方式。 WWW 重要性甚至可以从 W3C 对它的定义中看出来: "The World Wide Web is the universe of network-accessible information, an embodiment ofhuman knowledge." C 万维网是信息的来源、 知识的化身)。 9.1.1 Web 如此简单 Web 的简单性使之能够得到广泛的普及,并且成为互联网的标准。它由 U阳、 HπP 和 HTML 三个基本的标准构成。 HTTP 是 Web 的核心,就是一个简单的请求一回复的传输协议, 客户端请求什么,服务端就给什么,并且每次消息交换均是独立的。 HTTP 是一种文档化的 协议 C Documented Protocol) ,客户端将请求文档置于 Hη? 请求封套 CEnvelope) 中发送给 服务端,而服务端将响应文档置于 Hπp 响应封套中返回给客户端。 GET http://www.microsoft.com/en-us/default.aspx HTTP/l.l Host: www.microsoft.com Connection: keep-alive Cache-Control: max-age=O User-Agent: Mozilla/5. 0 (Windows NT 6.1) AppleWebKi t/535. 7 (KHTML , like Gecko) Chrome/16.0.912.75 Safari/535.7 Accept: text/html, application/xhtml+xml, application/xml;q=O.9, */*;q=O.8 Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US, en;q=O.8 Accept-Charset: ISO-8859-1 , utf-8;q=O.7 ,女 ;q=O.3 Cookie: 上面这个文本片段反映的是我们通过 Chrome 浏览器访问微软的官网 C www.microsoft. com) 对应的 HTTP 请求,我们借此来简单谈谈作为 HTTP 请求封套的基本结构。第一行体 现了 HTTP 请求的三个基本属性,即 HTTP 方法 C GET )、目标资源 C http://www.microsoft.comJ en-us/ default. aspx) 和协议版本 CHTτ'P/l. l) 。 Hπ? 方法 (Hπ'P Method 或者 HTTPVerb) 对于 REST 来说是一个非常重要的概念。 如果将请求的目标内容视为一种网络资源的话, HTTP 方法反映了针对该资源的操作类型。 我们常用的几种 HTTp 方法(比如 GET.庄ffiAD 、 PUT 、 POST 和 DELETE) 分别体现了针对 目标资源的获取、添加、修改和删除操作。 除首行文字之外的所有内容被称为 HTTP 请求报头 CHeader) ,而每个报头实际上是一 个键一值对。 HTTP 协议本身定义一系列原生的报头,我们也可以根据需要在报头集合中添 加任意的请求报头。上面给出的 HTTP 请求中包含了一些常用的请求报头,我们可以根据它 们获得主机名称、采用的缓存策略、浏览器相关信息,以及客户端支持的媒体类型 CMedia ASP. NET MVC 4 框架揭秘9.1 Web 、 REST 与 WebAPI _ 447 Type) 、编码方式、语言和字符集等。 除了请求报头集合之外,一个 HTTP 请求可以具有相应的主体内容。一个 HTTP 请求的 主体可以具有任意的格式,比如以 XML 或者 JSON 表示的请求数据,或者是一个结构化的 HTML 文档(比如一个包含提交表单的 HTML 文档来向服务端提供用户在浏览器上输入的 内容)。 HTTP/1.1 200 OK Cache-Contro1: no-cache pragrna: no-cache Content-Type: text/htrnl; charset=utf-8 Content-Encoding: gzip Expires: -1 Vary: Accept-Encoding Server: Microsoft-IIS/7.5 X-AspNet-Version: 2.0.50727 VTag: 791897542300000000 P3P: CP="ALL IND DSP COR ADM CONo CUR CUSo IVAo IVDo PSA PSD TAI TELo OUR SAMo CNT COM INT NAV ONL PHY PRE PUR UNI" X-Powered-By: ASP.NET Oate: Wed , 18 Jan 2012 07:06:25 GMT Content-Length: 34237 ... 前面的 HTTP 请求通过浏览器发送给服务端之后会接收到具有如上内容的 HTTP 响应, 该消息的第 1 行表示采用的协议版本和响应 HTTP 状态码/文字描述。 "2000K" 表示请求被 正常接收处理,常见的响应状态码包括"40 1, N ot Authorized" (授权失败)和 "404 , Not Found" (请求资源不存在〉等。 与 HTTP 请求消息一样, HTTP 响应消息依然具有一组报头,而响应的内容被封装到 Hπp 响应消息的主体部分。响应内容的格式(比如 XML 、 JSON 、 HTML 等)一般与表示 媒体类型的响应报头" Content-Type" (比如" applicaitonJxml"、" applicationJj son" 和" textlhtml " 等)一致。上面这个 HTTP 响应的主体内容为 HTML 文档,所以" Content-Type" 报头表示 的媒体类型为"textlhtml "。 9.1.2 REST 是什么 阻ST 与技术无关,代表的是一种软件架构风格。阻ST 是 Representational State Transfer 的简称,中文翻译为"表征状态转移", Roy Fielding 博士(他同时也是 HTTP 的制定者之一) 于 2000 年在其博士论文 Architectural Styles and the Design 01 Network-based So)阳Jare Architectures (http://www.i cs.uc i. edu/-fieldinglpubs/dissertationJtop.htm) 中最早提出这种 Web 服务的架构风格,在这之前 Web 服务具有两种主流的架构风格,即 SOAP 和 XML-RPC 。 REST 从资源的角度来审视整个网络,它将分布在网络中某个节点中的资源通过 U阳 ASP. NET MVC 4 框架揭秘448 :.第 9 章 AS P. NET Web API 进行标识,客户端应用通过 URI 来获取资源的表征,获得这些表征致使这些应用程序转变 了状态。随着不断获取资源的表征,客户端应用不断地在转变着状态。 为什么会起这么一个名字呢? Roy Fielding 是这样解释的: "设计良好的网络应用表现为 一系列的网页,这些网页可以看作虚拟的状态机,用户选择这些链接导致下一网页传输到用 户端展现给使用的人,而这正代表了状态的转变。"这貌似在说 Web 站点而不是 Web 服务, 两者之间的不同之处在于消费者的不同, Web 站点的消费者是人,而 Web 服务的客户端是 机器(应用)。不过我们完全可以将两者统一起来,采用相同的架构风格来构架这两种不同 的 Web 应用。 虽然说作为架构风格的阻ST 与具体网络协议无关,但是我们所说的绝大部分基于 REST 的 Web 服务都是建立在 HTTP 之上的。 REST 通常使用 HTTP 、 U阳、 XML 以及 HT:t\但 这些广泛采用的协议和标准。对于 REST 这种面向资源的架构风格,有人甚至提出了一种全 新的架构理念,即面向资源架构 (ROA: Resource OrientedArchitecture) ,现在我们简单地 讨论一下关于 RESTIROA 的一些基本的设计原则。 一切数据都是资源 所有的数据,不论是通过网络请求获取的还是操作(创建、修改和删除)的数据,都是 资源。这不仅仅包括图片、 .MP3 和视频这些通过具体文件承载的物理资源,还包括通过关 系型数据库保存的数据,甚至包括一些经过实时计算得到的数据。将一切数据视为资源是 阻ST 区别于其他架构风格的最为本质的属性。 所青的资源周可被唯一标识 资源能够通过网络访问的形式被获取和操作,不仅仅要求它们是可被寻址 (Addressable) 的,还要求它们能够被唯一标识。我们通过 U阳来唯一标识某个资源,如下的列表表示某 个 CRM 服务标识某个或者某类客户的 URI 。 • 所有的客户: http://www.artech.comlCRMService/Customers • 所有金牌客户: http://www.artech.comlCRMService /Customers/Gold • 一个编码 COOl 的金牌客户: http://www.artech.comlCRMService/Customers/Gold/COO 1 采用统一而简单的接口 一个基于 RESTIROA 的 Web 服务操作请求只需要体现两点,即资源的唯一标识和操作 类型。资源通过 URI 来标识,而操作类型则可以通过 HTTP 方法 (GET.庄IEAD 、 POST 、 PUT 和 DELETE 等)来体现。这和基于 SOAP 的架构风格完全不同,后者实际上是通过 SOAP 消息的 报头来进行操作识别的。同样以上面提到的 CRM 服务为例,如果我们需要 分别针对某个单一客户进行获取、添加、修改和删除操作,其报头值可能被定义成 如下的形式。 ASP.NET MVC 4 在架揭秘9.1 Web 、 REST 与 WebAPI :11 449 • http://CRMService /GetCustomer • http://CRMService /AddCustomer • h忧p://CRMService /UpdateCustomer • h句://CRMService lDeleteCustomer 如果采用 REST 风格来构建 CRMService 这个 Web 服务,请求 U阳就表示被操作的某 个客户端,而四种基本的操作类型则通过相应的 HTTP 方法来体现。比如我们要操作一个编 号为 COOl 的客户,可以采用如下具有相同 URL 但是具有不同 HTTP 方法的请求实现对该客 户的获取、添加、修改和删除等操作。 • h忧p://CRMService/Customers/COO 1 + GET • h忧p:!/CRMService/Customers/COOl + PUT1 • h忧p://CRMService/Customers/COOl + POST • h忧p://CRMService/Customers/COOl + DELETE 基于表征的通信 阻ST 的首字母 R (Representational) 可以理解为对资源一种"表征气无论是作为请求 还是响应的数据都是对相应资源的表征。由于 REST 主要面向 Web ,我们倾向于直接采用 X岛1L或者 JSON 来表示被操作的资源。 r e m o ←」s u 户」s s a ---C C .工14 b u p{ public Id{get;set;} public Narne{get;set;} public string Province{get;set;} public string City{get;set;} 比如对于通过 C# 定义的数据类型 Customer ,我们可以直接通过如下所示的泊在L 或者 JSON 片段来表示某个 Customer 对象。 XML: C001 张三 江苏 苏州 JSON: Id : 00 1, l 采用 HTTP-PUT 方法的 Hπ? 请求执行客户添加操作时,请求地址可以不需要指定添加客户的 ID ,因为包含添 加客户端数据的主体部分往往包含对应的 ID 。对于执行客户修改操作的 HTTP-POST 请求亦是如此。 ASP. NET MVC 4 框架揭秘450 组第 9 章 ASP.NET Web API Name :张二, Province :江苏, City :苏州| 无状态服务调用 服务调用采用完全独立的消息交换方式,不需要为客户端保持会话状态,这不仅仅是为 了"迎合 "Hπ? 无状态的特性,同时赋予了服务较强的可伸缩性。对于无状态的 Web 服务, 负载均衡 (Load Balance) 变得很容易。 9.1.3 ASP.NET Web API ASP.NET Web API 直接借鉴了 ASP.NETMVC 的设计,所以两者具有非常类似的编程 模式。我们以 Controller 的形式来定义服务,而 Controller 中的 Action 方法则代表具体的 操作。接下来通过实例演示的方式来介绍如何定义和调用 WebAPI 。这个实例是一个单页 的 Web 应用,模拟对联系人数据的 CRUD 操作。从如图 9-1 所示的应用截图可以看到, Web 页面中会显示所有联系人列表,针对联系人的添加、修改和删除都在同一个页面中 完成。 (S901) 匮噩噩噩噩噩E ,••••••••••••• 四M+WIj _ Q localhost 104豆二一一一一一一一一一一一一一一立回国 图 9-1 "联系人管理器"应用截图 针对联系人信息的 CRUD 操作都是通过调用 Web API 的形式来完成的。具体来说,我 们利用了 jQuery 以 Aj 缸的方式调用 WebAPI 。至于界面的设计,我们充分利用了 ASP.NET MVC 提供的另一个名为Kn ockOut 的 JavaScript 框架实现了对数据的绑定,也就是说,整个 应用基本上只涉及前端的 HTML/JavaS 呻 t 和后端的 WebAPI 两个部分。 HttpController 我们现在就来简单地介绍这个作为"联系人管理器"的 Web 应用是如何创建的,先在 一个 ASP. 阳 TMVC 应用中定义了如下一个表示联系人的 Contact 类型。 ASP. NET MVC 4 框架揭秘9.1 Web 、 REST 与 WebAPI 黯 451 •L c a •L n 0 卢」s s a 14 C C .工14 b u p{ public string Id { get; set; } public string Name { get; set; } public string PhoneNo { get; set; } public string EmailAddress { get; set; } 然后在 Controllers 目录下定义如下一个表示联系人管理服务的 ContactsController ,它的 基类不再是我们熟悉的抽象类 Controller ,而是 System. Web.Http.ApiController 0 ApiController 实现了接口 System. Web.Http.Controllers .I HttpController ,所以在本章后续部分将 ASP.NET WebAPI 中的 Controller 称为 HttpController 。 public class ContactsController : ApiController private static List contacts = new List new Contact Id ="001" , ..:1,,, -.. Name = "5ií二-二", PhoneNo ="123" , EmailAddress=..zhangsan@gmail.com .. new Contact Id ="002" , Name = "李四", PhoneNo ="456" , EmailAddress=..lisi@gmail.com .. public IEnumerable Get() return co 口 tacts; public Contact Get(string id) return contacts.FirstOrDefault(c => c.ld == id); public void Put(Contact contact) contact.ld = Guid.NewGuid() .ToString(); contacts.Add(contact); public void Post(Contact contact) Delete(contact.ld); contacts.Add(contact); ASPNETMVC4 框架揭秘452 ë Ìl第 9 章 ASP.NET Web API public void Delete(string id) Contact contact = contacts.FirstOrDefault(c => c.ld == id); contacts.Remove(contact); 我们直接利用一个静态 Contact 列表字段来表示数据存储,针对联系人的所有操作都是 针对这个列表进行的。对于代表 CRUD 操作的五个 Action 方法,我们直接采用相应的 HTTP 方法 (Put 、 Get 、 Post 和 Delete) 来对其进行命名。如果 Web API 根据 URL 路由不能得到 目标 Action 的名称,它会将当前请求的 HTTP 方法作为前缀并借助于得到的路由数据找到 匹配的 Action 方法。 路由注册 与 ASP.NETMVC 一样, Web API 同样通过 URL 路由机制根据请求的地址得到需要激 活的 ApiController 和对应的 Action 名称(只有在 URL 路由不能解析出 Action 名称的情况下 才会根据 HTTP 方法对目标 Action 进行选择)。当我们利用 Visual Studio 提供的向导一个创 建 AS卫NETWebAPI 或者 ASP.NETMVC 应用的时候,在自动生成的静态类型 WebApiConfig 中会默认为我们注册如下一个路由。 public static class WebApiConfig public static void Register(HttpCo口 figuration config) config.Routes.MapHttpRoute( e ←」a 14 p ms et 巾414eu eta mu 在中 aoe nrd "DefaultApi" , "api/{controller}/{id}", new { id = RouteParameter.Optional } 如上面的代码片段所示,针对 WebAPI 路由的注册通过调用 RouteCollection 的扩展方法 MapH忧pRoute 实现的,它注册的是一个 System. Web.Http. WebHost.Routing.Http WebRoute 对 象,该类型定义在程序集 System. Web.Http. WebHost 中。默认的 URL 模板为 "apν{controller}/ {id} ",针对 Action 名称的路由变量并没有包含在模板中,所以最终针对目标 Action 的选择 是根据 HTTP 方法完成的。 根据 Hπp 方法与 Action 名称的匹配机制,如果我们通过浏览器访问地址"/apνcontacts" 或者 "ap iÎcontacts/OO 1 ",用于返回所有联系人列表的 Get 方法和返回指定联系人信息的 Get 方法会被视为匹配的 Action 方法被执行。如图 9-2 所示,当我们通过 Chrome 来访问这两个 地址时,返回的联系人会以 XML 的形式直接显示在浏览器上。 ASP. NET MVC 4 框架揭秘9.1 Web 、 REST 与 WebAPI 也 453 L吃~::3 ,!:: r.h t. ~D:! I .<:c :,; ':.ac~> Thi sX孔伍组e doesnot 碍lpeaf to have any s t;1e information associated wí也住Th e docum副 tree is sho'吼咽 belo飞飞,_ .. z !l angsan@gmail. com< / Ema l. 1Address> OOl< /1 d> 张三 45612 3 〈士,:::-, tact> < / 二工主盈工工~~乞::t. a~~> 图 9-2 通过浏览器调用 Web API (Chrome) 有人可能会问这样一个问题,为什么通过浏览器发起对 WebAPI 的调用得到的数据是以 XML 格式而不是以 JSON 的方式被序列化的呢?实际上 ASP.NET Web API 对数据对象的序 列化是非常智能的,它能够根据请求希望的媒体类型选择适合的序列化方式。在 ASP.NET Web API 中对请求/响应进行序列化/反序列化的序列化器是与某种媒体类型关联的,它能够 根据请求消息携带的媒体类型"智能地"选择匹配的序列化器。 具体来说, ASP.NET Web API 会提取 H 口?请求的" Accept" 报头得到客户端可以接受 的媒体类型列表,按照先后顺序〈从左到右)去获取匹配的序列化器,优先匹配的序列化器 会被使用。如果没有匹配的序列化器,默认使用的是针对 JSON 的序列化器。 如下所示的是通过 Chrome 访问地址 "/apνcontacts/OO 1 "的请求消息