Blazor Server App:移除导航父项show CSS类的实现问题
问题与解决方案
问题说明
- 目标:实现多级可折叠子菜单导航,点击子菜单项时关闭对应最上层父菜单,并移除顶级导航锚点的"show" CSS类
- 当前异常:默认所有父导航项显示为展开状态,仅点击菜单链接后才恢复非展开的默认状态;且所有顶级菜单共享同一展开状态,无法独立控制
代码问题分析
- 使用**全局变量
navMenuExpanded**控制所有顶级菜单状态,导致所有菜单同步展开/折叠,不符合多级菜单独立控制需求 - CSS类逻辑错误:
NavMenuCssClassCollapse默认返回"collapse",与Bootstrap原生dropdown的show类控制逻辑冲突,导致默认状态异常 - 子菜单项点击事件仅切换全局状态,未针对当前顶级菜单执行精准关闭操作
修正后的代码
@inject ISiteMapData SiteMapData @inject IJSRuntime JS <div> <nav class="navbar navbar-expand-lg bg-body-tertiary"> <div class="container-fluid"> <a class="navbar-brand" href="https://my.boeing.com"> <img class="img-fluid" src="/img/boeing_logo_white.png" alt="Boeing Logo"> </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav me-auto mb-2 mb-lg-0"> @if (siteMap is not null) { IEnumerable<SiteMapModel> parentNavigationItems = siteMap.Where(x => x.ParentNodeID == null).OrderBy(o => o.SortOrder); @foreach (SiteMapModel parentNavigationItem in parentNavigationItems) { IEnumerable<SiteMapModel>? childNavigationItems = ChildRows(parentNavigationItem.NodeID); @if (childNavigationItems.Count() != 0) { bool isExpanded = _expandedMenus.TryGetValue(parentNavigationItem.NodeID, out bool expanded) ? expanded : false; <li class="nav-item dropdown"> <a title="@parentNavigationItem.Description" class="dropdown-item dropdown-toggle nav-link @(isExpanded ? "show" : "")" tabindex="-1" data-node-id="@parentNavigationItem.NodeID" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="@isExpanded.ToString().ToLower()"> @parentNavigationItem.Title </a> <ul class="dropdown-menu dropdown-submenu @(isExpanded ? "show" : "")"> @foreach (SiteMapModel childNavigationItem in childNavigationItems) { IEnumerable<SiteMapModel>? secondChildNavigationItems = ChildRows(childNavigationItem.NodeID); if (secondChildNavigationItems.Count() != 0) { <li class="dropend"> <a class="dropdown-item dropdown-toggle" tabindex="-1" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false"> @childNavigationItem.Title </a> <ul class="dropdown-menu dropdown-submenu"> @foreach (SiteMapModel secondChildNavigationItem in secondChildNavigationItems) { IEnumerable<SiteMapModel>? thirdChildNavigationItems = ChildRows(secondChildNavigationItem.NodeID); if (thirdChildNavigationItems.Count() != 0) { <li class="dropstart"> <a class="dropdown-item dropdown-toggle" tabindex="-1" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false"> @secondChildNavigationItem.Title </a> <ul class="dropdown-menu dropdown-submenu"> @foreach (SiteMapModel thirdChildNavigationItem in thirdChildNavigationItems) { <li title="@thirdChildNavigationItem.Description"> <a class="dropdown-item" href="@thirdChildNavigationItem.URL" @onclick="() => CloseParentMenu(parentNavigationItem.NodeID)">@thirdChildNavigationItem.Title</a> </li> } </ul> </li> } else { <li title="@secondChildNavigationItem.Description"> <a class="dropdown-item" href="@secondChildNavigationItem.URL" @onclick="() => CloseParentMenu(parentNavigationItem.NodeID)">@secondChildNavigationItem.Title</a> </li> } } </ul> </li> } else { <li title="@childNavigationItem.Description"> <a class="dropdown-item" href="@childNavigationItem.URL" @onclick="() => CloseParentMenu(parentNavigationItem.NodeID)">@childNavigationItem.Title</a> </li> } } </ul> </li> } else { <li class="nav-item"> <a id="@parentNavigationItem.Title.ToLower()" class="nav-link" aria-current="page" href="" title="@parentNavigationItem.Description"> @parentNavigationItem.Title </a> </li> } } } </ul> <form class="d-flex search-area" role="search"> <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search"> <button class="btn btn-outline-success search-button" type="submit">Search</button> </form> </div> </div> </nav> </div> @code { private IEnumerable<SiteMapModel>? siteMap; // 用字典记录每个顶级菜单的独立展开状态 private Dictionary<int, bool> _expandedMenus = new(); protected override async Task OnInitializedAsync() { siteMap = await SiteMapData.GetSiteMap(1234567890); // 初始化所有顶级菜单为折叠状态 if (siteMap != null) { var parentIds = siteMap.Where(x => x.ParentNodeID == null).Select(x => x.NodeID); foreach (var id in parentIds) { _expandedMenus[id] = false; } } } private IEnumerable<SiteMapModel>? ChildRows(int parentNavigationItemID) { if (siteMap is not null) { return siteMap.Where(x => x.ParentNodeID == parentNavigationItemID).OrderBy(o => o.SortOrder); } return null; } // 精准关闭指定顶级父菜单 private void CloseParentMenu(int parentNodeId) { if (_expandedMenus.ContainsKey(parentNodeId)) { _expandedMenus[parentNodeId] = false; // 调用JS关闭Bootstrap dropdown,保证状态完全同步 _ = JS.InvokeVoidAsync("document.querySelector('[data-node-id=\"" + parentNodeId + "\"]').dispatchEvent(new Event('hide.bs.dropdown'))"); } } }
关键修改点
- 替换全局状态为字典:用
Dictionary<int, bool>记录每个顶级菜单的独立展开状态,默认全部设为折叠 - 修正Bootstrap样式绑定:根据菜单状态添加/移除
show类,同步aria-expanded属性,符合原生组件规范 - 子菜单点击事件优化:每个子菜单项点击时调用
CloseParentMenu,传入对应顶级菜单ID,实现精准关闭 - JS同步状态:调用JS触发Bootstrap dropdown的隐藏事件,保证UI与本地状态完全一致
内容的提问来源于stack exchange,提问作者Daniel Carriveau
相关产品推荐
相关产品推荐

