Fork me on GitHub

.NET/ASP.NETMVC 深入剖析 Model元数据、HtmlHelper、自定义模板、模板的装饰者模式(二)

阅读目录:

  • 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命名空间提供的公共组件设计框架中提供的关于数据注解方面的功能来方便的开发有关元数据注解方面的程序特性;

 

posted @ 2013-12-16 15:22  王清培  阅读(3519)  评论(5编辑  收藏  举报