適合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呢?
不管這些了。我們最後總結一下:
- 如果您在使用WebForm模型,請像ViewPage那樣保留當前Writer,並且向Writer內輸出,不要搞Response.Write/Output。
- 如果您在編寫檢視的輔助方法,請向HtmlHelper.Output輸出,而不是Reponse.Write/Output。
- 如果您發現其他專案在使用Response.Write/Output,請將它修改成頁面的Writer輸出。
- ……
嗯?您說HtmlHelper沒有Output屬性?沒關係,下載程式碼以後自己修改編譯一下,或直接使用MvcPatch吧。