ASP.NET MVC的Model元数据与Model模板:模板的获取与执行策略

作者:袖梨 2022-06-25

当我们调用HtmlHelper或者HtmlHelper的模板方法对整个Model或者Model的某个数据成员以某种模式(显示模式或者编辑模式)进行呈现的时候,通过预先创建的代表Model元数据的ModelMetadata对象都可以找到相应的模板。如果模板对应着某个自定义的分部View,那么只需要执行该View即可;对于默认模板,则直接可以得到相应的HTML。本篇文章着重讨论模板的获取和执行机制,不过在这之前,顺便来讨论一下DataTypeAttribute和模板的关系。[本文已经同步到《How ASP.NET MVC Works?》中]

一、 DataTypeAttribute和模板有何关系?

通过《初识Model元数据》针对Model元数据定义的介绍,我们知道通过DataTypeAttribute特性对目标元素设置的数据类型最终会反映在表示Model元数据的ModelMetadata对象的DataTypeName属性上。此外,对于某些设置的数据类型,比如Date、Time、Duration和Currency等,还会随之创建一个DisplayFormatAttribute应用到ModelMetadata上。那么ModelMetadata的DataTypeName属性对目标元素的最终呈现具有怎样的影响呢?

实际上在模板匹配的过程中会将ModelMetadata的DataTypeName属性当作模板名称来看待,所以下面两种形式的Model类型定义可以看成是等效的。通过UIHintAttribute特性设置的模板名称和通过DataTypeAttribute特性设置的数据类型的唯一不同之处在于前者具有更高的优先级。换句话说,如果将UIHintAttribute和DataTypeAttribute同时应用到同一个数据成员分别将模板名称和数据类型设置为ABC和123,自定义模板123只有在模板ABC不存在的情况下才会被使用。

 代码如下 复制代码

   public class Model

 {

    [DataType(DataType.Html)]

    public string Foo { get; set; }

  

    [DataType(DataType.MultilineText)]

     public string Bar { get; set; }

  

      [DataType(DataType.Url)]

    public string Baz { get; set; }

 }

 

 public class Model

  {

    [UIHint("Html")]

    public string Foo { get; set; }

 

     [UIHint("MultilineText")]

     public string Bar { get; set; }

 

    [UIHint("Url")]

     public string Baz { get; set; }

 }

 

实例演示:证明DataTypeName与模板名称的等效性

为了证明通过DataTypeAttribute特性设置数据类型在针对目标元素进行可视化呈现过程中被视为模板名称,我们来做一个简单的实例演示。在这个实例中我们定义了如下一个表示三角形的数据类型Triangle,其属性A、B和C是一个Point对象,表示三个角所在的坐标。

 代码如下 复制代码

public class Triangle

 {

     [DataType("PointInfo")]

      public Point A { get; set; }

 

    [DataType("PointInfo")]
      public Point B { get; set; }

 

    [DataType("PointInfo")]

     public Point C { get; set; }

 }

 

 [TypeConverter(typeof(PointTypeConverter))]

 public class Point

 {

    public double X { get; set; }

    public double Y { get; set; }
     public Point(double x, double y)

    {

        this.X = x;

        this.Y = y;
    }

 

    public static Point Parse(string point)

     {

         string[] split = point.Split(',');

        if (split.Length != 2)

        {

             throw new FormatException("Invalid point expression.");

         }

        double x;
        double y;
         if (!double.TryParse(split[0], out x) ||!double.TryParse(split[1], out y))

         {

             throw new FormatException("Invalid point expression.");

         }

         return new Point(x, y);

     }

 }
 
 public class PointTypeConverter : TypeConverter

 {
     public override bool CanConvertFrom(ITypeDescriptorContext context,Type sourceType)

     {

         return sourceType == typeof(string);
    }
 

      public override object ConvertFrom(ITypeDescriptorContext context,CultureInfo culture, object value)

     {

        if (value is string)

         {

             return Point.Parse(value as string);

         }

         return base.ConvertFrom(context, culture, value);

     }

 }

 

对于类型Triangle和Point的定义,有两点值得注意:其一,Triangle的三个A、B和C属性上应用了DataTypeAttribute特性并将自定义数据类型设置为PointInfo(不是Point);其二,Point类型上应用了TypeConverterAttribute特性并将TypeConverter类型设置为PointTypeConverter,后者支持源自字符串的类型转换。通过前面对复杂类型(Complex Type)的介绍,这样会将Triangle的三个属性从复杂类型成员转换成简单类型成员。根据前提介绍的关于Object模板对数据成员的便利规则,Triangle的这三个属性才能被最终呈现出来。

现在我们创建一个Model类型为Point的强类型分部View作为模板,并将其命名为PointInfo(和前面通过DataTypeAttribute特性指定的自定义数据类型一致)。我们只为Point定义关于显示模式的模板,所以我们将该分部View文件放在ViewsSharedDisplayTemplates中。如下面的代码片断所示,我们将一个Point对象显示为(X,Y)的形式。

 代码如下 复制代码

@model MvcApp.Models.Point

(@Model.X, @Model.Y)

 

现在我们创建一个默认的HomeCtroller。如下面的代码片断所示,在默认的Index操作方法中我们创建了一个Triangle对象将其呈现在默认的View中。

 

 代码如下 复制代码

public class HomeController : Controller

 {

     public ActionResult Index()

     {

         Triangle triangle = new Triangle

        {
             A = new Point(1,2),

             B = new Point(2,3),

             C = new Point(3,4)

         };

         return View(triangle);
     }

 }

 

下面是对应的View的定义,这是一个Model类型为Triangle的强类型View,我们仅仅调用了HtmlHelper的DisplayModel方法将作为Model的Triangle对象以显示模式呈现出来。

 代码如下 复制代码

   1: @model MvcApp.Models.Triangle

   2: @Html.DisplayForModel()

 

运行该Web应用会在浏览器中得到如下图所示的呈现效果,我们可以看到作为我们创建的Triangle对象的A、B和C属性表示的三个角的坐标是完全按照我们定义的PointInfo模板的方式进行呈现的。

 

二、模板的获取与执行

当我们调用HtmlHelper或者HtmlHelper的模板方法对整个Model或者Model的某个数据成员以某种模式(显示模式或者编辑模式)进行呈现的时候,通过预先创建的代表Model元数据的ModelMetadata对象都可以找到相应的模板。如果模板对应着某个自定义的分部View,那么只需要执行该View即可;对于默认模板,则直接可以得到相应的HTML。

根据Model元数据对目标模板的解析是整个模板方法执行流程中最核心的部分,也是本篇讨论的重点。我们以针对HtmlHelper的扩展方法DisplayFor为例,看看针对通过表达式expression获取的Model对象是如何以显示模式呈现出来的。

 代码如下 复制代码

 public static class DisplayExtensions
{

     public static MvcHtmlString DisplayFor(this HtmlHelper html, Expression> expression, string templateName);
}

 

在DisplayFor被调用的时候,如果通过参数expression表示的Model获取表达式是针对某个属性的,那么属性名会被获取出来。然后执行表达式得到一个作为Model的对象,该对象连同属性名(如果有)一起被用于表示Model元数据的Metadatadata对象。接下来会根据该Metadatadata对象得到一系列表示分部模板View名称的列表,这些View名称按照优先级排列如下:

作为参数templateName传入的模板名称(如果不为空)。

Metadatadata的TemplateHint属性值(如果不为空)。

Metadatadata的DataTypeName属性值(如果不为空)。

如果Model对象的真实类型为非空值类型,该类型名作为模板View名;否则底层(Underlying)类型名作为模板View名(比如说,对于int?类型则将Int32作为模板View名)。

如果Model对象的真实类型为非复杂类型,则使用String模板(由于非复杂类型能够实现与String类型之间的转换,所以可以转换成String进行呈现)。

在Model的声明类型为接口情况下,如果该接口继承自IEnuerable则采用Collection模板。

在Model的声明类型为接口情况下,使用Object模板。

如果Model声明类型不是接口类型,按照其类型继承关系向上追溯知道Object类型,逐个将类型名称作为模板View名称。如果声明类型实现了IEnuerable接口,则将最后的Object替换成Collection。

相关文章

精彩推荐