ASP.NET MVC的Model元數據與Model模板:將”ListControl”引入ASP.NET MVC
我們不僅可以創建相應的模板來根據Model元數據控製種類型的數據在UI界麵上的呈現方法,還可以通過一些擴展來控製Model元數據本身。在某些情況下通過這兩者的結合往往可以解決很多特殊數據的呈現問題,我們接下來演示的實例就是典型的例子。[本文已經同步到《How ASP.NET MVC Works?》中]
傳統的ASP.NET具有一組重要的控件類型叫做列表控件(ListControl),它的子類包括DropDownList、ListBox、RadioButtonList和CheckBoxList等。對於ASP.NET MVC來說,我們可以通過HtmlHelper/HtmlHelper<TModel>的擴展方法DropDownList/DropDownListFor和ListBox/ListBox在界麵上呈現一個下拉框和列表框,但是我們需要手工指定包含的所有列表選項。在一般的Web應用中,尤其是企業應用中,我們會選擇將這些列表進行單獨地維護,如果我們在構建“列表控件”的時候能夠免去手工提供列表的工作,這無疑會為開發帶來極大的遍曆,而這實際上很容易實現。[源代碼從這裏下載]
我們先來看看通過該擴展最終實現的效果。在通過Visual Studio的ASP.NET MVC項目模板創建的空Web應用中,我們定義一個作為Model表示員工的Employee類型。如下麵的代碼片斷所示,表示性別、學曆、部門和技能的屬性分別應用了、、和四個特性。從名稱可以看出來,這四個特性分別代表了目標元素呈現在UI界麵上的形式,即對應著傳統ASP.NET Web應用中的四種類型的列表控件:RadioButtonList、DropdownList、ListBox和CheckBoxList。特性中指定的字符串表示預定義列表的名稱。
1: public class Employee
2: {
3: [DisplayName("姓名")]
4: public string Name { get; set; }
5:
6: [RadioButtonList("Gender")]
7: [DisplayName("性別")]
8: public string Gender { get; set; }
9:
10: [DropdownList("Education")]
11: [DisplayName("學曆")]
12: public string Education { get; set; }
13:
14: [ListBox("Department")]
15: [DisplayName("所在部門")]
16: public IEnumerable<string> Departments { get; set; }
17:
18: [CheckBoxList("Skill")]
19: [DisplayName("擅長技能")]
20: public IEnumerable<string> Skills { get; set; }
21: }
在創建的默認HomeController中,我們定義了如下一個Index操作方法。在該方法中,我們創建了一個具體的Employee對象並對它的所有屬性進行了相應設置,最終將該對象呈現在默認的View中。
1: public class HomeController : Controller
2: {
3: public ActionResult Index()
4: {
5: Employee employee = new Employee
6: {
7: Name = "張三",
8: Gender = "M",
9: Education = "M",
10: Departments= new string[] { "HR", "AD" },
11: Skills = new string[] { "CSharp", "AdoNet" }
12: };
13: return View(employee);
14: }
15: }
如下所示的是上麵的Index操作對應的View定義,這是一個以Model類型為Employee的強類型View,我們通過調用HtmlHelper<TModel>的模板方法EditorFor將作為Model的Employee對象的所有屬性以編輯模式呈現出來。
1: @model Employee
2: <table>
3: <tr>
4: <td>@Html.LabelFor(m => m.Name)</td><td>@Html.EditorFor(m => m.Name)</td>
5: </tr>
6: <tr>
7: <td>@Html.LabelFor(m => m.Gender)</td><td>@Html.EditorFor(m => m.Gender)</td>
8: </tr>
9: <tr>
10: <td>@Html.LabelFor(m => m.Education)</td><td>@Html.EditorFor(m => m.Education)</td>
11: </tr>
12: <tr>
13: <td>@Html.LabelFor(m => m.Departments)</td><td>@Html.EditorFor(m => m.Departments)</td>
14: </tr>
15: <tr>
16: <td>@Html.LabelFor(m => m.Skills)</td><td>@Html.EditorFor(m => m.Skills)</td>
17: </tr>
18: </table>
下圖體現了該Web應用運行時的效果。我們可以看到,四個屬性分別以四種不同的“列表控件”呈現出來,並且對應在它們上麵的四個字定義的列表特性(RadioButtonListAttribute、DropdownListAttribute、ListBoxAttribute和CheckBoxListAttribubte)。
現在對體現在上麵演示實例的基於列表數據的UI定製的設計進行簡單地介紹。我們首先來定義如下一個表示列表中某個條目(列表項)的類型ListItem,簡單起見,我們緊緊定義Text和Value兩個屬性,它們分別表示顯示的文字和代表的值。比如對於一組表示國家的列表,列表項的Text屬性表示成國家名稱(比如“中國”),具體的值則可能是國家的代碼(比如“CN”)。
1: public class ListItem
2: {
3: public string Text { get; set; }
4: public string Value { get; set; }
5: }
我們將提供列表數據的組件稱為ListProvider,它們實現了IListProvider接口。如下麵的代碼片斷所示,IListProvider具有唯一的方法GetListItems根據指定的列表名稱獲取所有的列表項。通過實現IListProvider,我們定義了一個默認的DefaultListProvider。簡單起見,DefaultListProvider直接通過一個靜態字段模擬列表的存儲,在真正的項目中一般會保存在數據庫中。DefaultListProvider維護了四組列表,分別表示性別、學曆、部門和技能,它們正好對應著Employee的四個屬性。
1: public interface IListProvider
2: {
3: IEnumerable<ListItem> GetListItems(string listName);
4: }
5: public class DefaultListProvider : IListProvider
6: {
7: private static Dictionary<string, IEnumerable<ListItem>> listItems = new Dictionary<string, IEnumerable<ListItem>>();
8: static DefaultListProvider()
9: {
10: var items = new ListItem[]{
11: new ListItem{ Text = "男", Value="M"},
12: new ListItem{ Text = "女", Value="F"}};
13: listItems.Add("Gender", items);
14:
15: items = new ListItem[]{
16: new ListItem{ Text = "高中", Value="H"} ,
17: new ListItem{ Text = "大學本科", Value="B"},
18: new ListItem{ Text = "碩士", Value="M"} ,
19: new ListItem{ Text = "博士", Value="D"}};
20: listItems.Add("Education", items);
21:
22: items = new ListItem[]{
23: new ListItem{ Text = "人事部", Value="HR"} ,
24: new ListItem{ Text = "行政部", Value="AD"},
25: new ListItem{ Text = "IT部", Value="IT"}};
26: listItems.Add("Department", items);
27:
28: items = new ListItem[]{
29: new ListItem{ Text = "C#", Value="CSharp"} ,
30: new ListItem{ Text = "ASP.NET", Value="AspNet"},
31: new ListItem{ Text = "ADO.NET", Value="AdoNet"}};
32: listItems.Add("Skill", items);
33: }
34: public IEnumerable<ListItem> GetListItems(string listName)
35: {
36: IEnumerable<ListItem> items;
37: if (listItems.TryGetValue(listName, out items))
38: {
39: return items;
40: }
41: return new ListItem[0];
42: }
43: }
接下來我們定義如下一個ListProviders類型,它的靜態隻讀屬性Current表示當前的ListProvider,而對當前ListProvider的注冊通過靜態方法SetListProvider來實現。如果沒有對當前ListProvider進行顯式注冊,則默認采用DefaultListProvider。
1: public static class ListProviders
2: {
3: public static IListProvider Current { get; private set; }
4: static ListProviders()
5: {
6: Current = new DefaultListProvider();
7: }
8: public static void SetListProvider(Func<IListProvider> providerAccessor)
9: {
10: Current = providerAccessor();
11: }
12: }
基於四種“列表控件”的HTML生成是通過定義HtmlHelper的擴展方法來實現的,如下麵的代碼所示,定義在ListControlExtensions中的四個擴展方法實現了針對這四種列表控件的UI呈現。參數listName表示使用的預定義列表的名稱,而value和values則表示綁定的值。RadioButtonList/DropdownList隻允許單項選擇,而ListBox/CheckBoxList允許多項選擇,所以對應的值類型分別是string和IEnumerable<string>。
1: public static class ListControlExtensions
2: {
3: //其他成員
4: public static MvcHtmlString RadioButtonList( this HtmlHelper htmlHelper,string name, string listName, string value)
5: {
6: return RadioButtonCheckBoxList(htmlHelper, listName, item =>
7: htmlHelper.RadioButton(name, item.Value, value == item.Value));
8: }
9:
10: public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper, string name, string listName, IEnumerable<string> values)
11: {
12: return RadioButtonCheckBoxList(htmlHelper, listName, item => CheckBoxWithValue(htmlHelper, name, values.Contains(item.Value), item.Value));
13: }
14:
15: public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, string listName, IEnumerable<string> values)
16: {
17: var listItems = ListProviders.Current.GetListItems(listName);
18: List<SelectListItem> selectListItems = new List<SelectListItem>();
19: foreach (var item in listItems)
20: {
21: selectListItems.Add(new SelectListItem { Value = item.Value,
22: Text = item.Text,
23: Selected = values.Any(value => value == item.Value) });
24: }
25: return htmlHelper.ListBox(name, selectListItems);
26: }
27:
28: public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, string listName, string value)
29: {
30: var listItems = ListProviders.Current.GetListItems(listName);
31: List<SelectListItem> selectListItems = new List<SelectListItem>();
32: foreach (var item in listItems)
33: {
34: selectListItems.Add(new SelectListItem { Value = item.Value,
35: Text = item.Text, Selected = value == item.Value});
36: }
37: return htmlHelper.DropDownList(name, selectListItems);
38: }
39: }
從上麵的代碼片斷可以看到,在ListBox和DropDownList方法中我們通過當前的ListProvider獲取指定列表名稱的所有列表項並生成相應的SelectListItem列表,最終通過調用HtmlHelper現有的擴展方法ListBox和DropDownList實現HTML的呈現。而RadioButtonList和MvcHtmlString最終調用了輔助方法RadioButtonCheckBoxList顯示了最終的HTML生成,該方法定義如下。
1: public static class ListControlExtensions
2: {
3: public static MvcHtmlString CheckBoxWithValue(this HtmlHelper htmlHelper, string name, bool isChecked, string value)
4: {
5: string fullHtmlFieldName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
6: ModelState modelState;
7:
8: //將ModelState設置為表示是否勾選布爾值
9: if (htmlHelper.ViewData.ModelState.TryGetValue(fullHtmlFieldName, out modelState))
10: {
11: htmlHelper.ViewData.ModelState.SetModelValue(fullHtmlFieldName, new ValueProviderResult(isChecked, isChecked.ToString(), CultureInfo.CurrentCulture));
12: }
13: MvcHtmlString html;
14: try
15: {
16: html = htmlHelper.CheckBox(name, isChecked);
17: }
18: finally
19: {
20: //將ModelState還原
21: if (null != modelState)
22: {
23: htmlHelper.ViewData.ModelState[fullHtmlFieldName] = modelState;
24: }
25: }
26: string htmlString = html.ToHtmlString();
27: var index = htmlString.LastIndexOf('<');
28: //過濾掉類型為"hidden"的<input>元素
29: XElement element = XElement.Parse(htmlString.Substring(0, index));
30: element.SetAttributeValue("value", value);
31: return new MvcHtmlString(element.ToString());
32: }
33:
34: private static MvcHtmlString RadioButtonCheckBoxList(HtmlHelper htmlHelper, string listName, Func<ListItem, MvcHtmlString> elementHtmlAccessor)
35: {
36: var listItems = ListProviders.Current.GetListItems(listName);
37: TagBuilder table = new TagBuilder("table");
38: TagBuilder tr = new TagBuilder("tr");
39: foreach (var listItem in listItems)
40: {
41: TagBuilder td = new TagBuilder("td");
42: td.InnerHtml += elementHtmlAccessor(listItem).ToHtmlString();
43: td.InnerHtml += listItem.Text;
44: tr.InnerHtml += td.ToString();
45: }
46: table.InnerHtml = tr.ToString();
47: return new MvcHtmlString(table.ToString());
48: }
49: }
方法RadioButtonCheckBoxList在生成RadioButtonList和CheckBoxList的時候才用<table>進行布局。組成RadioButtonList的單個RadioButton最終是調用HtmlHelper現有的擴展方法RadioButton生成的,而CheckBoxList中的CheckBox則是通過調用我們自定義的CheckBoxWithValue方法生成的。CheckBoxWithValue最終還是調用HtmlHelper現有的擴展方法CheckBox生成單個CheckBox對應的HTML,但是方法值支持布爾值的綁定,並且會生成一個在這裏不需要的Hidden元素,所以我們不得不在調用該方法的前後作一些手腳。
現在我們來介紹應用在Employee屬性上的四個特性的定義。如下麵的代碼片斷所示,基於四種“列表控件”的特性均繼承自抽象特性ListAttribute。ListAttribute實現了接口,在實現的OnMetadataCreated方法中將在構造函數中指定的代表列表名稱的ListName屬性添加到表示Model元數據的ModelMetadata對象的AdditionalValues屬性中。四個具體的列表特性重寫了OnMetadataCreated方法,並在此基礎上將ModelMetadata的TemplateHint分別設置為DropdownList、ListBox、RadioButtonList和CheckBoxList。
1: [AttributeUsage(AttributeTargets.Property)]
2: public abstract class ListAttribute : Attribute, IMetadataAware
3: {
4: public string ListName { get; private set; }
5: public ListAttribute(string listName)
6: {
7: this.ListName = listName;
8: }
9: public virtual void OnMetadataCreated(ModelMetadata metadata)
10: {
11: metadata.AdditionalValues.Add("ListName", this.ListName);
12: }
13: }
14:
15: [AttributeUsage(AttributeTargets.Property)]
16: public class DropdownListAttribute : ListAttribute
17: {
18: public DropdownListAttribute(string listName)
19: : base(listName)
20: { }
21: public override void OnMetadataCreated(ModelMetadata metadata)
22: {
23: base.OnMetadataCreated(metadata);
24: metadata.TemplateHint = "DropdownList";
25: }
26: }
27:
28: [AttributeUsage(AttributeTargets.Property)]
29: public class ListBoxAttribute : ListAttribute
30: {
31: public ListBoxAttribute(string listName)
32: : base(listName)
33: { }
34: public override void OnMetadataCreated(ModelMetadata metadata)
35: {
36: base.OnMetadataCreated(metadata);
37: metadata.TemplateHint = "ListBox";
38: }
39: }
40:
41: [AttributeUsage(AttributeTargets.Property)]
42: public class RadioButtonListAttribute : ListAttribute
43: {
44: public RadioButtonListAttribute(string listName)
45: : base(listName)
46: { }
47:
48: public override void OnMetadataCreated(ModelMetadata metadata)
49: {
50: base.OnMetadataCreated(metadata);
51: metadata.TemplateHint = "RadioButtonList";
52: }
53: }
54:
55: [AttributeUsage(AttributeTargets.Property)]
56: public class CheckBoxListAttribute : ListAttribute
57: {
58: public CheckBoxListAttribute(string listName)
59: : base(listName)
60: { }
61:
62: public override void OnMetadataCreated(ModelMetadata metadata)
63: {
64: base.OnMetadataCreated(metadata);
65: metadata.TemplateHint = "CheckBoxList";
66: }
67: }
由於四個具體的ListAttribute已經對表示模板名稱的ModelMetadata的TemplateHint進行了設置,那麼我們針對它們定義相應的分部View作為對應的模板,那麼在調用HtmlHelper/HtmlHelper<TModel>相應模板方法的時候就會按照這些模板對目標元素進行呈現。實現如上圖所示的效果的四個模板定義如下,它們被保存在View\Shared\EditorTemplates目錄下麵。
1: DropdownList.cshtml:
2: @model string
3: @{
4: string listName = (string)ViewData.ModelMetadata.AdditionalValues["ListName"];
5: @Html.DropDownList("",listName,Model)
6: }
7:
8: ListBox.cshtml:
9: @model IEnumerable<string>
10: @{
11: string listName = (string)ViewData.ModelMetadata.AdditionalValues["ListName"];
12: @Html.ListBox("",listName,Model)
13: }
14:
15: RadioButtonList.cshtml:
16: @model string
17: @{
18: string listName = (string)ViewData.ModelMetadata.AdditionalValues["ListName"];
19: @Html.RadioButtonList("",listName,Model)
20: }
21:
22: CheckBoxList.cshtml:
23: @model IEnumerable<string>
24: @{
25: string listName = (string)ViewData.ModelMetadata.AdditionalValues["ListName"];
26: @Html.CheckBoxList("", listName, Model)
27: }
ASP.NET MVC的Model元數據與Model模板:預定義模板
ASP.NET MVC的Model元數據與Model模板:模板的獲取與執行策略
ASP.NET MVC的Model元數據與Model模板:將ListControl引入ASP.NET MVC
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-26 11:34:47