從零開始學習 ASP.NET MVC 1.0 (三) Controller/Action 深入解析與應用實例
《從零開始學習ASP.NET MVC 1.0》 文章導航
- (一) 開天辟地入門篇
- (二) 識別URL的Routing組件
- (三) Controller/Action 深入解析與應用實例
- (四) View/Model 全解
- (五) ViewEngine 深入解析與應用實例
一.摘要
一個Url請求經過了Routing處理后會調用Controller的Action方法. 中間的過程是怎樣的? Action方法中返回ActionResult對象后,如何到達View的? 本文將講解Controller的基本用法, 深入分析Controller的運行機制, 并且提供了創(chuàng)建所有類型Action的代碼. 值得學習ASP.NET MVC時參考.
二.承上啟下
在上一篇文章中, 我已經學會了如何使用Routing獲取Controller和Action, 隨后的程序會調用Controller中的Action方法.
每個Action方法都要返回一個ActionResult對象. 一個Action會將數(shù)據(jù)傳遞給View,如圖:
三.Controller與Action的作用
1.職責
Controller負責將獲取Model數(shù)據(jù)并將Model傳遞給View對象.通知View對象顯示.
2.ASP.NET MVC中的Controller和Action
在ASP.NET MVC中, 一個Controller可以包含多個Action. 每一個Action都是一個方法, 返回一個ActionResult實例.
ActionResult類包括ExecuteResult方法, 當ActionResult對象返回后會執(zhí)行此方法.
下面分層次的總結Controller 處理流程:
1. 頁面處理流程
發(fā)送請求 –> UrlRoutingModule捕獲請求 –> MvcRouteHandler.GetHttpHandler() –> MvcHandler.ProcessRequest()
2.MvcHandler.ProcessRequest() 處理流程:
使用工廠方法獲取具體的Controller –> Controller.Execute() –> 釋放Controller對象
3.Controller.Execute() 處理流程
獲取Action –> 調用Action方法獲取返回的ActionResult –> 調用ActionResult.ExecuteResult() 方法
4.ActionResult.ExecuteResult() 處理流程
獲取IView對象-> 根據(jù)IView對象中的頁面路徑獲取Page類-> 調用IView.RenderView() 方法(內部調用Page.RenderView方法)
通過對MVC源代碼的分析,我們了解到Controller對象的職責是傳遞數(shù)據(jù),獲取View對象(實現(xiàn)了IView接口的類),通知View對象顯示.
View對象的作用是顯示.雖然顯示的方法RenderView()是由Controller調用的,但是Controller僅僅是一個"指揮官"的作用, 具體的顯示邏輯仍然在View對象中.
需要注意IView接口與具體的ViewPage之間的聯(lián)系.在Controller和View之間還存在著IView對象.對于ASP.NET程序提供了WebFormView對象實現(xiàn)了IView接口.WebFormView負責根據(jù)虛擬目錄獲取具體的Page類,然后調用Page.RenderView().
四.ActionResult解析
通過上面的流程,我們知道了ActionResult對象在整個流程中的作用.ActionResult是一個抽象類, 在Action中返回的都是其派生類.下面是我整理的ASP.NET MVC 1.0 版本中提供的ActionResult派生類:
| 類名 | 抽象類 | 父類 | 功能 |
| ContentResult | 根據(jù)內容的類型和編碼,數(shù)據(jù)內容. | ||
| EmptyResult | 空方法. | ||
| FileResult | abstract | 寫入文件內容,具體的寫入方式在派生類中. | |
| FileContentResult | FileResult | 通過 文件byte[] 寫入文件. | |
| FilePathResult | FileResult | 通過 文件路徑 寫入文件. | |
| FileStreamResult | FileResult | 通過 文件Stream 寫入文件. | |
| HttpUnauthorizedResult | 拋出401錯誤 | ||
| JavaScriptResult | 返回javascript文件 | ||
| JsonResult | 返回Json格式的數(shù)據(jù) | ||
| RedirectResult | 使用Response.Redirect重定向頁面 | ||
| RedirectToRouteResult | 根據(jù)Route規(guī)則重定向頁面 | ||
| ViewResultBase | abstract | 調用IView.Render() | |
| PartialViewResult | ViewResultBase | 調用父類ViewResultBase 的ExecuteResult方法. 重寫了父類的FindView方法. 尋找用戶控件.ascx文件 | |
| ViewResult | ViewResultBase | 調用父類ViewResultBase 的ExecuteResult方法. 重寫了父類的FindView方法. 尋找頁面.aspx文件 |
目前ASP.NET MVC還沒有提供官方的ActionResult列表.上面的列表是我在源代碼中分析得出的.有些解釋的可能不夠清楚,請諒解.
下面我將列舉各個ActionResult的實例.
五.實例應用
1.添加Controller
安裝了ASP.NET MVC后, 在項目上點擊右鍵會找到添加Controller項:
2.添加Action
下面這個類提供了返回各種類型的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 深入分析
在研究Controller/Action的流程過程中, 發(fā)現(xiàn)了ASP.NET MVC一些問題.
1.Routing組件與MVC框架的結合
Routing組件和ASP.NET MVC并不是一個項目, 在ASP.NET MVC中僅僅是使用了Routing組件, 在源代碼中是通過dll的方式引用的.Routing組件已經包含在.net framework 3.5 sp1中了.
那么ASP.NET MVC是如何應用Routing組件的呢?
Routing組件獲取了Url中的數(shù)據(jù)后, 會將數(shù)據(jù)保存在一個 RouteData 對象中.并將請求傳遞給一個實現(xiàn)了IRouteHandler接口的對象. 在Asp.net MVC中提供的MvcRouteHandler類實現(xiàn)了此接口, 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就夠了. 現(xiàn)在知道了為何要定義這個接口. 主要是為了傳遞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 中實現(xiàn)了IHttpHandler接口的類是MvcHandler, MvcRouteHandler.GetHttpHandler方法就是返回一個MvcHandler對象. MvcHandler類的構造函數(shù)需要傳入一個RequestContext對象. 實現(xiàn)的IHttpHandler接口方法處理過程中都需要依賴這個對象.
但是微軟在這里的處理有一些不足. MvcHandler雖然實現(xiàn)了IHttpHandler接口但是不能被當作IHttpHandler接口使用. 因為IHttpHandler中沒有定義RequestContext屬性, 如果一個MvcHandler對象此屬性沒有賦值則會出錯, 也沒有將默認的無參數(shù)構造函數(shù)設置為private, 所以理論上可以很隨意的實例化一個MvcHandler而不為其RequestContext屬性賦值.
IRouteHandler想實現(xiàn)的語意是: 返回一個具有RequestContext屬性的IHttpHandler對象.
但是最后的實現(xiàn)結果是: 提供"返回IHttpHandler對象"的方法, 此方法接收RequestContext對象參數(shù).
還需要注意ControllerContext類. 在Controller的處理過程中使用此對象作為保存上下文數(shù)據(jù)的容器.下面是這幾個類的包含關系:
可以看到在ControllerContext中包含了RequestContext對象,但是又將RequestContext對象中的兩個屬性提取到自己的類中.如果僅僅是為了使用方便而這么做, 個人認為不是一個好的設計.數(shù)據(jù)對象的存儲職責也應該明確,使用ControllerContext.RequestContext.RouteData 的方式更容易被人理解.
PS:這種方式類似于方法內聯(lián).對于屬性JIT為了效率會幫助我們做內聯(lián).而僅僅是為了使用方便.
2.IView 與 View對象的關系
所以從系統(tǒng)的角度上看, 實現(xiàn)了IView接口的對象才是View.
但是從實現(xiàn)效果上看, 具體的aspx或者ascx頁面才是View.
當?shù)谝淮慰吹絀View接口時我認為它應該是"View角色"需要實現(xiàn)的接口. 但是結果并不是這樣.
在我們的系統(tǒng)中View對象應該是aspx或者ascx文件. 而且并不是所有的ActionResult都需要找到aspx或者ascx文件, 事實上只有PartialViewResult 和 ViewResult 才會去尋找View對象.其他的ActionResult要么是返回文件, 要么是跳轉等等.
那么兩者的關系到底是怎樣的? 其實其中的過程需要牽扯到這幾個接口和類:
IViewEngine, ViewEngineResult, ViewEngineCollection
ViewEngine是View引擎, ViewEngineCollection是一個引擎集合,里面保存了各種尋找View的引擎.但是在目前的源代碼中只有WebFormViewEngine : VirtualPathProviderViewEngine : IViewEngine
這一系列WebForm使用的引擎.引擎的作用有兩個:
1.尋找Page/用戶控件的路徑
2.根據(jù)路徑創(chuàng)建IView對象.也就是根據(jù)頁面的物理文件創(chuàng)建IView接口對象.
而且目前實現(xiàn)了IView接口的對象也只有一個:
WebFormView
WebFormViewEngine 根據(jù)頁面路徑, 將一個頁面地址轉化為一個WebFormView對象,也就是一個IView接口對象.
至此IView接口和Page頁面類仍然沒有任何關系, IView對象只是保存了頁面的物理路徑.
接著在IView的Render事件中,根據(jù)物理路徑創(chuàng)建了一個頁面的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 就是通過物理路徑創(chuàng)建的頁面對象.但是他的類型是object, 而且程序嘗試將其分別轉化為ViewPage對象和ViewUserControl對象.
我想很多人都看到了這里的設計不足.現(xiàn)在我們只能"約定": 所有的MVC中的頁面對象都必須繼承自ViewPage或者ViewUserControl類, 否則程序就會出錯.產生這種不足的原因就是IView接口和ViewPage沒有任何的耦合性, 完全是硬編碼進去的.
為什么不讓頁面直接實現(xiàn)IView接口? 然后嘗試將頁面轉化為IView接口對象, 而不是ViewPage, 這樣才是好的設計. 其實微軟知道什么是好的設計, 我猜測他們遇到的困難是Page對象和IView接口的沖突. 因為兩者都需要Render. 如果在IView中定義自己的Render名稱, 那就意味著ASP.NET MVC開發(fā)小組要自己處理頁面的顯示邏輯, 而現(xiàn)在ASP.NET WebForm模式下面的頁面顯示引擎又不能復用, 重新開發(fā)自己的一套顯示引擎成本又太大, 才出此下策.
以上只是猜測.這種設計的缺陷雖然可以接受, 但是真的是讓我好幾天陷入了看不懂代碼的痛苦之中.還好, 現(xiàn)在可以解脫了.
七.如何在MVC項目中使用MVC源代碼項目
另外在為了跟蹤實現(xiàn)過程, 我將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中, 但是我們現(xiàn)在不希望使用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帶來幫助.
實例源代碼下載地址:
出處:http://www.rzrgm.cn/zhangziqiu/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
浙公網安備 33010602011771號