在 Web 開發中,「省市區三級聯動」是經典的前端交互場景。本文將透過一個統一的數據模型,分別展示如何使用 ASP.NET 內建 Ajax(UpdatePanel)、jQuery 1.4.1 + ASHX、ExtJS 3.2 + ASHX 三種主流框架實現該功能,並提供完整的後端模型、介面定義與前端頁面代碼,方便開發者快速對比與選型。
為了讓三個示例共用同一套數據,我們先建立領域模型與資料存取介面:
namespace Downmoon.Framework.Model
{
public class Area
{
public string Area_ID { get; set; }
public string Area_Name { get; set; }
public double Area_Order { get; set; }
public int Area_Layer { get; set; }
public string Area_FatherID { get; set; }
public Area() { }
public Area(string id, string name, double order, int layer, string father)
{
Area_ID = id; Area_Name = name; Area_Order = order; Area_Layer = layer; Area_FatherID = father;
}
}
}
namespace Downmoon.Framework.Controllers
{
public interface IArea
{
List<Area> GetAreaList();
List<Area> GetAreaListFindByParentID(string filterID);
}
}
namespace Downmoon.Framework.Controllers
{
public class AreaList : IArea
{
private static AreaList _instance;
public static AreaList Instance => _instance ??= new AreaList();
public List<Area> GetAreaList()
{
var list = new List<Area>();
// 省份
list.Add(new Area("110000", "北京市", 0, 1, "000000"));
list.Add(new Area("330000", "浙江省", 0, 1, "000000"));
// 市
list.Add(new Area("110100", "市轄區", 0, 2, "110000"));
list.Add(new Area("330100", "杭州市", 0, 2, "330000"));
list.Add(new Area("330200", "寧波市", 0, 2, "330000"));
// 區/縣
list.Add(new Area("110101", "東城區", 0, 3, "110100"));
list.Add(new Area("110102", "西城區", 0, 3, "110100"));
list.Add(new Area("330102", "上城區", 0, 3, "330100"));
list.Add(new Area("330103", "下城區", 0, 3, "330100"));
list.Add(new Area("330203", "海曙區", 0, 3, "330200"));
// …其餘資料省略
return list;
}
public List<Area> GetAreaListFindByParentID(string filter)
{
return GetAreaList().FindAll(a => a.Area_FatherID == filter);
}
}
}
namespace Downmoon.Framework.Controllers
{
public class Factory
{
public static IArea GetAreaController() => AreaList.Instance;
}
}

優點:與 ASP.NET 深度整合,幾乎不寫 JavaScript;缺點:.NET Framework 2.0 需額外配置 web.config,4.0 以上則零配置。
<configuration>
<configSections>
<sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<sectionGroup name="scripting" type="System.Web.Configuration.ScriptingSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<section name="scriptResourceHandler" type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication"/>
<sectionGroup name="webServices" type="System.Web.Configuration.ScriptingWebServicesSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<section name="jsonSerialization" type="System.Web.Configuration.ScriptingJsonSerializationSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="Everywhere"/>
</sectionGroup>
</sectionGroup>
</sectionGroup>
</configSections>
<system.web>
<pages>
<controls>
<add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</controls>
</pages>
<httpHandlers>
<add verb="*" path="*.asmx" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="false"/>
</httpHandlers>
<httpModules>
<add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</httpModules>
</system.web>
</configuration>
<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="true" />
<div>
<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<table width="500" border="0" cellpadding="0" cellspacing="2">
<tr>
<td height="25" bgcolor="#EAEAEA">請選擇省/市/區:</td>
<td bgcolor="#f7f7f7">
<asp:DropDownList ID="dpProvince" runat="server" AutoPostBack="true" OnSelectedIndexChanged="dpProvince_SelectedIndexChanged" />
<asp:DropDownList ID="dpCity" runat="server" AutoPostBack="true" OnSelectedIndexChanged="dpCity_SelectedIndexChanged" />
<asp:DropDownList ID="dpArea" runat="server" AutoPostBack="false" />
</td>
</tr>
<tr>
<td height="25" bgcolor="#EAEAEA"></td>
<td bgcolor="#f7f7f7">
<asp:UpdateProgress ID="UpdateProgress1" runat="server">
<ProgressTemplate>正在查詢,請稍候……</ProgressTemplate>
</asp:UpdateProgress>
</td>
</tr>
</table>
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="dpProvince" EventName="SelectedIndexChanged" />
<asp:AsyncPostBackTrigger ControlID="dpCity" EventName="SelectedIndexChanged" />
</Triggers>
</asp:UpdatePanel>
</div>
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack) BindProvince();
}
private void BindProvince()
{
var list = Factory.GetAreaController().GetAreaListFindByParentID("000000");
dpProvince.DataSource = list; dpProvince.DataTextField = "Area_Name"; dpProvince.DataValueField = "Area_ID"; dpProvince.DataBind();
dpProvince.Items.Insert(0, new ListItem("請選擇...", ""));
}
protected void dpProvince_SelectedIndexChanged(object sender, EventArgs e)
{
dpCity.Items.Clear(); dpArea.Items.Clear();
if (!string.IsNullOrEmpty(dpProvince.SelectedValue))
{
var list = Factory.GetAreaController().GetAreaListFindByParentID(dpProvince.SelectedValue);
dpCity.DataSource = list; dpCity.DataTextField = "Area_Name"; dpCity.DataValueField = "Area_ID"; dpCity.DataBind();
dpCity.Items.Insert(0, new ListItem("請選擇...", ""));
}
}
protected void dpCity_SelectedIndexChanged(object sender, EventArgs e)
{
dpArea.Items.Clear();
if (!string.IsNullOrEmpty(dpCity.SelectedValue))
{
var list = Factory.GetAreaController().GetAreaListFindByParentID(dpCity.SelectedValue);
dpArea.DataSource = list; dpArea.DataTextField = "Area_Name"; dpArea.DataValueField = "Area_ID"; dpArea.DataBind();
dpArea.Items.Insert(0, new ListItem("請選擇...", ""));
}
}

優點:輕量、跨瀏覽器、易於維護,適合現代 ASP.NET 專案。
public class AjaxRequest : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string pid = context.Request["pid"] ?? "000000";
var list = Factory.GetAreaController().GetAreaListFindByParentID(pid);
context.Response.ContentType = "application/json";
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.Write(ListToJson(list));
}
private string ListToJson(List<Area> list)
{
var sb = new StringBuilder("[");
for (int i = 0; i < list.Count; i++)
{
sb.Append($"{{\"Area_ID\":\"{list[i].Area_ID}\",\"Area_Name\":\"{list[i].Area_Name}\"}}");
if (i < list.Count - 1) sb.Append(",");
}
sb.Append("]");
return sb.ToString();
}
public bool IsReusable => false;
}
<head>
<script src="Scripts/jquery-1.4.1.js"></script>
<style>
#dpCity, #dpArea { display: none; position: relative; }
</style>
</head>
<body>
<form id="form1" runat="server">
<div>
請選擇省/市/區:
<asp:DropDownList ID="dpProvince" runat="server" />
<asp:DropDownList ID="dpCity" runat="server" />
<asp:DropDownList ID="dpArea" runat="server" />
</div>
<script>
$(function () {
var $dp1 = $("#dpProvince"), $dp2 = $("#dpCity"), $dp3 = $("select[name$=dpArea]");
loadAreas("000000", "0");
$dp1.on("change", function () {
if (this.value) { loadAreas(this.value, "1"); $dp2.fadeIn(); } else $dp2.fadeOut();
});
$dp2.on("change", function () {
if (this.value) { loadAreas(this.value, "2"); $dp3.fadeIn(); } else $dp3.fadeOut();
});
});
function loadAreas(pid, level) {
$.getJSON("AjaxRequest.ashx?pid=" + pid, function (data) {
var $target = level === "0" ? $dp1 : level === "1" ? $dp2 : $dp3;
$target.empty().append($("").val("").text("請選擇..."));
$.each(data, function (i, item) {
$target.append($("").val(item.Area_ID).text(item.Area_Name));
});
});
}
</script>
</form>
</body>

優點:UI 元件豐富、適合複雜企業級應用;缺點:學習曲線較陡峭、檔案體積大。
public class GetAreaXml : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string pid = context.Request["pid"] ?? "000000";
var list = Factory.GetAreaController().GetAreaListFindByParentID(pid);
context.Response.ContentType = "text/html";
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.AddHeader("Cache-Control", "no-cache, must-revalidate");
var sb = new StringBuilder();
foreach (var a in list)
sb.Append($"{{\"Area_Name\":\"{a.Area_Name}\",\"Area_ID\":\"{a.Area_ID}\"}},");
string json = sb.ToString().TrimEnd(',');
context.Response.Write("{\"Results\":[" + json + "]}");
}
public bool IsReusable => false;
}
<head>
<link rel="stylesheet" href="Scripts/ext/3.2/resources/css/ext-all.css" />
<script src="Scripts/ext/3.2/adapter/ext/ext-base.js"></script>
<script src="Scripts/ext/3.2/ext-all.js"></script>
<script>
Ext.onReady(function () {
function GetAreas(parentCtrl, childContainer, url) {
var select = childContainer.child("select");
select.dom.options.length = 0;
if (!parentCtrl.getValue()) { childContainer.hide(); return; }
select.disabled = true;
Ext.Ajax.request({
url: url, params: { pid: parentCtrl.getValue() }, method: 'GET',
success: function (res) {
var data = Ext.util.JSON.decode(res.responseText);
if (data.Results.length > 0) {
select.dom.options.add(new Option("請選擇", "000000"));
data.Results.forEach(r => select.dom.options.add(new Option(r.Area_Name, r.Area_ID)));
select.disabled = false; childContainer.show();
} else childContainer.hide();
}
});
}
});
</script>
</head>
<body>
<form id="form1" runat="server">
<table width="500"><tr><td>
<div style="float:left;line-height:20px;">請選擇</div>
<asp:DropDownList ID="dropProvince" runat="server" Style="float:left;" />
<div style="float:left;line-height:20px;">省</div>
<asp:Panel ID="panelArea" runat="server">
<asp:DropDownList ID="dropArea" runat="server" Style="float:left;" />
<div style="float:left;line-height:20px;">市</div>
</asp:Panel>
<asp:Panel ID="panelArea2" runat="server">
<asp:DropDownList ID="dropArea2" runat="server" Style="float:left;" />
<div style="float:left;line-height:20px;">縣</div>
</asp:Panel>
</td></tr></table>
</form>
</body>

| 框架 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|
| ASP.NET UpdatePanel | 傳統 WebForms 專案、快速原型 | 零 JS、全自動局部更新 | ViewState 大、難以客製化、舊版需繁瑣配置 |
| jQuery + ASHX | 現代化專案、輕量級、需 SEO | 輕量、標準 JSON、易除錯、社群龐大 | 需手寫 JS、手動處理 UI 狀態 |
| ExtJS 3.2 | 企業級後台、複雜表單/表格 | 元件完整、資料綁定強、UI 專業 | 學習曲線高、檔案大、授權需注意 |
實務上建議:新專案優先採用 jQuery / Fetch API + Web API / ASP.NET Core;維護舊版 WebForms 專案時,UpdatePanel 仍是最低成本方案;若團隊已具 ExtJS 經驗且需大量 Grid/Tree/Window,則 ExtJS 能大幅提升開發效率。