在開發過程中往往會有一個需求,就是將一個樹狀的數據結構在視圖中表示出來。例如最傳統的多級分類,系統中有一系列根分類,每個分類中又帶有一些子分類,而我們的目標便是在頁面上生成一個由ul和li嵌套組成的Html結構。這個問題看似簡單,但是如何讓實現變的輕松、易於使用也是一個值得討論的問題。這次就來談談這部分的情況。
實現目標
首先來明確一下實現目標。例如我們有一個Category對象,表示一個類別:
public class Category
{
public string Name { get; set; }
public List<Category> Children { get; set; }
}
然後我們准備一個嵌套的數據結構:
public ActionResult CategorIEs()
{
var model = new List<Category>
{
new Category
{
Name = "Category 1",
Children = new List<Category>
{
new Category
{
Name = "Category 1 - 1",
Children = new List<Category>()
},
new Category
{
Name = "Category 1 - 2",
Children = new List<Category>()
},
}
},
new Category
{
Name = "Category 2",
Children = new List<Category>
{
new Category
{
Name = "Category 2 - 1",
Children = new List<Category>()
},
new Category
{
Name = "Category 2 - 2",
Children = new List<Category>()
},
}
},
};
return VIEw(model);
}
自然還會有一個Model類型為List<Category>的視圖:
<%@ Page Language="C#" Inherits="System.Web.Mvc.VIEwPage<List<Category>>" %>
...
而我們的目標,便是要在視圖中顯示出這樣的Html:
<ul>
<li>Category 1
<ul>
<li>Category 1 - 1 </li>
<li>Category 1 - 2 </li>
</ul>
</li>
<li>Category 2
<ul>
<li>Category 2 - 1 </li>
<li>Category 2 - 2 </li>
</ul>
</li>
</ul>
那麼我們又該怎麼做呢?
使用局部視圖
如果在平時讓我們處理這種數據結構,很明顯會使用遞歸。但是,在視圖模板中表示遞歸是非常困難的,因此我們會借助局部視圖。例如:
<%@ Control Language="C#" Inherits="VIEwUserControl<List<Category>>" %>
<% if (Model.Count > 0) { %>
<ul>
<% foreach (var cat in Model) { %>
<li>
<%= Html.Encode(cat.Name) %>
<% Html.RenderPartial("CategoryTree", cat.Children); %>
</li>
<% } %>
</ul>
<% } %>
這個局部視圖的作用便是生成我們想要的HTML片段。在局部視圖內部還會調用自身來生成下一級的Html。在主視圖中生成局部視圖也很容易:
<% Html.RenderPartial("CategoryTree", Model); %>
這就實現了遞歸,也是實現這一功能最易於理解的方式。只可惜這種做法比較麻煩,需要定義額外的局部視圖。這種局部視圖往往只是為一個主視圖服務的,它會和主視圖的前後環境相關,分離開去在維護上就會有些不便了。
在頁面中定義委托
我們知道,在運行時ASP.Net頁面會被編譯為一個類,而其中的各種標記,或內嵌的代碼都會被作為一個方法裡定義或執行的局部變量。如果說我們要在一個方法內“定義”另一個方法,自然只能是使用匿名方法的特性來構造一個委托了。這個委托為了可以“遞歸”調用,就必須這麼寫:
<% Action<L
ist<Category>> renderCategorIEs = null; // 先設為null %>
<% renderCategories = (categorIEs) => { // 再定義 %>
<% if (categorIEs.Count > 0) { %>
<ul>
<% foreach (var cat in categorIEs) { %>
<li>
<%= Html.Encode(cat.Name) %>
<% renderCategorIEs(cat.Children); %>
</li>
<% } %>
</ul>
<% } %>
<% }; %>
<% renderCategorIEs(Model); // 最後再調用,即生成Html %>
這段代碼的確可以生成HTML,但是我不喜歡。我不喜歡的原因倒不是因為這是我眼中的“偽遞歸”,而是因為這在頁面將“定義”與“執行”分開了。事實上,在我們看到Html標記及邏輯控制的地方並沒有在“同時”生成內容,這只是在“定義”。生成內容的時機其實是在最後對 renderCategorIEs委托的調用,這容易造成一定誤導,因為最後的“生成”可能會遺漏,而定義和生成之間可能會插入一些其他內容。
這種做法的優勢,就是在於不用額外分離出一個局部視圖,它直接寫在主視圖中,易於維護,也相對易於理解。
使用Lambda表達式構建遞歸方法
“定義”與“執行”分離的一個重要原因,還是因為Lambda表達式無法定義遞歸函數。否則,我們就可以直接定義一個遞歸執行的委托,並在最後跟上Invoke或直接調用即可。
因此,其實這裡就正是使用Lambda表達式編寫遞歸函數的用武之地。例如,我們補充一個類似的Fix方法:
public static class HtmlExtensions
{
public static Action<T> Fix<T>(this HtmlHelper helper, Func<Action<T>, Action<T>> f)
{
return x => f(Fix(helper, f))(x);
}
}
於是在視圖中便可以:
<% Html.Fix<List<Category>>(render => categorIEs => { %>
<% if (categorIEs.Count > 0) { %>
<ul>
<% foreach (var cat in categorIEs) { %>
<li>
<%= Html.Encode(cat.Name) %>
<% render(cat.Children); %>
</li>
<% } %>
</ul>
<% } %>
<% }).Invoke(Model); %>
不過嚴格說來,它還是“定義”與“執行”分離的,只是我們現在可以把它們寫在一塊兒。此外,Fix方法對於模板中的Html生成實在沒有什麼意義。
提供一個Render方法輔助遞歸
Fix方法對頁面生成沒有什麼作用,不過如果有一個可以輔助遞歸的Render方法便有意義多了:
public static class HtmlExtensions
{
private static Action<T> Fix<T>(Func<Action<T>, Action<T>> f)
{
return x => f(Fix(f))(x);
}
public static void Render<T>(this HtmlHelper helper, T model, Func<Action<T>, Action<T>> f)
{
Fix(f)(model);
}
}
於是,我們在頁面中就可以這麼寫:
<% Html.Render(Model, render => categorIEs => { %>
<% if (categorIEs.Count > 0) { %>
<ul>
<% foreach (var cat in categorIEs) { %>
<li>
<%= Html.Encode(cat.Name) %>
<% render(cat.Children); %>
</li>
<% } %>
</ul>
<% } %>
<% }); %>
您是否覺得這麼做難以理解?我不這麼認為,因為從語法上來說,這種Html生成方式是很簡單的
<% Html.Render(參數, 用於遞歸的方法 => 當前參數 => { %>
...
<% 遞歸調用 %>
...
<% }); %>
至於背後的原理?關心那些做什
麼。
性能
可惜,根據性能比較,使用Fix構造遞歸的做法,比使用SelfApplicable的做法要慢上許多。雖然我認為這裡不會是性能的關鍵,但如果您實在覺得無法接受的話,也可以利用SelfApplicable來構造遞歸的Html呈現方式。其輔助方法為:
public delegate void SelfApplicable<T>(SelfApplicable<T> self, T arg);
public static class HtmlExtensions
{
public static void Render<T>(this HtmlHelper helper, T model, SelfApplicable<T> f)
{
f(f, model);
}
}
於是在視圖中:
<% Html.Render(Model, (render, categorIEs) => { %>
<% if (categorIEs.Count > 0) { %>
<ul>
<% foreach (var cat in categorIEs) { %>
<li>
<%= Html.Encode(cat.Name) %>
<% render(render, cat.Children); %>
</li>
<% } %>
</ul>
<% } %>
<% }); %>
同樣,我們只要記住這麼做的“語法”就可以了。
總結
相比之下,我喜歡最後兩種做法。因為他們直接構造了“HTML生成”的功能,且“內置”了遞歸。如果使用一個額外的局部視圖,雖然“樸素”但使用較為麻煩。使用“偽遞歸”的方式,從概念上看這不太像是在生成Html,程序構造的痕跡(先聲明,再定義,最後調用)過於明顯了。
您喜歡哪種做法呢?如果您遇到了我這樣的需求,您會怎麼做呢?
最後我想進行一個小調查:您滿意WebForm的頁面作為視圖模板引擎嗎?您平時最喜歡使用什麼視圖模板引擎,為什麼呢?