標籤:des cWeb style blog http color os 使用 io
我們在上一篇中討論了如何利用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
View Code
第二步,在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 }
View Code
在上面的程式碼範例中,我們實現了三個擴充方法,它們分別對應與原有的方法:
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 }
View Code
運行頁面得到如下結果:
值得一提的是,該方案與ModelMetadata沒有任何衝突,因為該擴充方未曾調用ModelMetadataProvider。也就是說,這兩種方案可以並存。當我們的頁面模型類型中不存在多個相同類型的屬性需要顯示在介面,則可以直接使用LabelFor方法,使用Provider重設顯示的內容;而當我們有多個相同類型的導覽屬性,並且需要將其在頁面展示時進行區分時,可使用我們新增的擴充方法(實際上,任何屬性的顯示,都可以使用這種擴充方法,而不僅局限於此例。如LableForPropery(p => p.UserName))。
這樣自訂資源的自主訪問便得到了完善。
其實大家注意到,我們此擴充方法實際上就是在Razor視圖中調用Html.Lable(string expression, string labelText)。但不同點在於,我們將硬式編碼labelText資源索引值改變為動態產生資源索引值,這種書寫方式是值得提倡的。試想假如有一天,資源key值方案改變,例如所有key值前面會附加一個Application名稱作為首碼,用於區分不同的應用,那麼寫入程式碼情況下,我們需要進行大量的修改。而是用擴充方法,我們僅需要修改擴充方法,追加首碼就可以一次性解決,和樂而不為之呢?
.NET MVC4 實訓記錄之七(實現資源的自主訪問後續)