1. 程式人生 > >適合ASP.NET MVC的檢視片斷快取方式(下):頁面輸出原則

適合ASP.NET MVC的檢視片斷快取方式(下):頁面輸出原則

上一篇文章裡已經把Html.Cache打造成了非常具有可用性的API,需要快取時我們只需在頁面上做一個標記即可:

<% Html.Cache("cache_key", DateTime.Now.AddSeconds(10), () => { %>
 
    <% foreach (var article in Model.Articles) { %> 
        <p><%= article.Body %></p>
    <% } %>
    
<% }); %>

標記內部的寫法和普通檢視的寫法相同,您可以for/foreach/if,也可以<%= %>,或者使用RenderPartial等其他輔助方法輸出內容,都會被一併快取下來。只可惜,上次文章末尾我提到有些效果是有前提的。

這個前提就是:某些RenderPartial和其他一些輔助方法的實現需要進行修改。好吧,再說的直接一些:如果您使用標準的ASP.NET MVC,就無法使用RenderPartial的功能。我認為造成這種問題的原因是ASP.NET MVC框架在實現時沒有遵守頁面內容輸出的準則。所以我建議您使用MvcPatch專案進行ASP.NET MVC開發。

不過現在,我們還是來討論一下準則吧。下面有些內容涉及到ASP.NET WebForm頁面的輸出方式,如果您遇到了不理解的地方,可以去看一下這篇文章,它是我為“頁面片段快取”原理介紹而寫的“鋪墊”。

在普通情況下,一個ASP.NET頁面輸出時是向一個封裝了Response.Output的HtmlTextWriter中寫入內容的:

而我們的片段快取實現為了“捕獲”某個快取塊輸出的內容,則在HtmlTextWriter與Response.Output之間又插入了一個RecordWriter:

那麼,在快取命中的時候,我們的Cache方法把快取中的內容寫到什麼地方去了呢?

public static void Cache(
    this HtmlHelper htmlHelper,
    ...)
{
    var content = ...

    if (content == null)
    {
        ...
    }
    else
    {
        htmlHelper.Output
.Write(content); } }

Output是什麼?如果您觀察ASP.NET MVC的原始碼,您會發現HtmlHelper並沒有這個屬性。這是我在MvcPatch中暴露出來的一個TextWriter,它便是當前正用於頁面輸出的HtmlTextWriter物件。因此,我這裡提出一個原則:如果您是在向頁面輸出內容,請務必將所有內容通過頁面的Writer輸出

在原來的ASP.NET MVC實現中,由於無法從HtmlHelper中獲得頁面的Writer,因此如果需要輸出內容,則只能通過Response.Write方法,或由Response.Output輸出內容了。根據上圖可知,如果我們直接從Response.Output輸出,那麼這部分內容是無法被RecordWriter捕獲的。這意味著什麼呢?這意味著,如果我們上面不是通過HtmlHelper.Output,而是直接向Response.Output輸出,在Html.Cache巢狀的情況下,內層快取塊的輸出無法被外層快取塊捕獲到。因此,如果內層快取命中,而外層重新生成內容,則會發現內層快取塊的內容被沒有被外層記錄下來。

我們可以想的再遠一些。我們這種TextWriter的巢狀其實是一種什麼模式呢?應該算是裝飾器模式吧。裝飾器模式要求我們所有的輸出都從鏈條的頂部輸入,這樣所有的“裝飾”作用才會生效。如果我們獲取了其中的某一個環節,直接從這個環節輸入引數,那麼自然是失敗的。這意味著……假如又有另外一個元件在“行使”它的擴充套件權力呢?如果又有另一個元件,它在我們的RecordWriter外層又進行了包裝呢?我們的片斷快取解決方案是一種擴充套件,作為擴充套件方案,不應該破壞其他元件正常擴充套件的能力。因此,我們需要從頁面的Writer中輸出內容。

一個很好的反例就是ASP.NET MVC框架,您看RenderPartial方法的輸出目標是什麼:Response.Output。還有FormExtensions及MvcForm物件的輸出目標是什麼:還是Response.Output。這意味著,ASP.NET MVC框架的做法直接破壞了檢視的擴充套件能力。也直接放倒了我們的片斷快取實現。因此,我最終構建了MvcPatch專案,因為在這一點上(以及其他一些方面,之前也有所提及)使用擴充套件的方式實在是無法進行修補的。

所以國外社群有種調侃稱,微軟產品是好的,但是他們自己不知道該如何用好自己的產品。例如我一直說的WebForms的濫用,還有這裡ASP.NET MVC實現。前者更像是一種商業策略,而後者可能……就令人摸不著頭腦了。

我沒有說“微軟的確不知道如何用好自己的產品”。因為從ASP.NET MVC的程式碼中可以發現,好像他們並非不知道我剛提出的頁面輸出原則。證據在於,他們已經在ViewPage中留有一個“入口”了:

public class ViewPage : Page, IViewDataContainer
    ...

    public HtmlTextWriter Writer
    {
        get;
        private set;
    }

    protected override void Render(HtmlTextWriter writer)
    {
        Writer = writer;
        try
        {
            base.Render(writer);
        }
        finally
        {
            Writer = null;
        }
    }
}

看看這段程式碼在做什麼?這段程式碼重寫了Render方法,將外部傳入的HtmlTextWriter物件保留了起來!這意味著ViewPage.Writer屬性獲得的便是當前正在輸出的HtmlTextWriter物件!也就是說,ASP.NET MVC似乎在建議您說,如果您非要在頁面上使用Response.Output輸出的話,現在就改成Writer的輸出吧:

<% Response.Write("Hello World"); %>
<% Writer.Write("Hello World"); %>

不知道是可惜還是可笑,如果您在程式碼中對Writer屬性使用Find All References,您會發現除了在ViewMasterPage或ViewUserControl中繼續暴露Writer屬性之外,就再也沒有使用過了……那麼RenderPartial在做什麼?FormExtensions在做什麼?誰知道……我同樣不知道的是,如果微軟自己沒有這個“意識”,那麼又為什麼要主動保留Render時的Writer呢?

不管這些了。我們最後總結一下:

  1. 如果您在使用WebForm模型,請像ViewPage那樣保留當前Writer,並且向Writer內輸出,不要搞Response.Write/Output。
  2. 如果您在編寫檢視的輔助方法,請向HtmlHelper.Output輸出,而不是Reponse.Write/Output。
  3. 如果您發現其他專案在使用Response.Write/Output,請將它修改成頁面的Writer輸出。
  4. ……

嗯?您說HtmlHelper沒有Output屬性?沒關係,下載程式碼以後自己修改編譯一下,或直接使用MvcPatch吧。

相關文章