ASP.Net請求處理機制初步探索之旅 - Part 5 ASP.Net MVC請求處理流程

開篇:上一篇咱們瞭解了在WebForm模式下一個Page頁面的生命週期,它經歷了初始化Init、加載Load以及呈現Render三個重要階段,其中構造了頁面控件樹,並對頁面控件樹進行了大量的遞歸操做,最後將與模板結合生成的HTML返回給了瀏覽器。那麼,在ASP.NET MVC模式下,一個頁面的生命週期又經歷了哪些步湊呢?別急,本篇漫漫道來!html

(1)Part 1:前奏web

(2)Part 2:核心編程

(3)Part 3:管道瀏覽器

(4)Part 4:WebForm頁面生命週期mvc

(5)Part 5:ASP.NET MVC請求處理流程app

1、開放的ASP.NET MVC代碼

  2009年,Microsoft推出了ASP.NET MVC,也將ASP.NET MVC項目做爲開源項目推送到了開源社區中,至今時間也過去快6年了,ASP.NET MVC已經到了5.0的版本階段了。咱們看到ASP.NET MVC從一個不完整的小孩長成一個日漸成熟的巨人,咱們能夠從開源社區找到ASP.NET MVC的源碼,相比以前咱們須要Reflector進行反編譯查看,此次則輕鬆得多。asp.net

  這裏咱們選擇ASP.NET MVC 4的源碼做爲分析對象,我已經將其上傳到了網盤中,你能夠經過下面這個地址進行下載:ide

  傳送門:http://pan.baidu.com/s/1bnF8ZPt函數

  下載完成後,打開ASP.NET MVC 4的源代碼,你會看到以下解決方案:這裏咱們主要關注System.Web.Mvc這個類庫項目源碼分析

2、從MvcHandler.ProcessRequest開始

  從Part 3中咱們知道了在請求處理管道中的第7個事件生成了MvcHandler,在第11和第12個事件之間調用了MvcHandler的ProcessRequest方法開始了ASP.NET MVC的處理響應之旅。那麼,咱們就從MvcHandler的ProcessRequest方法開始查看,一個ASP.NET MVC頁面是如何加載出來一個HTML頁的!

(1)Controller的激活

  ①藉助HttpConetxtWrapper封裝HttpContext

    protected virtual void ProcessRequest(HttpContext httpContext)
    {
          HttpContextBase httpContextBase = new HttpContextWrapper(httpContext);
          ProcessRequest(httpContextBase);
     }

  能夠看出,這裏經過了一個基於包裝器(又稱裝飾者)模式實現的一個HttpContextWrapper類對HttpContext進行了一個封裝,並調用重載的另外一個ProcessRequest方法進行繼續處理。

PS:有關ASP.NET MVC中HttpContext, HttpContextBase, HttpContextWrapper三者之間的聯繫請參考:http://blog.csdn.net/sundacheng1989/article/details/10551091

  ②控制器工廠根據URL建立控制器

        protected internal virtual void ProcessRequest(HttpContextBase httpContext)
        {
            IController controller;
            IControllerFactory factory;
            ProcessRequestInit(httpContext, out controller, out factory);

            try
            {
                controller.Execute(RequestContext);
            }
            finally
            {
                factory.ReleaseController(controller);
            }
        }

  能夠看出,這裏經過調用ProcessRequestInit方法將上下文對象傳入進行處理,而後返回生成的控制器實例以及控制器工廠。所以,咱們轉入ProcessRequestInit方法看看:

        private void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory)
        {
            ......
            string controllerName = RequestContext.RouteData.GetRequiredString("controller");
            factory = ControllerBuilder.GetControllerFactory();
            controller = factory.CreateController(RequestContext, controllerName);
            ......
        }

  在這個方法中,首先根據RouteData路由數據取得要請求的Controller名稱,而後取得ControllerFactory(控制器工廠)對象,經過ControllerFactory來建立指定名稱的控制器,最後將控制器做爲out參數傳遞出去。

  ③調用控制器的Execute方法進入Action

  具體實現了IController接口的Controller對象經過調用Excute方法開始執行具體的Action,那麼Action究竟又是怎樣被觸發的呢?

    public interface IController
    {
        void Execute(RequestContext requestContext);
    }

(2)Action的觸發

  ①從ControllerBase的Excute方法開始 

    public abstract class ControllerBase : IController
    {
        protected virtual void Execute(RequestContext requestContext)
        {
            if (requestContext == null)
            {
                throw new ArgumentNullException("requestContext");
            }
            if (requestContext.HttpContext == null)
            {
                throw new ArgumentException(MvcResources.ControllerBase_CannotExecuteWithNullHttpContext, "requestContext");
            }

            VerifyExecuteCalledOnce();
            Initialize(requestContext);

            using (ScopeStorage.CreateTransientScope())
            {
                ExecuteCore();
            }
        }
     // 抽象方法-讓Controller去具體實現
        protected abstract void ExecuteCore();
    }

  首先,Controller並無實現IController接口,而是Controller的基類ControllerBase實現了IController接口;而後,ControllerBase中定義了一個抽象方法ExcuteCore,讓其子類去具體執行,這裏主要是讓Controller類對象執行這個方法。

  ②根據URL獲取Action名稱並準備觸發Action

    public abstract class Controller : ControllerBase, IActionFilter, IAuthenticationFilter, IAuthorizationFilter, IDisposable, IExceptionFilter, IResultFilter, IAsyncController, IAsyncManagerContainer
    {
        protected override void ExecuteCore()
        {
            PossiblyLoadTempData();
            try
            {
                string actionName = GetActionName(RouteData);
                if (!ActionInvoker.InvokeAction(ControllerContext, actionName))
                {
                    HandleUnknownAction(actionName);
                }
            }
            finally
            {
                PossiblySaveTempData();
            }
        }
    }

  首先,經過路由數據獲取Action名稱,例如請求URL爲:http://xxx.com/Home/Index,這裏獲取的Action名稱即爲Index。而後,經過ActionInvoker.InvokeAction去執行具體的Action。那麼問題來了,這個ActionInvoker又是啥東東?咱們先看看這個接口的定義:

    public interface IActionInvoker
    {
        bool InvokeAction(ControllerContext controllerContext, string actionName);
    }

  經過查閱資料,咱們發現原來是一個叫作ControllerActionInvoker的類實現了IActionInvoker接口,那麼咱們就去看看這個ControllerActionInvoker類吧。

  ③獲取Controller與Action的描述信息和過濾器信息

    public virtual bool InvokeAction(ControllerContext controllerContext, string actionName)
    {
        ......
        ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext);
        ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName);
        if (actionDescriptor != null)
        {
           FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor);
           ......
        }   
        ......
    }

  看到這裏,也許會有人問什麼是描述信息?那麼看到咱們在開發中常常給Controller或者Action添加的Attribute信息也許就不會感到陌生了:例如咱們給某個名爲Index的Action添加了[HttpPost]或者[HttpGet]特性,在請求時須要經過HTTP報文請求方式來區分這兩個Action。

  那麼,什麼又是過濾器信息?首先,過濾器涉及到一個叫作AOP(面向切面編程)的概念,咱們能夠經過前面的請求處理管道進行理解,雖然咱們的ASP.NET頁面請求處理部分只是其中一小部分,可是在這部分執行以前還經歷了許多事件,在這以後又經歷了許多事件,而這些事件都是能夠自定義邏輯的,它們均可以叫作過濾器。ASP.NET MVC默認爲咱們提供了四種類型的過濾器(Filter),以下圖所示:

Filters

PS:對過濾器不熟悉的朋友能夠看看個人另外一篇對ASP.NET MVC基礎知識中的過濾器(Filter)的介紹:http://www.cnblogs.com/edisonchou/p/3932640.html

  ④獲取參數信息並開始真正執行Action:Filter->Action->Filter

    public virtual bool InvokeAction(ControllerContext controllerContext, string actionName)
    {
        ......
        IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor);
        ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters);
        ......
    }

  經過上面所獲取的各類描述信息與過濾器信息找到Action並獲取所需的參數,而後調用InvokeActionMethodWithFilters方法執行Action。所以,再轉到InvokeActionMethodWithFilters方法看看:

        protected virtual ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
        {
            ActionExecutingContext preContext = new ActionExecutingContext(controllerContext, actionDescriptor, parameters);
            Func<ActionExecutedContext> continuation = () =>
                new ActionExecutedContext(controllerContext, actionDescriptor, false /* canceled */, null /* exception */)
                {
                     Result = InvokeActionMethod(controllerContext, actionDescriptor, parameters)
                };

            Func<ActionExecutedContext> thunk = filters.Reverse().Aggregate(continuation,
                                                                            (next, filter) => () => InvokeActionMethodFilter(filter, preContext, next));
            return thunk();
        }

  在這個方法中,首先將上下文對象、描述信息、參數信息傳入InvokeActionMethod方法中,獲得了一個Result對象。這個Result對象又是什麼?轉到定義一看,原來不就是咱們在開發中常常返回的ActionResult類型嗎?

    public ActionResult Result
    {
        get { return _result ?? EmptyResult.Instance; }
        set { _result = value; }
    }

  那麼,在InvokeActionMethod方法中又是如何返回Result的呢?再次轉到定義看看:

        protected virtual ActionResult InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
        {
            object returnValue = actionDescriptor.Execute(controllerContext, parameters);
            ActionResult result = CreateActionResult(controllerContext, actionDescriptor, returnValue);
            return result;
        }

  在這個方法中,首先執行了指定的Action,而後得到了一個returnValue返回值,經過傳入返回值建立具體類型的ActionResult做爲方法的返回值。這裏須要注意的是,ActionResult是一個抽象類,像什麼JsonResult、EmptyResult、ViewResult等都是其子類,而這裏的CreateActionResult就是要建立其具體子類的實例並返回。

  如今將目光返回到InvokeActionMethodWithFilters方法中,看到代碼最後聲明瞭一個委託thunk,它是過濾器結合通過反轉以後再合併以前聲明的委託continuation以後的一個新委託(它所持有的委託鏈順序會協調一致),目的是爲了完成AOP的效果,好比首先要執行Action執行以前的過濾器,才能執行Action方法。

  ⑤ActionResult閃亮登場:Filter->Result

    public virtual bool InvokeAction(ControllerContext controllerContext, string actionName)
    {
        ......
        InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, challengeContext.Result ?? postActionContext.Result);
        ......
    }

  如今回到InvokeAction這個主方法中,剛剛執行完Action以後將結果都保存在了postActionContext中的Result中,如今繼續執行過濾器(好比:能夠對剛剛的Action結果進行一些處理),目的也是爲了完成AOP的效果,好比執行完Action以後,必需要執行Action結束後的過濾器業務邏輯方法。那麼,這裏又是進行了什麼操做呢?轉到InvokeActionResultWithFilters方法中去看看:

    private ResultExecutedContext InvokeActionResultFilterRecursive(IList<IResultFilter> filters, int filterIndex, ResultExecutingContext preContext, ControllerContext controllerContext, ActionResult actionResult)
    {
         ......
         if (filterIndex > filters.Count - 1)
         {
              InvokeActionResult(controllerContext, actionResult);
              return new ResultExecutedContext(controllerContext, actionResult, canceled: false, exception: null);
         }  
         
         IResultFilter filter = filters[filterIndex];
         filter.OnResultExecuting(preContext);  
         ......
         int nextFilterIndex = filterIndex + 1;
         postContext = InvokeActionResultFilterRecursive(filters, nextFilterIndex, preContext, controllerContext, actionResult);
         ......
    }

  首先,判斷過濾器執行的序號是否已經到了最後,若是不是,則繼續遞歸執行本方法調用過濾器(這裏對應的過濾器是OnResultExecuting事件,即在Result被生成時以前進行觸發)。若是到了最後,則開始生成最終的ActionResult。看看這個InvokeActionResult方法,它是一個虛方法。

        protected virtual void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)
        {
            actionResult.ExecuteResult(controllerContext);
        }

(3)View的呈現

  咱們知道ActionResult是一個抽象類,那麼這個InvokeActionResult應該是由其之類來實現。因而,咱們找到ViewResult,可是其並未直接繼承於ActionResult,再找到其父類ViewResultBase,它則繼承了ActionResult。因而,咱們來查看它的ExecuteResult方法:

  ①約定大於配置的緣故

        public override void ExecuteResult(ControllerContext context)
        {
            ......
            if (String.IsNullOrEmpty(ViewName))
            {
                ViewName = context.RouteData.GetRequiredString("action");
            }
            ......
        }

  咱們在平常開發中,老是被告知約定大於配置,View中的名字必須與Controller中Action的名字一致。在這了,咱們知道了緣由,能夠看出,這裏就是國通URL來取得ViewName而後去查找View的。

  ②找到ViewEngine視圖引擎並獲取ViewEngineResult

  首先,咱們瞭解一下什麼是ViewEngine視圖引擎:咱們在ASP.NET MVC開發中通常會有兩個選擇,一個是aspx視圖引擎,另外一個是ASP.NET MVC 3.0推出的Razor視圖引擎。Razor視圖引擎在減小代碼冗餘、加強代碼可讀性和Visual Studio智能感知方面,都有着突出的優點。所以,Razor一經推出就深受廣大ASP.Net開發者的喜好。

    public override void ExecuteResult(ControllerContext context)
    {
        ......
        ViewEngineResult result = null;
        if (View == null)
        {
            result = FindView(context);
            View = result.View;
        }
        ......      
    }

   這裏經過FindView方法獲取到具體的View對象,而FindView又是ViewResultBase的一個抽象方法。這時,咱們須要到ViewResult中去看看這個FindView方法。

        protected override ViewEngineResult FindView(ControllerContext context)
        {
            ViewEngineResult result = ViewEngineCollection.FindView(context, ViewName, MasterName);
            if (result.View != null)
            {
                return result;
            }
            ......
        }    

  這裏經過在ViewEngineCollection視圖引擎集合中調用FindView方法返回一個ViewEngineResult對象,而View則做爲屬性存在於這個ViewEngineResult對象之中。

  ③加載ViewData/TempData等數據生成ViewContext

        protected override ViewEngineResult FindView(ControllerContext context)
        {
            ......
            TextWriter writer = context.HttpContext.Response.Output;
            ViewContext viewContext = new ViewContext(context, View, ViewData, TempData, writer);
            ......
        }    

  這裏開始加載ViewData、TempData等數據生成ViewContext,能夠在ViewContext的構造函數中看到以下代碼:

        public ViewContext(ControllerContext controllerContext, IView view, ViewDataDictionary viewData, TempDataDictionary tempData, TextWriter writer)
            : base(controllerContext)
        {
            if (controllerContext == null)
            {
                throw new ArgumentNullException("controllerContext");
            }
            if (view == null)
            {
                throw new ArgumentNullException("view");
            }
            if (viewData == null)
            {
                throw new ArgumentNullException("viewData");
            }
            if (tempData == null)
            {
                throw new ArgumentNullException("tempData");
            }
            if (writer == null)
            {
                throw new ArgumentNullException("writer");
            }

            View = view;
            ViewData = viewData;
            Writer = writer;
            TempData = tempData;
        }

  如今知道咱們在Action方法中定義的那些ViewData或者TempData是在哪裏被存入上下文了吧?

  ④開始Render:HTML頁面的呈現

        protected override ViewEngineResult FindView(ControllerContext context)
        {
            ......
            View.Render(viewContext, writer);
            ......
        }    

  ViewContext上下文對象已生成好,TextWriter已經拿到,如今就開始對View進行正式的呈現了,也就是返回給瀏覽器端請求的HTML。因爲這裏View對象是一個實現了IView接口的類對象,因而咱們找到RazorView,可是它並未直接實現IView接口,因而咱們找到它的父類BuildManagerCompiledView 

    public abstract class BuildManagerCompiledView : IView
    {
        public virtual void Render(ViewContext viewContext, TextWriter writer)
        {
            if (viewContext == null)
            {
                throw new ArgumentNullException("viewContext");
            }

            object instance = null;

            Type type = BuildManager.GetCompiledType(ViewPath);
            if (type != null)
            {
                instance = ViewPageActivator.Create(_controllerContext, type);
            }

            if (instance == null)
            {
                throw new InvalidOperationException(
                    String.Format(
                        CultureInfo.CurrentCulture,
                        MvcResources.CshtmlView_ViewCouldNotBeCreated,
                        ViewPath));
            }

            RenderView(viewContext, writer, instance);
        }
    }

  首先,經過ViewPath獲取View的類型(Type),這裏也是經過BuildManger來完成的,每一個cshtml都會被asp.net編譯成一個類。而後,經過反射生成了View的具體實例。最後,經過RendView方法進行下一步的呈現工做。RenderView是一個抽象方法,具體實現是在RazorView類或WebFormView類中。

        protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance)
        {
            if (writer == null)
            {
                throw new ArgumentNullException("writer");
            }

            WebViewPage webViewPage = instance as WebViewPage;
            if (webViewPage == null)
            {
                throw new InvalidOperationException(
                    String.Format(
                        CultureInfo.CurrentCulture,
                        MvcResources.CshtmlView_WrongViewBase,
                        ViewPath));
            }

            webViewPage.OverridenLayoutPath = LayoutPath;
            webViewPage.VirtualPath = ViewPath;
            webViewPage.ViewContext = viewContext;
            webViewPage.ViewData = viewContext.ViewData;

            webViewPage.InitHelpers();

            if (VirtualPathFactory != null)
            {
                webViewPage.VirtualPathFactory = VirtualPathFactory;
            }
            if (DisplayModeProvider != null)
            {
                webViewPage.DisplayModeProvider = DisplayModeProvider;
            }

            WebPageRenderingBase startPage = null;
            if (RunViewStartPages)
            {
                startPage = StartPageLookup(webViewPage, RazorViewEngine.ViewStartFileName, ViewStartFileExtensions);
            }
            webViewPage.ExecutePageHierarchy(new WebPageContext(context: viewContext.HttpContext, page: null, model: null), writer, startPage);
        }

  在此方法中,首先將傳遞過來的實例轉換成了一個WebViewPage類的實例,而後將ViewContext、ViewData等數據賦給WebViewPage實例做爲屬性,以便在View中獲取。而後,若是有開始頁則先執行開始頁。最後,將HttpContext、Page與Model對象封裝爲一個WebPageContext對象傳入ExecutePageHierarchy方法中進行執行頁面的渲染。

  首先,咱們從字面上來看,Hierarchy表明層次,那麼方法名的意思大概是:根據層次執行頁面。那麼,什麼是頁面的層次?

  在執行ExecutePageHierachy這個方法來渲染View時,這個方法裏面要完成至關多的工做,主要是ViewStart的執行,和Layout的執行。這裏的困難之處在於對於有Layout的頁面來講,Layout的內容是先輸出的,而後是RenderBody內的內容,最後仍是Layout的內容。若是僅僅是這樣的話,只要初始化一個TextWriter,循序漸進的往裏面寫東西就能夠了,可是實際上,Layout並不能首先執行,而應該是View的代碼先執行,這樣的話View就有可能進行必要的初始化,供Layout使用。例如咱們有以下的一個View:

@{
    ViewBag.Title = "Code in View";
    Layout = "_LayoutPage1.cshtml";
}

  這個Layout的內容以下:

@{ 
    Layout = "~/Views/Shared/_Layout.cshtml";
    ViewBag.ToView = "Data from Layout";
}
<div>
    Data In View: @ViewBag.Title
</div>
<div>
    @RenderBody();    
</div>

  這樣能夠在頁面顯示Code in View字樣。 可是反過來,若是試圖在View中顯示在Layout裏面的"Data from Layout" 則是行不通的,什麼也不會被顯示。因此RenderBody是先於Layout中其餘代碼執行的,這種Layout的結構稱爲 Page Hierachy

  在這樣的代碼執行順序下,還要實現文本輸出的順序,所以asp.net mvc這裏的實現中就使用了棧,這個棧是OutputStack,裏面壓入了TextWriter。注意到這只是一個頁面的處理過程,一個頁面之中還會有Partial View 和 Action等,這些的處理方式都是同樣的,所以還須要一個棧來記錄處理到了哪一個(子)頁面,所以還有一個棧,稱之爲TemplateStack,裏面壓入的是PageContext,PageContext維護了view的必要信息,好比Model之類的,固然也包括上面提到的OutputStack。有了上面的基本信息,下面看代碼,先看入口點:

        public void ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer, WebPageRenderingBase startPage) 
        {
            PushContext(pageContext, writer);
            if (startPage != null) {
                if (startPage != this) {
                    var startPageContext = Util.CreateNestedPageContext<object>(parentContext: pageContext, pageData: null, model: null, isLayoutPage: false);
                    startPageContext.Page = startPage;
                    startPage.PageContext = startPageContext;
                }
                startPage.ExecutePageHierarchy();
            }
            else {
                ExecutePageHierarchy();
            }
            PopContext();
        }

  這個方法中,第一步首先將pageContext入棧:PushContext

        public void PushContext(WebPageContext pageContext, TextWriter writer)
        {
            _currentWriter = writer;
            PageContext = pageContext;
            pageContext.Page = this;

            InitializePage();

            // Create a temporary writer
            _tempWriter = new StringWriter(CultureInfo.InvariantCulture);

            // Render the page into it
            OutputStack.Push(_tempWriter);
            SectionWritersStack.Push(new Dictionary<string, SectionWriter>(StringComparer.OrdinalIgnoreCase));

            // If the body is defined in the ViewData, remove it and store it on the instance
            // so that it won't affect rendering of partial pages when they call VerifyRenderedBodyOrSections
            if (PageContext.BodyAction != null)
            {
                _body = PageContext.BodyAction;
                PageContext.BodyAction = null;
            }
        }

  第二步判斷是否存在ViewStart文件,若是有,就執行startPage.ExecutePageHierachy()。若是不存在,則直接執行ExecutePageHierachy()

    public override void ExecutePageHierarchy()
    {
           ......
           TemplateStack.Push(Context, this);
           try
           {
               // Execute the developer-written code of the WebPage
               Execute();
           }
           finally
           {
               TemplateStack.Pop(Context);
           }        
    }    

  這個方法就是將context壓棧,而後執行相應的view的代碼,而後出棧。有了這些出入棧的操做,能夠保證View的代碼,也就是Execute的時候的writer是正確的。Execute中的方法除去PartialView,Action之類的,最終調用的是WebPageBase中的WriteLiteral方法:

        public override void WriteLiteral(object value)
        {
            Output.Write(value);
        }

  這裏的Output屬性是:

        public TextWriter Output
        {
            get 
            { 
                return OutputStack.Peek(); 
            }
        }

  在調用了Excute方法後,頁面上的HTML內容基本輸出完畢,至此View就渲染完畢了

  第三步,pageContext出棧,主要是棧中的元素的清理工做。

3、一圖勝千言,整體上概覽

參考資料

致謝:本文參閱了大量園友的相關文章,向如下文章做者表示感謝!

(1)Darren Ji,《ASP.NET MVC請求處理管道聲明週期的19個關鍵環節》:http://www.cnblogs.com/darrenji/p/3795661.html

(2)初心不可忘,《綜述:ASP.NET MVC請求處理管道》:http://www.cnblogs.com/luguobin/archive/2013/03/15/2962458.html

(3)學而不思則罔,《ASP.NET Routing與MVC之二:請求如何激活Controller與Action》:http://www.cnblogs.com/acejason/p/3886968.html

(4)王承偉,《ASP.NET MVC請求原理與源碼分析》:http://bbs.itheima.com/thread-134340-1-1.html

(5)Ivony,《經過源代碼研究ASP.NET MVC中的Conroller和View》:http://www.cnblogs.com/Ivony/archive/2010/11/13/aspnet-mvc-by-source-1.html

(6)痞子一毛,《ASP.NET MVC請求處理圖解》:http://www.cnblogs.com/piziyimao/archive/2013/02/27/2935969.html

(7)蔣金楠,《ASP.NET MVC中的View是如何被呈現出來的》:http://www.cnblogs.com/artech/archive/2012/08/22/view-engine-01.html

(8)yinzixin,《深刻ASP.NET MVC之七:ActionResult的執行》:http://www.cnblogs.com/yinzixin/archive/2012/12/05/2799459.html (一篇好文,值得閱讀)

 

相關文章
相關標籤/搜索