.NET MVC4 实训记录之六(利用ModelMetadata实现资源的自主访问)
上一篇我们已经实现自定义资源文件的访问,该篇我们使用它配合ModelMetadata实现资源文件的自主访问。这样做是为了我们能更简单的用MVC原生的方式使用资源文件。由于我的文章旨在记录MVC项目的实现,因此不做框架底层实现方面的讲解(其实考虑到自己的能力,也不能为大家讲解的多么深入。如需要更深入的了解MVC底层实现,请自行搜索。在这里我推荐蒋金楠(Artech)老师的相关博文)。
对于使用EF,我们不得不知道System.ComponentModel.DataAnnotations。DataAnnotations下定义了一系列的Attribute,用于我们的属性字段注解方案。例如DisplayAttribue,用于定义属性所显示的名称的文本信息。RequiredAttribute用于定义属性是否必填,以及必填校验失败后的提示信息。它们是我们最常用的注解属性中的两个,我们一般都使用它们来描述我们的字段在用户界面的显示效果。例如我们在UserProfile定义的UserName属性上引入如下Attribute:
1 [Column(Order = 1)] 2 [Required(ErrorMessage = "The User Name is required!")] 3 [Display(Name = "Filed: User Name")] 4 public string UserName { get; set; }
在EditUser.cshtml视图中已如下方式显示该字段:
1 @using (Html.BeginForm("EditUser", "Account", FormMethod.Post)) 2 { 3 <p>@Html.LabelFor(p=>p.UserName)</p> 4 <p>@Html.TextBoxFor(p=>p.UserName)</p> 5 6 <input type="submit" value="提交" /> 7 }
运行项目并打开该页面,直接点击提交按钮,会得到如下的显示效果:
这说明MVC框架已经帮我们将字段注册的UI显示信息打印到当前页面。 可是,我们的项目本身的资源语言未定,或者说,需要多语言支持,这个方案就有些牵强。虽然System.ComponentModel.DataAnnotations已经提供了使用自定义资源的相关定义,但都是针对.resx或者已经进行编译过的资源类。无法使用我们自定义的XML资源文件。对此我们有两种方案对其进行改造,以使用我们自定义的资源文件。它们分别是:自定义CustmorDisplayAttribute,改造MVC框架。第一个方案的使用方式与上面例子相同,只不过我们需要为每个需要显示在页面上的Field都引入自定义的属性注释。后两者则更加便捷,无需对类型定义做出任何改变。在这里,我们不讨论第一种方案。
第二种方案属于“高级方案”,也就是说从设计层面解决这个问题。
针对这种方案,我们先要了解ModelMetadata,以及ModelMetadataProvider。在此仅附上相关的代码,不做深入讨论。
首先,是要定义我们自己的ModelMetadataProvider。
1 public class AppModelMetadataProvider : CachedDataAnnotationsModelMetadataProvider 2 { 3 protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor) 4 { 5 CachedDataAnnotationsModelMetadata metadata = base.CreateMetadataFromPrototype(prototype, modelAccessor); 6 if (metadata.ContainerType != null && !String.IsNullOrEmpty(metadata.PropertyName)) 7 { 8 metadata.DisplayName = Resource.GetDisplay(string.Format("{0}.{1}", metadata.ContainerType.Name, metadata.PropertyName)); 9 } 10 return metadata; 11 } 12 }
我们仅重写了CachedDataAnnotationsModelMetadataProvider类型中的CreateMetadataFromPrototype方法,在该方法中,我们将Model(这里的Model并非只是页面定义的强类型,它也可以是强类型的属性。当页面初始化阶段,需要获取当前页面的模型类型。因此,这个时候的Model就是页面绑定的强类型实例,其ContainerType为空。但当页面访问到此类型的属性,例如 p => p.UserName,此时的Model就是UserName,ContainerType则为页面绑定的强类型)的DisplayName属性进行修改。原本DisplayName会返回属性的DisplayAttribute注释中定义的资源内容,现在我们将其修改为从自定义资源文件中获取内容(注意:资源的键要严格按照“容器类型名.属性名”进行定义)。
最后,在Global文件的Application_Start()中添加代码以使用我们自定义的Provider。
1 protected void Application_Start() 2 { 3 AreaRegistration.RegisterAllAreas(); 4 5 WebApiConfig.Register(GlobalConfiguration.Configuration); 6 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); 7 RouteConfig.RegisterRoutes(RouteTable.Routes); 8 BundleConfig.RegisterBundles(BundleTable.Bundles); 9 AuthConfig.RegisterAuth(); 10 11 Bootstrapper.Initialise(); //初始化IOC容器 12 13 ModelMetadataProviders.Current = new AppModelMetadataProvider(); 14 }
在视图文件中,我们修改成利用MVC自定义的HtmlHelper显示字段名。如下:
1 @using (Html.BeginForm("EditUser", "Account", FormMethod.Post)) 2 { 3 @Html.ValidationSummary() 4 <div> 5 @Html.LabelFor(p => p.UserName) 6 @Html.TextBoxFor(p => p.UserName) 7 </div> 8 <input type="submit" value="提交" /> 9 }
第二种方案已完成。运行项目,观察一下我们的页面变化。然后修改一下资源文件中的内容,资源文件的修改也能被立刻应用,而不需要重新编译。
然后我们在UserProfile类型定义中添加一个Address类型的属性。
1 [Table("UserProfile")] 2 public class UserProfile 3 { 4 //无关代码省略...... 5 6 public int? AddressId { get; set; } 7 8 [ForeignKey("AddressId")] 9 public Address Address { get; set; } 10 } 11 12 [Table("Address")] 13 public class Address: BaseEntity<int> 14 { 15 public string City { get; set; } 16 }
修改EditUser视图,如下:
1 @using (Html.BeginForm("EditUser", "Account", FormMethod.Post)) 2 { 3 @Html.ValidationSummary() 4 <div> 5 @Html.LabelFor(p => p.UserName) 6 @Html.TextBoxFor(p => p.UserName) 7 @Html.LabelFor(p => p.Address.City) 8 @Html.TextBoxFor(p => p.Address.City) 9 </div> 10 <input type="submit" value="提交" /> 11 }
在资源文件中添加
1 <resource key="Address.City" value="City"/>
那么运行项目打开页面后,可以看到如下效果:
导航属性的自定义资源信息也会被显示。这里就可以看出p => p.Address.City时,Provider中的ContainerType则为Address的实际类型,ModelMetadata中的Model则为实际上要显示的子类型的属性。
问题:
我们通过对ModelMetadata内部的属性进行修改,从而实现自定义资源的使用。这种方式属于高阶应用,有必要深入了解MVC的模型绑定相关的知识。本人在完成这篇文章的时候,耗费了相当长的时间(粗略统计大概有3天时间)。主要是想解决多个同类型属性该如何显示不同的自定义资源。例如在UserProfile类型中定义两个同为Address类型的属性,分别为UserAddress、和CompanyAddress。若同时在页面显示这两个属性的City名称,则显示的内容是相同的,都指向同一个资源:
<resource key="Address.City" value="City"/>。但始终未能在第二种方案下找到适合的解决办法。希望有达人能为在下解惑,不胜感激!!!
下期预告:第三中方案解决同类型导航属性显示不同的自定义资源。