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

王清培發表於2013-12-16

閱讀目錄:

  • 4.ModelMetadata(ModelMetadata後設資料如何支撐Model與View之間的組合關係)
    • 4.1.ModelMetadata後設資料結構(後設資料與資料實體的結構關係)
    • 4.2.View與Model的基本關係及使用方式(View的呈現基礎)
  • 5.通過對ViewModel使用預定義Attribute設定ModelMetadata(擴充套件後設資料設定IMetadataAware)
    • 5.1.ViewModel的領域型別(型別的兩個層面的含義,CLR型別、領域語言)
    • 5.2.DataAnnotations中後設資料控制特性與ASP.NETMVC中後設資料控制特性
    • 5.3.IMetadataAware與擴充套件後設資料定製介面(適當繼承預定義後設資料控制物件)
  • 6.資料註釋後設資料控制機制(面向UI框架的基礎System.ComponentModel.DataAnnotations名稱空間)
    • 6.1.System.ComponentModel 元件物件模型的生命週期(系統元件的基本特徵)
    • 6.2.設計時元件後設資料(設計時在VS中暴露出來的設定後設資料)
    • 6.3.System.ComponentModel.DataAnnotations UI層框架的通用資料註解元件
    • 6.4.使用System.ComponentModel.DataAnnotations中的獲取後設資料設定特性功能

4.ModelMetadata(ModelMetadata後設資料如何支撐Model與View之間的組合關係)

ModelMetadata是ASP.NETMVC中用來表示Model的後設資料物件,它包含了一個Model的所有的相關後設資料資訊,當然這取決Model的使用方向,不同的使用方向會有不同型別的後設資料,我們這裡的ModelMetadata是針對View顯示相關的後設資料;ModelMetadata中絕大部分後設資料是用來作為最終在View生成環節當中需要使用到的,比如:如何確定一個領域相關的屬性(Address)該如何展現,這裡的Address可能不是一個簡單的String型別表示,而是由一組複雜的型別表示,這樣的情況下我們就需要通過自定義後設資料來控制最終使用的呈現模板(PartialView);

在MVC的定義中,Model準確意思是ViewModel(顯示Model,只是用來作為介面呈現使用的資料實體),它是直接提供給View作為呈現使用的資料實體,通常情況下還將作為DTO型別的資料實體,負責資料的往返傳輸;ASP.NETMVC提供一種自定義Model呈現方式的介面,它允許我們通過自定義某個ViewModel中的屬性顯示檢視(PartialView部分檢視),從而可以對ViewModel進行非常細粒度的呈現控制,但是這一擴充套件機制的背後正是ModelMetadata的功勞;

ModelMetadata起到中間橋樑的作用,在橋樑的一端是ViewModel,另一端是View,然而我們可以在ViewModel上通過定義Attribute的方式進行後設資料的自定義,可以通過改變某個ViewModel的ModelMetadata來操縱最終的呈現;

4.1.ModelMetadata後設資料結構(後設資料與資料實體的結構關係)

圖1:Customer ViewModel

圖2:Customer ModelMetadata

後設資料的層次結構與所要表示的ViewModel的結構是一致的,比如上圖中的Customer實體中有一個Shopping屬性,該屬性表示實體中的配送資訊,然後Shopping中還包含一個Address屬性表示配送地址,對應的ModelMetadata也是這種包含的層次結構,在每個ModelMetadata內部都有一個型別為IEnumerable<ModelMetadata>的Properties屬性來引用它的下級ModelMetadata,這就形成了一個無限巢狀的後設資料表示結構,在ModelMetadata通過下面兩行程式碼來儲存屬性的這種巢狀依賴關係;

1 public class ModelMetadata { 
2 
3 public virtual IEnumerable<ModelMetadata> Properties {} /*型別的子物件後設資料*/ 
4 
5 public string PropertyName {} /*所表示的屬性名稱*/ 
6 
7 } 
View Code

4.2.View與Model的基本關係及使用方式(View的呈現基礎)

當我們有了一個ViewModel之後就可以在任何一個View中顯示它,View的呈現是強型別的,也就是說必須具有一個實體的型別作為資料呈現容器的基礎在View中引入,因為一系列的HtmlHelper擴充套件方法都是基於這個強型別,我們通過一個簡單的示例,來大概的瞭解一下ASP.NETMVC使用方式;

Customer ViewModel 程式碼:

 1 namespace MvcApplication4.Models
 2 {
 3     public class Customer
 4     {
 5         public string CustomerId { get; set; }
 6         public Shopping Shopping { get; set; }
 7     }
 8     public class Shopping
 9     {
10         public string ShoppingId { get; set; }
11         public Address Address { get; set; }
12     }
13     public class Address
14     {
15         public string AddressId { get; set; }
16         public string CountryCode { get; set; }
17         public string City { get; set; }
18         public string Street { get; set; }
19     }
20 } 
View Code

這是一個簡單的以Customer為主的ViewModel,在Customer中定義了一個Shopping型別的屬性,然後在Shopping型別中又定義了一個String型別的Address屬性,這是一個很常用的巢狀物件結構;

HomePage Controller 程式碼:

 1 namespace MvcApplication4.Controllers
 2 {
 3     using Models; 
 4 
 5     public class HomePageController : Controller
 6     {
 7         public ActionResult Index()
 8         {
 9             Customer customer = new Customer()
10             {
11                 CustomerId = "Customer123456",
12                 Shopping = new Shopping()
13                 {
14                     ShoppingId = "Shopping123456",
15                     Address = new Address()
16                     {
17                         AddressId = "Address123456",
18                         CountryCode = "CN",
19                         City = "Shanghai",
20                         Street = "Jiangsu Road"
21                     }
22                 }
23             };
24             return View(customer);
25         } 
26 
27         public ActionResult Edit(Customer customer)
28         {
29             if (customer != null)
30                 return new ContentResult() { Content = "Is Ok" };
31             return new ContentResult() { Content = "Is Error" };
32         }
33     }
34 } 
View Code

控制器什麼事情也沒做,直接例項化了一個巢狀層次結構的Customer物件並初始化了一些測試資料,該Action使用ViewResult型別作為返回結果;

Index View 程式碼:

 1 @model  MvcApplication4.Models.Customer 
 2 
 3 <table>
 4     <tr>
 5         <td>
 6             <h2>Model Details Display.</h2>
 7             @Html.DisplayForModel()
 8             @Html.DisplayFor(model => model.Shopping)
 9             @Html.DisplayFor(model => model.Shopping.Address) 
10 
11         </td>
12         <td></td>
13         <td>
14             <h2>Model details Editor.</h2>
15             @using (Html.BeginForm("Edit", "HomePage", FormMethod.Post))
16             {
17                 @Html.EditorForModel()
18                 @Html.EditorFor(model => model.Shopping)
19                 @Html.EditorFor(model => model.Shopping.Address)
20                 <input type="submit" value="Submit" /> 
21             }</td>
22     </tr>
23 </table> 
View Code

檢視分別對Customer型別的巢狀屬性進行了編輯、顯示定義,這裡需要說明的是EditorForModel()、DisplayForModel()不會做到對巢狀型別的編輯、顯示,因為這不符合日常使用,我們需要明確的編碼需要編輯、顯示的屬性,通過EditorFor()、DisplayFor()方法進行選擇;

這是一個最基本的MVC使用方式,Customer是需要View進行顯示的ViewModel,在View中通過HtmlHelper擴充套件方法對Customer實體生成編輯、顯示時的所有HTML,這確實方便了很多,我們不需要去管到底如何生成這些HTML了;

圖3:

背後為我們自動生成了編輯、顯示所需要的HTML;

圖4(以下兩幅):

自動化生成是好事,但是有些時候我們並不希望它幫我們生成一些不需要的HTML或者說我們希望能對生成的過程進行一些控制,比如:這裡的Customer物件,在物件內部的一些屬性(如:CustomerId)我們根本不希望暴露出來被編輯或被顯示,我們希望能通過簡單的方式控制這種現實方式;當然MVC為我們提供了一整套自動化機制,同樣也為我們提供了控制這些自動化機制的介面;

ViewModel在介面上呈現的方式只有兩種,要麼顯示(Display)要麼編輯(Editor),上圖中已經給出MVC預設生成的HTML格式;這是作為預設的方式輸出,我們並沒有參與到輸出過程的任何環節中,要想控制ViewModel的某個屬性的展現方式我們必須對ModelMetadata進行控制,因為最終生成的這些HTML是根據Model後設資料來定的,準確點講HtmlHelper物件和一系列圍繞HtmlHelper的擴充套件方法都是基於某個ViewModel的ModelMetadata進行最終的生成,所有跟生成相關的選項都是在ModelMetadata中設定的,如果我們沒有對ViewModel的ModelMetadata進行設定那麼它將有一些預設的資料選項作為最終生成的基礎;

ASP.NETMVC提供一個叫做 “資料註釋 DataAnnotations” 的方式對某個ViewModel的Model的後設資料進行設定,通過在ViewModel中運用一些預定義好的特性來設定本屬性所要展現的方式;比如:上面的Customer實體我們想控制他的CustomerId只能顯示在介面上,不能對其進行編輯,也就是說我們只能看不能改;

Customer 程式碼:

 1 namespace MvcApplication4.Models
 2 {
 3     public class Customer
 4     {
 5         [HiddenInput] /*設定CustomerId不出現Input輸入框*/
 6         public string CustomerId { get; set; }
 7         public Shopping Shopping { get; set; }
 8     }
 9     public class Shopping
10     {
11         public string ShoppingId { get; set; }
12         public Address Address { get; set; }
13     }
14     public class Address
15     {
16         public string AddressId { get; set; }
17         public string CountryCode { get; set; }
18         public string City { get; set; }
19         public string Street { get; set; }
20     }
21 }
View Code

圖5:

我們通過使用 HiddenInput特性把CustomerId的輸入框Input隱藏起來了,通過上圖中的CustomerId部分的HTML程式碼,我們能清晰的看見CustomerId的Input的Type被設定成了Hidden,也符合HiddenInput的定義,只將其隱藏起來而不是不輸出HTMLDom;HiddenInput特性中有一個唯一的屬性引數DisplayValue,該屬性引數意思是說隱藏Input元素但是是否要顯示該屬性的值,它是一個Bool型別引數(true:顯示該屬性值,false:不顯示,並且在Display模式下也不顯示);

這裡我就有一個疑問了,在 Display模式下也不顯示,但是一般很多場景下都是需要顯示的,而且這樣的一個特性會導致兩種模式下的顯示衝突;這裡的CustomerId假設我需要在Display下顯示出來,但是在編輯模式下我就是要不顯示出CustomerId屬性值;其實這個時候就需要我們自己擴充套件這些設定顯示方式的特性了,前提是我們得很清楚它是如何控制HTMLDOM輸出的,到底是如何與HtmlHelper物件協調的,又如何參與到後設資料設定當中的;

5.通過對ViewModel使用預定義Attribute設定ModelMetadata(擴充套件後設資料設定IMetadataAware)

在ASP.NETMVC中有一組預先定義好的Attribute,這些Attribute是專門用來控制某個ViewModel中的屬性後設資料選項;在大多數情況下,我們可以使用這些預先定義好的Attribute來解決一般的業務場景,但是實踐經驗告訴我們一般的業務場景不多見,通常都是需要我們對後設資料進行自定義控制,這樣我們才能做到對當前業務邏輯最大粒度的抽象,從而達到在某個層面上能做到面向特定領域的範圍;

Customer 程式碼:

 1 namespace MvcApplication4.Models
 2 {
 3     public class Customer
 4     {
 5         [Display(Name = "客戶ID")]
 6         public string CustomerId { get; set; }
 7         public Shopping Shopping { get; set; }
 8     }
 9     public class Shopping
10     {
11         [Display(Name = "配送ID")]
12         public string ShoppingId { get; set; }
13         public Address Address { get; set; }
14     }
15     public class Address
16     {
17         [Display(Name = "地址")]
18         public string AddressId { get; set; }
19         [Display(Name = "國家編碼")]
20         public string CountryCode { get; set; }
21         [Display(Name = "城市編碼")]
22         public string City { get; set; }
23         [Display(Name = "街道")]
24         public string Street { get; set; }
25     }
26 }
View Code

這裡通過Diaplay預定義特性來控制後設資料顯示選項,在Display特性中有很多可選屬性用來進一步設定顯示選項,這裡我們只使用了Name屬性來設定該屬性在介面上顯示的文字資訊,用來替換原本顯示程式碼屬性名稱的預設選項;

圖6:

可以做到將介面上原本顯示欄位名稱的地方換成使用領域語言顯示,也就是我們通過Diaplay特性設定的顯示文字;

5.1.ViewModel的領域型別(型別的兩個層面的含義,CLR型別、領域語言)

ViewModel中的屬性有兩種型別的含義,比如:在Address資料實體中CountryCode預設是字串型別,但是它的領域型別是一個表示國家程式碼的編號;雖然很多時候我們可以使用字串、數字等這些CLR型別來表達任何一種領域概念,這僅僅是程式碼層面的表示而已,而一旦我們將該實體作為領域物件在介面呈現時就需要還原出領域相關的特性;很常見的情況就是我們經常將字串型別的Email用特定的格式在介面上表示,這就是說明該欄位是一個領域相關的特性;程式碼是給我們程式設計師看的,而領域語言是給相關的領域參與者看的,所以在ViewModel中設定的這些預定義後設資料控制特性大體可以歸來為這兩類;

5.2.System.ComponentModel.DataAnnotations中後設資料控制特性與ASP.NETMVC中後設資料控制特性

在ASP.NETMVC中大部分使用的預定義特性都是位於System.ComponentModel.DataAnnotations名稱空間中,唯獨HiddenInput特性是孤身一人在System.Web.Mvc名稱空間中,這可能對你造成了一些理解上的困擾;明明是ASP.NETMVC框架使用的物件為什麼會跑到System.ComponentModel.DataAnnotations名稱空間中去,又為什麼偏偏HiddenInput就在System.Web.Mvc名稱空間中,按道理說也應該是在System.Web.Mvc開頭的名稱空間中才對;其實這要想說清楚就牽扯到一些.NET元件程式設計相關的理論知識,所以會在下一個章節詳細的分析它為什麼會在System.ComponentModel.DataAnnotations名稱空間中,這些設計到底是為了什麼;

5.3.IMetadataAware與擴充套件後設資料定製介面(適當繼承預定義後設資料控制物件)

在ASP.NETMVC中大部分預先定義好的後設資料控制特性都是密封型別的,只有很少一部分是公開型別的,所以如果我們需要擴充套件的物件能從這部分物件上繼承那將會很方便,可以省掉很多工作;有些特性不是一個簡單的資料宣告標識,其中會有一些預定義行為會被走到,所以如果我們重寫這部分的行為就可以做到簡單的擴充套件這部分物件來輕鬆的達到擴充套件目的;

但是很大程度上我們需要自己能從根本上定製一個後設資料控制特性物件,我們不希望通過繼承原有的預定義的後設資料控制特性物件來進行簡單的擴充套件,我們需要最大粒度的設計,我想這個要求一點都不過分,誰願意在礙手礙腳的地方Happy呢;

ASP.NETMVC提供IMetadataAware介面讓我們可以為所欲為的控制後設資料,控制後設資料就可以控制最終根據後設資料生成的邏輯;

CustomDisplayName 程式碼:

 1 [AttributeUsage(AttributeTargets.Property)]
 2 public class CustomDisplayName : Attribute, IMetadataAware
 3 {
 4     public string Name { get; set; } //預設顯示名稱
 5     public void OnMetadataCreated(ModelMetadata metadata)
 6     {
 7         metadata.DisplayName = string.Format("{0}/{1}",
 8             string.IsNullOrEmpty(this.Name) ? metadata.DisplayName : this.Name, metadata.PropertyName);
 9     }
10 } 
View Code

這是一個很簡單的自定義後設資料物件,當我們將CustomDisplayName 特性物件設定在指定的ViewModel中的任何一個屬性上時,將可以在執行時獲取到系統自動生成的後設資料物件模型ModelMetadata,這個時候我們就可以對當前的後設資料進行隨意的控制,甚至可以一直追述後設資料的所有關聯後設資料;

上面的示例程式碼將複寫通過預定義特性Display特性設定的後設資料資訊DisplayName:

1 public class Customer
2 {
3     [CustomDisplayName(Name = "自定義")]
4     [Display(Name = "客戶ID")]
5     public string CustomerId { get; set; }
6     public Shopping Shopping { get; set; }
7 } 
View Code

在CustomerId屬性上我們設定了兩個特性,一個是系統預定義的Display特性,該特性將會對後設資料物件ModelMetadata的DisplayName屬性進行設定,還有一個正是我們自定義的CustomDisplayName特性,在我們自定義特性的內部邏輯中,如果我們設定了CustomDisplayName物件的Name屬性,那麼我們將使用該值複寫通過預定義特性Display特性所設定的預設後設資料資訊,從而達到控制最終後設資料的目的;

圖7:

當前這個值是我們通過Display預定義特性設定的;

圖8:

在CustomDisplayName中的Name屬性是我們設定的預設要顯示的文字,如果我們設定了預設值將使用該值複寫預定義特性Display設定的值;

圖9:

使用IMetadataAware介面我們可以設計自定義的後設資料設定物件,這也是ASP.NETMVC目前公開的唯一一個後設資料定義介面;當然如果遇見非常複雜的業務場景時就需要我們對後設資料提供程式進行控制,可以將後設資料的定義方式從宣告式遷移到配置檔案中,當然這需要有業務需要才行,純粹的技術實現沒有太多的意義;

6.資料註釋後設資料控制機制(面向UI框架的基礎System.ComponentModel.DataAnnotations名稱空間)

在ASP.NETMVC中,大部分的後設資料控制特性都是定義在System.ComponentModel.DataAnnotations名稱空間中,當然也有一小部分是ASP.NETMVC直接固定的,這些都是跟ASP.NETMVCWEB程式設計直接相關的(如:HiddenInput後設資料庫控制特性,用來隱藏HTML中的Input Dom元素),但是大部分都是位於元件物件模型名稱空間中;這就會給我們帶來一些疑問,為什麼跟ASP.NETMVC框架相關的物件模型會被定義在System.ComponentModel.DataAnnotations名稱空間中,而該名稱空間中的物件模型卻是跟系統元件設計相關的領域,如果你沒有系統元件開發經驗或者沒有Winform程式開發經驗的對你來說可能真的很困惑,因為System.ComponentModel.DataAnnotations名稱空間基本上是用來支撐所有.NET平臺上的基礎框架,如果你想擴充套件VS外掛、編寫設計時元件,這些跟.NET平臺相關的領域都會需要該名稱空間的支援;

6.1.System.ComponentModel 元件物件模型的生命週期(系統元件的基本特徵)

可以簡單定義System.ComponentModel.DataAnnotations名稱空間的作用,該名稱空間主要是用來支撐跟.NET平臺元件開發相關的領域,在該名稱空間中的物件模型都是用來支援VisualStudio設計時及基礎框架的通用組成部分;

元件模型通常具有三個基本的生命週期,設計時編譯時執行時,這裡的元件與我們通常理解的執行時元件不是一個概念,這裡的元件的參照物是.NET基礎框架,作為以VS為開發工具的.NET程式,在設計時我們都需要視覺化程式設計,將一個簡單的物件以圖形介面的方式呈現出來並且提供設計時支援,這些才這是我們這裡所說的元件,如果你的元件並沒有提供設計時、編譯時、執行時這三個基本的生命週期事件,那麼只能說你的元件是不完整的;

設計時:當我們在使用傳統ASP.NET開發程式的時候最常用的就是拖拽一個控制元件放入介面上,此時會出現一個GUI的設計介面,讓我們點選相應的位置設定一些選項,這就是設計時支援,被拖拽的可以視為一個可以重用的元件,這是它在設計時的一個生命週期;

編譯時:當我們啟動VS進行編譯時,元件有一個自我屬性檢查的過程,通常是用來檢查我們的預設定項是否正確,比如一些WindowsService,是否填寫了正確的啟動項屬性,這就是元件的編譯時支援;

執行時:這個比較好理解,執行時就是在程式執行過程中提供的功能,當然你的元件可以不提供執行時支援,而僅僅提供設計時、編譯時的支援;

6.2.設計時元件後設資料(設計時在VS中暴露出來的設定後設資料)

元件設計時後設資料和ASP.NETMVC Model後設資料很相似,為什麼說相似,是因為都需要經過一個對後設資料獲取的過程;在ASP.NETMVC中Model後設資料的設定過程需要通過提取作用於Model上的後設資料控制特性並且逐一順序執行後才能完成,而這裡的元件設計時後設資料提取過程可以看成是和ASP.NETMVC Model後設資料設定過程中的提取後設資料控制特性過程完全一致的複用功能;

圖10:

上圖中被圈出的部分是對設計時後設資料的控制特性,通過對需要繫結到VS屬性視窗中的模型運用類似ASP.NETMVC中定義Model控制後設資料特性的一樣的方式來達到控制被使用的模型,唯一不同的是背後的後設資料處理程式不同而已,但是可以進行類似的理解;

6.3.System.ComponentModel.DataAnnotations UI層框架的通用資料註解元件

經過上面兩個小結的講解,我們知道什麼是系統元件及元件的一個基本的特徵,如:生命週期,更為重要的是我們知道了一些跟ASP.NETMVC後設資料相似的功能出現在系統元件開發的功能集中,這為我們理解為什麼ASP.NETMVC後設資料註解特性物件會定義在系統元件名稱空間中做了很多充足的準備;

System.ComponentModel.DataAnnotationns名稱空間是位於System.ComponentModel名稱空間下,表示它是一個系統元件開發相關的資料註解元件;幫助我們在開發系統元件時進行很好的資料註解宣告,最有意義的是可以很輕鬆的實現後設資料驅動設計契約式設計等類似需要藉助資料註解功能的設計方法;

既然定義在System.ComponentModel下也就意味著可以供.NET平臺上的所有跟元件設計相關的框架使用,在.NET平臺中有很多需要藉助資料註解特性功能的場景(比如:在WPF中需要藉助資料註解功能來達到MVVM模式的使用);

圖11:

System.ComponentModel.DataAnnotations中的資料註解特性是提供給所有.NET平臺上應用框架使用的,這些框架都或多或少在一些設計上需要資料註解功能,這樣就不需要重複定義這些類似功能了;在ASP.NETMVC中,我們使用這些資料註解特性來宣告後設資料控制選項,在其他的應用框架中如:WPF中,可能需要用來指定UI上的雙向繫結事件,這些都是需要建立在這些資料註解特性上的;

6.4.使用System.ComponentModel.DataAnnotations中的獲取後設資料設定特性功能

在System.ComponentModel.DataAnnotations中有一個擴充套件自System.ComponentModel.TypeDescriptionProvider的型別:

// 摘要:
//     通過新增在關聯類中定義的特性和屬性資訊,從而擴充套件某個類的後設資料資訊。
public class AssociatedMetadataTypeTypeDescriptionProvider : TypeDescriptionProvider
{
} 

該型別擴充套件了原本很單純的元件型別描述提供程式,新增了關聯類的資料描述獲取功能;意思是說我們可以使用該類來獲取所有預定義的關聯後設資料控制特性;

 1 [AttributeUsage(AttributeTargets.Property)]
 2 public class ValidatorAttribute : Attribute /*自定義的關聯類特性*/
 3 {
 4     public string ValidatorFormatString { get; set; }
 5 } 
 6 public class Customer
 7 { 
 8 
 9     [Validator(ValidatorFormatString = "XXX")]/*設定關聯特性*/
10     [CustomDisplayName(Name = "自定義")]
11     [Display(Name = "客戶ID")]
12     public string CustomerId { get; set; }
13     public Shopping Shopping { get; set; }
14 }
View Code
AssociatedMetadataTypeTypeDescriptionProvider provider = new AssociatedMetadataTypeTypeDescriptionProvider(typeof(ValidatorAttribute));
var result = provider.GetTypeDescriptor(customer).GetProperties()[0].Attributes;

通過使用AssociatedMetadataTypeTypeDescriptionProvider 公共關聯類型別描述提供程式獲取所有關聯類的後設資料控制宣告;

圖12:

我們可以使用System.ComponentModel.DataAnnotations名稱空間提供的公共元件設計框架中提供的關於資料註解方面的功能來方便的開發有關後設資料註解方面的程式特性;

 

相關文章