随着ASP.NET MVC 1.0版本的正式发布, 我将本系列文章也更新到了1.0, 对于已经发表的文章我都会根据1.0版本重新编辑. 希望本系列文章能打给大家帮助.
ASP.NET MVC是微软官方提供的开源MVC框架. 在经过了漫长的preview和2个RC版本后, 终于发布了1.0版本.其实从RC开始ASP.NET MVC的核心代码就已经趋于稳定.所以现在开始学习ASP.NET MVC是一个很好的时间点.学习的早,而且不用担心知识过时.
本系列教程是我在学习ASP.NET MVC时的总结, 比如虽然我学习老赵的视频教程, 但是因为版本较老, 一般我都要从最新版本上进行知识的校验与升华, 然后将最后的精华总结出来写成文字. 现在还在不断的学习, 不断地写作.希望大家喜欢本系列文章, 我将用精益求精的精神写本系列文章.
现在开始跟着我一步一步学习ASP.NET MVC.在什么都不知道之前, 还是让我们先下载一个ASP.NET MVC程序. 当看到程序运行后, 我的心才会踏实很多.
首先去ASP.NET MVC的官网看看:
这里我们可以下载ASP.NET MVC的安装文件.目前最新版本是1.0,另外我们可以在CodePlex上获取源代码:
http://www.codeplex.com/aspnet
在官网上下载ASP.NET MVC安装文件, 一路回车完成安装. 启动VS2008 SP1(不是VS2008?不是SP1?作为一个专业的IT人士,水平可以菜,软件咱一定要用新的!去找个新版本吧!),点击新建,在Web中可以找到:
点击确定即创建了一个默认的MVC项目.同时还会创建一个测试项目. 这些我现在都不懂也不关注, 直接将Web项目中的default.aspx设置为启动页, 运行项目, 一个ASP.NET MVC的项目已经运行在我的电脑上了:
而且那个Home和About还能点击!虽然现在我什么都不懂, 不过看着能运行的实例, 心里就踏实多了!
一个ASP.NET页面通常需要做这些事情:
MVC即Model, View, Controller
Model就是我们1中获取的网页需要的数据
Controller就是我们获取数据,然后将数据绑定到页面控件的这个业务流程.不十分正确但是可以先这样理解: Controller就是我们的Page_Load方法逻辑.
View就是我们的aspx页面,注意这是一个不包含后台代码文件的aspx页面.(其实带有.asp.cs文件也不会有编译错误,但是这样写代码就违背了MVC的设计初衷)
下面这张图很好的概括了MVC的作用:
一个URL请求, ASP.NET MVC引擎会分析这个URL要使用那个Controller, 这个Controller(实际上真正的方法是Controller的Action)从数据库或者其他数据源获取数据,通常这些数据是一个业务的模型类(即MVC中的Model). Controller将Model对象传递给页面(即MVC中的View), 页面显示在浏览器上.(这一步是ViewEngine的功能, 我们一般的ASPX页面使用的是WebForm的ViewEngine,当然也可以替换.)
简单的概念也有了.实例也能运行了.现在就是看看这个实例是如何使用ASP.NET MVC的.从首页下手.
首页网站的地址应该是 http://localhost:1847/home/index (1847是端口号,随机生成,所以会不同), 为什么地址不是具体的页面但是最后却将请求提交给了view/home/Index.aspx 页面? 很明显是使用了URL重写. ASP.NET中叫做UrlRouting,对应的程序集是System.Web.Routing, 打开项目的Global.asax.cs文件, 会找到我们建立的页面重写规则:
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = "" } // Parameter defaults ); } protected void Application_Start() { RegisterRoutes(RouteTable.Routes); }
关于Routing的深入了解将在以后介绍, 这里我们完全可以照葫芦画瓢, 按照实例添加新的重写规则.
最后, http://localhost:1847/home/index 就被解析为:
Controller为Home, Action为Index, 没有id参数.
在Controllers文件夹下我们可以找到HomeController.cs, 这里使用了一个约定, 就是如果URL中获取到的Controller名字是Home, 则他的Controller类名就是HomeController. 在URL中的名字后加上”Controller”.
实例中Controller都放在Controllers文件夹, 所以我们可以按照命名约定很容易就可以找到HomeController类
打开HomeControllers.cs, 发现里面有两个方法:
public ActionResult Index() { ViewData["Message"] = "Welcome to ASP.NET MVC!"; return View(); } public ActionResult About() { return View(); }
其中的Index和About都是Action.这个Action是个什么东东呢?目前我只知道一个Controller可以包含多个Action, MVC模式中Controller角色的具体实现逻辑都是在Action中的.
因为我们的Action是Index, 所以自然就要调用Index()方法.这里将"Webcome to ASP.NET MVC!", 这里的Model角色就是这句话, 也就是一个字符串对象.
Controller的一个重要作用就是将Model对象传递给View,也就是具体的页面. 传递的方法就是将这个字符串放到ViewData这个集合对象中, 然后在页面上也调用这个对象获取数据. 这个ViewData 对象一定是静态的, 或者至少是和HttpContext对象关联的, 否则到了View页面上是不能够调用的.但是也不知道ASP.NET MVC是在什么时候创建了这个对象.有心深入的一会去源代码里找找就能找到答案了.
Model有了,数据有了, 接下来要跳转到View去显示数据了.第一个问题就是如何从Controller中跳到View上? return View() 这句话实现了此功能.其实如果这样写大家会更清楚:
public ActionResult Index() { ViewData["Message"] = "Welcome to ASP.NET MVC!"; return View("index"); }
View方法中可以带一个名字, 这个名字就是View的名字.如果把index改成about,那么访问/home/index就会跳转到about页!
虽然知道了我们要返回到名称是"index"的View上, 但是这个View的页面在哪里呢?在Web中有一个Views文件夹:
这里面存放的都是View对象, 也就是只有显示功能的aspx页面文件.但是aspx文件要遵循约定: Views下面要按照Controller创建文件夹, 比如HomeController就对应Home文件夹, 然后在里面添加view, 比如index.aspx, 那么在HomeController中返回到名为Index的View对象实际就会返回Views/Home/Index.aspx页面.
如果不写View的名字, 则认为Action的名称就是View的名称.
最后的工作就是View页面使用Model数据完整页面显示工作, 在index.aspx这个没有后台代码的View对象中, 通过ViewData对象获取Model:
<%= Html.Encode(ViewData["Message"]) %>
接下来ViewEngine即页面引擎会将aspx中的HTML部分以及上面的数据部分和在一起返回给浏览器.
关于View对象我注意到此页面是继承自System.Web.Mvc.ViewPage而不是直接继承自System.Web.UI.Page, 而这个ViewData对象就是ViewPage中的一个属性. 这里的ViewData一定是页面级别的,当页面编译完毕这个对象就会被注销(HTTP是无状态的协议,每次请求其实都是生成一个新的ViewPage对象).
虽然仅仅是第一篇入门文章, 但是是不是觉得已经会用ASP.NET MVC了? 虽然学习不可以骄傲, 但是可以自豪的是ASP.NET MVC我已经不再陌生, 即使不能驾轻就熟, 偶尔说出来几个概念,给你解释解释啥是MVC, 还是很能吓唬人的. 在后续文章中,我将对MVC的各种细节做具体的讲解.希望大家喜欢本系列文章!
本篇文章从基础到深入的介绍ASP.NET MVC中的Routing组件. Routing翻译过来是"路由选择", 负责ASP.NET MVC的第一个工作:识别URL, 将一个Url请求"路由"给Controller.
第一篇文章中我们已经学会了如何使用ASP.NET MVC, 虽然其中还有很多的细节没有深入了解, 但是对基本的处理流程已经有了认识:来了一个Url请求, 从中找到Controller和Action的值, 将请求传递给Controller处理. Controller获取Model数据对象, 并且将Model传递给View, 最后View负责呈现页面.
而Routing的作用就是负责分析Url, 从Url中识别参数, 如图:
这一讲就让我们细致的了解System.Web.Routing及其相关的扩展知识.
第一讲中实例的首页地址是: localhost/home/index
我们发现访问上面的地址, 最后会传递给 HomeController中名为index的action(即HomeController类中的index方法).
当然服务器端不会自己去实现这个功能, 关键点就是在Global.asax.cs文件中的下列代码:
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = "" } // Parameter defaults ); } protected void Application_Start() { RegisterRoutes(RouteTable.Routes); }
回来看我们的Url: localhost/home/index
localhost是域名, 所以首先要去掉域名部分: home/index
对应了上面代码中的这种URL结构: {controller}/{action}/{id}
因为我们建立了这种Url结构的识别规则, 所以能够识别出 Controller是home, action是index, id没有则为默认值"".
这就是Routing的第一个作用:
1.从Url中识别出数据.比如controller,action和各种参数.
如果跟踪程序, 接下来我们会跳转到HomeController中的Index()方法. 这是Routing内部为实现的第二个作用:
2.根据识别出来的数据, 将请求传递给Controller和Action.
但从实例中我们并不知道Routing如何做的这部份工作.第五部分我做了深入讲解.
在分析Routing的实现原理前, 先学习如何使用Routing为ASP.NET MVC程序添加路由规则.
这是最简单的为ASP.NET MVC添加识别规则的方法.此方法有如下重载:
MapRoute( string name, string url); MapRoute( string name, string url, object defaults); MapRoute( string name, string url, string[] namespaces); MapRoute( string name, string url, object defaults, object constraints); MapRoute( string name, string url, object defaults, string[] namespaces); MapRoute( string name, string url, object defaults, object constraints, string[] namespaces);
规则名称, 可以随意起名.当时不可以重名,否则会发生错误:
路由集合中已经存在名为“Default”的路由。路由名必须是唯一的。
url获取数据的规则, 这里不是正则表达式, 将要识别的参数括起来即可, 比如: {controller}/{action}
最少只需要传递name和url参数就可以建立一条Routing(路由)规则.比如实例中的规则完全可以改为:
routes.MapRoute( "Default", "{controller}/{action}");
url参数的默认值.如果一个url只有controller: localhost/home/
而且我们只建立了一条url获取数据规则: {controller}/{action}
那么这时就会为action参数设置defaults参数中规定的默认值. defaults参数是Object类型,所以可以传递一个匿名类型来初始化默认值:
new { controller = "Home", action = "Index" }
实例中使用的是三个参数的MapRoute方法:
routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = "" } // Parameter defaults );
用来限定每个参数的规则或Http请求的类型.constraints属性是一个RouteValueDictionary对象,也就是一个字典表, 但是这个字典表的值可以有两种:
用于定义正则表达式的字符串。正则表达式不区分大小写。
一个用于实现 IRouteConstraint 接口且包含 Match 方法的对象。
通过使用正则表达式可以规定参数格式,比如controller参数只能为4位数字:
new { controller = @"\d{4}"}
通过第IRouteConstraint 接口目前可以限制请求的类型.因为System.Web.Routing中提供了HttpMethodConstraint类, 这个类实现了IRouteConstraint 接口. 我们可以通过为RouteValueDictionary字典对象添加键为"httpMethod", 值为一个HttpMethodConstraint对象来为路由规则添加HTTP 谓词的限制, 比如限制一条路由规则只能处理GET请求:
httpMethod = new HttpMethodConstraint( "GET", "POST" )
完整的代码如下:
routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = "" }, // Parameter defaults new { controller = @"\d{4}" , httpMethod = new HttpMethodConstraint( "GET", "POST" ) } );
当然我们也可以在外部先创建一个RouteValueDictionary对象在作为MapRoute的参数传入, 这只是语法问题.
此参数对应Route.DataTokens属性. 官方的解释是:
获取或设置传递到路由处理程序但未用于确定该路由是否匹配 URL 模式的自定义值。
我目前不知道如何使用. 请高手指点
下面通过实例来应用MapRoute方法. 对于一个网站,为了SEO友好,一个网址的URL层次不要超过三层:
localhost/{频道}/{具体网页}
其中域名第一层, 频道第二层, 那么最后的网页就只剩下最后一层了. 如果使用默认实例中的"{controller}/{action}/{其他参数}"的形式会影响网站的SEO.
假设我们的网站结构如下:
下面以酒店频道为例, 是我创建的Routing规则:
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); #region 酒店频道部分 // hotels/list-beijing-100,200-3 routes.MapRoute( "酒店列表页", "hotels/{action}-{city}-{price}-{star}", new { controller = "Hotel", action = "list", city = "beijing", price="-1,-1", star="-1" }, new { city=@"[a-zA-Z]*",price=@"(\d)+\,(\d)+", star="[-1-5]"} ); //hotels/所有匹配 routes.MapRoute( "酒店首页", "hotels/{*values}", new { controller = "Hotel", action = "default", hotelid = "" } ); #endregion //网站首页. routes.MapRoute( "网站首页", "{*values}", new { controller = "Home", action = "index"} ); }
实现的功能:
(1)访问 localhost/hotels/list-beijing-100,200-3 会访问酒店频道的列表页,并传入查询参数
(2)访问 localhost/hotels 下面的任何其他页面地址, 都会跳转到酒店首页.
(3)访问 localhost 下面的任何地址, 如果未匹配上面2条, 则跳转到首页.
简单总结:
(1)Routing规则有顺序(按照添加是的顺序), 如果一个url匹配了多个Routing规则, 则按照第一个匹配的Routing规则执行.
(2)由于上面的规则, 要将具体频道的具体页面放在最上方, 将频道首页 和 网站首页 放在最下方.
(3) {*values} 表示后面可以使任意的格式.
MapRoute方法虽然简单, 但是他是本质也是通过创建Route类的实例, 为RouteCollection集合添加成员.
下载最新版本的MSDN-Visual Studio 20008 SP1, 已经可以找到Route类的说明.
创建一个Route类实例,最关键的是为以下几个属性赋值:
属性名称 | 说明 | 举例 |
Constraints | 获取或设置为 URL 参数指定有效值的表达式的词典。 | {controller}/{action}/{id} |
DataTokens | 获取或设置传递到路由处理程序但未用于确定该路由是否匹配 URL 模式的自定义值。 | new RouteValueDictionary { { "format", "short" } } |
Defaults | 获取或设置要在 URL 不包含所有参数时使用的值。 | new { controller = "Home", action = "Index", id = "" } |
RouteHandler | 获取或设置处理路由请求的对象。 | new MvcRouteHandler() |
Url | 获取或设置路由的 URL 模式。 | new { controller = @"[^\.]*" } |
这些属性除了RouteHandler以外, 其他的都对应MapRoute方法的参数.RouteHandler是实现了IRouteHandler接口的对象.关于此接口的作用在第五部分Routing深入解析中做讲解.
对于一个一般开发人员来说, 上面的知识已经完全足够你使用ASP.NET MVC时使用Routing了.
接下来的部分我将深入Routing的机制讲解Routing的高级应用.但是因为是"高级应用", 加上这篇文章已经太长了, 再加上马上今天就过去了, "每日一篇"的承诺一定要兑现的, 所以不会对所有细节进行讲解. 或者也可以略过此部分.
Routing如何将请求传递给Controller?上面讲解Routing作用的时候, 我们就分析出Routing会将请求传递给Controller, 但是Routing如何做的这部份工作我们却看不到.关键在于MapRoute()这个方法封装了具体的细节.
虽然MapRoute方法是RouteCollection对象的方法,但是却被放置在System.Web.Mvc程序集中, 如果你的程序只引用了System.Web.Routing, 那么RouteCollection对象是不会有MapRoute方法的. 但是如果你同又引用了System.Web.Mvc, 则在mvc的dll中为RouteCollection对象添加了扩展方法:
public static void IgnoreRoute(this RouteCollection routes, string url); public static void IgnoreRoute(this RouteCollection routes, string url, object constraints); public static Route MapRoute(this RouteCollection routes, string name, string url); public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults); public static Route MapRoute(this RouteCollection routes, string name, string url, string[] namespaces); public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints); public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces); public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces);
RouteCollection是一个集合,他的每一项应该是一个Route对象. 但是我们使用MapRoute时并没有创建这个对象, 这是因为当我们将MapRoute方法需要的参数传入时, 在方法内部会根据参数创建一个Route对象:
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces) { if (routes == null) { throw new ArgumentNullException("routes"); } if (url == null) { throw new ArgumentNullException("url"); } Route route = new Route(url, new MvcRouteHandler()) { Defaults = new RouteValueDictionary(defaults), Constraints = new RouteValueDictionary(constraints) }; if ((namespaces != null) && (namespaces.Length > 0)) { route.DataTokens = new RouteValueDictionary(); route.DataTokens["Namespaces"] = namespaces; } routes.Add(name, route); return route; }
上面就是MapRoute方法的实现, 至于在创建Route对象时第二个参数是一个MvcRouteHandler, 它是一个实现了IRouteHandler接口的类. IRouteHandler十分简单只有一个方法:
IHttpHandler GetHttpHandler(RequestContext requestContext);
参数是一个RequestContext 类实例, 这个类的结构也很简单:
public class RequestContext { public RequestContext(HttpContextBase httpContext, RouteData routeData); public HttpContextBase HttpContext { get; } public RouteData RouteData { get; } }
其中的一个属性RouteData就包含了Routing根据Url识别出来各种参数的值, 其中就有Controller和Action的值.
归根结底, ASP.NET MVC最后还是使用HttpHandler处理请求. ASP.NET MVC定义了自己的实现了IHttpHandler接口的Handler:MvcHandler, 因为MvcRouteHandler的GetHttpHandler方法最后返回的就是MvcHandler.
MvcHandler的构造函数需要传入RequestContext 对象, 也就是传入了所有的所有需要的数据, 所以最后可以找到对应的Controller和Action, 已经各种参数.
因为一个Url会匹配多个routing规则, 最后常常会遇到规则写错或者顺序不对的问题.于是我们希望能够看到Url匹配Routing的结果.
其中最简单的办法就是使用RouteDebug辅助类. 这个类需要单独下载dll组件, 我将此组件的下载放在了博客园上:
http://files.cnblogs.com/zhangziqiu/RouteDebug-Binary.zip
解压缩后是一个DLL文件, 将这个DLL文件添加到项目中并且添加引用.
使用方法很简单, 只需要在Application_Start方法中添加一句话:
RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);
比如下面是我的示例中的代码:
protected void Application_Start() { RegisterRoutes(RouteTable.Routes); RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes); }
现在你访问任何URL, 都会出现RouteDebug页面, 如下:
其中不仅有你的所有Routing规则, 还显示了是否匹配.并且按照顺序列出. 还有识别的参数列表.
当你不想测试Routing规则的时候则注释掉这一段, 即可回复跳转到View对象上.
本文讲解了ASP.NET MVC中一个关键的组件:Routing的使用. System.Web.Routing在Framework3.5 SP1中已经集成, 也就是说虽然我们还没有ASP.NET MVC的正式版, 但是Routing组件却已经提早发布了. 因为Routing是一个相对独立的组件, 不仅能和ASP.NET MVC配额使用, 也可以用于任何需要URL路由的项目. 另外Routing的作用和Url重写(Url Rewrite)是有区别的, 你会发现Routing和Url Rewrite相比其实很麻烦, 无论是添加规则还是传递参数.对UrlRewite感兴趣的可以去寻找UrlRewrite.dll这个组件, 很简单很强大, 有关两者的异同以及如何使用UrlRewrite这里不在多说了.
本文的示例下载地址:
一个Url请求经过了Routing处理后会调用Controller的Action方法. 中间的过程是怎样的? Action方法中返回ActionResult对象后,如何到达View的? 本文将讲解Controller的基本用法, 深入分析Controller的运行机制, 并且提供了创建所有类型Action的代码. 值得学习ASP.NET MVC时参考.
在上一篇文章中, 我已经学会了如何使用Routing获取Controller和Action, 随后的程序会调用Controller中的Action方法.
每个Action方法都要返回一个ActionResult对象. 一个Action会将数据传递给View,如图:
Controller负责将获取Model数据并将Model传递给View对象.通知View对象显示.
在ASP.NET MVC中, 一个Controller可以包含多个Action. 每一个Action都是一个方法, 返回一个ActionResult实例.
ActionResult类包括ExecuteResult方法, 当ActionResult对象返回后会执行此方法.
发送请求 –> UrlRoutingModule捕获请求 –> MvcRouteHandler.GetHttpHandler() –> MvcHandler.ProcessRequest()
使用工厂方法获取具体的Controller –> Controller.Execute() –> 释放Controller对象
获取Action –> 调用Action方法获取返回的ActionResult –> 调用ActionResult.ExecuteResult() 方法
获取IView对象-> 根据IView对象中的页面路径获取Page类-> 调用IView.RenderView() 方法(内部调用Page.RenderView方法)
通过对MVC源代码的分析,我们了解到Controller对象的职责是传递数据,获取View对象(实现了IView接口的类),通知View对象显示.
View对象的作用是显示.虽然显示的方法RenderView()是由Controller调用的,但是Controller仅仅是一个"指挥官"的作用, 具体的显示逻辑仍然在View对象中.
需要注意IView接口与具体的ViewPage之间的联系.在Controller和View之间还存在着IView对象.对于ASP.NET程序提供了WebFormView对象实现了IView接口.WebFormView负责根据虚拟目录获取具体的Page类,然后调用Page.RenderView().
通过上面的流程,我们知道了ActionResult对象在整个流程中的作用.ActionResult是一个抽象类, 在Action中返回的都是其派生类.下面是我整理的ASP.NET MVC 1.0 版本中提供的ActionResult派生类:
类名 | 抽象类 | 父类 | 功能 |
ContentResult | 根据内容的类型和编码,数据内容. | ||
EmptyResult | 空方法. | ||
FileResult | abstract | 写入文件内容,具体的写入方式在派生类中. | |
FileContentResult | FileResult | 通过 文件byte[] 写入文件. | |
FilePathResult | FileResult | 通过 文件路径 写入文件. | |
FileStreamResult | FileResult | 通过 文件Stream 写入文件. | |
HttpUnauthorizedResult | 抛出401错误 | ||
JavaScriptResult | 返回javascript文件 | ||
JsonResult | 返回Json格式的数据 | ||
RedirectResult | 使用Response.Redirect重定向页面 | ||
RedirectToRouteResult | 根据Route规则重定向页面 | ||
ViewResultBase | abstract | 调用IView.Render() | |
PartialViewResult | ViewResultBase | 调用父类ViewResultBase 的ExecuteResult方法. 重写了父类的FindView方法. 寻找用户控件.ascx文件 |
|
ViewResult | ViewResultBase | 调用父类ViewResultBase 的ExecuteResult方法. 重写了父类的FindView方法. 寻找页面.aspx文件 |
目前ASP.NET MVC还没有提供官方的ActionResult列表.上面的列表是我在源代码中分析得出的.有些解释的可能不够清楚,请谅解.
下面我将列举各个ActionResult的实例.
安装了ASP.NET MVC后, 在项目上点击右键会找到添加Controller项:
下面这个类提供了返回各种类型的ActionResult的Action实例:
public class DemoController : Controller { /// <summary> /// http://localhost:1847/Demo/ContentResultDemo /// </summary> /// <returns></returns> public ActionResult ContentResultDemo() { string contentString = "ContextResultDemo!"; return Content(contentString); } /// <summary> /// http://localhost:1847/Demo/EmptyResultDemo /// </summary> /// <returns></returns> public ActionResult EmptyResultDemo() { return new EmptyResult(); } /// <summary> /// http://localhost:1847/Demo/FileContentResultDemo /// </summary> /// <returns></returns> public ActionResult FileContentResultDemo() { FileStream fs = new FileStream(Server.MapPath(@"/resource/Images/1.gif"), FileMode.Open, FileAccess.Read); byte[] buffer = new byte[Convert.ToInt32(fs.Length)]; fs.Read(buffer, 0, Convert.ToInt32(fs.Length) ); return File(buffer, @"image/gif"); } /// <summary> /// http://localhost:1847/Demo/FilePathResultDemo /// </summary> /// <returns></returns> public ActionResult FilePathResultDemo() { //可以将一个jpg格式的图像输出为gif格式 return File(Server.MapPath(@"/resource/Images/2.jpg"), @"image/gif"); } /// <summary> /// http://localhost:1847/Demo/FileStreamResultDemo /// </summary> /// <returns></returns> public ActionResult FileStreamResultDemo() { FileStream fs = new FileStream(Server.MapPath(@"/resource/Images/1.gif"), FileMode.Open, FileAccess.Read); return File(fs, @"image/gif"); } /// <summary> /// http://localhost:1847/Demo/HttpUnauthorizedResultDemo /// </summary> /// <returns></returns> public ActionResult HttpUnauthorizedResultDemo() { return new HttpUnauthorizedResult(); } /// <summary> /// http://localhost:1847/Demo/JavaScriptResultDemo /// </summary> /// <returns></returns> public ActionResult JavaScriptResultDemo() { return JavaScript(@"alert(""Test JavaScriptResultDemo!"")"); } /// <summary> /// http://localhost:1847/Demo/JsonResultDemo /// </summary> /// <returns></returns> public ActionResult JsonResultDemo() { var tempObj = new { Controller = "DemoController", Action = "JsonResultDemo" }; return Json(tempObj); } /// <summary> /// http://localhost:1847/Demo/RedirectResultDemo /// </summary> /// <returns></returns> public ActionResult RedirectResultDemo() { return Redirect(@"http://localhost:1847/Demo/ContentResultDemo"); } /// <summary> /// http://localhost:1847/Demo/RedirectToRouteResultDemo /// </summary> /// <returns></returns> public ActionResult RedirectToRouteResultDemo() { return RedirectToAction(@"FileStreamResultDemo"); } /// <summary> /// http://localhost:1847/Demo/PartialViewResultDemo /// </summary> /// <returns></returns> public ActionResult PartialViewResultDemo() { return PartialView(); } /// <summary> /// http://localhost:1847/Demo/RedirectToRouteResultDemo /// </summary> /// <returns></returns> public ActionResult ViewResultDemo() { //如果没有传入View名称, 默认寻找与Action名称相同的View页面. return View(); } }
在文章最后提供有完整实例代码下载.
在研究Controller/Action的流程过程中, 发现了ASP.NET MVC一些问题.
Routing组件和ASP.NET MVC并不是一个项目, 在ASP.NET MVC中仅仅是使用了Routing组件, 在源代码中是通过dll的方式引用的.Routing组件已经包含在.net framework 3.5 sp1中了.
那么ASP.NET MVC是如何应用Routing组件的呢?
Routing组件获取了Url中的数据后, 会将数据保存在一个 RouteData 对象中.并将请求传递给一个实现了IRouteHandler接口的对象. 在Asp.net MVC中提供的MvcRouteHandler类实现了此接口, Routing 将请求传递给MvcRouteHandler的GetHttpHandler方法.下面是源代码:
IRouteHandler接口:
public interface IRouteHandler { IHttpHandler GetHttpHandler(RequestContext requestContext); }
MvcRouteHandler类:
public class MvcRouteHandler : IRouteHandler { protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) { return new MvcHandler(requestContext); } #region IRouteHandler Members IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) { return GetHttpHandler(requestContext); } #endregion }
曾经我认为IRouteHandler是多余的, 用IHttpHandler就够了. 现在知道了为何要定义这个接口. 主要是为了传递RouteData对象.GetHttpHandler方法需要一个RequestContext 对象.RequestContext 是 System.Web.Routing程序集中的类, 里面除了处理请求需要的HttpContextBase对象,还包括了一个RouteData对象.
RequestContext类:
public class RequestContext { public RequestContext(HttpContextBase httpContext, RouteData routeData); public HttpContextBase HttpContext { get; } public RouteData RouteData { get; } }
Routing组件在Web.Config中注册了一个HttpModule: System.Web.Routing.UrlRoutingModule, 而不是HttpHandler:
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
可惜看不到这个类的源代码. 所有请求最后都是要传递给IHttpHandler对象处理, 主要的工作是编译页面, 所以我猜测这个Module将请求截获后通过IRouteHandler接口对象获取一个HttpHandler, 然后将处理移交给获取到的HttpHandler.
ASP.NET MVC 中实现了IHttpHandler接口的类是MvcHandler, MvcRouteHandler.GetHttpHandler方法就是返回一个MvcHandler对象. MvcHandler类的构造函数需要传入一个RequestContext对象. 实现的IHttpHandler接口方法处理过程中都需要依赖这个对象.
但是微软在这里的处理有一些不足. MvcHandler虽然实现了IHttpHandler接口但是不能被当作IHttpHandler接口使用. 因为IHttpHandler中没有定义RequestContext属性, 如果一个MvcHandler对象此属性没有赋值则会出错, 也没有将默认的无参数构造函数设置为private, 所以理论上可以很随意的实例化一个MvcHandler而不为其RequestContext属性赋值.
IRouteHandler想实现的语意是: 返回一个具有RequestContext属性的IHttpHandler对象.
但是最后的实现结果是: 提供"返回IHttpHandler对象"的方法, 此方法接收RequestContext对象参数.
还需要注意ControllerContext类. 在Controller的处理过程中使用此对象作为保存上下文数据的容器.下面是这几个类的包含关系:
可以看到在ControllerContext中包含了RequestContext对象,但是又将RequestContext对象中的两个属性提取到自己的类中.如果仅仅是为了使用方便而这么做, 个人认为不是一个好的设计.数据对象的存储职责也应该明确,使用ControllerContext.RequestContext.RouteData 的方式更容易被人理解.
PS:这种方式类似于方法内联.对于属性JIT为了效率会帮助我们做内联.而仅仅是为了使用方便.
所以从系统的角度上看, 实现了IView接口的对象才是View.
但是从实现效果上看, 具体的aspx或者ascx页面才是View.
当第一次看到IView接口时我认为它应该是"View角色"需要实现的接口. 但是结果并不是这样.
在我们的系统中View对象应该是aspx或者ascx文件. 而且并不是所有的ActionResult都需要找到aspx或者ascx文件, 事实上只有PartialViewResult 和 ViewResult 才会去寻找View对象.其他的ActionResult要么是返回文件, 要么是跳转等等.
那么两者的关系到底是怎样的? 其实其中的过程需要牵扯到这几个接口和类:
IViewEngine, ViewEngineResult, ViewEngineCollection
ViewEngine是View引擎, ViewEngineCollection是一个引擎集合,里面保存了各种寻找View的引擎.但是在目前的源代码中只有WebFormViewEngine : VirtualPathProviderViewEngine : IViewEngine
这一系列WebForm使用的引擎.引擎的作用有两个:
1.寻找Page/用户控件的路径
2.根据路径创建IView对象.也就是根据页面的物理文件创建IView接口对象.
而且目前实现了IView接口的对象也只有一个:
WebFormView
WebFormViewEngine 根据页面路径, 将一个页面地址转化为一个WebFormView对象,也就是一个IView接口对象.
至此IView接口和Page页面类仍然没有任何关系, IView对象只是保存了页面的物理路径.
接着在IView的Render事件中,根据物理路径创建了一个页面的object实例,注意看这一段代码:
object viewInstance = BuildManager.CreateInstanceFromVirtualPath(ViewPath, typeof(object)); if (viewInstance == null) { throw new InvalidOperationException( String.Format( CultureInfo.CurrentUICulture, MvcResources.WebFormViewEngine_ViewCouldNotBeCreated, ViewPath)); } ViewPage viewPage = viewInstance as ViewPage; if (viewPage != null) { RenderViewPage(viewContext, viewPage); return; } ViewUserControl viewUserControl = viewInstance as ViewUserControl; if (viewUserControl != null) { RenderViewUserControl(viewContext, viewUserControl); return; }
viewInstance 就是通过物理路径创建的页面对象.但是他的类型是object, 而且程序尝试将其分别转化为ViewPage对象和ViewUserControl对象.
我想很多人都看到了这里的设计不足.现在我们只能"约定": 所有的MVC中的页面对象都必须继承自ViewPage或者ViewUserControl类, 否则程序就会出错.产生这种不足的原因就是IView接口和ViewPage没有任何的耦合性, 完全是硬编码进去的.
为什么不让页面直接实现IView接口? 然后尝试将页面转化为IView接口对象, 而不是ViewPage, 这样才是好的设计. 其实微软知道什么是好的设计, 我猜测他们遇到的困难是Page对象和IView接口的冲突. 因为两者都需要Render. 如果在IView中定义自己的Render名称, 那就意味着ASP.NET MVC开发小组要自己处理页面的显示逻辑, 而现在ASP.NET WebForm模式下面的页面显示引擎又不能复用, 重新开发自己的一套显示引擎成本又太大, 才出此下策.
以上只是猜测.这种设计的缺陷虽然可以接受, 但是真的是让我好几天陷入了看不懂代码的痛苦之中.还好, 现在可以解脱了.
另外在为了跟踪实现过程, 我将ASP.NET MVC的源代码项目添加到了实例项目中, 其中有一些需要注意的地方:
1. 将实例项目中的System.Web.Mvc引用删除, 改成项目引用.
2. 需要在Web.Config中注释掉程序集引用:
<compilation debug="true"> <assemblies> <add assembly="System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/> <add assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/> <add assembly="System.Web.Abstractions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/> <add assembly="System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/> <!-- <add assembly="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>--> <add assembly="System.Data.DataSetExtensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/> <add assembly="System.Xml.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/> <add assembly="System.Data.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/> </assemblies> </compilation>
注释掉的程序集存在于GAC中, 但是我们现在不希望使用GAC中的程序集, 而是引用项目.
3. 将View目录下的Web.Config中的所有System.Web.Mvc相关的 PublicKeyToken 都修改为 null:
<pages validateRequest="false" pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"> <controls> <add assembly="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" namespace="System.Web.Mvc" tagPrefix="mvc" /> </controls> </pages>
首先很抱歉在本系列文章开篇时承诺的每日一篇仅仅坚持了2天.具体原因就不解释了.这篇文章的出炉历时半个月, 并且经历了ASP.NET MVC从RC,RC2直到最近的1.0版本的演变. 在查看MVC源代码上花费了大量的时间, 希望付出的努力能够为大家研究学习ASP.NET MVC带来帮助.
实例源代码下载地址:
本文讲解在Action中向View传递Model的几种方式.以及View获取Model以后如何编写显示逻辑.还详细的介绍了ASP.NET MVC框架提供的Html Helper类的使用及如何为Html Helper类添加自定义扩展方法.
上一篇文章中我们学习了Controller处理一次请求的全过程.在Controller的Action中, 会传递数据给View,还会通知View对象开始显示.所以Model是在Action中获取的, 并由Action传递给View. View对象接到Action通知后会使用自己的显示逻辑展示页面.
下面首先让我们学习如何将Model传递给View对象.
在MVC中,Model对象是指包含了数据的模型. Controller将Model传递给View以后, View对象中不应该做任何的业务逻辑处理, 仅仅根据Model对象做一些显示逻辑的处理.
传递Model对象时, 我们有两种选择:
1.传递一个弱类型的集合, 即成员为object类型的集合, 在View中需要将每个成员转换成我们需要的类型,比如int, string,自定义类型等.
2.传递强类型对象, 这些类型是我们自定义的. 在View中直接使用我们传递的强类型对象, 不需要再转换类型.
如果让我们自己设计一个MVC框架, 我们也会想到上面两种实现方式,接下来看看在ASP.NET MVC中的实现.
ASP.NET MVC框架定义了ViewContext类, 直译后是"View上下文", 其中保存和View有关的所有数据, 其中Model对象也封装在了此类型中.
ViewContext对象包含三个属性:
其中ViewData集合和TempData集合都是用来保存Model对象的.在一个Controller的Action中, 我们可以用如下方式为这两个集合赋值:
/// <summary> /// 传递弱类型Model的Action示例 /// </summary> /// <returns>ViewResult</returns> public ActionResult WeakTypedDemo() { ViewData["model"] = "Weak Type Data in ViewData"; TempData["model"] = "Weak Type Data in TempData"; return View("WeakTypedDemo"); }
在页面中, 是用如下方式使用这两个集合:
<div> <% = ViewData["model"] %><br /> <% = TempData["model"] %><br /> </div>
请注意Action中的赋值语句实际上操作的是Controller类的ViewData和TempData属性, 此时并没有任何的数据传递.上一篇文章中我们已经学到, return View语句会返回一个ViewResult对象, 并且接下来要执行ViewResult的Executeresult方法. Controller的View方法会将Controller类的ViewData和TempData属性值传递给ViewResult,代码如下:
protected internal virtual ViewResult View(IView view, object model) { if (model != null) { ViewData.Model = model; } return new ViewResult { View = view, ViewData = ViewData, TempData = TempData }; }
然后在ViewResult中根据ViewData和TempData构建ViewContext对象:
public override void ExecuteResult(ControllerContext context) { //... ViewContext viewContext = new ViewContext(context, View, ViewData, TempData); View.Render(viewContext, context.HttpContext.Response.Output); //... }
ViewContext对象最终会传递给ViewPage, 也就是说ViewData和TempData集合传递到了ViewPage. 我这里简化了最后的传递流程, 实际上ViewData对象并不是通过ViewContext传递到ViewPage中的, ViewPage上的ViewData是一个单独的属性, 并没有像TempData一样其实访问的是ViewContext.TempData. 这么做容易产生奇异, 本类ViewContext是一个很好理解职责十分清晰的类. 作为使用者的我们暂时可以忽略这点不足, 因为如此实现ViewData完全是为了下面使用强类型对象.
虽然ViewData和TempData都可以传递弱类型数据,但是两者的使用是有区别的:
我在系统中建立了一个模型类:StrongTypedDemoDTO
从名字可以看出, 这个模型类是数据传输时使用的(Data Transfer Object).而且是我为一个View单独创建的.
添加一个传递强类型Model的Action,使用如下代码:
public ActionResult StrongTypedDemo() { StrongTypedDemoDTO model = new StrongTypedDemoDTO() { UserName="ziqiu.zhang", UserPassword="123456" }; return View(model); }
使用了Controller.View()方法的一个重载, 将model对象传递给View对象.下面省略此对象的传输过程, 先让我们来看看如何在View中使用此对象.
在创建一个View时, 会弹出下面的弹出框:
勾选"Create a strongly-typed view"即表示要创建一个强类型的View, 在"View data class"中选择我们的数据模型类.
在"view content"中如下选项:
这里是选择我们的View的"模板", 不同的模板会生成不同的View页面代码. 虽然这些代码不一定满足我们需要, 但是有时候的确能节省一些时间,尤其是在写文章做Demo的时候. 比如我们的View是添加数据使用的,那就选择"Create".如果是显示一条数据用的, 就选择"Detail".
以选择Detail为例, 自动生成了下列代码:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<DemoRC.Models.DTO.TransferModelController.StrongTypedDemoDTO>" %> ... <body> <fieldset> <legend>Fields</legend> <p> UserName: <%= Html.Encode(Model.UserName) %> </p> <p> UserPassword: <%= Html.Encode(Model.UserPassword) %> </p> </fieldset> <p> <%=Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) %> | <%=Html.ActionLink("Back to List", "Index") %> </p> </body> ...
页面的Model属性就是一个强类型对象, 在这里就是StrongTypedDemoDTO类实例.page页面指令可以看出, 这里的页面继承自ViewPage<T>类, 而不是ViewPage, 用T已经确定为StrongTypedDemoDTO类型, 所以Model的类型就是StrongTypedDemoDTO.
使用强类型数据要优于弱类型数据, 老赵也曾提出过. 强类型有太多的好处, 智能提示, 语意明确, 提高性能,编译时发现错误等等.
所以在实际应用中, 我们应该传递强类型的数据.
目前ASP.NET MVC还存在一些不足. 将页面类型化, 导致了只能传递一种数据类型实例到页面上. 而且内部代码的实现上并不十分完美.尤其是虽然我们已经知道传递的是StrongTypedDemoDTO类型, 页面的Model属性也是StrongTypedDemoDTO类型, 但是仍然需要进行强制类型转换, 原因是Controller的View(object model)方法重载接收的是一个object类型.
还有, 为每个View建立一个模型类, 也是一件繁琐的工作. 也许我们的业务模型中的两个类组合在一起就是View页面需要的数据, 但是却不得不建立一个类将其封装起来.模型类的膨胀也是需要控制一个事情. 尤其是对于互谅网应用而非企业内部的系统, 页面往往会有成百上千个.而且复用较少.
当然目前来说既然要使用强类型的Model, 我提出一些组织Model类型的实践方法.下面是我项目中的Model类型组织结构:
这里Model是一个文件夹, 稍大一些的系统可以建立一个Model项目. 另外我们需要建立一个DTO文件夹, 用来区分Model的类型. MVC中的Model应该对应DTO文件夹中的类.在DTO中按照Controller再建立文件夹, 因为Action和View大部分都是按照Controller组织的, 所以Model也应该按照Controller组织.
在Controller文件夹里放置具体的Model类. 其实两个Controller文件夹中可以同相同的类名称, 我们通过命名空间区分同名的Model类:
namespace DemoRC.Models.DTO.TransferModelController { /// <summary> /// Action为StrongTypedDemo的数据传输模型 /// </summary> public class StrongTypedDemoDTO { /// <summary> /// 用户名 /// </summary> public string UserName { get; set; } /// <summary> /// 用户密码 /// </summary> public string UserPassword { get; set; } } }
使用时也要通过带有Controller的命名空间使用比如DTO.TransferModelController.StrongTypedDemoDTO, 或者建立一些自己的约定.
View对象获取了Model以后, 我们可以通过两种方式使用数据:
熟悉ASP,PHP等页面语言的人都会很熟悉这种直接在页面上书写代码的方式.但是在View中应该只书写和显示逻辑有关的代码,而不要增加任何的业务逻辑代码.
假设我们创建了下面这个Action,为ViewData添加了三条记录:
/// <summary> /// Action示例:使用内嵌代码输出ViewData /// </summary> /// <returns></returns> public ActionResult ShowModelWithInsideCodeDemo() { ViewData["k1"] = @"<script type=""text/javascript"">"; ViewData["k2"] = @"alert(""Hello ASP.NET MVC !"");"; ViewData["k3"] = @"</script>"; return View("ShowModelWithInsideCode"); }
在ShowModelWithInsideCode中, 我们可以通过内嵌代码的方式, 遍历ViewData集合:
<html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>使用内嵌代码输出ViewData</title> <% foreach(KeyValuePair<string, object> item in ViewData ) {%> <% = item.Value %> <% } %> </head> <body> <div> <div>此页面运行的脚本代码为:</div> <fieldset> <% foreach(KeyValuePair<string, object> item in ViewData ) {%> <% = Html.Encode(item.Value) %> <br /> <% } %> </fieldset> </div> </body> </html>
页面上遍历了两遍ViewData,第一次是作为脚本输出的, 第二次由于进行了HTML编码,所以将作为文字显示在页面上.
使用这种方式, 我们可以美工做好的HTML页面的动态部分, 使用<% %>的形式转化为编码区域, 通过程序控制输出.由于只剩下显示逻辑要处理,所以这种开发方式往往要比CodeBehind的编码方式更有效率, 维护起来一目了然.
最让我高兴是使用这种方式后,我们终于可以只使用HTML控件了.虽然ASP.NET WebFrom编程模型中自带了很多服务器控件, 功能很好很强大, 但是那终究是别人开发的控件, 这些控件是可以随意变化的, 而且实现原理也对使用者封闭. 使用原始的页面模型和HTML控件将使我们真正的做程序的主人.而且不会因为明天服务器控件换了个用法就要更新知识, 要知道几乎所有的HTML控件几乎是被所有浏览器支持且不会轻易改变的.
注意虽然我们同样可以在ASP.NET MVC中使用服务器端控件, 但是在MVC中这并不是一个好的使用方式.建议不要使用.
要使用服务器端控件, 我们就需要在后台代码中为控件绑定数据. ASP.NET MVC框架提供的添加一个View对象的方法已经不能创建后台代码, 也就是说已经摒弃了这种方式.但是我们仍然可以自己添加.
首先创建一个带有后台代码的(.cs文件),一般的Web Form页面(aspx页面),然后修改页面的继承关系, 改为继承自ViewPage:
public partial class ShowModelWithControl : System.Web.Mvc.ViewPage
在页面上放置一个Repeater控件用来显示数据:
<body> <form id="form1" runat="server"> <div> <asp:Repeater ID="rptView" runat="server"> <ItemTemplate> <%# ((KeyValuePair<string, object>)Container.DataItem).Value %><br /> </ItemTemplate> </asp:Repeater> </div> </form> </body>
在Page_Load方法中, 为Repeater绑定数据:
public partial class ShowModelWithControl : System.Web.Mvc.ViewPage { protected void Page_Load(object sender, EventArgs e) { rptView.DataSource = ViewData; rptView.DataBind(); } }
在Controller中创建Action, 为ViewData赋值:
/// <summary> /// Action示例:使用服务器控件输出ViewData /// </summary> /// <returns></returns> public ActionResult ShowModelWithControlDemo() { ViewData["k1"] = @"This"; ViewData["k2"] = @"is"; ViewData["k3"] = @"a"; ViewData["k4"] = @"page"; return View("ShowModelWithControl"); }
运行结果:
再次强调, 在ASP.NET MVC中我们应该尽量避免使用这种方式.
HTML Helper类是ASP.NET MVC框架中提供的生成HTML控件代码的类. 本质上与第一种方式一样, 只是我们可以不必手工书写HTML代码,而是借助HTML Helper类中的方法帮助我们生成HTML控件代码.
同时,我们也可以使用扩展方法为HTML Helper类添加自定义的生成控件的方法.
HTML Helper类的大部分方法都是返回一个HTML控件的完整字符串, 所以可以直接在需要调用的地方使用<% =Html.ActionLink() %>的形式输出字符串.
在ViewPage中提供了Html属性, 这就是一个HtmlHelper类的实例. ASP.NET MVC框架自带了下面这些方法:
上面列举的常用的HtmlHelper类的方法,并不是完整列表.
下面的例子演示如何使用HtmlHelper类:
<div> <% using (Html.BeginForm()) { %> <label style="width:60px;display:inline-block;">用户名:</label> <% =Html.TextBox("UserName", "ziqiu.zhang", new { style="width:200px;" })%> <br /><br /> <label style="width:60px;display:inline-block;">密码:</label> <% =Html.Password("Psssword", "123456", new { style = "width:200px;" })%> <% }%> </div>
上面的代码使用Html.BeginForm输出一个表单对象, 并在表单对象中添加了两个Input, 一个使用Html.TextBox输出, 另一个使用Html.Password输出,区别是Html.Password输出的input控件的type类型为password.效果如图:
我们可以自己扩展HtmlHelper类, 为HtmlHelper类添加新的扩展方法, 从而实现更多的功能.
在项目中建立Extensions文件夹, 在其中创建SpanExtensions.cs文件.源代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace System.Web.Mvc { public static class SpanExtensions { public static string Span(this HtmlHelper helper,string id, string text) { return String.Format(@"<span id=""{0}"">{1}</span>", id, text); } } }
上面的代码我们为HtmlHelper类添加了一个Span()方法, 能够返回一个Span的完整HTML字符串.
因为命名空间是System.Web.Mvc,所以页面使用的时候不需要再做修改,Visual Studio会自动识别出来:
请大家一定要注意命名空间, 如果不使用System.Web.Mvc命名空间, 那么一定要在页面上引用你的扩展方法所在的命名空间, 否则我们的扩展方法将不会被识别.
接下来在页面上可以使用我们的扩展方法:
<div> <!-- 使用自定义的Span方法扩展HTML Helper --> <% =Html.Span("textSpan", "使用自定义的Span方法扩展HtmlHelper类生成的Span") %> </div>
上面自定义的Span()方法十分简单, 但是有时候我们要构造具有复杂结构的Html元素, 如果用字符串拼接的方法就有些笨拙.
ASP.NET MVC框架提供了一个帮助我们构造Html元素的类:TagBuilder
TagBuilder类有如下方法帮助我们构建Html控件字符串:
方法名称 | 用途 |
AddCssClass() | 添加class=””属性 |
GenerateId() | 添加Id, 会将Id名称中的"."替换为IdAttributeDotReplacement 属性值的字符.默认替换成"_" |
MergeAttribute() | 添加一个属性,有多种重载方法. |
SetInnerText() | 设置标签内容, 如果标签中没有再嵌套标签,则与设置InnerHTML 属性获得的效果相同. |
ToString() | 输出Html标签的字符串, 带有一个参数的重载可以设置标签的输出形式为以下枚举值:
|
同时一个TagBuilder还有下列关键属性:
属性名称 | 用途 |
Attributes | Tag的所有属性 |
IdAttributeDotReplacement | 添加Id时替换"."的目标字符 |
InnerHTML | Tag的内部HTML内容 |
TagName | Html标签名, TagBuilder只有带一个参数-TagName的构造函数.所以TagName是必填属性 |
下面在添加一个自定义的HtmlHelper类扩展方法,同样是输出一个<Span>标签:
public static string Span(this HtmlHelper helper, string id, string text, string css, object htmlAttributes) { //创意某一个Tag的TagBuilder var builder = new TagBuilder("span"); //创建Id,注意要先设置IdAttributeDotReplacement属性后再执行GenerateId方法. builder.IdAttributeDotReplacement = "-"; builder.GenerateId(id); //添加属性 builder.MergeAttributes(new RouteValueDictionary(htmlAttributes)); //添加样式 builder.AddCssClass(css); //或者用下面这句的形式也可以: builder.MergeAttribute("class", css); //添加内容,以下两种方式均可 //builder.InnerHtml = text; builder.SetInnerText(text); //输出控件 return builder.ToString(TagRenderMode.Normal); }
在页面上,调用这个方法:
<% =Html.Span("span.test", "使用TagBuilder帮助构建扩展方法", "ColorRed", new { style="font-size:15px;" })%>
生成的Html代码为:
<span id="span-test" class="ColorRed" style="font-size: 15px;">使用TagBuilder帮助构建扩展方法</span>
注意已经将id中的"."替换为了"-"字符.
本来打算在本文中直接将ViewEngine的使用也加进来, 但是感觉本文已经写了很长的内容, (虽然不多,但是很长......)所以将ViewEngine作为下一章单独介绍.
前些天 Scott Guthrie's的博客上放出了"ASP.NET MVC免费教程", 里面介绍了创建一名为"NerdDinner"项目的全过程, 使用LINQ+ASP.NET MVC, 但是其中对于技术细节没有详细介绍(和本系列文章比较一下就能明显感觉出来), 下面提供本书的pdf文件下载地址以及源代码下载地址:
源代码是英文版本, 其实我最近有做一个中文的"Nerd Dinner"的想法, 但是因为要写文章而且还要工作已经让我焦头烂额, 写文章真的是一件体力活.如果有人同样有兴趣翻译代码, 我可以提供域名和服务器空间.
差点提供忘记本篇文章的示例源代码下载:
本文讲解ViewEngine的作用, 并且深入解析了实现ViewEngine相关的所有接口和类, 最后演示了如何开发一个自定义的ViewEngine. 本系列文章已经全部更新为ASP.NET MVC 1.0版本.希望大家多多支持!
首先注意: 我会将大家在MVC之前一直使用的ASP.NET页面编程模型称作ASP.NET WebForm编程模型.
上一讲中我们已经学习了如何向View传递Model, 以及如何在View中使用Model对象. 目前为止我们使用的都还是ASP.NET WebForm的页面模型,比如aspx页面,用户控件,母版页等. 最后这些页面中都要转换为HTML代码. 比如页面中的内嵌代码:
<% = ViewData["model"] %>
你是否思考过, 为何页面会支持<% %>这种语法? 为何最后一个aspx页面会在浏览器中以HTML代码的形式展现?
有人会回答这是ASP.NET自带的语法和功能. 没有错, ASP.NET帮我们做了编译页面, 输出HTML, 返回HTML给客户端浏览器等一系列工作.但是这些工作在MVC框架中有很多是属于View角色的职责. 为了继续使用原有的ASP.NET WebForm页面引擎, ASP.NET MVC抽象出来了ViewEngine这个角色. 顾名思义ViewEngine即视图引擎, 其主要作用就是找到View对象, 编译View对象中的语言代码(执行语言逻辑), 并且输出HTML. 下面讲解的WebFormViewEngine就是使用ASP.NET WebForm的页面编译/呈现功能实现的.
下面将讲解和ViewEngine有关的各个接口和类.
IView接口是对MVC结构中View对象的抽象, 此接口只有一个方法:
void Render(ViewContext viewContext, TextWriter writer);
Render方法的作用就是展示View对象, 通常是将页面HTML写入到Writer中供浏览器展示.
在本系列第三篇文章中我曾经分析过, 虽然IView对象是MVC中View角色的抽象, 并且提供了Render方法, 但是实际上真正的View角色的显示逻辑在ViewPage/ViewUserControl类中. 这是由于ASP.NET MVC提供的WebFormViewEngine视图引擎是使用原有的ASP.NET Web From的页面显示机制, 我们无法直接将WebForm模型中的页面转化为IView对象.
于是最后使用了一个折中的办法:
在IView对象的Render方法中调用WebForm页面的Render方法. WebFormView是目前ASP.NET MVC中唯一实现了IView接口的类
所以如果我们使用自定义的ViewEngine引擎, 就可以直接创建一个实现了IView接口的类实现Render方法.
ViewEngine即视图引擎, 在ASP.NET MVC中将ViewEngine的作用抽象成了 IViewEngine 接口.
虽然IViewEngine的职责是寻找View对象, 但是其定义的两个方法:
返回的结果是ViewEngineResult对象, 并不是View对象. 我们可以将ViewEngineResult理解为一次查询的结果, 在ViewEngineResult对象中包含有本次找到的IView对象.
ASP.NET MVC 提供了下面两个实现了IViewEngine接口的类:
WebFormViewEngine是VirtualPathProviderViewEngine的派生类.
VirtualPathProviderViewEngine类实现了FindPartialView/FindView方法, 更够根据指定的路径格式搜索页面文件, 并且使用了提供了Cache机制缓存数据. 注意因为使用的是ASP.NET Cache,依赖HttpContext对象, 这就导致Cache无法在WebService或者WCf等项目中使用. VirtualPathProviderViewEngine寻找页面的方法依赖下面三个属性:
在VirtualPathProviderViewEngine中只定义了这三个属性, 具体的值在派生类WebFormViewEngine中指定:
public WebFormViewEngine() { MasterLocationFormats = new[] { "~/Views/{1}/{0}.master", "~/Views/Shared/{0}.master" }; ViewLocationFormats = new[] { "~/Views/{1}/{0}.aspx", "~/Views/{1}/{0}.ascx", "~/Views/Shared/{0}.aspx", "~/Views/Shared/{0}.ascx" }; PartialViewLocationFormats = ViewLocationFormats; }
上面的代码中我们可以一步了然ViewEngine都搜索哪些路径.甚至还可以添加我们自己的路径和文件类型.
因为有了VirtualPathProviderViewEngine类, 在开发自定义的ViewEngine时不需要再编写搜索View文件的逻辑了.只需要定义搜索路径即可. 如果不使用ASP.NET WebForm的页面显示方式, 就需要自己定义的View对象如何显最后转化为HTML代码.
在后面的实例中会演示创建一个我们自定义的ViewEngine.
ViewEngineResult是ViewEngine寻找View的查询结果.ViewEngineResult类没有派生类, 也就是说不同的ViewEngine返回的结果都是ViewEngineResult对象.
ViewEngineResult类有一个很重要的构造函数:
public ViewEngineResult(IView view, IViewEngine viewEngine)
以WebFormViewEngine为例, 在WebFormViewEngine类中定义了 MasterLocationFormats/ViewLocationFormats /PartialViewLocationFormats , 在调用FindPartialView/FindView方法时, 首先找到View对象的磁盘路径, 然后使用CreatePartialView/CreateView方法将磁盘路径转化实现了IView接口的WebFormView对象.
WebFormView中依然保存这页面对象的磁盘路径, 在调用Render时会根据磁盘路径创建ViewPage对象, 调用页面的Render方法.ASP.NET MVC编译页面时, 使用了.NET Framework 2.0以上的版本中提供的根据虚拟路径编译页面的函数:
BuildManager.CreateInstanceFromVirtualPath(string virtualPath, Type requiredBaseType)
命名空间为System.Web.Compilation.
ViewEngineCollection是IViewEngine对象的集合类. 在我们的系统中可以使用多个ViewEngine, 在寻找时会返回第一个匹配的ViewEngineResult, 下面是ViewEngineCollection类的Find方法代码:
private ViewEngineResult Find(Func<IViewEngine, ViewEngineResult> cacheLocator, Func<IViewEngine, ViewEngineResult> locator) { ViewEngineResult result; foreach (IViewEngine engine in Items) { if (engine != null) { result = cacheLocator(engine); if (result.View != null) { return result; } } } List<string> searched = new List<string>(); foreach (IViewEngine engine in Items) { if (engine != null) { result = locator(engine); if (result.View != null) { return result; } searched.AddRange(result.SearchedLocations); } } return new ViewEngineResult(searched); }
通过上面的代码我们了解到, ViewEngineCollection会首先从Cache中搜索, 如果没有搜索到结果,则根据路径格式搜索. 如果最后还是没有搜索到View对象则抛出找不到View的异常.所以虽然我们可以添加多个ViewEngine, 但是永远不要为两个ViewEngine指定同样的搜索格式(路径+文件类型), 因为如果出现一个页面对象符合两个ViewEngine的搜索格式的情况, 将无法控制使用哪一个ViewEngine输出页面.
在ViewBaseResult.ExecuteResult() 方法中, 调用了ViewEngineCollection.Find方法获取ViewEngineResult对象,并调用其中的IView.Render()方法完成View对象的显示.
下面通过示例演示如何开发自己的ViewEngine.其中要用到StringTemplate这个模板引擎, 在老赵的的MVC视频教程中也使用的此引擎演示ViewEngine. StringTemplate负责翻译一个模板页上面的占位符(aspx页面中的内嵌代码), 输出HTML.目前在StringTemplate的官方网站上已经提供了针对Asp.Net Mvc的ViewEngine.但是官方的ViewEngine模板没有使用VirtualPathProviderViewEngine基类.下面我将提供一种不能说更好但至少是另一种实现的StringTemplateViewEngine.其中需要使用StringTemplate的模版功能.
要开发一个自己的ViewEngine, 首先要创建实现了IView接口的类, 在此我们创建了名为StringTemplateView的类
public class StringTemplateView : IView { #region 属性 Properties /// <summary> /// StringTemplate 对象, 在构造函数中创建 /// </summary> private StringTemplate StringTemplate { get; set; } #endregion #region 构造函数 Constructed Function private StringTemplateView() { //不用于使用不带参数的构造函数 this.StringTemplate = new StringTemplate(); } public StringTemplateView(StringTemplate template) { //null check if (template == null) throw new ArgumentNullException("template"); //set template this.StringTemplate = template; } #endregion #region IView 成员 void IView.Render(ViewContext viewContext, System.IO.TextWriter writer) { foreach(var item in viewContext.ViewData) { this.StringTemplate.SetAttribute(item.Key.ToString(), item.Value.ToString()); } //为StringTemplate设置HttpContext this.StringTemplate.SetAttribute("context", viewContext.HttpContext); //输出模板 NoIndentWriter noIndentWriter = new NoIndentWriter(writer); this.StringTemplate.Write(noIndentWriter); } #endregion }
StringTemplateView是在StringTemplate视图引擎中View角色的抽象, 所以功能是实现呈现页面的Render方法. StringTemplate的核心功能就是一套自己定义的模板输出引擎, 所以在构造StringTemplateView对象是必须传入一个StringTemplate实例,在Render时只是调用StringTemplate对象的模板输出方法.
有了IView对象. 接下来就要实现最核心的IViewEngine接口. 在具体的StringTemplateViewEngine类中, 要返回一个带有StringTemplateView对象的ViewEngineResult.
在我的实现方法中,使用了ASP.NET MVC已经提供的VirtualPathProviderViewEngine类作为我们的基类. VirtualPathProviderViewEngine类实现了IViewEngine接口的方法, 提供了在程序中寻找View物理文件路径的机制, 搜索时要使用在派生类中赋值的搜索路径.
下面是我们的StringTemplateViewEngine类实现:
public class StringTemplateViewEngine : VirtualPathProviderViewEngine { private string _AppPath = string.Empty; #region 属性 Properties public static FileSystemTemplateLoader Loader { get; private set; } public static StringTemplateGroup Group { get; private set; } #endregion public StringTemplateViewEngine(string appPath) { _AppPath = appPath; Loader = new FileSystemTemplateLoader(appPath); Group = new StringTemplateGroup("views", Loader); MasterLocationFormats = new[] { "/Views/{1}/{0}.st", "/Views//Shared/{0}.st" }; ViewLocationFormats = MasterLocationFormats; PartialViewLocationFormats = MasterLocationFormats; } protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) { return this.CreateView(controllerContext, partialPath, String.Empty); } protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) { StringTemplate stringTemplate = Group.GetInstanceOf(viewPath.Replace(".st", "")); StringTemplateView result = new StringTemplateView(stringTemplate); return result; } }
注意首先在我们的StringTemplateViewEngine中,提供了搜索模板文件的路径,即先从View/{controller}中搜索,再从View/Share中搜索. 这样VirtualPathProviderViewEngine基类的方法就可以找到我们.st模板文件的具体路径, 然后使用StringTemplateViewEngine中提供的创建StringTemplateView的方法, 根据具体路径创建StringTemplateView对象.
在一些开源的ViewEngine中,尤其是MvcContrib项目中的ViewEngine都将创建View对象的功能放在一个ViewFactory类中, 个人认为这个更好的设计, 但是由于我们的StringTemplateViewEngine要继承VirtualPathProviderViewEngine, 所以没办法拆分创建View的方法.
至此我们已经完成了StringTemplateViewEngine的全部工作.
首先做一些准备工作. 因为我们的StringTemplate模板文件后缀是".st", 里面写的大部分都是HTML代码. 默认情况下Visual Studio是不会在编辑.st功能的时候提供智能感知支持的. 但是可以通过如下设置实现:
单击菜单中的"工具"->"选项":
在"文本编辑器"的文件扩展名中, 如图所示的为.st扩展名增加"HTML编辑器".
接下来在.st文件中就可以识别HTML代码了:
StringTemplate引擎支持模板的嵌套, 所以可以讲两个页面公用的菜单栏放在menu.st文件中. 而且我们将此文件放在share文件夹中以便供所有模板页调用. menu.st文件代码如下:
<ul id="menu"> <li><a href="/StringTemplate/HelloST">HelloST</a></li> <li><a href="/StringTemplate/SharedST">SharedST</a></li> </ul>
在Controller文件夹中, 创建StringTemplateController用于跳转到我们的模板页:
public class StringTemplateController : Controller { public ActionResult HelloST() { ViewData["msg"] = "Hello String Template ! "; return View("HelloST"); } }
在View文件夹中创建StringTemplate文件夹, 添加一个HelloST.st文件:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>在Shared文件夹中的st页面</title> <link href="../Content/Site.css" rel="stylesheet" type="text/css" /></head> <body> <div class="page"> <h1>StringTemplateViewEngine示例程序</h1> <div id="menucontainer"> $Views/Shared/menu()$ </div> <div id="main"> $msg$ </div> </div> </body> </html>
示例中的代码十分简单, "$msg$"是StringTemplate的模板语言, 可以识别名称为"msg"的变量. "$Views/Shared/menu()$"也是StringTemplate中的语法, 作用是加载名为menu的模板.
虽然Controller和View文件都建立好了, 但是因为ASP.NET MVC默认的视图引擎是WebFormViewEngine, 但是可以同时使用多个视图引擎, 比如可以为所有".st"后缀名的文件使用StringTemplateViewEngine视图引擎.在Global.asax文件中, 在程序启动时注册我们的ViewEngine:
protected void Application_Start() { RegisterRoutes(RouteTable.Routes); //添加StringTemplate视图引擎 StringTemplateViewEngine engine = new StringTemplateViewEngine(Server.MapPath("/")); ViewEngines.Engines.Add(engine); }
现在, 访问"localhost/StringTemplate/HelloST" ,就可以看到我们的自定义的模板引擎的输出结果了:
在文章最后会提供本实例附带StringTemplateViewEngine的完整源代码.
除了自己开发, 目前已经有了很多为ASP.NET MVC提供的ViewEngine:
MVCContrib项目中的ViewEngine:
StringTemplateViewEngine
这是StringTemplate项目为ASP.NET MVC开发的ViewEngine, 官方以及下载网址是
http://www.stringtemplate.org/
另外在著名的MonoRail项目中, 还有一些类似于StringTemplate的页面显示引擎, 虽然都没有为ASP.NET MVC开发专门的ViewEngine, 但是还是很有参考价值的.我们可以用上面介绍的方法, 在页面显示引擎的基础上自己开发ASP.NET MVC的ViewEngine:
MonoRail项目中的三个ViewEngine:
本篇文章详细介绍了ViewEngine相关类, 已经如何开发自己的ViewEngine. 花了2周时间创作完成, 让大家久等了. 说道最近博客园首页的文章问题, 我觉得一篇文章除了要有知识点, 还有能够很好的讲解, 让大家明白比让自己明白更重要.我没有为了速度草草发表文章,就是希望写出来的东西能够有资格发表到博客园首页.
我希望大家都通过自律来建设博客园, 明白分享知识是一件光荣而且快乐的事情!
文章示例代码下载: