ASP.NET MVC Model元数据及其定制:一个重要的接口IMetadataAware

在介绍用于自定义Model元数据属性的AdditionalMetadataAttribute特性时我们提到了它实现的接口IMedataAware,我们说这是一个非常重要并且有用的接口,通过自定义实现该接口的特性我们可以对最终生成的Model元数据进行自由地定制。如下面的代码片断所示,IMedataAware接口具有唯一的方法成员OnMetadataCreated。当Model元数据被创建出来后,会先获取上述的这一系列标注特性对其进行初始化,然后获取应用在目标元素上所有实现了IMedataAware接口的特性,并将初始化的ModelMetadata对象作为参数调用OnMetadataCreated方法。所以我们通过创建实现该接口的特性不仅仅可以添加一些额外的元数据属性,也可以修改已经通过相应的标注特性初始化的相关属性。[本文已经同步到《How ASP.NET MVC Works?》中]

   1: public interface IMetadataAware
   2: {    
   3:     void OnMetadataCreated(ModelMetadata metadata);
   4: }

ASP.NET MVC定义了两个实现了IMedataAware接口的特性,一个就是我们已经介绍过的AdditionalMetadataAttribute,另一个则是AllowHtmlAttribute

一、AllowHtmlAttribute

为了防止最终用于通过在针对某个数据的输入中注入一些HTML来攻击我们的Web应用,ASP.NET MVC在进行Model绑定之前会对对应的请求数据进行验证,确保没有任何HTML标记包含其。这个针对HTML标记的验证通过ModelMetadata的RequestValidationEnabled来控制,如下面的代码片断所示,这是一个布尔类型的可读写属性。该属性在默认情况下为True,意味着默认开启针对HTML标记的请求验证。

   1: public class ModelMetadata
   2: {
   3:     //其他成员
   4:     public virtual bool RequestValidationEnabled { get; set; } 
   5: }

AllowHtmlAttribute特性,顾名思义,就是运行作为目标元素的内容包含HTML标记。如下面的代码片断所示,AllowHtmlAttribute是实现了IMetadataAware 接口,在OnMetadataCreated方法中它直接将作为参数的ModelMetadata对象的RequestValidationEnabled属性设置为False,从而使针对目标对象的请求验证被忽略掉。

   1: [AttributeUsage(AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
   2: public sealed class AllowHtmlAttribute : Attribute, IMetadataAware
   3: {
   4:     public void OnMetadataCreated(ModelMetadata metadata)
   5:     {
   6:         //其他操作
   7:         metadata.RequestValidationEnabled = false;
   8:     }
   9: }

为了验证ASP.NET MVC针对HTML标记的请求验证和AllowHtmlAttribute的作用,我们来做一个简单的实例演示。在通过Visual Studio提供的ASP.NET MVC项目模板创建的空Web应用中,我们定义了如下一个数据类型Foo,其中属性Baz上应用了AllowHtmlAttribute特性。

   1: public class Foo
   2: {
   3:     public string Bar { get; set; }
   4:  
   5:     [AllowHtml]
   6:     public string Baz { get; set; }
   7: }

然后我们创建如下一个默认的HomeController,默认的Index操作方法中具有一个类型为Foo的参数,该参数直接作为Model呈现在默认的View中。

   1: public class HomeController : Controller
   2: {
   3:     public ActionResult Index(Foo foo)
   4:     {
   5:         return View(foo);
   6:     }
   7: }

如下所示的Index操作对应的View定义,这是一个以Foo为Model的强类型View。在该View中,我们直接调用HtmlHelper<Model>的EditorForModel方法将Foo对象以编辑模式呈现出来。

   1: @model Foo
   2: @{
   3:     ViewBag.Title = "Index";
   4: }
   5: @Html.EditorForModel()

现在我们直接运行该Web应用。根据Model绑定的规则我们知道,如果我们通过浏览器访问HomeController的Index操作,可以通过查询字符串的方式对该操作方法的参数进行初始化。具体来说,我们可以分别指定名称为Bar和Baz的查询字符串对作为参数的Foo对象的两个属性进行初始化。为了验证对包含HTML标记的输入的验证,我们将最终绑定到Model上的查询字符串设置为<script/>。

如下图所示,由于Foo的属性Baz上应用了AllowHtmlAttribute特性是之支持包含HTML标记的数据,所以我们以查询字符串方式指定的包含HTML标记的内容(<script/>)直接显示在相应的文本框中。但是Bar属性在默认情况下是不运行绑定的数据具有任何HTML标记的,所以会将输入的数据视为恶意注入的HTML,直接抛出异常。

image

二、实例演示:创建实现IMetadataAware接口的特性定制Model元数据

通过上面对Model元数据定义的介绍我们知道显示名称可以通过在数据类型或者属性成员上应用DisplayAttribute特性来定义。在使用该特性的时候,我们需要显式制定表示显示名称的Name属性,如果需要进行本地化处理,需要将显示内容定义在某个资源文件中,并通过ResourceType属性指定该资源文件生成的类型。[源代码从这里下载]

为了简化,我们通过实现IMetadataAware接口的方式定义了如下一个DisplayTextAttribute特性。该特性的属性DisplayName/ResourceType与DisplayAttribute的Name/ResourceType具有相同的作用,唯一不同的是DisplayTextAttribute的这两个属性均是可以缺省的。如果DisplayName没有显式指定,则默认使用属性名称或者类型名称;如果ResourceType没有显式指定,则采用通过静态字段staticResourceType表示的默认资源类型,该类型通过静态方法SetResourceType进行注册。

   1: [AttributeUsage(AttributeTargets.Class| AttributeTargets.Property)]
   2: public class DisplayTextAttribute: Attribute, IMetadataAware
   3: {
   4:     private static Type staticResourceType;
   5:     public string DisplayName { get; set; }
   6:     public Type ResourceType { get; set; }
   7:  
   8:     public DisplayTextAttribute()
   9:     {
  10:         this.ResourceType = staticResourceType;
  11:     }
  12:  
  13:     public void OnMetadataCreated(ModelMetadata metadata)
  14:     {
  15:         this.DisplayName = this.DisplayName ?? (metadata.PropertyName ?? metadata.ModelType.Name);
  16:         if (null == this.ResourceType)
  17:         {
  18:             metadata.DisplayName = this.DisplayName;
  19:             return;
  20:         }
  21:         PropertyInfo property = this.ResourceType.GetProperty(this.DisplayName, BindingFlags.NonPublic|BindingFlags.Public| BindingFlags.Static);
  22:         metadata.DisplayName = property.GetValue(null, null).ToString();
  23:     }
  24:  
  25:     public static void SetResourceType(Type resourceType)
  26:     {
  27:         staticResourceType = resourceType;
  28:     }
  29: }

DisplayTextAttribute对Model元数据的定制实现在OnMetadataCreated方法中。具体来说,我们根据设置的DisplayName和ResourceType属性解析出最终作为目标元素显示名称的文本作为ModelMetadata的DisplayName属性值。

接下来我们来演示如何使用这个DisplayTextAttribute特性来替换DisplayAttribute特性进行显示名称的设置,为此我们在通过Visual Studio的ASP.NET MVC 项目模板创建的空Web应用中创建如下一个表示员工的Employee类型。Employee所有的属性上均应用了DisplayTextAttribute特性,而DisplayName和ReourceType属性没有显式指定。

   1: public class Employee
   2: {
   3:     [DisplayText]
   4:     public string Name { get; set; }
   5:  
   6:     [DisplayText]
   7:     public string Gender { get; set; }
   8:  
   9:     [DisplayText]
  10:     [DataType(DataType.Date)]
  11:     public DateTime BirthDate { get; set; }
  12:  
  13:     [DisplayText]
  14:     public string Department { get; set; }
  15: }

接下来我们打开项目的属性对话框并选择“资源(Rources)”Tab页,按照如下图所示为Employee中的四个属性定义相应的资源字符串作为显示的名称,资源字符串条目的名称为属性名。

image

该资源文件会自动生成一个类型为Resources的内部类型。由于应用在Employee属性上的DisplayTextAttribute特性并没有显式指定资源类型,所以我们需要在Global.asax文件中通过如下的方式将Resources类型注册为默认的资源类型。

   1: public class MvcApplication : System.Web.HttpApplication
   2: {    
   3:     //其他成员
   4:     protected void Application_Start()
   5:     {
   6:         //其他操作
   7:         DisplayTextAttribute.SetResourceType(typeof(Resources));
   8:     }
   9: }

现在我们通过调用HtmlHelper<TModel>的EditorForModel方法将一个具体的Employee对象以编辑模式显示在某个Model类型为Employee的强类型View上,会呈现出如下图所示的效果,我们可以看到作为标签显示的文字正式我们定义在资源文件中的内容。

image

ASP.NET MVC Model元数据及其定制: 初识Model元数据
ASP.NET MVC Model元数据及其定制: Model元数据的定制
ASP.NET MVC Model元数据及其定制:一个重要的接口IMetadataAware

posted @ 2012-04-13 06:40 Artech 阅读(...) 评论(...) 编辑 收藏