Fork me on GitHub

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

阅读目录:

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上的展现重用功能;

posted @ 2014-01-13 11:58  王清培  阅读(3272)  评论(8编辑  收藏  举报