1. 程式人生 > >從零開始學習 ASP.NET MVC 1.0 (五) ViewEngine 深入解析與應用例項

從零開始學習 ASP.NET MVC 1.0 (五) ViewEngine 深入解析與應用例項

《從零開始學習ASP.NET MVC 1.0》 文章導航

一.摘要

本文講解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解析

下面將講解和ViewEngine有關的各個介面和類.

IView介面

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方法.

IViewEngine介面

ViewEngine即檢視引擎, 在ASP.NET MVC中將ViewEngine的作用抽象成了 IViewEngine 介面.

雖然IViewEngine的職責是尋找View物件, 但是其定義的兩個方法:

  • FindPartialView
  • FindView

返回的結果是ViewEngineResult物件, 並不是View物件. 我們可以將ViewEngineResult理解為一次查詢的結果, 在ViewEngineResult物件中包含有本次找到的IView物件.

ASP.NET MVC 提供了下面兩個實現了IViewEngine介面的類:

  • VirtualPathProviderViewEngine
  • WebFormViewEngine

WebFormViewEngine是VirtualPathProviderViewEngine的派生類.

VirtualPathProviderViewEngine類實現了FindPartialView/FindView方法, 更夠根據指定的路徑格式搜尋頁面檔案, 並且使用了提供了Cache機制快取資料. 注意因為使用的是ASP.NET Cache,依賴HttpContext物件, 這就導致Cache無法在WebService或者WCf等專案中使用. VirtualPathProviderViewEngine尋找頁面的方法依賴下面三個屬性:

  • MasterLocationFormats
  • ViewLocationFormats
  • PartialViewLocationFormats

在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

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

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

下面通過示例演示如何開發自己的ViewEngine.其中要用到StringTemplate這個模板引擎, 在老趙的的MVC視訊教程中也使用的此引擎演示ViewEngine. StringTemplate負責翻譯一個模板頁上面的佔位符(aspx頁面中的內嵌程式碼), 輸出HTML.目前在StringTemplate的官方網站上已經提供了針對Asp.Net Mvc的ViewEngine.但是官方的ViewEngine模板沒有使用VirtualPathProviderViewEngine基類.下面我將提供一種不能說更好但至少是另一種實現的StringTemplateViewEngine.其中需要使用StringTemplate的模版功能.

1. 實現IView介面

要開發一個自己的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物件的模板輸出方法.

2. 實現IViewEngine介面

有了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的全部工作.

3.使用StringTemplateViewEngine

(1)為 .st 模板頁增加智慧感知

首先做一些準備工作. 因為我們的StringTemplate模板檔案字尾是".st", 裡面寫的大部分都是HTML程式碼. 預設情況下Visual Studio是不會在編輯.st功能的時候提供智慧感知支援的. 但是可以通過如下設定實現:

單擊選單中的"工具"->"選項":

image

在"文字編輯器"的副檔名中, 如圖所示的為.st副檔名增加"HTML編輯器".

接下來在.st檔案中就可以識別HTML程式碼了:

image 

(2) 建立公用的選單模板

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> 

(3) 建立頁面模板和Controller

在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的模板.

(4) 載入StringTemplateViewEngine 模板引擎

雖然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",就可以看到我們的自定義的模板引擎的輸出結果了:

image

在文章最後會提供本例項附帶StringTemplateViewEngine的完整原始碼.

五.其他ViewEngine簡介

除了自己開發, 目前已經有了很多為ASP.NET MVC提供的ViewEngine:

MVCContrib專案中的ViewEngine:

  • SparkViewEngine(不推薦)
  • BrailViewEngine
  • XsltViewEngine

StringTemplateViewEngine

這是StringTemplate專案為ASP.NET MVC開發的ViewEngine, 官方以及下載網址是

另外在著名的MonoRail專案中, 還有一些類似於StringTemplate的頁面顯示引擎, 雖然都沒有為ASP.NET MVC開發專門的ViewEngine, 但是還是很有參考價值的.我們可以用上面介紹的方法, 在頁面顯示引擎的基礎上自己開發ASP.NET MVC的ViewEngine:

MonoRail專案中的三個ViewEngine:

  • AspNetViewEngine:用傳統的.aspx檔案做模板, 可以照常使用aspx語法和伺服器控制元件, 但是由於Webform的生命週期和MonoRail完全不同, 有時候會讓人覺得彆扭, 有部分特性也受到了限制.
  • NVelocityViewEngine: 用NVelocity做模板引擎, 需要學習VTL語法, 但是使用很簡單, 特別是很多java程式設計師已經熟悉velocity. 簡單的語法也強迫程式設計師把邏輯和介面很好的分離開來, 方便跟美工配合.
  • BrailViewEngine:基於Boo的模板引擎, Boo是一種語法類似python的.NET語言, 據MonoRail的參考說, Brail引擎是功能最強, 效能最好的選擇, 但Boo是一種陌生的語言, 這成了Brail引擎應用的最大障礙.

六.總結

本篇文章詳細介紹了ViewEngine相關類, 已經如何開發自己的ViewEngine. 花了2周時間創作完成, 讓大家久等了. 說道最近部落格園首頁的文章問題, 我覺得一篇文章除了要有知識點, 還有能夠很好的講解, 讓大家明白比讓自己明白更重要.我沒有為了速度草草發表文章,就是希望寫出來的東西能夠有資格發表到部落格園首頁.

我希望大家都通過自律來建設部落格園,  明白分享知識是一件光榮而且快樂的事情!

文章示例程式碼下載: