網頁

2013年9月23日 星期一

ASP.NET MVC 建立可重複使用的縣市鄉鎮市區連動下拉選單 (Reuseable Cascade DropDownList)

這個部落格裡,光是連動下拉選單為主題的文章我就寫了好幾篇,如下:

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

jQuery 練習題:三層式連動下拉選單(無後端整合)

ASP.NET MVC 3 - 基本三層連動式下拉選單應用

jQuery 練習題:ASP.NET MVC 連動下拉選單與 jQuery UI Autocomplete ComboBox

連動下拉選單 - 使用 jQuery EasyUI ComboBox

其實連動下拉選單的功能並不難做,當頁面上需要有連動下拉選單功能時,都需要再去動手做,從頁面、Javascript 程式以及後端程式,一次兩次的需求的話是還好,不會太麻煩,但是有很多頁面都需要做連動下拉選單的時候,就會覺得很麻煩了,最常見的就是「縣市鄉鎮市區連動下拉選單」,尤其是當有一堆表單或是同一個頁面上有很多個別欄位都需要縣市與鄉鎮市區資料的時候。

所以這邊就使用 ASP.NET MVC 的 Partial View 把縣市鄉鎮市區連動下拉選單的做成可以重複使用,而且可以應付同一個頁面有多組資料都需要縣市、鄉鎮市區連動下拉選單的需求。

P.S. 此功能是顯示台灣的縣市與鄉鎮市區資料。

 


Step.1

使用 ASP.NET MVC 4 with Bootstrap Layout 建立新專案,

image

有關「ASP.NET MVC 4 with Bootstrap Layout」的相關訊息可以參閱之前的文章:「使用 ASP.NET MVC 4 Bootstrap Layout Template (VS2012)

建立完成的專案內容:

image

 

Step.2

加入 LocalDB 並且匯入資料以及建立 ADO.NET 實體資料模型

image

ZipCode

image

image

建立 ADO.NET 實體資料模型

image

SNAGHTML37f800

SNAGHTML387232

image

 

Step.3

建立 LocalDB 與 ADO.NET 實體資料模型之後,正當要重新建置專案卻出現了錯誤訊息,

image

這是因為 「ASP.NET MVC 4 with Bootstrap Layout」這個 Project Template 所建立的專案檔預設是會對 View 進行編譯的,

image

image

如果把 csproj 檔案裡的 MvcBuildViews 修改為 false,那麼編譯就不會報錯,但是還是要解決 MvcBuildViews 為 true 時編譯出現錯誤的問題,解決的方式就是在專案根目錄下的 Web.Config 去加入 System.Data.Entity.Design 的 Assembly,

image

<compilation debug="true" targetFramework="4.5">
    <assemblies>
        <add assembly="System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
        <add assembly="System.Data.Entity.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    </assemblies>
</compilation>

 

Step.4

建立 IZipCodeService 與其實作 ZipCodeService,

image

image

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Linq.Expressions;
   5: using System.Web;
   6: using ReuseableCascadeDropdownlist.Models;
   7:  
   8: namespace ReuseableCascadeDropdownlist.Services
   9: {
  10:     public class ZipCodeService : IZipCodeService
  11:     {
  12:         private DatabaseEntities db = new DatabaseEntities();
  13:  
  14:         /// <summary>
  15:         /// Determines whether the specified predicate is exists.
  16:         /// </summary>
  17:         /// <param name="predicate">The predicate.</param>
  18:         /// <returns></returns>
  19:         public bool IsExists(Expression<Func<ZipCode, bool>> predicate)
  20:         {
  21:             return this.db.ZipCode.Any(predicate);
  22:         }
  23:  
  24:         /// <summary>
  25:         /// Totals the count.
  26:         /// </summary>
  27:         /// <param name="predicate">The predicate.</param>
  28:         /// <returns></returns>
  29:         public int TotalCount(Expression<Func<ZipCode, bool>> predicate)
  30:         {
  31:             return this.db.ZipCode.Count(predicate);
  32:         }
  33:  
  34:  
  35:         /// <summary>
  36:         /// Finds the one.
  37:         /// </summary>
  38:         /// <param name="id">The identifier.</param>
  39:         /// <returns></returns>
  40:         public ZipCode FindOne(int id)
  41:         {
  42:             if (!this.IsExists(x => x.ID == id))
  43:             {
  44:                 return null;
  45:             }
  46:             if (!this.IsExists(x => x.ID == id))
  47:             {
  48:                 return null;
  49:             }
  50:             return this.db.ZipCode.FirstOrDefault(x => x.ID == id);
  51:         }
  52:  
  53:         /// <summary>
  54:         /// Finds the one by postal code.
  55:         /// </summary>
  56:         /// <param name="postalCode">The postal code.</param>
  57:         /// <returns></returns>
  58:         /// <exception cref="System.ArgumentNullException">沒有輸入 PostalCode</exception>
  59:         public ZipCode FindOneByPostalCode(int postalCode)
  60:         {
  61:             if (!this.IsExists(x => x.PostalCode == postalCode))
  62:             {
  63:                 return null;
  64:             }
  65:             if (!this.IsExists(x => x.PostalCode == postalCode))
  66:             {
  67:                 return null;
  68:             }
  69:             return this.db.ZipCode.FirstOrDefault(x => x.PostalCode == postalCode);
  70:         }
  71:  
  72:         /// <summary>
  73:         /// Finds all.
  74:         /// </summary>
  75:         /// <returns></returns>
  76:         public IQueryable<ZipCode> FindAll()
  77:         {
  78:             return this.db.ZipCode.AsQueryable();
  79:         }
  80:  
  81:         /// <summary>
  82:         /// Finds the specified predicate.
  83:         /// </summary>
  84:         /// <param name="predicate">The predicate.</param>
  85:         /// <returns></returns>
  86:         /// <exception cref="System.NotImplementedException"></exception>
  87:         public IQueryable<ZipCode> Find(Expression<Func<ZipCode, bool>> predicate)
  88:         {
  89:             return this.db.ZipCode.Where(predicate);
  90:         }
  91:  
  92:  
  93:         /// <summary>
  94:         /// Gets all cities.
  95:         /// </summary>
  96:         /// <returns></returns>
  97:         public Dictionary<string, string> GetAllCities()
  98:         {
  99:             var query = (from c in this.FindAll()
 100:                          where c.IsEnabled
 101:                          select new
 102:                          {
 103:                              CityCode = c.CitySort.ToString(),
 104:                              CityName = c.City
 105:                          })
 106:                         .Distinct().OrderBy(x => x.CityCode);
 107:  
 108:             return query.ToDictionary(x => x.CityCode, x => x.CityName);
 109:         }
 110:  
 111:         /// <summary>
 112:         /// Gets all city dictinoary.
 113:         /// </summary>
 114:         /// <returns></returns>
 115:         /// <exception cref="System.NotImplementedException"></exception>
 116:         public Dictionary<string, string> GetAllCityDictinoary()
 117:         {
 118:             var query = (from c in this.FindAll()
 119:                          where c.IsEnabled
 120:                          select new
 121:                          {
 122:                              CityCode = c.CitySort,
 123:                              CityName = c.City
 124:                          })
 125:                         .Distinct().OrderBy(x => x.CityCode);
 126:  
 127:             Dictionary<string, string> dict = new Dictionary<string, string>();
 128:  
 129:             foreach (var item in query)
 130:             {
 131:                 if (dict.Keys.Count(x => x.Equals(item.CityName)).Equals(0))
 132:                 {
 133:                     dict.Add(item.CityName, item.CityName);
 134:                 }
 135:             }
 136:             return dict;
 137:         }
 138:  
 139:         /// <summary>
 140:         /// Gets the name of the county by city.
 141:         /// </summary>
 142:         /// <param name="cityName">Name of the city.</param>
 143:         /// <returns></returns>
 144:         public Dictionary<string, string> GetCountyByCityName(string cityName)
 145:         {
 146:             var query = (from c in this.FindAll()
 147:                          where c.IsEnabled && c.City == cityName
 148:                          select new
 149:                          {
 150:                              PostalCode = c.PostalCode,
 151:                              CountyName = c.County,
 152:                              Sort = c.PostalCode
 153:                          })
 154:                         .Distinct().OrderBy(x => x.Sort);
 155:  
 156:             return query.ToDictionary(x => x.PostalCode.ToString(), x => x.CountyName);
 157:         }
 158:  
 159:  
 160:         public void Dispose()
 161:         {
 162:             this.db.Dispose();
 163:         }
 164:     }
 165: }

 

Step.5

建立 ZipCodeController

image

2014-07-03 更新:
誤把 ZipCodeService.cs 的程式內容給貼上來,而沒有貼上正確的 ZipCodeController.cs,不過在 GitHub 上面所提供的程式並沒有問題,都是正確且可以執行。

using System.Text;
using System.Web.Mvc;
using ReuseableCascadeDropdownlist.Services;
 
namespace ReuseableCascadeDropdownlist.Controllers
{
    public class ZipCodeController : Controller
    {
        private readonly IZipCodeService service;
 
        public ZipCodeController()
        {
            this.service = new ZipCodeService();
        }
 
        /// <summary>
        /// Gets the city drop downlist.
        /// </summary>
        /// <param name="selectedCity">The selected city.</param>
        /// <returns></returns>
        public ActionResult GetCityDropDownlist(string selectedCity)
        {
            StringBuilder sb = new StringBuilder();
 
            var cities = this.service.GetAllCityDictinoary();
 
            if (string.IsNullOrWhiteSpace(selectedCity))
            {
                foreach (var item in cities)
                {
                    sb.AppendFormat("<option value=\"{0}\">{1}</option>", item.Key, item.Value);
                }
            }
            else
            {
                foreach (var item in cities)
                {
                    sb.AppendFormat("<option value=\"{0}\" {2}>{1}</option>",
                        item.Key,
                        item.Value,
                        item.Key.Equals(selectedCity) ? "selected=\"selected\"" : "");
                }
            }
            return Content(sb.ToString());
        }
 
        /// <summary>
        /// Gets the county drop downlist.
        /// </summary>
        /// <param name="cityName">Name of the city.</param>
        /// <param name="selectedCounty">The selected county.</param>
        /// <returns></returns>
        public ActionResult GetCountyDropDownlist(string cityName, string selectedCounty)
        {
            if (!string.IsNullOrWhiteSpace(cityName))
            {
                StringBuilder sb = new StringBuilder();
 
                var counties = this.service.GetCountyByCityName(cityName);
 
                if (string.IsNullOrWhiteSpace(selectedCounty))
                {
                    foreach (var item in counties)
                    {
                        sb.AppendFormat("<option value=\"{0}\">{1}</option>",
                            item.Key,
                            string.Concat(item.Key, " ", item.Value)
                        );
                    }
                }
                else
                {
                    foreach (var item in counties)
                    {
                        sb.AppendFormat("<option value=\"{0}\" {2}>{1}</option>",
                            item.Key,
                            string.Concat(item.Key, " ", item.Value),
                            item.Key.Equals(selectedCounty) ? "selected=\"selected\"" : "");
                    }
                }
 
                return Content(sb.ToString());
            }
            return Content(string.Empty);
        }
 
 
        protected override void Dispose(bool disposing)
        {
            this.service.Dispose();
            base.Dispose(disposing);
        }
    }
}

 

Step.6

建立 TaiwanZipCode.js

image

   1: ;
   2: (function (window) {
   3:     //===========================================================================================
   4:     if (typeof (jQuery) === 'undefined') { alert('jQuery Library NotFound.'); return; }
   5:  
   6:     var TaiwanZipCode = window.TaiwanZipCode =
   7:     {
   8:         ActionUrls: {}
   9:     };
  10:     //===========================================================================================
  11:  
  12:     jQuery.extend(TaiwanZipCode, {
  13:  
  14:         Initialize: function (actionUrls) {
  15:             /// <summary>初始化函式</summary>
  16:             /// <param name="actionUrls" type="Object"></param>
  17:  
  18:             jQuery.extend(TaiwanZipCode.ActionUrls, actionUrls);
  19:         },
  20:  
  21:         Settings: function (options) {
  22:  
  23:             //var options = {
  24:             //    CityID: '縣市下拉選單 Tag ID',
  25:             //    CountyID: '鄉鎮市區下拉選單 Tag ID',
  26:             //    SelectedCity: '已選擇縣市',
  27:             //    SelectedCounty: '已選擇鄉鎮市區'
  28:             //};
  29:  
  30:             TaiwanZipCode.SetCityDropDownlist(options.CityID, options.SelectedCity);
  31:             $(options.CityID).change(function () {
  32:                 TaiwanZipCode.SetCountyDropDownlist(options.CityID, options.CountyID, options.SelectedCounty);
  33:             });
  34:             $(options.CityID).trigger('change');
  35:         },
  36:  
  37:         SetCityDropDownlist: function (cityDropDownListID, selectedCityCode) {
  38:             /// <summary>設定縣市下拉選單</summary>
  39:             /// <param name="cityDropDownListID" type="Object">縣市下拉選單ID</param>
  40:             /// <param name="selectedCityCode" type="Object">預選縣市編號</param>
  41:  
  42:             $(cityDropDownListID).empty().append($('<option></option>').val('').text('請選擇'));
  43:             $.ajax({
  44:                 url: TaiwanZipCode.ActionUrls.GetCityDropDownlist,
  45:                 data: { selectedCity: selectedCityCode },
  46:                 type: 'post',
  47:                 cache: false,
  48:                 async: false,
  49:                 dataType: 'html',
  50:                 success: function (data) {
  51:                     if (data.length > 0) {
  52:                         $(cityDropDownListID).append(data);
  53:                     }
  54:                 }
  55:             });
  56:         },
  57:  
  58:         SetCountyDropDownlist: function (cityDropDownListID, countyDropDownListID, selectedPostalCode) {
  59:             /// <summary>設定鄉鎮市區下拉選單</summary>
  60:             /// <param name="cityDropDownListID" type="Object">縣市下拉選單ID</param>
  61:             /// <param name="countyDropDownListID" type="Object">鄉鎮市區下拉選單ID</param>
  62:             /// <param name="selectedPostalCode" type="Object">預選鄉鎮市區號</param>
  63:  
  64:             var selectedCity = $.trim($(cityDropDownListID + ' option:selected').val());
  65:             $(countyDropDownListID).empty().append($('<option></option>').val('').text('請選擇'));
  66:             $.ajax({
  67:                 url: TaiwanZipCode.ActionUrls.GetCountyDropDownlist,
  68:                 data: { cityName: selectedCity, selectedCounty: selectedPostalCode },
  69:                 type: 'post',
  70:                 cache: false,
  71:                 async: false,
  72:                 dataType: 'html',
  73:                 success: function (data) {
  74:                     if (data.length > 0) {
  75:                         $(countyDropDownListID).append(data);
  76:                     }
  77:                 }
  78:             });
  79:         }
  80:  
  81:     });
  82: })
  83: (window);

 

Step.7

在 ~/Views/Shared 目錄下建立 Partial View「_TanwanZipCode.cshtml」

image

   1: <script src="~/Scripts/TaiwanZipCode.js"></script>
   2: <script type="text/javascript">
   3:     $(function () {
   4:         var ActionUrls =
   5:         {
   6:             GetCityDropDownlist: '@Url.Action("GetCityDropDownlist", "ZipCode", new { Area = "" })',
   7:             GetCountyDropDownlist: '@Url.Action("GetCountyDropDownlist", "ZipCode", new { Area = "" })'
   8:         };
   9:         TaiwanZipCode.Initialize(ActionUrls);
  10:     });
  11: </script>

 

使用一

前面的步驟完成了縣市、鄉鎮市區下拉選單的後端程式、前端程式,而接下來要實際使用在頁面上,先建立一個 SampleController,並且加入一個檢視,在這個檢視頁面上放置兩個下拉選單,分別為縣市與鄉鎮市區,

image

接著就是在這個檢視頁面上使用剛才所建立的 TaiwanZipCode,Partial View「_TaiwanZipCode.cshtml」的內容其實是用來設定取得縣市與鄉鎮市區資料的連結位置,所以放置的地方會在 scripts 這個 setcion 裡,也就是載入 jQuery 之後的地方,

image

執行結果:

ReusableCascadeDropdownlist_1

 

使用二

在頁面上如果有兩組資料都需要使用到縣市鄉鎮市區連動下拉選單,則兩組資料所使用的 Select Tag 必須要有所區隔,除此之外,使用上的方式與上面是相同的,

   1: @{
   2:     ViewBag.Title = "Index2";
   3: }
   4:  
   5: <h2>Sample - 兩組資料</h2>
   6:  
   7: <form class="form-horizontal well">
   8:     <fieldset>
   9:         <legend>Personal</legend>
  10:         <div class="control-group">
  11:             <label class="control-label" for="PersonalCityDDL">縣市</label>
  12:             <div class="controls">
  13:                 <select id="PersonalCityDDL"></select>
  14:             </div>
  15:         </div>
  16:         <div class="control-group">
  17:             <label class="control-label" for="PersonalCountyDDL">鄉鎮市區</label>
  18:             <div class="controls">
  19:                 <select id="PersonalCountyDDL"></select>
  20:             </div>
  21:         </div>
  22:     </fieldset>
  23: </form>
  24:  
  25: <form class="form-horizontal well">
  26:     <fieldset>
  27:         <legend>Company</legend>
  28:         <div class="control-group">
  29:             <label class="control-label" for="CompanyCityDDL">縣市</label>
  30:             <div class="controls">
  31:                 <select id="CompanyCityDDL"></select>
  32:             </div>
  33:         </div>
  34:         <div class="control-group">
  35:             <label class="control-label" for="CompanyCountyDDL">鄉鎮市區</label>
  36:             <div class="controls">
  37:                 <select id="CompanyCountyDDL"></select>
  38:             </div>
  39:         </div>
  40:     </fieldset>
  41: </form>

image

執行結果:

ReusableCascadeDropdownlist_2

 

使用三

在編輯的時候會先把原本的資料給帶出來,那麼在這個連動下拉選單也可以設定已選擇的資料,無論是在後端使用 ViewBag 或 Model 還是在前端程式裡設定都可以,

建立檢視頁面所需要用到的 Model,

image

Controller 的設定

image

檢視頁面的 Html 內容

   1: @model ReuseableCascadeDropdownlist.ViewModels.SampleViewModel
   2: @{
   3:     ViewBag.Title = "Index3";
   4: }
   5:  
   6: <h2>Sample - 設定已選擇項目</h2>
   7:  
   8: <form class="form-horizontal well">
   9:     <fieldset>
  10:         <legend>Normal</legend>
  11:         <div class="control-group">
  12:             <label class="control-label" for="CityDDL">縣市</label>
  13:             <div class="controls">
  14:                 <select id="CityDDL"></select>
  15:             </div>
  16:         </div>
  17:         <div class="control-group">
  18:             <label class="control-label" for="CountyDDL">鄉鎮市區</label>
  19:             <div class="controls">
  20:                 <select id="CountyDDL"></select>
  21:             </div>
  22:         </div>
  23:     </fieldset>
  24: </form>
  25:  
  26: <form class="form-horizontal well">
  27:     <fieldset>
  28:         <legend>Personal</legend>
  29:         <div class="control-group">
  30:             <label class="control-label" for="PersonalCityDDL">縣市</label>
  31:             <div class="controls">
  32:                 <select id="PersonalCityDDL"></select>
  33:             </div>
  34:         </div>
  35:         <div class="control-group">
  36:             <label class="control-label" for="PersonalCountyDDL">鄉鎮市區</label>
  37:             <div class="controls">
  38:                 <select id="PersonalCountyDDL"></select>
  39:             </div>
  40:         </div>
  41:     </fieldset>
  42: </form>
  43:  
  44: <form class="form-horizontal well">
  45:     <fieldset>
  46:         <legend>Company</legend>
  47:         <div class="control-group">
  48:             <label class="control-label" for="CompanyCityDDL">縣市</label>
  49:             <div class="controls">
  50:                 <select id="CompanyCityDDL"></select>
  51:             </div>
  52:         </div>
  53:         <div class="control-group">
  54:             <label class="control-label" for="CompanyCountyDDL">鄉鎮市區</label>
  55:             <div class="controls">
  56:                 <select id="CompanyCountyDDL"></select>
  57:             </div>
  58:         </div>
  59:     </fieldset>
  60: </form>

Javascript 設定內容

image

執行結果

image

 

GitHub

已經把此次實作的專案發佈到 GitHub 上,Repository 位置如下:

https://github.com/kevintsengtw/MVC_Reuseable_Cascade_DropDownList

image

 


這次算是做個簡單的應用,利用 ASP.NET MVC Partial View 的特性以及前端程式的應用,前端程式的部分並沒有做得很彈性,而只限於縣市與鄉鎮市區的資料連動,不是只有縣市鄉鎮市區連動下拉選單能用這樣的處理,一些常常會用到的連動下拉選單也可以使用相同的方式來做。

 

以上

2 則留言:

  1. ZipCodeController的code放成ZipCodeService 的Code了

    回覆刪除
    回覆
    1. Hello, 你好
      感謝你的告知,已經將內容做更正了,謝謝。

      刪除