將”ListControl”引入ASP.NET MVC

lee_lgw發表於2021-09-09

我們不僅可以建立相應的模板來根據Model後設資料控制種型別的資料在UI介面上的呈現方法,還可以透過一些擴充套件來控制Model後設資料本身。在某些情況下透過這兩者的結合往往可以解決很多特殊資料的呈現問題,我們接下來演示的例項就是典型的例子。[本文已經同步到《How ASP.NET MVC Works?》中]

傳統的ASP.NET具有一組重要的控制元件型別叫做列表控制元件(ListControl),它的子類包括DropDownList、ListBox、RadioButtonList和CheckBoxList等。對於ASP.NET MVC來說,我們可以透過HtmlHelper/HtmlHelper的擴充套件方法DropDownList/DropDownListFor和ListBox/ListBox在介面上呈現一個下拉框和列表框,但是我們需要手工指定包含的所有列表選項。在一般的Web應用中,尤其是企業應用中,我們會選擇將這些列表進行單獨地維護,如果我們在構建“列表控制元件”的時候能夠免去手工提供列表的工作,這無疑會為開發帶來極大的遍歷,而這實際上很容易實現。[原始碼從這裡下載]

一、實現的效果

我們先來看看透過該擴充套件最終實現的效果。在透過Visual Studio的ASP.NET MVC專案模板建立的空Web應用中,我們定義一個作為Model表示員工的Employee型別。如下面的程式碼片斷所示,表示性別、學歷、部門和技能的屬性分別應用了RadioButtonListAttributeDropdownListAttributeListBoxAttributeCheckBoxListAttribubte四個特性。從名稱可以看出來,這四個特性分別代表了目標元素呈現在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 Departments { get; set; }

   

  17:

   

  18:     [CheckBoxList("Skill")]

   

  19:     [DisplayName("擅長技能")]

   

  20:     public IEnumerable 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的模板方法EditorFor將作為Model的Employee物件的所有屬性以編輯模式呈現出來。

   1: @model Employee

   

   2: 

   

   3:     

   

   4:         

   

   5:     

   

   6:       

   

   7:         

   

   8:     

   

   9:       

   

  10:         

   

  11:     

   

  12:      

   

  13:         

   

  14:     

   

  15:      

   

  16:         

   

  17:     

   

  18: 
@Html.LabelFor(m => m.Name)@Html.EditorFor(m => m.Name)
@Html.LabelFor(m => m.Gender)@Html.EditorFor(m => m.Gender)
@Html.LabelFor(m => m.Education)@Html.EditorFor(m => m.Education)
@Html.LabelFor(m => m.Departments)@Html.EditorFor(m => m.Departments)
@Html.LabelFor(m => m.Skills)@Html.EditorFor(m => m.Skills)

下圖體現了該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:     IEnumerable GetListItems(string listName);

   

   4: }

   

   5: public class DefaultListProvider : IListProvider

   

   6: {

   

   7:     private static Dictionary> listItems = new Dictionary>();

   

   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 GetListItems(string listName)

   

  35:     {

   

  36:         IEnumerable 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 providerAccessor)

   

   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 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 values)

   

  16:     {

   

  17:         var listItems = ListProviders.Current.GetListItems(listName);

   

  18:         List selectListItems = 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:         List selectListItems = 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, Func 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的時候才用

進行佈局。組成RadioButtonList的單個RadioButton最終是呼叫HtmlHelper現有的擴充套件方法RadioButton生成的,而CheckBoxList中的CheckBox則是透過呼叫我們自定義的CheckBoxWithValue方法生成的。CheckBoxWithValue最終還是呼叫HtmlHelper現有的擴充套件方法CheckBox生成單個CheckBox對應的HTML,但是方法值支援布林值的繫結,並且會生成一個在這裡不需要的Hidden元素,所以我們不得不在呼叫該方法的前後作一些手腳。

四、ListAttribute

現在我們來介紹應用在Employee屬性上的四個特性的定義。如下面的程式碼片斷所示,基於四種“列表控制元件”的特性均繼承自抽象特性ListAttribute。ListAttribute實現了IMetadataAware介面,在實現的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: }


五、模板View的定義

由於四個具體的ListAttribute已經對錶示模板名稱的ModelMetadata的TemplateHint進行了設定,那麼我們針對它們定義相應的分部View作為對應的模板,那麼在呼叫HtmlHelper/HtmlHelper相應模板方法的時候就會按照這些模板對目標元素進行呈現。實現如上圖所示的效果的四個模板定義如下,它們被儲存在ViewSharedEditorTemplates目錄下面。

   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

   

  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

   

  24: @{

   

  25:     string listName = (string)ViewData.ModelMetadata.AdditionalValues["ListName"];

   

  26:     @Html.CheckBoxList("", listName, Model)

   

  27:  }

 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1020/viewspace-2810171/,如需轉載,請註明出處,否則將追究法律責任。

相關文章