企業應用常需呈現不定層級的功能選單(如:商品分類、組織架構、知識庫目錄),資料來源為資料庫遞迴關係表(ParentID 自關聯),需支援動態載入、展開/摺疊、權限過濾、連結導航。
CREATE TABLE MenuItems (
MenuId INT IDENTITY PRIMARY KEY,
ParentId INT NULL REFERENCES MenuItems(MenuId),
Text NVARCHAR(100) NOT NULL,
NavigateUrl NVARCHAR(500),
Target NVARCHAR(50) DEFAULT '_self',
SortOrder INT DEFAULT 0,
IconCssClass NVARCHAR(100),
IsVisible BIT DEFAULT 1,
Roles NVARCHAR(500) -- 逗號分隔角色代碼,空白=公開
);
WITH MenuTree AS (
SELECT MenuId, ParentId, Text, NavigateUrl, Target, SortOrder, IconCssClass, 0 AS Level
FROM MenuItems WHERE ParentId IS NULL AND IsVisible = 1
UNION ALL
SELECT m.MenuId, m.ParentId, m.Text, m.NavigateUrl, m.Target, m.SortOrder, m.IconCssClass, mt.Level + 1
FROM MenuItems m INNER JOIN MenuTree mt ON m.ParentId = mt.MenuId
WHERE m.IsVisible = 1
)
SELECT * FROM MenuTree ORDER BY Level, SortOrder;
public class MenuNode {
public int Id { get; set; }
public int? ParentId { get; set; }
public string Text { get; set; }
public string NavigateUrl { get; set; }
public string Target { get; set; } = "_self";
public string IconCssClass { get; set; }
public List Children { get; } = new List();
}
public List BuildTree(IEnumerable flatList) {
var lookup = flatList.ToLookup(n => n.ParentId ?? 0);
Action attachChildren = null;
attachChildren = node => {
node.Children.AddRange(lookup[node.Id].OrderBy(c => c.SortOrder));
foreach (var child in node.Children) attachChildren(child);
};
var roots = lookup[0].OrderBy(r => r.SortOrder).ToList();
roots.ForEach(attachChildren);
return roots;
}
<asp:TreeView ID="tvMenu" runat="server" DataSourceID="sitemapDs">
<DataBindings>
<asp:TreeNodeBinding DataMember="MenuItem" TextField="Title" NavigateUrlField="Url" TargetField="Target" />
</DataBindings>
</asp:TreeView>
<asp:SiteMapDataSource ID="sitemapDs" runat="server" ShowStartingNode="False" />
private void BindTreeView(List roots) {
tvMenu.Nodes.Clear();
foreach (var root in roots) {
tvMenu.Nodes.Add(CreateTreeNode(root));
}
}
private TreeNode CreateTreeNode(MenuNode node) {
var tn = new TreeNode {
Text = node.Text,
Value = node.Id.ToString(),
NavigateUrl = node.NavigateUrl,
Target = node.Target,
ToolTip = node.Text,
ImageUrl = string.IsNullOrEmpty(node.IconCssClass) ? null : ResolveIconUrl(node.IconCssClass),
SelectAction = string.IsNullOrEmpty(node.NavigateUrl) ? TreeNodeSelectAction.Expand : TreeNodeSelectAction.Select
};
foreach (var child in node.Children) {
tn.ChildNodes.Add(CreateTreeNode(child));
}
return tn;
}
如原文所示,TreeNodeSrc="menu.xml" 可載入外部 XML,適合靜態選單、跨專案共用結構。XML 結構:
<TREENODES>
<treenode text="產品管理" NavigateUrl="~/Product/List.aspx" Target="main">
<treenode text="產品清單" NavigateUrl="~/Product/List.aspx" />
<treenode text="新增產品" NavigateUrl="~/Product/Create.aspx" />
</treenode>
<treenode text="訂單管理" NavigateUrl="~/Order/List.aspx" Target="main">
<treenode text="訂單清單" NavigateUrl="~/Order/List.aspx" />
</treenode>
</TREENODES>
大型選單(>500 節點)一次載入會導致 ViewState 膨脹、頁面渲染緩慢。啟用 PopulateOnDemand="true" + TreeNodePopulate 事件:
<asp:TreeView ID="tvMenu" runat="server" PopulateOnDemand="true" OnTreeNodePopulate="tvMenu_TreeNodePopulate"></asp:TreeView>
protected void tvMenu_TreeNodePopulate(object sender, TreeNodeEventArgs e) {
int parentId = int.Parse(e.Node.Value);
var children = MenuService.GetChildren(parentId, CurrentUser.Roles);
foreach (var child in children) {
var tn = new TreeNode(child.Text, child.Id.ToString()) {
NavigateUrl = child.NavigateUrl,
Target = child.Target,
PopulateOnDemand = child.HasChildren
};
e.Node.ChildNodes.Add(tn);
}
e.Node.Populated = true;
}
public IEnumerable GetVisibleMenus(IPrincipal user) {
var all = MenuRepository.GetAll();
return all.Where(m => m.IsVisible && (string.IsNullOrEmpty(m.Roles) || m.Roles.Split(',').Any(r => user.IsInRole(r.Trim()))));
}
後端提供 GET /api/menus/tree 回傳巢狀 JSON,前端元件負責渲染、展開、搜尋、拖曳排序,完全分離關注點。