iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 24
2

前言

繼承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
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渲染顯示

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文件虛擬路徑.

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掌控執行開始頁面判斷
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屬性表示在獲取目標搜索過程中使用的搜索位置列表

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原始碼.

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物件.

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
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

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

只允許某個View副檔名

Razor有支援兩個副檔名

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

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

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:透過虛擬路徑找到要執行頁面(透過一些機制).

上一篇
[Day23] 6個基本(ActionResult) View是如何被建立(二)
下一篇
[Day25] 動態產生程式碼(WebViewPage) View是如何被建立(四)
系列文
從Asp.net框架角度進入Asp.net MVC原始碼30

尚未有邦友留言

立即登入留言