2011年9月12日 星期一

jQuery 對下拉選單 DropDownList 的操作 - 2:連動下拉選單


連動式下拉選單在網站中也算是相當常見的功能,在ASP.NET WebForm裡,很多開發者都會直接在UpdatePanel中做解決,但是這樣的方式其實對於效能來說並不是很好,而往往很多開發者若是沒有對UpdatePanel做好配置的話,常常會有些開發者整個網頁就只有一個UadetePanel,這樣的Ajax也只能說是做假的,而有的開發者如果細分到每個下拉選單都給一個UpdatePanel,那在開發上也蠻瑣碎的,因為頁面上要做Ajax效果的地方通常也不是只有下拉選單而已。

而在ASP.NET MVC的ViewPage則是比較沒有這種顧慮,因為在MVC是用不到UpdatePanel,充分搭配 jQuery 下,ASP.NET MVC所做的Ajax效果是可以比WebForm來得更為靈活以及輕巧,以下就介紹幾種在ASP.NET MVC下製作連動式下拉選單的方法。

 



在前面幾篇文章有介紹過在ASP.NET MVC下產生更為簡潔的下拉選單的方式:「ASP.NET MVC 後端產生DropDownList」,所以在Controller以及Service層產生下拉選單的詳細內容屆不再多做介紹,下圖為測試網頁的網頁畫面。

image

畫面上就很簡單的兩個下拉選單的物件,一個是 CityDDL而另一個是CountyDDL,一開是在CityDDL的內容是有值的,而CountyDDL除了第一個選項之外就沒有資料。

 

方法一:$.getJSON()與SelectListItem的搭配使用

首先看看Controller內的程式部分:

#region GetCountyDDL
/// <summary>
/// Gets the county DDL.
/// </summary>
/// <param name="cityId">The city id.</param>
/// <returns></returns>
public JsonResult GetCountyDDL(string cityId)
{
  List<SelectListItem> items = new List<SelectListItem>();
  if (!string.IsNullOrWhiteSpace(cityId))
  {
    var counties = this._service.GetCounties(cityId);
    foreach (var item in counties)
    {
      items.Add(new SelectListItem()
      {
        Text = string.Concat(item.POSTAL_CODE.ToString(), " ", item.NAME),
        Value = item.POSTAL_CODE.ToString()
      });
    }
    if (!items.Count.Equals(0))
    {
      items.Insert(0, new SelectListItem() { Text = "請選擇", Value = "" });
    }
  }
  return this.Json(items, JsonRequestBehavior.AllowGet);
} 
#endregion

在ASP.NET MVC中要產生下拉選單的內容,就會想到使用SelectListItem,所以這裡為了搭配前端使用$.getJSON(),所以後端就把產生的List<SelectListItem>給轉為JSON再傳給前端,

而前端的程式如下:

$(document.ready(function()
{
  //Method1
  $('#CityDDL').change(function () { ChangeCity(); });
});
function SetCountyDDLEmpty()
{
  $('#CountyDDL').empty();
  $('#CountyDDL').append($('<option></option>').val('').text('請選擇'));
}
function ChangeCity()
{
  var selectedCityId = $.trim($('#CityDDL option:selected').val());
  if (selectedCityId.length == 0)
  {
    SetCountyDDLEmpty();
  }
  else
  {
    //ActionUrls.GetCountyDDL 為Controller Action的URL
    $.getJSON(ActionUrls.GetCountyDDL, { cityId: selectedCityId }, function (data)
    {
      $('#CountyDDL').empty();
      $.each(data, function (i, item)
      {
        $('#CountyDDL').append($('<option></option>').val(item.Value).text(item.Text));
      });
    });
  }
}

上面的程式可以看到,當CityDDL觸發Change()事件後,於ChangeCity()裡面去取得CityDDL的被選取項目的值,然後使用$.getJSON()方法去取得後端程式所產生的JSON資料,接收到JSON資料後再以each()方法逐一的去增加CountyDDL的option項目,下圖是接收到後端傳回的回應內容以及JSON詳細內容,

image

image

其實這個方法不是很好,因為要在Controller中先將取出的資料集合去處理為List<SelectListItem>,而傳回到前端後,還要用迴圈方式處理資料去產生option後再append到下拉選單中,方式是很直覺,但是無論前端還是後端,都還需要額外處理。

 

方法二:於後端直接產生下拉選單的option資料

後端程式:

#region GetCountyOptions
/// <summary>
/// Gets the county options.
/// </summary>
/// <param name="cityId">The city id.</param>
/// <returns></returns>
[HttpPost]
public ActionResult GetCountyOptions(string cityId)
{
  StringBuilder sb = new StringBuilder();
  if (!string.IsNullOrWhiteSpace(cityId))
  {
    var counties = this._service.GetCounties(cityId);
    foreach (var item in counties)
    {
      sb.AppendFormat("<option value=\"{0}\">{1}</option>",
        item.POSTAL_CODE.ToString(),
        string.Concat(item.POSTAL_CODE.ToString(), " ", item.NAME)
      );
    }
  }
  return Content(sb.ToString());
} 
#endregion

在後端的Action中就不再去將資料處理成List<SelectListItem>,而是直接產生下拉選單的option內容,以字串的方式傳回前端,而前端就只需要將接收到的資料append到CountyDDL下拉選單就好。

前端程式:

$(document.ready(function()
{
  //Method2
  $('#CityDDL').change(function () { ChangeCountyOptions(); });
});
function SetCountyDDLEmpty()
{
  $('#CountyDDL').empty();
  $('#CountyDDL').append($('<option></option>').val('').text('請選擇'));
}
function ChangeCountyOptions()
{
  var selectedCityId = $.trim($('#CityDDL option:selected').val());
  if (selectedCityId.length == 0)
  {
    SetCountyDDLEmpty();
  }
  else
  {
    $.ajax(
    {
      url: ActionUrls.GetCountyOptions,
      data: { cityId: selectedCityId },
      type: 'post',
      cache: false,
      async: false,
      dataType: 'html',
      success: function (data)
      {
        if (data.length > 0)
        {
          $('#CountyDDL').append(data);
        }
      }
    });
  }
}

但這樣的方式,還是不夠好,因為後端還是需要在Controller的Action中去做一次迴圈處理,而後端的迴圈處理看來是無法避免,那就必須思考如何在後端將迴圈處理的部份給抽出,下圖所示為前端所接收的傳回內容。

image

因為這個方法還不是很好的作法,所以接下來再介紹另外的方法。

 

 

方法三:後端使用下拉選單產生方法,使用一致性方法簡化後端處理

在此篇文章的一開始就有說到,之前有篇文章「ASP.NET MVC 後端產生DropDownList」,就是介紹後端一致性的下拉選單產生方法,既然已經有個一致性的下拉選單產生方法,那在後端Action就可以拿來用。

後端程式部分:

[HttpPost]
public ActionResult GetCountyDDLHtml(string cityId)
{
  string tagIdName = "CountyDDL";
  string _html = string.Empty;
  if (!string.IsNullOrWhiteSpace(cityId))
  {
    _html = this._service.GetCountyDDL(cityId, tagIdName, null);
  }
  return Content(_html);
}

2011-09-14, 補充 Service 裡產生下拉選單 Html Tag 的程式部分:

/// <summary>
/// Gets the county DDL.
/// </summary>
/// <param name="tagIdName">Name of the tag id.</param>
/// <param name="selectedValue">The selected value.</param>
/// <returns></returns>
public string GetCountyDDL(string cityId, string tagIdName, string selectedValue)
{
  using (TestDBEntities db = new TestDBEntities())
  {
    var query = db.PostalCounty.Where(x=>x.CITY_ID == cityId).OrderBy(x => x.POSTAL_CODE);
    Dictionary<string, string> optionData = new Dictionary<string, string>();
    string keyText = string.Empty;
    foreach (var item in query)
    {
      keyText = string.Concat(item.POSTAL_CODE.ToString(), " ", item.NAME.Trim());
      if (optionData.Keys.Where(x => x == keyText).Count().Equals(0))
      {
        optionData.Add(keyText, item.POSTAL_CODE.ToString());
      }
    }
    if (string.IsNullOrWhiteSpace(tagIdName))
    {
      tagIdName = "CountyDDL";
    }
    string _html = DropDownListHelper.GetDropdownList(tagIdName, tagIdName, optionData, null, selectedValue, true, null);
    return _html;
  }
} 

而前端所接收到的傳回內容是這樣:

image

傳回一個完整的下拉選單Html Tag,而前端程式的處理,我們則是會使用jQuery的replaceWith()

jQuery .replaceWith()

因為我們要使用後端所傳回的HTML Tag內容去取代既有的CountyDDL,所以使用 .replaceWith()方法,注意喔,使用 .replaceWith() 其Content內容必須是完整正確的Html Tag內容,如此就可以確保頁面上的物件變化內容會減少因為變動所帶來的影響(如內容的錯誤…etc),而且replaceWith()是各種瀏覽器版本都可以使用的!

前端程式部分:

$(document.ready(function()
{
  //Method4
  $('#CityDDL').change(function () { ChangeCountyUseReplaceWith(); });
});
function SetCountyDDLEmpty()
{
  $('#CountyDDL').empty();
  $('#CountyDDL').append($('<option></option>').val('').text('請選擇'));
}
function ChangeCountyUseReplaceWith()
{
  var selectedCityId = $.trim($('#CityDDL option:selected').val());
  if (selectedCityId.length == 0)
  {
    SetCountyDDLEmpty();
  }
  else
  {
    $.ajax(
    {
      url: ActionUrls.GetCountyDDLHtml,
      data: { cityId: selectedCityId },
      type: 'post',
      cache: false,
      async: false,
      dataType: 'html',
      success: function (data)
      {
        if (data.length > 0)
        {
          $('#CountyDDL').replaceWith(data);
        }
      }
    });
  }
}

 

 

最不推的方法:使用非W3C標準的outerHTML方法

為什麼最不推薦呢?因為outerHTML不是一個W3C標準的JavaScript方法

雖然現在各種瀏覽器幾乎都有支援,但Firefox就是不支援這個方法,以下的連結內容可以了解哪些瀏覽器版本有支援。

W3C DOM Compatibility - HTML - outerHTML

outerHTML這個方法是「可以用來更改或獲取元素內所有的html和文本內容,包含引用該方法元素自身的標籤.」

在上個方法中,已經可以使用jQuery的replaceWith()來達成,所以並不需要用outerHTML()來做為解決方法。

而為了要讓Firefox可以支援outerHTML(),在網路上也就有人提供了以下的解決方案:

//================== 解決 FireFox 不支援 outerHTML 的問題 =====================================
if ($.browser.mozilla)
{
  if (typeof (HTMLElement) != "undefined" && !window.opera)
  {
    HTMLElement.prototype.__defineGetter__("outerHTML", function ()
    {
      var a = this.attributes, str = "<" + this.tagName, i = 0;
      for (; i < a.length; i++)
      {
        if (a[i].specified)
        {
          str += " " + a[i].name + '="' + a[i].value + '"';
        }
      }
      if (!this.canHaveChildren)
      {
        return str + " />";
      }
      return str + ">" + this.innerHTML + "</" + this.tagName + ">";
    });
    HTMLElement.prototype.__defineSetter__("outerHTML", function (s)
    {
      var r = this.ownerDocument.createRange();
      r.setStartBefore(this);
      var df = r.createContextualFragment(s);
      this.parentNode.replaceChild(df, this);
      return s;
    });
    HTMLElement.prototype.__defineGetter__("canHaveChildren", function ()
    {
      return !/^(area|base|basefont|col|frame|hr|img|br|input|isindex|link|meta|param)$/.test(this.tagName.toLowerCase());
    });
  }
}
//================== 解決 FireFox 不支援 outerHTML 的問題 =====================================

後端Controller的Action部分沿用方法三的內容。

而前端程式的部份:

$(document.ready(function()
{
  //Method4
  $('#CityDDL').change(function () { ChangeCountyOuterHtml(); });
});
function SetCountyDDLEmpty()
{
  $('#CountyDDL').empty();
  $('#CountyDDL').append($('<option></option>').val('').text('請選擇'));
}
function ChangeCountyOuterHtml()
{
  var selectedCityId = $.trim($('#CityDDL option:selected').val());
  if (selectedCityId.length == 0)
  {
    SetCountyDDLEmpty();
  }
  else
  {
    $.ajax(
    {
      url: ActionUrls.GetCountyDDLHtml,
      data: { cityId: selectedCityId },
      type: 'post',
      cache: false,
      async: false,
      dataType: 'html',
      success: function (data)
      {
        if (data.length > 0)
        {
          $('#CountyDDL')[0].outerHTML = data;
        }
      }
    });
  }
}

由上面的前端程式內容可以看得出來我不推薦的原因之一,那就是還要把已經用jQuery Selector取出的jQuery物件再去轉為DOM物件…… 畫蛇添足多增加了一個讓Firefox支援 outerHTML()的方法外,還要去額外處理轉換的動作,反正就是「多此一舉」!

 


以上幾種方法,如果專案中沒有使用下拉選單產生方法,那麼方法一會是比較直覺的處理方式,如果有使用一致的下拉選單產生方法,我會推薦方法三,至於方法二……也不是很推薦使用,而最後的方法,也是一樣把它給忘了,千萬不要做傻事!!!

 

以上。

2 則留言:

  1. 您好:
    真的非常抱歉,想請問要如何連結至指定的檔案呢?是否有完整的語法,不好意思,是否可以麻煩拜託請您教教我呢?千萬拜託,感激不盡,謝謝您喔!

    回覆刪除
    回覆
    1. 啊?為什麼同樣的問題要連貼三篇文章,而且跟文章主題不一樣呀

      刪除

提醒

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