2011年7月17日 星期日

ASP.NET MVC 後端產生 DropDownList

其實我很少在ViewPage去直接下Html.DropDownList(…….)的指令去產生下拉選單,因為我覺得我還需要另外去處理 IEnumerable<SelectListItem>,如此我才會有下拉選單選項,所以我通常會在Controller/Action中就處理好要產生的下拉選單Html,放到ViewData中,再由前端ViewPage於頁面中直接做呈現就可以。

會這麼做的原因是,不希望有太多的程式判斷出現在ViewPage之中,因為ViewPage的程式常常會是ASP.NET MVC開發時的一個大漏洞,ASP.NET MVC 於編譯的時侯,會預設不去編譯 ViewPage 的,如果不知道去更改設定的人,系統開發時,如果喜歡在ViewPage中加入一堆的程式,而編譯時不會出現錯誤訊息,但是一旦到了開啟頁面做偵錯時才發現到頁面上的那一段程式出了錯誤,所以為了減少這樣的問題出現,除了更改編譯設定外,就是減少於ViewPage寫程式的機會。

有關ASP.NET MVC更改編譯設定,讓系統重新建置時也對ViewPage做編譯,請看以下的文章介紹:

Will保哥:ASP.NET MVC 開發心得分享 (11):對 Views 進行編譯檢查

黃偉榮的學習筆記:解決TFS Build Asp.Net Mvc開啟MvcBuildViews後無法載入組件問題


進入主題…

先看一個用原有的Html.DropDownList方式產生一個下拉選單,先在後端Controller/Action裡面準備好要給DropDownList的選項資料,

image

而在前端的ViewPage則是以下面的方式接後端存放資料給前端的ViewData,前端先判斷是否為null後再轉為IEnumerable<SelectListItem>

image

可以看到前端ViewPage還必須要要去判斷後端的資料有沒有產生並存放在ViewData中,總覺得這樣的方式是很彈性,但是ViewPage必須要加上程式來做判斷,如果有個方式可以在後端就將下拉選單的Select Html Code就產生好,前端只需要做顯示的工作就好,

ASP.NET MVC有個非正式的設計口訣「Model要重、Controller要輕、View要笨」

所謂View要笨,就是說View盡量讓他做最簡單的就好,最簡單的就是後端給什麼、View就顯示什麼就好。如果View中有很多的程式要加上去,也就是說這樣的View其會造成的變動可能性太大,這時候應該要做的應該是要檢討 View有很多程式是不是正確的(並不是指前端的JavaScript程式)。

而且既然說可以在同一個程序中將View的下拉選單選項資料集合給準備好,那為什麼不一次到位,就在Controller/Action裡就把下拉選單給產生出來,前端就只負責顯示即可。

說了這麼多,先讓大家看看,整個完成並且實際使用的程式。

image

前端就很簡單,只要負責顯示就可以。

image

image

兩種方式所產生的Html Code是相同的,而有可能會有人說,那如果表單(Form)送回到後端然後在View上面是否能夠保持原有的選項呢?當然可以,完全都在後端處理,前端ViewPage完全不用去判斷.

後端Controller/Action

  1:     public ActionResult DropDownListTest()
  2:     {
  3:       var cities = _cityService.GetCollection();
  4: 
  5:       //後端就產生好DropDwonList Html Code
  6:       //以IDictionary傳入下拉選單的值
  7:       Dictionary<string, string> dict = new Dictionary<string, string>();
  8:       foreach (var city in cities)
  9:       {
 10:         dict.Add(city.NAME, city.SORT.ToString());
 11:       }
 12:       ViewData["CityDDL2"] = DropDownListHelper.GetDropdownList
 13:       (
 14:         "CityDDL2", 
 15:         dict, 
 16:         new { id = "CityDDL2" }, 
 17:         null, 
 18:         true, 
 19:         "請選一個城市"
 20:       );
 21:       return View();
 22:     }
 23: 
 24:     [HttpPost]
 25:     public ActionResult DropDownListTest(FormCollection collection)
 26:     {
 27:       var cities = _cityService.GetCollection();
 28:       Dictionary<string, string> dict = new Dictionary<string, string>();
 29:       foreach (var city in cities)
 30:       {
 31:         dict.Add(city.NAME, city.SORT.ToString());
 32:       }
 33:       ViewData["CityDDL2"] = DropDownListHelper.GetDropdownList
 34:       (
 35:         "CityDDL2", 
 36:         dict, 
 37:         new { id = "CityDDL2" },
 38:         collection["CityDDL2"] ?? null, 
 39:         true, 
 40:         "請選一個城市"
 41:       ); 
 42:       return View();
 43:     }

前端

  1:   <form id="form2" action="<%= Url.Action("DropDownListTest", "Home") %>" method="post">
  2:     <%= ViewData["CityDDL2"] %>
  3:     <input type="submit" value="送出" />
  4:   </form>

看看執行Submit之後的結果

image

 


以下就是後端產生DropDownList的Method內容
    /// <summary>
    /// 產生下拉選單html(以IDictionary傳入下拉選單的值).
    /// </summary>
    /// <param name="tagName">拉選單的Tag Name</param>
    /// <param name="optionData">下拉選單Option的Text與Value.</param>
    /// <param name="htmlAttributes">The HTML attributes.</param>
    /// <param name="defaultSelectValue">預選值.</param>
    /// <param name="appendOptionLabel">是否加入預設空白選項.</param>
    /// <param name="optionLabel">如果appendOptionLabel為true,optionLabel為第一個項目要顯示的文字,如果沒有指定則顯示[請選擇].</param>
    /// <returns></returns>
    public static string GetDropdownList(string name, IDictionary<string, string> optionData, object htmlAttributes, string defaultSelectValue, bool appendOptionLabel, string optionLabel)
    {
      if (string.IsNullOrEmpty(name))
      {
        throw new ArgumentNullException("name", "產生DropDownList時 tag Name 不得為空");
      }
      TagBuilder select = new TagBuilder("select");
      select.Attributes.Add("name", name);
      StringBuilder renderHtmlTag = new StringBuilder();
      IDictionary<string, string> newOptionData = new Dictionary<string, string>();
      if (appendOptionLabel)
      {
        newOptionData.Add(new KeyValuePair<string, string>(optionLabel ?? "請選擇", ""));
      }
      foreach (var item in optionData)
      {
        newOptionData.Add(item);
      }
      foreach (var option in newOptionData)
      {
        TagBuilder optionTag = new TagBuilder("option");
        optionTag.Attributes.Add("value", option.Value);
        if (!string.IsNullOrEmpty(defaultSelectValue) && defaultSelectValue.Equals(option.Value))
        {
          optionTag.Attributes.Add("selected", "selected");
        }
        optionTag.SetInnerText(option.Key);
        renderHtmlTag.AppendLine(optionTag.ToString(TagRenderMode.Normal));
      }
      select.MergeAttributes(new RouteValueDictionary(htmlAttributes));
      select.InnerHtml = renderHtmlTag.ToString();
      return select.ToString();
    }

也許有人會覺得為什麼要這樣設計一個後端產生DropDownList的方法,用原本方式不就好了嗎?

我在前面已有說過,這麼做是要讓會產生變動的可能性降到最低,而ViewPage要去做程式判斷,這就是一個變動的因子,既然List<SelectListItem>是從ViewData中取出,而ViewData內的資料是從後端產生的,那同樣是Server要處理的程式,為什麼不把放在ViewPage中的程式在Controller/Action一次處理完畢呢?

ViewPage就是讓他簡單、單純的呈現後端提供的資料就好,另外,用IDictionary<string, string>來當做資料集合容器,這是因為直接、簡單,只要將要顯示在Option Text、Value給存放到IDictionary<string, string>即可,或許有人也會說,不就是這樣嗎?下拉選單的選項不就是給Key跟Value而已。

相信我,我就遇到一個實際遇到並且讓我訝異不已的方式,這個要給下拉選單產生option的資料集合,竟然是給TModel,就是直接給 EF或Linq To SQL的Model集合… 而產生的TModel資料集合,找了兩個任意欄位(最好是string),然後欄位的資料再塞要顯示的Key與Value資料,

大致上如下:

public static string GetDropdownList(string name, IQueryable<TModel> models, object htmlAttributes, string[] params)
{
  //params,長度為3的字串陣列
  //params[0]是要做為顯示 Option Text的TModel欄位名稱
  //params[1]是要做為顯示 Option Value的TModel欄位名稱
  //params[2]是要做為預設選項的Value
}
實際使用
ProjectHelper.GetDropdownList("fooDDL", models, { id = "fooDDL" }, { "CODE_ID", "TITLE_ID", "abcdefg" });

這不是很奇怪,為什麼要去改變從資料庫所取出的資料呢?然後去重組或是改變既有欄位內的資料,這麼做只為了想要用IQueryable<TModel>的資料集合直接給這個Method,想要用一個方法、一次的處理就做完動作。

只是為了想要達到這樣的目的,就去任意扭曲資料的內容,這真的是很奇怪的作法,非常的不鼓勵,從資料所取出的資料,其所對應的欄位都是各有意義的,去改變一個實體的資料也只有在刪除或是修改的時候。

總之一句話,「不是只把事情做對就好,把事情做對固然重要,但是用正確的作法去做對才是更重的。」

把事情做正確,而不是只把事情做對而已。

 

以上

10 則留言:

  1. 如果 View 是用 razor engine
    razor 語法 應該怎樣寫才對?
    試過 @ViewData["CityDDL2"]
    結果會經過 htmlencode

    回覆刪除
    回覆
    1. 解:@Html.Raw(ViewData["CityDDL2"])

      參考一:
      C# Razor Syntax Quick Reference
      http://goo.gl/q91hB

      參考二:
      Introduction to ASP.NET Web Programming Using the Razor Syntax (C#)
      http://goo.gl/bQE4L

      刪除
  2. Kevin大,你好:
    如果想採用你的後端 DropDownList的方法,要如何結合 ViewModel 的作法呢?

    回覆刪除
    回覆
    1. 老實說,其實我不太懂你的需求....

      刪除
    2. Kevin大,你好:
      不好意思,表達不清楚~

      讀過文章後,知道了,後端 DropDownList 方法回傳string,丟給 ViewData["CityDDL2"]~
      之後以 @Html.Raw(ViewData["CityDDL2"]) 方式,將結果畫出來

      目前自己的做法,想將 DropDownList selected的結果存回db,使用了 FormCollection collection
      抓取前端 collection["CityDDL2"]的資料寫入db

      但看過其他文章後,想採用 ViewModel 的方式來取得 DropDownList selected 結果
      不知道有甚麼方向可以思考與實踐這個做法?

      感謝 Kevin大

      刪除
    3. ViewModel 的話,別想得太過複雜,不過就是個類別而已,
      只要在 ViewModel 裡的屬性與名稱在頁面上有符合的內容就會自動 Binding
      所以你需要把這個後端產生的 DropDownList 名稱設定跟 ViewModel 的指定屬性名稱相同
      這樣應該就可以達成你的需求

      刪除
    4. Kevin大,你好:
      不好意思,晚兩天回覆,感謝,看了您 ViewModel 系列文章,做了練習,有些成果了,謝謝您的分享~

      刪除
    5. 感謝你的回應,很高興對你有些幫助。

      刪除
  3. 您好凱文,

    你的解釋都非常好,將使而不是使用下拉列表的例子,使用的CheckedListBox。

    回覆刪除
  4. 網誌管理員已經移除這則留言。

    回覆刪除

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。