我们不仅可以创建相应的模板来根据Model元数据控制种类型的数据在UI界面上的呈现方法,还可以通过一些扩展来控制Model元数据本身。在某些情况下通过这两者的结合往往可以解决很多特殊数据的呈现问题,我们接下来演示的实例就是典型的例子。[本文已经同步到《How ASP.NET MVC Works?》中]
传统的ASP.NET具有一组重要的控件类型叫做列表控件(ListControl),它的子类包括DropDownList、ListBox、RadioButtonList和CheckBoxList等。对于ASP.NET MVC来说,我们可以通过HtmlHelper/HtmlHelper
一、实现的效果
我们先来看看通过该扩展最终实现的效果。在通过Visual Studio的ASP.NET MVC项目模板创建的空Web应用中,我们定义一个作为Model表示员工的Employee类型。如下面的代码片断所示,表示性别、学历、部门和技能的属性分别应用了RadioButtonListAttribute、DropdownListAttribute、ListBoxAttribute和CheckBoxListAttribubte四个特性。从名称可以看出来,这四个特性分别代表了目标元素呈现在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
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)。
二、ListItem与ListProvider
现在对体现在上面演示实例的基于列表数据的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: IEnumerableGetListItems(string listName);
4: }
5: public class DefaultListProvider : IListProvider
6: {
7: private static Dictionary<string, IEnumerable> listItems = new Dictionary<string, IEnumerable >();
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 IEnumerableGetListItems(string listName)
35: {
36: IEnumerableitems;
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(FuncproviderAccessor)
9: {
10: Current = providerAccessor();
11: }
12: }
三、通过对HtmlHelper/HtmlHelper的扩展生成“ListControl”的HTML
基于四种“列表控件”的HTML生成是通过定义HtmlHelper的扩展方法来实现的,如下面的代码所示,定义在ListControlExtensions中的四个扩展方法实现了针对这四种列表控件的UI呈现。参数listName表示使用的预定义列表的名称,而value和values则表示绑定的值。RadioButtonList/DropdownList只允许单项选择,而ListBox/CheckBoxList允许多项选择,所以对应的值类型分别是string和IEnumerable
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: ListselectListItems = new List ();
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: ListselectListItems = new List ();
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"的元素
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, FuncelementHtmlAccessor)
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的时候才用