探討ViewEngine機制 View是如何被建立(三) (第24天)

Agenda

前言

繼承ActiontResult類別中ViewResultBase最為複雜,因為ViewResultBase要找到實現IViewEngine物件取得取得View檔案,在透過實現IView物件把頁面渲染出來.

這篇會跟大家分享值型上面動作核心類別.

個人覺得MVC運用很多物件導向概念和用法,在讀程式時有件事情很重要是理解類別負責的工作和類別之間關係.就像現實生活中人與人的關係要了解清楚.

我有做一個可以針對於Asp.net MVC Debugger的專案,只要下中斷點就可輕易進入Asp.net MVC原始碼.

ViewResultBase.ExecuteResult

因為ExecuteResult是最終被呼叫方法,我們來解析ViewResultBase.ExecuteResult方法邏輯.

  1. 透過子類別實現FindView取得View相關資料.
  2. 呼叫實現IView物件Render方法,並將渲染出來資料透過Response.Output輸出到Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (String.IsNullOrEmpty(ViewName))
{
ViewName = context.RouteData.GetRequiredString("action");
}

ViewEngineResult result = null;

if (View == null)
{
result = FindView(context);
View = result.View;
}

TextWriter writer = context.HttpContext.Response.Output;
ViewContext viewContext = new ViewContext(context, View, ViewData, TempData, writer);
View.Render(viewContext, writer);

if (result != null)
{
result.ViewEngine.ReleaseView(context, View);
}
}

protected abstract ViewEngineResult FindView(ControllerContext context);

這張UML表示ViewResultBase繼承關係圖.

我們在Controller呼叫的View()PartailView()方法就是建立PartialViewResultViewResult方法並且呼叫ExecuteResult進行View頁面渲染.

UML_Model

IView

View是一個實現了IView介面物件。IView定義非常簡單,僅僅具有唯一Render方法根據指定ViewContextTextWriter物件達成對於View渲染顯示

1
2
3
4
public interface IView
{
void Render(ViewContext viewContext, TextWriter writer);
}

BuildManagerCompiledView

BuildManagerCompiledView類別實現Render對於View如何被渲染呈現.

主要透過下面幾個步驟.

  1. .cshtml,.aspx頁面程式碼會轉成編譯成一個繼承WebViewPage類別的dll檔案.BuildManagerWrapper靜態方法GetCompiledType依據指定View檔案虛擬路徑得到編譯後WebPageView類型
  2. IViewPageActivator(DefaultViewPageActivator)利用反射建立WebPageView物件由頁面程式產生的View物件
  3. 最後再呼叫由子類實現RenderView方法

BuildManagerCompiledView屬性ViewPath表示的就是View文件虛擬路徑.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public abstract class BuildManagerCompiledView : IView
{
internal IViewPageActivator ViewPageActivator;
private IBuildManager _buildManager;
private ControllerContext _controllerContext;

internal IBuildManager BuildManager
{
get
{
if (_buildManager == null)
{
_buildManager = new BuildManagerWrapper();
}
return _buildManager;
}
set { _buildManager = value; }
}

public string ViewPath { get; protected set; }

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

object instance = null;
//取得view型態
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);
}

protected abstract void RenderView(ViewContext viewContext, TextWriter writer, object instance);
}

RazorView

RazorView繼承BuildManagerCompiledView,RazorView具有三個只讀屬性

  • LayoutPathView佈局檔案虛擬路徑
  • ViewStartFileExtensions:表示開始頁面文件的擴展名,對於Razor引擎默認創建RazorView,通過_ViewStart.cshtml檔案定義開始頁面相關資訊.
  • RunViewStartPages:這個bool掌控執行開始頁面判斷
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    public class RazorView : BuildManagerCompiledView
    {
    public string LayoutPath { get; private set; }

    public bool RunViewStartPages { get; private set; }

    public IEnumerable<string> ViewStartFileExtensions { get; private set; }

    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);
    }
    }

RenderView方法執行幾個步驟.

  1. RenderView方法將BuildManagerCompiledView方法取得instance物件轉換型別成WebViewPage
  2. 資料初始化(建立UrlHelp,….)物件
  3. 判斷是否使用Razor共用樣板
  4. 呼叫ExecutePageHierarchy,進行頁面渲染,最主要呼叫Execute方法來執行子類別實現邏輯.

下面是IView類別關係圖

UML_Model

最後由WebFormView,RazorView實現頁面的渲染工作.

IViewEngine

這個介面提供找尋使用ViewEngineResult,ViewViewEngine屬性找到View物件和使用的ViewEngine物件,SearchedLocations屬性表示在獲取目標搜索過程中使用的搜索位置列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface IViewEngine
{
ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);
ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);
void ReleaseView(ControllerContext controllerContext, IView view);
}

public class ViewEngineResult
{
public IEnumerable<string> SearchedLocations { get; private set; }

public IView View { get; private set; }

public IViewEngine ViewEngine { get; private set; }
}

VirtualPathProviderViewEngine

VirtualPathProviderViewEngine這個抽象類別,實現FindPartialViewFindView方法,另外提供一個抽象方法CreateViewCreatePartialView提供子類(WebFormViewEngine,RazorViewEngine)來實現.

下面是FindView原始碼.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
//....

string[] viewLocationsSearched;
string[] masterLocationsSearched;

string controllerName = controllerContext.RouteData.GetRequiredString("controller");
string viewPath = GetPath(controllerContext, ViewLocationFormats, AreaViewLocationFormats, "ViewLocationFormats", viewName, controllerName, CacheKeyPrefixView, useCache, out viewLocationsSearched);
string masterPath = GetPath(controllerContext, MasterLocationFormats, AreaMasterLocationFormats, "MasterLocationFormats", masterName, controllerName, CacheKeyPrefixMaster, useCache, out masterLocationsSearched);

if (String.IsNullOrEmpty(viewPath) || (String.IsNullOrEmpty(masterPath) && !String.IsNullOrEmpty(masterName)))
{
return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched));
}

return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
}

RazorViewEngine

前面有提到VirtualPathProviderViewEngine提供一個抽象類別給子類來實現如何建立一個IView物件.

RazorViewEngine透過上面資訊建立一個RazorView(此類別實現IView介面),最終ViewBaseResult就是呼叫IViewRender方法.

RazorViewEngine就是建立到時候要RenderOutputStream物件.

1
2
3
4
5
6
7
8
9
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
var view = new RazorView(controllerContext, viewPath,
layoutPath: masterPath, runViewStartPages: true, viewStartFileExtensions: FileExtensions, viewPageActivator: ViewPageActivator)
{
DisplayModeProvider = DisplayModeProvider
};
return view;
}

ViewEngines and ViewEngineCollection

透過ViewEngines.Engines可以取得目前可以使用View引擎.

ASP.NET MVC為我們提供了兩種View引擎(RazorViewEngine,WebFormViewEngine),

  • 提供傳統Web Form引擎,.aspx頁面一致WebFormViewEngine
  • 另一種預設使用也是推薦使用Razor引擎RazorViewEngine
1
2
3
4
5
6
7
8
9
10
11
12
13
public static class ViewEngines
{
private static readonly ViewEngineCollection _engines = new ViewEngineCollection
{
new WebFormViewEngine(),
new RazorViewEngine(),
};

public static ViewEngineCollection Engines
{
get { return _engines; }
}
}

ViewEngine類別關係圖如下

UML_Model

這邊以RazorViewEngine來介紹

  • ViewLocationFormats:預設找尋View實體檔案位置
  • PartialViewLocationFormats:預設找尋PartialView實體檔案位置
  • FileExtensions:Razor使用附檔名.

提升執行效率小技巧

這裡有個小技巧可提高MVC執行效率.

移除不必要ViewEngine提升執行效率

MVC藉由ViewEngineCollection這個集合來判斷使用ViewEngine,且它預設有兩個ViewEngines提供給我們使用(RazorViewEngine,WebFormViewEngine)一般來說我們只使用一個ViewEngine另一個就不會用到.

如果我們只使用RazorViewEngine就可在Global.cs上撰寫這段程式碼,主要是把不必要ViewEngine移除只關注在我們使用ViewEngine

1
2
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewEngine());

只允許某個View副檔名

Razor有支援兩個副檔名

  1. vbhtml:vb使用
  2. cshtml:c#使用

如果我們想強制這個專案都使用C#的Razor撰寫view,可藉由幾個屬性來幫我們限制完成.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
ViewEngines.Engines.Add(new RazorViewEngine()
{
AreaViewLocationFormats = new[]
{
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
},
AreaMasterLocationFormats = new[]
{
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
},
AreaPartialViewLocationFormats = new[]
{
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml"
},

ViewLocationFormats = new[]
{
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml",
},
MasterLocationFormats = new[]
{
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
},
PartialViewLocationFormats = new[]
{
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
},
FileExtensions = new[]
{
"cshtml",
}
});

小結:

本篇大致上把產生View頁面使用到的幾個核心介面和類別介紹完了,我們主要會使用繼承ViewResultBase物件並透過,相對應實現IView物件來進行畫面渲染,如何取得使用的IView物件就透過ViewEngines集合.

上面介紹了三個抽象類別和介面,每個都有自己核心職責並且和其他物件有清晰關係

  • ViewResultBase:實現ActionResult提供Controller呼叫產生頁面ExecuteResult方法.
  • IView:提供如何渲染頁面
  • IViewEngine:透過虛擬路徑找到要執行頁面(透過一些機制).

此文作者:Daniel Shih(石頭)
此文地址https://isdaniel.github.io/ithelp-day24/
版權聲明:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!


如果本文對您幫助很大,可街口支付斗內鼓勵石頭^^