在 Web 表單中呈現階層式資料(如部門架構、產品分類)時,下拉式樹狀選單是優雅的解決方案。本文介紹三種實作方式:DataGridTree 控制項繼承、遞迴綁定 DropDownList、介面導向的通用元件。
private void MakeTree(DataTable dtNodes, string parentColumn, string rootValue,
string indexColumn, string textColumn, DropDownList ddlBind, int level)
{
level++;
DataView dv = new DataView(dtNodes);
dv.RowFilter = $"{parentColumn} = '{rootValue}'";
string padding = new string(' ', level); // 全形空格縮排
foreach (DataRowView drv in dv)
{
string nodeId = drv[indexColumn].ToString();
string nodeText = padding + "├" + drv[textColumn].ToString();
ddlBind.Items.Add(new ListItem(nodeText, nodeId));
// 遞迴處理子節點
MakeTree(dtNodes, parentColumn, nodeId, indexColumn, textColumn, ddlBind, level);
}
level--;
}
// 呼叫範例
private void CreateTree()
{
string sql = "SELECT * FROM Master_Department WHERE Parent_Department = '003'";
DataSet ds = DB.GetDS();
MakeTree(ds.Tables[0], "Parent_Department", "003",
"Department_Code", "Department_Name", DropDownList1, -1);
}
定義 `IDropDownTree` 介面,實作類別只需專注「取得子節點」邏輯,綁定、顯示邏輯由 `DropDownTree` 類別統一處理,符合單一職責原則。
public interface IDropDownTree : IDisposable
{
/// <summary>取得指定父節點的子節點集合</summary>
Dictionary<string, string> GetChildCategory(string parentId);
/// <summary>取得綁定器實例</summary>
DropDownTree DropDownTree { get; }
}
public sealed class DropDownTree
{
private readonly IDropDownTree _tree;
public DropDownTree(IDropDownTree tree) => _tree = tree;
/// <summary>產生樹狀前綴符號(├─、└─、│)</summary>
private string GetPrefix(bool isLast, bool hasChild, string parentPrefix)
{
string result = "";
if (!string.IsNullOrEmpty(parentPrefix))
{
// 移除最後一個符號,將 ├→│、└→空白
result = parentPrefix.Substring(0, parentPrefix.Length - 1)
.Replace("├", "│").Replace("└", " ");
}
result += isLast ? "└" : "├";
result += hasChild ? "┬" : "─";
return result;
}
/// <summary>綁定至 DropDownList</summary>
public void BindToDropDownList(DropDownList ddl, string removeId = null, string parentId = null, bool autoDispose = true)
{
if (ddl == null) return;
string currentId = parentId ?? Guid.Empty.ToString();
string currentSign = "", parentSign = "";
bool hasChild = true;
var parentKeyQueue = new Queue<string>();
var parentSignQueue = new Queue<string>();
int insertIndex = 0;
while (hasChild)
{
int lastCount = 1;
var children = _tree.GetChildCategory(currentId);
if (children != null && children.Count > 0)
{
if (!string.IsNullOrEmpty(removeId) && children.ContainsKey(removeId))
children.Remove(removeId);
foreach (var entry in children)
{
bool hasGrandChild = _tree.GetChildCategory(entry.Key) != null;
currentSign = GetPrefix(lastCount == children.Count, hasGrandChild, parentSign);
var item = new ListItem(currentSign + entry.Value, entry.Key);
if (ddl.Items.Count > 0)
insertIndex = string.IsNullOrEmpty(currentId) ? insertIndex + 1
: ddl.Items.IndexOf(ddl.Items.FindByValue(currentId)) + lastCount;
ddl.Items.Insert(insertIndex, item);
lastCount++;
if (hasGrandChild)
{
parentKeyQueue.Enqueue(entry.Key);
parentSignQueue.Enqueue(currentSign);
}
}
if (parentKeyQueue.Count > 0)
{
currentId = parentKeyQueue.Dequeue();
parentSign = parentSignQueue.Dequeue();
}
else
{
hasChild = false;
}
}
else
{
break;
}
}
if (autoDispose) _tree.Dispose();
}
}
public class DepartmentService : IDropDownTree
{
public Dictionary<string, string> GetChildCategory(string parentId)
{
string where = string.IsNullOrEmpty(parentId)
? "ParentId IS NULL OR ParentId = '00000000-0000-0000-0000-000000000000'"
: $"ParentId = '{parentId}'";
var list = SelectList(0, where, "", false);
if (list?.Count > 0)
{
return list.ToDictionary(x => x.Id.ToString(), x => x.CategoryName);
}
return null;
}
public DropDownTree DropDownTree => new DropDownTree(this);
public void Dispose() { /* 釋放資源 */ }
// 其他業務方法...
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
var service = new DepartmentService();
service.DropDownTree.BindToDropDownList(ddlDepartment);
}
}
後端提供 JSON API,前端使用 zTree、jstree 或 Select2 + AJAX 實作按需載入,避免一次傳送大量 `
[WebMethod]
public static List<TreeNode> GetChildren(string parentId)
{
// 回傳 { id, text, hasChildren } 結構
}
| 方案 | 適用情境 | 優點 | 缺點 |
|---|---|---|---|
| DataGridTree | 需表格式顯示、支援編輯、彙總 | 功能豐富、伺服端渲染 | 大量資料效能差、前端互動弱 |
| 遞迴綁定 DropDownList | 資料量 < 2000、單純選取 | 實作簡單、無相依套件 | HTML 體積大、無搜尋/延遲載入 |
| 介面導向通用元件 | 多處重用、架構導向專案 | 解耦合、可測試、可替換實作 | 初期建構較複雜 |
| 前端 JS 樹元件 | 大量資料、需搜尋、複雜互動 | 使用者體驗佳、分頁載入 | 需前端技能、雙端開發 |
現代專案建議採用「後端 API + 前端元件」架構;維護舊專案時,介面導向元件提供良好的封裝與擴充性。