ASP.NET MVC 单DropdownListFor如何展示带层级的主、子分类
分类菜单嵌套层级改造方案
整个改造的核心思路是把原本平铺的全量分类数据加工为带父子关联的树形结构,再通过递归渲染视图实现嵌套展示,整体改动分为5个核心部分:
1. 扩展分类模型承载层级子项
原有CategoryModel仅通过ParentId标记从属关系,缺少存储子分类的容器,需要新增对应的集合属性:
public class CategoryModel { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public string Image { get; set; } public int? ParentId { get; set; } // 新增:存储当前分类下的所有直接子分类 public List<CategoryModel> Children { get; set; } = new(); }
2. 实现平铺数据转树形结构的公共加工逻辑
不要把全量平铺集合直接传递给视图,需要先通过一次遍历把零散的分类节点挂载到对应的父节点下,最终提取出所有一级主分类作为根节点列表:
// 可作为ICategoryService的扩展方法全局复用 public static List<CategoryModel> BuildCategoryTree(IEnumerable<CategoryModel> allCategories) { // 以分类Id为键建立字典,实现节点的O(1)快速查找 var categoryDict = allCategories.ToDictionary(c => c.Id); var rootNodes = new List<CategoryModel>(); foreach (var category in allCategories) { if (category.ParentId == null) { // ParentId为空的节点即为一级主分类,直接加入根节点列表 rootNodes.Add(category); continue; } // 找到当前节点的父节点,把自身挂载到父节点的Children集合中 if (categoryDict.TryGetValue(category.ParentId.Value, out var parentNode)) { parentNode.Children.Add(category); } } return rootNodes; }
这个方法的时间复杂度是O(n),一次遍历就能完成全量挂载,不需要多次查库找父节点,性能完全满足日常站点的分类规模。
3. 改造ViewComponent的传参逻辑
保留原有的构造函数注入、路由选中值读取逻辑,仅调整传递给视图的数据源类型:
public class CategoryListViewComponent : ViewComponent { private readonly ICategoryService _categoryService; public CategoryListViewComponent(ICategoryService categoryService) { _categoryService = categoryService; } public IViewComponentResult Invoke() { // 原有逻辑:读取路由中携带的当前选中分类Id var selectedCategoryId = RouteData.Values["categoryId"] as int?; // 原有逻辑:拉取全量分类集合 var allCategories = _categoryService.GetAllCategories(); // 新增:将平铺集合转换为树形结构 var categoryTree = CategoryTreeBuilder.BuildCategoryTree(allCategories); ViewBag.SelectedCategoryId = selectedCategoryId; return View(categoryTree); } }
4. 改造视图为递归渲染嵌套DOM结构
不要在视图里硬写两层循环适配主/子分类,通过递归部分视图实现任意层级的嵌套渲染,这种写法不管后续加多少级分类,都不需要修改视图代码,扩展性比硬写两层循环好很多:
- 主视图(
Views/Shared/Components/CategoryList/Default.cshtml):接收根节点集合,渲染一级主分类列表@model List<CategoryModel> <ul class="category-menu"> @foreach (var mainCategory in Model) { <li class="main-category @(ViewBag.SelectedCategoryId == mainCategory.Id ? "active" : "")"> <a href="@Url.Action("CategoryList", "Product", new { categoryId = mainCategory.Id })"> @mainCategory.Name </a> <!-- 若当前分类存在子项,调用递归视图渲染子菜单 --> @if (mainCategory.Children.Any()) { <partial name="_CategorySubMenu" model="mainCategory.Children" /> } </li> } </ul> - 递归子菜单部分视图(
Views/Shared/_CategorySubMenu.cshtml):自动渲染任意深度的子分类列表@model List<CategoryModel> <ul class="sub-category-menu"> @foreach (var subCategory in Model) { <li class="sub-category @(ViewBag.SelectedCategoryId == subCategory.Id ? "active" : "")"> <a href="@Url.Action("CategoryList", "Product", new { categoryId = subCategory.Id })"> @subCategory.Name </a> <!-- 递归向下渲染更深层级的分类 --> @if (subCategory.Children.Any()) { <partial name="_CategorySubMenu" model="subCategory.Children" /> } </li> } </ul>
5. 适配后台CategoryList控制器动作
将原本直接返回全量平铺集合的逻辑,替换为调用公共树构建方法加工后的层级结构,保证接口返回数据和前端展示的层级一致性:
public IActionResult CategoryList(int? categoryId) { var allCategories = _categoryService.GetAllCategories(); // 复用公共树构建方法生成层级结构 var categoryTree = CategoryTreeBuilder.BuildCategoryTree(allCategories); return View(categoryTree); }
优化注意点
- 性能优化:全量分类的树构建结果可接入内存缓存,仅当分类数据变更时更新缓存,避免每次页面访问都重复遍历构建树结构
- 样式适配:由于DOM已经是完全嵌套的结构,仅需为不同层级的
ul/li元素添加缩进、展开收起交互、选中态样式即可实现目标菜单效果 - 选中态适配:如果需要选中子分类时自动高亮所有上级主分类,可在树构建阶段为每个节点存储自身到根节点的Id路径集合,判断选中态时匹配选中分类Id是否在对应路径内即可
内容的提问来源于stack exchange,提问作者RECEP AKYÜZ
相关产品推荐
相关产品推荐

