.NET/ASP.NETMVC 深入剖析 Model後設資料、HtmlHelper、自定義模板、模板的裝飾者模式(三)

王清培發表於2014-01-13

閱讀目錄:

7.HtmlHelper、HtmlHelper<T>中的ViewModel的型別推斷

8.控制ViewModel中的某個屬性的呈現(使用PartialView部分檢視細粒度控制ViewModel的呈現)

9.模板的裝飾者模式(PartialView與ViewModel的巢狀使用(簡))

7.HtmlHelper、HtmlHelper<T>中的ViewModel的型別推斷

在View中用來根據當前View中引入的強型別ViewModel生成HTMLDom結構的核心功能都被封裝在以HtmlHelper為首的物件模型中,包括HtmlHelper<T>泛型型別,它直接派生自HtmlHelper基類,這兩個型別的功能都是圍繞著如何生成前端所需要的HTML結構和一些常用的UI元素;

但是這兩個型別所能做的事情很有限,它們只是龐大生成功能的核心模型;我們使用的都是圍繞著這兩個型別的擴充套件方法,如:

@Html.EditorForModel()

在當前View中引用的Html屬性其實是一個HtmlHelper<T>型別的屬性,定義程式碼:

public HtmlHelper<TModel> Html { get; set; }

該型別被定義在public abstract class WebViewPage<TModel> : WebViewPage類中,其實該類是一個模板化程式碼生成的基類;我們在ASP.NETMVC專案中新增的所有View檔案都會直接或間接的繼承自該型別,在View中引入的型別定義:

@model  MvcApplication4.Models.Customer

正是這裡泛型型別的型別引數,所以圍繞著HtmlHelper<T>的擴充套件方法才變成靈活的泛型的程式碼生成介面;因為他們彼此通過強大的泛型型別推斷,依次的推斷下去,最終會到達擴充套件方法的內部,如:

@Html.EditorFor(model => model.Shopping)

這意思是說在View中輸出一個編輯model.Shopping屬性的文字框HtmlDom結構,但是我們呼叫的明明是一個沒有任何型別形參的方法,其實它已經通過上面說將的環節進行了型別關聯;

畫紅線的部分是View所使用的強型別HtmlHelper<T>物件,型別引數是我們在View中通過@model的方式定義的;畫綠色的部分也是強型別的EditorFor<T>方法,同樣該泛型方法已經被型別推斷過了,看泛型方法的定義:

public static class EditorExtensions
{
  public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression);
}

 

上述程式碼中加粗的部分正是關鍵所在,這裡擴充套件的其實是我們在View中引入的強型別引數的HtmlHelper<MvcApplication4.Models.Customer>,這樣任何圍繞HtmlHelper<T>進行擴充套件的擴充套件方法都會最終使用到型別ViewModel;

8.控制ViewModel中的某個屬性的呈現(使用PartialView部分檢視細粒度控制ViewModel呈現)

對於ViewModel的呈現一直都是被系統控制著,雖然一個簡單的字串型別欄位可以用一個文字框的HtmlDom結構方式呈現出來,但是那僅僅是代表著沒有任何業務概念的功能性設定,也就是出發點是從CLR型別系統考慮的,而不是特定領域角度;如果這個字串代表著某種業務概念,那麼我們希望通過更人性化的方式讓使用者使用,而不是一個硬生生的文字框;我們可能會需要提供了一個供自動輸入提示的HtmlDom結構,該結構可能還需要其他的UI成員協助,如:自動提示可能需要JS、後臺Service介面等一系列成員相互協調完成;

這是一個簡單的需求,在大型專案中這樣的功能很常見,也是到處會使用到,不單單是一個兩個頁面,N多頁面都會有一點點的差異性,但是整體功能都會差不多,這樣我們只需要在設計的時候適當的提供一些介面就可以了;

 

那麼ASP.NETMVC是如何生成前臺所需要的HtmlDom結構的呢? 前面一章我們總結了,對於ViewModel的呈現形式只會有兩種,一種是Edit一種是Display,不會有其他的呈現形式,所以在圍繞著HtmlHelper物件的擴充套件方法中大多數都是以這種類別區分的,Edit一組,Display一組;

到目前位置我們已經知道ViewModel與View之間的橋樑是Model後設資料,可以簡單的理解為HtmlHelper<T> 一系列擴充套件方法都是通過獲取Model後設資料資訊來控制到底需要輸出什麼形式的HtmlDom結構,而Model後設資料都是通過Model後設資料控制特性來完成的,這就可以通過控制Model後設資料來控制Model的呈現細節;

public class Address
    {
        [UIHint("CustomAddress")]
        [Display(Name = "地址")]
        public string AddressId { get; set; }

}

我們在Address型別中為AddressId屬性加上一個UIHint型別的特性,其實意思是想說明我們在程式內部使用的是使用地址ID,而在現實的時候我們希望將原來很單調的地址ID程式設計一個更人性化的地址顯示方式,比如:位於什麼省、什麼市等等一些其他的地理資訊;

在ASP.NETMVC內部有一個internal static class TemplateHelpers 型別的模板輔助類,該類是大部分模板化輸出的幫助介面,在該類的內部定義了一套模板化使用的字典:

檢視的型別:

static readonly Dictionary<DataBoundControlMode, string> modeViewPaths =
           new Dictionary<DataBoundControlMode, string> {
               { DataBoundControlMode.ReadOnly, "DisplayTemplates" },
               { DataBoundControlMode.Edit,     "EditorTemplates" }
           };

這裡定義了兩組型別,也就是顯示、編輯,這兩組型別將作為查詢自定義模板的物理資料夾路徑,同樣ModelMedata中的同一個屬性在不同的顯示型別中將有不同的判斷作用;

編輯、顯示:

static readonly Dictionary<string, Func<HtmlHelper, string>> defaultDisplayActions =
    new Dictionary<string, Func<HtmlHelper, string>>(StringComparer.OrdinalIgnoreCase) {
        { "EmailAddress",       DefaultDisplayTemplates.EmailAddressTemplate },
        { "HiddenInput",        DefaultDisplayTemplates.HiddenInputTemplate },
        { "Html",               DefaultDisplayTemplates.HtmlTemplate },
        { "Text",               DefaultDisplayTemplates.StringTemplate },
        { "Url",                DefaultDisplayTemplates.UrlTemplate },
        { "Collection",         DefaultDisplayTemplates.CollectionTemplate },
        { typeof(bool).Name,    DefaultDisplayTemplates.BooleanTemplate },
        { typeof(decimal).Name, DefaultDisplayTemplates.DecimalTemplate },
        { typeof(string).Name,  DefaultDisplayTemplates.StringTemplate },
        { typeof(object).Name,  DefaultDisplayTemplates.ObjectTemplate },
    }; 

static readonly Dictionary<string, Func<HtmlHelper, string>> defaultEditorActions =
    new Dictionary<string, Func<HtmlHelper, string>>(StringComparer.OrdinalIgnoreCase) {
        { "HiddenInput",        DefaultEditorTemplates.HiddenInputTemplate },
        { "MultilineText",      DefaultEditorTemplates.MultilineTextTemplate },
        { "Password",           DefaultEditorTemplates.PasswordTemplate },
        { "Text",               DefaultEditorTemplates.StringTemplate },
        { "Collection",         DefaultEditorTemplates.CollectionTemplate },
        { typeof(bool).Name,    DefaultEditorTemplates.BooleanTemplate },
        { typeof(decimal).Name, DefaultEditorTemplates.DecimalTemplate },
        { typeof(string).Name,  DefaultEditorTemplates.StringTemplate },
        { typeof(object).Name,  DefaultEditorTemplates.ObjectTemplate },
    };

這是兩組顯示模式的模板化操作方法的字典,可以看出同一個HiddenInput特性將在不同的顯示模式先輸出不同的HtmlDom結構;

在我們的ASP.NETMVC專案中要同樣的有兩組資料夾DisplayTemplates、EditorTemplates,這兩個資料夾將會是系統查詢的路徑;

我們在DisplayTemplates目錄下建立了一個用來顯示客戶地址資訊的自定義模板,其實也就是PartialView部分檢視,用來重用UI;在該部分檢視中,我們寫點測試資料:

@model string 

<div>
    <h2>@Model</h2>
    <h2>地址:上海市、長寧區</h2>
    <h2>氣溫:-1~10</h2>
    <h2>交通:方便出行</h2>
</div>

然後我們重新整理一下介面,看如何個性化了地址顯示;

這樣我們就可以控制細粒度的ViewModel顯示;

9.模板的裝飾者模式(PartialView與ViewModel的巢狀使用(簡))

其實我們應該能夠領悟到通過PartialView與HtmlHelper彼此互相巢狀能讓原本單一的部分檢視變成一個強大的具有設計模式功能的模板裝飾者模式;想想看,如果我們將這裡的AddressId型別再設計成複雜的型別,然後在該複雜的型別內部我們巢狀了一個原本在其他地方使用的地址型別ViewModel,而且剛好該型別也具有相應的部分是檢視,這樣我們就可以將ViewModel的巢狀使用與PartialView巢狀使用相結合,這樣就可以使用類似設計模式中的裝飾者模式來完成很多UI上的展現重用功能;

相關文章