递归商品分类树转ViewModel后子分类加载失败及模型匹配报错
解决递归商品分类树ViewModel中子分类为Null及视图类型不匹配问题
看起来你在把实体驱动的递归分类树改成ViewModel架构时遇到了两个典型问题:子分类Children属性为null,以及修改后出现的视图类型不匹配异常。我来帮你一步步解决这些问题:
问题根源分析
- 子分类为Null的原因:你在控制器的
Select投影里只映射了根分类的基础属性,但完全没处理子分类的递归映射——实体的Children是ProductCategory集合,不会自动转换成ViewModelProductCategory集合,而且你没把这部分数据映射到ViewModel里,所以视图里的item.Children自然是null。 - 视图类型不匹配的原因:后来你直接赋值
Children = v.Children,导致ViewModel的Children是实体类型(ProductCategory),和视图期望的ViewModelProductCategory类型不匹配;再加上新增的CategoryRecursiveModel和视图声明的模型类型不一致,就抛出了类型错误。
解决方案一:递归映射实体到ViewModel
要生成完整的ViewModel分类树,你需要一个递归方法来把每个实体节点(包括子节点)转换成对应的ViewModel。这样既能保证Children属性正确赋值,也能统一类型。
1. 编写实体转ViewModel的递归辅助方法
在你的控制器里添加这个私有方法(或者放到单独的映射类里,更符合SOLID原则):
private ViewModelProductCategory MapToViewModel(ProductCategory entity) { if (entity == null) return null; var vm = new ViewModelProductCategory { Id = entity.Id, ParentId = entity.ParentId, Title = entity.Title, SortOrder = entity.SortOrder, // 映射当前分类下的商品,支持ProductCountInfo计算 Products = entity.ProductInCategory? .Select(pc => new ViewModelProduct { Id = pc.Product.Id, Title = pc.Product.Title, Price = pc.Product.Price, Info = pc.Product.Info, SortOrder = pc.Product.SortOrder }) .OrderBy(s => s.SortOrder) .ToList(), // 递归处理子分类,确保整个树形结构完整 Children = entity.Children? .Select(child => MapToViewModel(child)) .OrderBy(s => s.SortOrder) .ToList() }; return vm; }
2. 修改控制器查询逻辑
现在你需要先查询完整的分类树(包括子分类和关联商品),再映射成ViewModel:
// 先加载所有分类及其子分类、关联商品,确保数据完整 var allCategories = _context.ProductCategories .Include(e => e.Children) // 加载子分类 .Include(e => e.ProductInCategory) .ThenInclude(pc => pc.Product) // 加载分类关联的商品 .OrderBy(s => s.SortOrder) .ToList(); // 筛选根分类并映射成ViewModel树形结构 var VMCategories = allCategories .Where(r => r.ParentId == null) .Select(MapToViewModel) .ToList(); // 单独查询无分类商品(不要放在每个分类里,避免重复查询) var orphanProducts = _context.Products .Where(p => !_context.ProductInCategories.Any(pc => pc.ProductId == p.Id)) .Select(p => new ViewModelProduct { Id = p.Id, Title = p.Title, Price = p.Price, Info = p.Info, SortOrder = p.SortOrder }) .OrderBy(s => s.SortOrder) .ToList(); // 用ViewBag传递无分类商品给视图,或者创建一个父ViewModel包裹分类树和无分类商品 ViewBag.OrphanProducts = orphanProducts; return View(VMCategories);
解决方案二:修复视图类型不匹配问题
现在你的ViewModel树形结构已经是ViewModelProductCategory类型了,需要确保递归视图的模型类型正确:
修改递归视图_CategoryRecursive.cshtml
把视图顶部的模型声明改成:
@model IEnumerable<ViewModelProductCategory> @if (Model != null && Model.Any()) { foreach (var item in Model.OrderBy(s => s.SortOrder)) { <li> @item.Title (@item.ProductCountInfo) <ul> // 这里传入的item.Children已经是ViewModelProductCategory集合,和视图模型匹配 @Html.Partial("_CategoryRecursive.cshtml", item.Children) </ul> </li> } }
额外优化建议
- 修正无分类商品的查询逻辑:你原来的代码里查询无分类商品的逻辑是错误的——
v.ProductInCategory.Where(o => !_context.ProductsInCategories.Any(pc => o.Id == pc.ProductId))是在当前分类的关联记录里找不存在的关联记录,这完全不对。正确的逻辑应该是从Products表中找没有任何ProductInCategory关联的商品,也就是上面代码里的orphanProducts查询。 - 避免重复查询:无分类商品只需要查询一次,不要放到每个分类ViewModel里,不然每个分类都会触发一次相同的SQL查询,严重影响性能。
- 使用AutoMapper简化映射:如果你的项目里有AutoMapper,可以配置递归映射规则,这样就不用手动写
MapToViewModel方法了,代码会更简洁。
内容的提问来源于stack exchange,提问作者Stian
相关产品推荐
相关产品推荐

