我们在上一篇中讨论了如何利用ModelMetadata实现国际化资源文件访问,但也留下了一些问题,即:如何利用ModelMetadata实现相同类型的属性信息的个性化资源显示。本人没有找到合适的方案,期待着高人的指点。
本章,介绍第三种资源访问方案,用于解决上述问题(该方案并非从设计角度解决问题)。
首先,描述下我们的问题。
第一步,在UserProfile类型中添加两个Address类型的属性:
1 #region 用户住址信息 2 3 public int? UserAddressId { get; set; } 4 5 [ForeignKey("UserAddressId")] 6 public Address UserAddress { get; set; } 7 8 #endregion 9 10 11 #region 用户公司地址信息 12 13 public int? CompanyAddressId { get; set; } 14 15 [ForeignKey("CompanyAddressId")] 16 public Address CompanyAddress { get; set; } 17 18 #endregion
第二步,在EditUser视图中展示这两个属性的City信息:
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 8 @Html.LabelFor(p => p.UserAddress.City) 9 @Html.TextBoxFor(p => p.UserAddress.City) 10 11 @Html.LabelFor(p => p.CompanyAddress.City) 12 @Html.TextBoxFor(p => p.CompanyAddress.City) 13 </div> 14 <input type="submit" value="提交" /> 15 }
第三步,在资源文件中添加相应的资源:
1 <resource key="UserProfile.UserAddress" value="User Address "/> 2 <resource key="UserProfile.UserAddress.City" value="User City "/> 3 <resource key="UserProfile.CompanyAddress" value="Company Address "/> 4 <resource key="UserProfile.CompanyAddress.City" value="Company City "/>
第四步,使用自定义ModelMetadataProvider绑定显示信息:参考上一篇博文。
运行项目打开页面,看到内容如下:
如图所示,自定义的资源信息未能如愿显示在页面上。这是由于在Provider中,此时的Container并非是当前页面绑定的强类型,而是当前属性的持有者的类型。
如上所示,构造出的资源键值为"Address.City"(针对这两个属性,我们会得到同样的结果),因此没有能够显示我们预想的信息。令人难过的是,虽然我们可以通过查看modelAccessor参数得知当前访问的属性来源,但仍然无法在运行期获取这个来源(至少我还没有找到方法)。
就是它困扰了我很久,明明可以看到,但就是无法获取。索性换个方式,使用HtmlHelper扩展方法。
扩展方法是一个很好的东西,它是framework3之后才引入的。其实现我们随处可见,也常常会使用。例如对集合进行过滤使用Collection.Where,或者排序OrderBy等,都是在使用Linq的扩展方法。它的好处显而易见,可以不使用继承,而直接对原有类型实现功能的扩展。OK,看看我们如何实现。
首先,需要一个静态类,一个静态方法(扩展方法是在一个静态类中定义一个静态方法,该方法将需要进行扩展的类型作为参数,并使用this关键字声明是对它的扩展)。
1 namespace MvcApp.Helpers 2 { 3 4 public static class HtmlHelperExt 5 { 6 public static MvcHtmlString LabelForProperty<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression) 7 { 8 return LabelForProperty(html, expression, null); 9 } 10 11 public static MvcHtmlString LabelForProperty<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, IDictionary<string, object> htmlAttributes) 12 { 13 string navPath = expression.Body.ToString().TrimStart(expression.Parameters.First().ToString().ToArray()).TrimStart('.'); 14 string res = Resource.GetDisplay(string.Format("{0}.{1}", typeof(TModel).Name, navPath)); 15 return LabelExtensions.Label(html, navPath, res ?? navPath, htmlAttributes); 16 } 17 public static MvcHtmlString LabelForProperty<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object htmlAttributes) 18 { 19 string navPath = expression.Body.ToString().TrimStart(expression.Parameters.First().ToString().ToArray()).TrimStart('.'); 20 string res = Resource.GetDisplay(string.Format("{0}.{1}", typeof(TModel).Name, navPath)); 21 return LabelExtensions.Label(html, navPath, res ?? navPath, htmlAttributes); 22 } 23 } 24 }
在上面的代码示例中,我们实现了三个扩展方法,它们分别对应与原有的方法:
LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression);
LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, IDictionary<string, object> htmlAttributes);
LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object htmlAttributes);
在实现扩展方法时,我们首先通过expression.Body.ToString()获取了当前属性的路径,对于p=>p.UserAddress.City,将返回它的字符串形式"p.UserAddress.City"。然后,再将字符串中的形参表达式 p 替换成当前类型(也就是页面绑定的类型)的名称,则生成字符串"UserProfile.UserAddress.City"。这个字符串符合我们对资源键值定义的格式:"类型名称.属性名"(次例子中,属性名实际上是"UserAddress.City",而非"City")。当获取到这个资源的键,通过资源访问器获取到资源内容,调用LabelExtensions类型的Label(this HtmlHelper html, string expression, string labelText, IDictionary<string, object> htmlAttributes)方法,构造一个label标签。最后,我们需要导入扩展方法实现的名空间,这样在Razor视图中才能使用这些扩展方法。只需在Views文件夹下的Web.config(而不是根目录下的Web.config)文件中添加扩展实现的名空间即可:
1 <system.web.webPages.razor> 2 <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" /> 3 <pages pageBaseType="System.Web.Mvc.WebViewPage"> 4 <namespaces> 5 ...... 10 <add namespace="MvcApp.Helpers" /> 11 </namespaces> 12 </pages> 13 </system.web.webPages.razor>
至此所有准备工作都已经完成,然后是修改我们的视图,使用新的扩展方法:
1 @using (Html.BeginForm("EditUser", "Account", FormMethod.Post)) 2 { 3 @Html.ValidationSummary() 4 <div> 5 @Html.LabelForProperty(p => p.UserName) 6 @Html.TextBoxFor(p => p.UserName) 7 8 @Html.LabelForProperty(p => p.UserAddress.City) 9 @Html.TextBoxFor(p => p.UserAddress.City) 10 11 @Html.LabelForProperty(p => p.CompanyAddress.City) 12 @Html.TextBoxFor(p => p.CompanyAddress.City) 13 </div> 14 <input type="submit" value="提交" /> 15 }
运行页面得到如下结果:
值得一提的是,该方案与ModelMetadata没有任何冲突,因为该扩展方未曾调用ModelMetadataProvider。也就是说,这两种方案可以并存。当我们的页面模型类型中不存在多个相同类型的属性需要显示在界面,则可以直接使用LabelFor方法,使用Provider重置显示的内容;而当我们有多个相同类型的导航属性,并且需要将其在页面展示时进行区分时,可使用我们新增的扩展方法(实际上,任何属性的显示,都可以使用这种扩展方法,而不仅局限于此例。如LableForPropery(p => p.UserName))。
这样自定义资源的自主访问便得到了完善。
其实大家注意到,我们此扩展方法实际上就是在Razor视图中调用Html.Lable(string expression, string labelText)。但不同点在于,我们将硬编码的labelText资源键值改变为动态生成资源键值,这种书写方式是值得提倡的。试想假如有一天,资源key值方案改变,例如所有key值前面会附加一个Application名称作为前缀,用于区分不同的应用,那么硬编码情况下,我们需要进行大量的修改。而是用扩展方法,我们仅需要修改扩展方法,追加前缀就可以一次性解决,和乐而不为之呢?