當輸入資料具備層級結構 (如產品分類、地區行政區劃、業務範圍) 且需多選時,DropDownList/CheckBoxList 難以勝任。TreeView + CheckBox 提供直觀的樹狀多選體驗,配合 JavaScript 級聯選取、伺服器端遞迴資料綁定、彈出視窗回傳值組裝,構建完整輸入方案。
<asp:TreeView ID="TreeView1" runat="server"
ShowCheckBoxes="All" ShowLines="True"
ExpandDepth="1" Font-Bold="False" ForeColor="#0000CC"
onclick="OnTreeNodeChecked();">
</asp:TreeView>
<script>
// 級聯選取:父節點勾選/取消 → 所有子節點跟隨
function OnTreeNodeChecked() {
var el = event.srcElement;
if (el.type !== 'checkbox') return;
var childrenDivId = el.id.replace('CheckBox', 'Nodes');
var div = document.getElementById(childrenDivId);
if (!div) return;
var checkboxes = div.getElementsByTagName('INPUT');
for (var i = 0; i < checkboxes.length; i++) {
if (checkboxes[i].type === 'checkbox')
checkboxes[i].checked = el.checked;
}
}
// 彈出視窗確定:收集所有勾選值 → 組裝格式化字串 → 回傳父視窗
function SubmitValue() {
var vals = [];
var inputs = document.getElementsByTagName('INPUT');
for (var i = 0; i < inputs.length; i++) {
if (inputs[i].type === 'checkbox' && inputs[i].checked)
vals.push(inputs[i].value);
}
window.returnValue = vals.join(',');
window.close();
}
</script>
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack) BindData();
}
private void BindData()
{
// 1. 從 BLL 取得樹狀資料結構
ArrayList scopeTree = BLLFactory<BusinessScope>.Instance.GetTree();
// 2. 遞迴建立 TreeNode
foreach (BusinessScopeNodeInfo nodeInfo in scopeTree)
{
var rootNode = new TreeNode(nodeInfo.Name);
rootNode.SelectAction = TreeNodeSelectAction.Expand; // 點擊文字展開而非導航
TreeView1.Nodes.Add(rootNode);
AddTreeNode(rootNode, nodeInfo);
}
}
private void AddTreeNode(TreeNode parentNode, BusinessScopeNodeInfo nodeInfo)
{
foreach (BusinessScopeNodeInfo child in nodeInfo.Children)
{
var node = new TreeNode(child.Name);
node.SelectAction = TreeNodeSelectAction.Expand;
parentNode.ChildNodes.Add(node);
AddTreeNode(node, child); // 遞迴
}
}
避免遞迴查詢資料庫 (N+1 問題),採用一次撈取所有節點 → DataTable.Select 記憶體篩選 → 遞迴建構物件樹:
public ArrayList GetTree()
{
var result = new ArrayList();
string sql = $"SELECT * FROM {tableName} ORDER BY PID, Seq";
var ds = db.ExecuteDataSet(db.GetSqlStringCommand(sql));
if (ds.Tables.Count > 0)
{
DataTable dt = ds.Tables[0];
// 取得根節點 (PID = -1)
DataRow[] roots = dt.Select("PID = -1");
foreach (DataRow row in roots)
{
int id = Convert.ToInt32(row["ID"]);
result.Add(BuildNode(id, dt));
}
}
return result;
}
private BusinessScopeNodeInfo BuildNode(int id, DataTable dt)
{
var info = FindByID(id); // 取得單一節點資料
var node = new BusinessScopeNodeInfo(info);
// 記憶體中找子節點
DataRow[] children = dt.Select($"PID = {id}");
foreach (DataRow row in children)
{
int childId = Convert.ToInt32(row["ID"]);
node.Children.Add(BuildNode(childId, dt));
}
return node;
}
[Serializable]
public class BusinessScopeInfo : BaseEntity
{
public virtual decimal Id { get; set; }
public virtual decimal Pid { get; set; } = -1;
public virtual string Name { get; set; } = "";
public virtual string Seq { get; set; } = "";
}
// 節點專用:包含子節點集合
public class BusinessScopeNodeInfo : BusinessScopeInfo
{
private ArrayList _children = new ArrayList();
public ArrayList Children
{
get => _children;
set => _children = value ?? new ArrayList();
}
public BusinessScopeNodeInfo() { }
public BusinessScopeNodeInfo(BusinessScopeInfo info)
{
Id = info.Id; Name = info.Name; Seq = info.Seq;
}
}
protected void btnSelect_Click(object sender, ImageClickEventArgs e)
{
var result = new StringBuilder();
foreach (TreeNode parent in TreeView1.Nodes)
{
foreach (TreeNode node in parent.ChildNodes)
{
var sb = new StringBuilder();
// 收集第三層勾選項
foreach (TreeNode sub in node.ChildNodes)
{
if (sub.Checked) sb.AppendFormat("{0},", sub.Text);
}
if (sb.Length > 0)
{
// 格式:父節點(子項1,子項2,...);
sb.Insert(0, $"{node.Text}(");
sb.Append(")");
result.Append(sb.ToString().Replace(",)", ")") + ";");
}
else if (node.Checked)
{
// 第二層直接勾選無第三層
result.Append(node.Text + ";");
}
}
}
// 透過 Helper 關閉視窗並回傳值
Helper.CloseWin(this, result.ToString().TrimEnd(';'));
}
<asp:TextBox ID="txtScope" runat="server" ReadOnly="true" Width="400px" />
<asp:Button ID="btnOpenScope" runat="server" Text="選擇範圍"
OnClientClick="window.open('ScopeSelector.aspx','_blank','width=500,height=400');return false;" />
<script>
function ReceiveScopeValue(value) {
document.getElementById('<%=txtScope.ClientID%>').value = value;
}
</script>
| 痛點 | 優化方案 |
|---|---|
| 大量節點渲染緩慢 | 虛擬滾動 / 延遲載入 (AJAX 按需載入子節點) / 改用 jsTree / zTree 前端元件 |
| ViewState 膨脹 | TreeView.ViewStateMode = Disabled;或改用純前端樹 + HiddenField 存值 |
| 級聯邏輯僅限前端 | 後端驗證時同步檢查父子邏輯一致性 |
| 資料來源變更頻繁 | 快取樹結構 (MemoryCache/Redis),依賴 SqlCacheDependency 或訊息佇列失效 |
| 行動裝置操作困難 | 改用 Select2/Chosen 下拉多選 + 群組標籤 模擬層級 |
// WebAPI 端點
[HttpGet("api/scopes/tree")]
public async Task<IEnumerable<ScopeTreeDto>> GetScopeTree()
{
var all = await _repo.GetAllAsync();
return BuildTree(all.Where(x => x.Pid == -1), all);
}
// 前端 Vue/React + Element Plus / Ant Design Tree
<el-tree
:data="scopeTree"
show-checkbox
node-key="id"
check-strictly="false"
:props="{ children: 'children', label: 'name' }"
@check-change="onCheckChange">
</el-tree>
ASP.NET TreeView 多選方案展示了 WebForms 時代伺服器端控制項 + 客戶端腳本協作的經典模式:後端遞迴綁定樹結構、前端 JS 處理級聯邏輯、後端遍歷組裝格式化回傳值。雖然現代專案多改用前端樹狀元件 + API,但此方案在零前端建置、快速開發內部系統、相容舊瀏覽器場景下仍具高效率。關鍵成功因素:單次查詢組裝記憶體樹、CheckBox 級聯邏輯、格式化回傳值協定。