使用Asp.Net MVC 動態產生View Part.1 - 使用VirtualPathProvider

現在的網站,越來越講求個人的客製化。不管是產生給個人用的Blog網站服務或者是電商平台上的商店,都希望讓個人或商店能夠有自己特有的頁面設計,在視覺上做出與其他人的差異化。如此一來,就需要為每個人客製化Asp.Net MVC中的View。但是,不管是個人的Blog網站服務或是電商平台都不可能為每個使用者客製化頁面。況且,就算是可以幫使用者客製化頁面,假設電商平台上擁有一萬家店,每家店有兩個Page是可以客製化,那就會多出39996個View file,這是一件多麼恐怖的事情,先不提如何更新,光是管理就夠讓人頭痛了。

所以,當系統需要這樣客製化的功能時,最基本的View機制可能就不敷使用了。既然,實體檔案的方式無法有效地解決這一個需求,那是不是有其他方法呢?如果,可以將每一個使用者客製化頁面的Html存在資料庫中,當有人要瀏覽客製化頁面時,再從資料庫將Html讀出,再轉成View應該就能夠有效的解決這樣的需求。而自訂VirtualPathProvider就能夠滿足這樣的流程。

先談談VirtualPathProvider的主要功能

VirtualPathProvider MSDN

在MSDN上的文件可以看到,「提供可讓Web應用程式從虛擬檔案系統截取資源的方法集」,看起來有點難懂,說穿了就只是從指定的位置取得內容,再將這個內容存成一個虛擬的檔案。轉換到我們需要的功能,就是從資料庫中取得Html,再將Html存成一個虛擬的cshtml,最後用這一個cshtml產生View丟回前端。下方的圖稍微敘述了從Action到最後產出View的流程

主要的重點在ViewEngine這一塊,而VirtualPathProvider也是屬於ViewEngine的其中一部份。以Default VirtualPathProvider來說,以流程進到這一塊時,會去檢查檔案是否存在。當檔案存在時,再去抓取檔案,最後轉成View吐出去。而當檔案不存在時,就會看到很常見找不到View的Exception畫面。

NotFindViewException

所以,知道原本的流程,也就可以知道要在哪邊作怪改寫了。如果可以讓在Check file exist能夠判斷現在要產生的View是動態View而回傳檔案存在,進到Get file時,知道要取的View是動態View而改向資料庫取資料,在將資料Compile成View,就能夠達成我們的需求。說了這麼多,來看看如何實作吧。

首先,要先自定義一個VirtualPathProvider,取代原本Default的VirtualPathProvider

/// <summary>
/// CustonmVirtualPathProvider
/// </summary>
public class CustomVirtualPathProvider : VirtualPathProvider
{
    /// <summary>
    /// 動態View的範圍
    /// </summary>
    private string _coutomRootPath;

    /// <summary>
    /// View的附檔名
    /// </summary>
    private string _viewExtension;

    public CustomVirtualPathProvider(string customRootPath, string viewExtension)
    {
        this._coutomRootPath = customRootPath;
        this._viewExtension = viewExtension;
    }

    /// <summary>
    /// 確認檔案是否存在
    /// </summary>
    /// <param name="virtualPath">檔案路徑</param>
    /// <returns></returns>
    public override bool FileExists(string virtualPath)
    {
        if (virtualPath.EndsWith(".cshtml"))
        {
            var isDynamicView = IsDynamicView(virtualPath);
            var isFileExist = base.FileExists(virtualPath);

            return isDynamicView || isFileExist;
        }

        return false;
    }

    /// <summary>
    /// 取得要編譯成View的資料
    /// </summary>
    /// <param name="virtualPath"></param>
    /// <returns></returns>
    public override VirtualFile GetFile(string virtualPath)
    {
        VirtualFile file = default(VirtualFile);
        if (IsDynamicView(virtualPath))
        {
            var content = GetViewSource(virtualPath);
            file = new CustomVirtualFile(virtualPath, content);
        }
        else
        {
            file = base.GetFile(virtualPath);
        }

        return file;
    }

    /// <summary>
    /// 判斷是否為動態View的範圍
    /// </summary>
    /// <param name="virtualPath"></param>
    /// <returns></returns>
    private bool IsDynamicView(string virtualPath)
    {
        if (!virtualPath.Contains(this._coutomRootPath))
        {
            return false;
        }

        var isCshtml = virtualPath.ToLower().Contains(this._viewExtension);
        return isCshtml;
    }

    /// <summary>
    /// 取得動態View的資料
    /// </summary>
    /// <param name="virtualPath">檔案路徑</param>
    /// <returns></returns>
    private string GetViewSource(string virtualPath)
    {
        var content = string.Empty;
        var pageName = Path.GetFileNameWithoutExtension(virtualPath);

        StringBuilder sb = new StringBuilder();
        var layoutPath = string.Empty;
        var layout = string.Empty;
        var dateTimeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        if (pageName == "View1")
        {
            content = string.Format("<div>View1</div><div>Now time : {0}</div>", dateTimeStr);

            sb.AppendLine("@model VirtualPathProvider_Doc_Demo.Models.Person");
            sb.AppendLine(content);
            sb.AppendLine("<div><p>@Model.Name</p></div>");
        }
        else if (pageName == "View2")
        {
            content = string.Format("<div>View2</div><div>Now time : {0}</div>", dateTimeStr);

            sb.AppendLine("@model VirtualPathProvider_Doc_Demo.Models.Person");
            sb.AppendLine(content);
            sb.AppendLine("<div><p>@Model.Name</p></div>");
        }

        return sb.ToString();
    }
}

這個自訂的VirtualPathProvider可以分成幾個部分解釋

  1. FileExists : 判斷要編譯成View的檔案是否存在。這邊原本只判斷base.FileExist的結果,因為我們有動態頁面,所以要多增加一個參數,也是就是傳進來的VirtualPath是否為動態頁面。如果是,那就會回傳檔案存在,因為等一下會產生虛擬的動態頁面檔案。
  2. GetFile : 當上一步確認檔案存在後,就會在這一個Method取得檔案的內容。所以,當是動態頁面時,會去取得存在資料庫的資料。這邊為了簡化這一個步驟,所以直接由GetViewSource回傳Content。在實務上,可以呼叫一個DAO來取得資料庫中的資料。

當經過上述兩個步驟之後,接下來就會將取得的資料送到底層的BuildManager進行編譯,產生View檔案。

預設編譯出來的檔案會放在,"%WinDir%\Microsoft.NET\Framework{Version No}\Temporary ASP.NET Files"或是"%User%\AppData\Local\Temp\Temporary ASP.NET Files"

但是,在VirtualPathProvider中,必須把動態頁面的內容轉成VirtualFile,所以還需要自訂一個CustomVirtualFile

public class CustomVirtualFile : VirtualFile
{
    private readonly string _content;
    public bool Exists
    {
        get { return (_content != null); }
    }
    public CustomVirtualFile(string virtualPath, string body)
        : base(virtualPath)
    {
        _content = body;
    }
    public override Stream Open()
    {
        return new MemoryStream(System.Text.Encoding.Default.GetBytes(_content), false);
    }
}
注意,因為是輸出MemoryStream,所以當Content有中文時,要注意編碼問題。

處理好了自訂的VirtualPathProvider,接著就要將它註冊到系統中。

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);

    HostingEnvironment.RegisterVirtualPathProvider(new CustomVirtualPathProvider("/Views/DynamicView/", ".cshtml"));
}

這邊傳進去的參數"/View/DynamicView/"就是動態頁面的範圍,這一個路徑下的View都是動態頁面。而另一個參數,".cshtml"就是需要產生動態頁面的附檔名。由於,Asp.Net MVC預設有兩個ViewEngine,所以會搜尋多種副檔名的檔案,這邊指定只為.cshtml產生動態頁面的內容。

接著,創建一個Controllerc和Action

public class DynamicViewController : Controller
{
    public ActionResult GetView(string id)
    {
        var person = new Person
        {
            Name = "Andy"
        };

        return View(id, person);
    }
}

附帶一提,雖然View的內容是動態產生,但是還是支援Razor的語法唷。

現在可以來看看動態產生View的成果了。這是存在系統的View content,Content裡的時候最後會用編譯時的時間。

@model VirtualPathProvider_Doc_Demo.Models.Person
<div>View1</div>
<div>Now time : 2016-01-15 08:00:00</div>
<div><p>@Model.Name</p></div>

顯示出來的View

中間的"Andy"就是ViewModel傳進去的參數。

放在站存資料夾下compile過的View檔案

結論

現在,有越來越多公司開始提供創建網站或是能夠有自己品牌官方網站的服務,讓使用者客製化頁面以達到每個網站的差異化,這樣的需求一定會越來越普遍。如何,能夠有效的儲存和管理使用者客製化的頁面,會是一個很大的問題。透過自訂VirtualPathProvider,可以讓View的來源可以自行決定,可以從資料庫,也可以從實體檔案。並且,不會移除工程師已經習慣的Razor語法,如此一來更增加了動態頁面的實用性。

參考資料

免責聲明:

"文章一定有好壞,文章內容有對有錯,使用前應詳閱公開說明書"