Xây dựng menu đa cấp với ASP.NET Core MVC
Một số bạn hỏi mình về cách xây dựng menu đa cấp trong ASP.NET MVC, vấn đề này cũng khá hay vì nhiều trường hợp ví dụ danh mục sản phẩm hay tin tức cần nhiều cấp thay vì 2 cấp, nếu giới hạn số cấp thì sẽ khó trong việc hiển thị dữ liệu đa cấp.
Để xây dựng menu đa cấp, các bạn cần có database dạng đa cấp với các trường sau, ví dụ ở đây mình sẽ tạo một bảng tên là Categories:
| Tên trường | Kiểu dữ liệu |
| Id | int NOT NULL |
| Name | nvarchar(200) |
| ParentId | int NULL |
Ở đây trường ParentId là trường chỉ ra khóa chính của bản ghi cha, ở cơ sở dữ liệu chúng ta lưu phẳng nhưng khi hiển thị lên chúng ta cần hiển thị dạng như thế này:

Dữ liệu của chúng ta trong database sẽ có dạng như thế này:
| Id | Name | ParentId |
| 1 | Phim | NULL |
| 2 | Phim hài | 1 |
| 3 | Phim hài bộ | 2 |
Như vậy ở đây chúng ta có menu 3 cấp, trong đó Phim là cấp 1, Phim hài là cấp 2 và Phim hài bộ là cấp 3. Vậy các bạn đã hiểu cấu trúc dữ liệu rồi chứ? Ở đây chúng ta có thể lưu trữ bao nhiêu cấp tùy thích.
Bây giờ chúng ta sẽ cùng triển khai code sau khi có cơ sở dữ liệu như trên nhé, cách tạo bảng sử dụng Entity Framework Core mình sẽ không hướng dẫn ở đây mà chúng ta giả sử là đã tạo ra được bảng Categories và có dữ liệu như trên rồi. Các bạn có thể tham khảo khóa học: Xây dựng ứng dụng CMS với ASP.NET Core 8.0 + Angular
Bước 1: Chúng ta sẽ tạo ra 1 view model như sau để hứng dữ liệu từ Repository Layer, class này chứa một thuộc tính HasChildren để check xem nó có chứa bản ghi con nào không? Còn thuộc tính Children chứa danh sách bản ghi con.
public class NavigationItemViewModel
{
public required string Id { get; set; }
public string? ParentId { get; set; }
public required string Name { get; set; }
public required string Url { get; set; }
public bool HasChildren
{
get
{
return Children.Any();
}
}
public IEnumerable<NavigationItemViewModel> Children { get; set; } = new List<NavigationItemViewModel>();
}
Tiếp theo chúng ta tạo một service tên là INavigationService:
public interface INavigationService
{
Task<IEnumerable<NavigationItemViewModel>> GetNavigationItemsAsync();
}
Và implement nó trong NavigationService, class này nhằm trả về một danh sách các NavigationItemViewModel đã được chuyển sạng dữ liệu phẳng sang dạng cha con theo Model ở trên:
public class NavigationService : INavigationService
{
private readonly IUnitOfWork _unitOfWork;
public NavigationService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task<IEnumerable<NavigationItemViewModel>> GetNavigationItemsAsync()
{
var allCates = await _unitOfWork.MovieCategories.GetAllAsync();
var allPostCates = await _unitOfWork.PostCategories.GetAllAsync();
return BuildNavigation(allCates, allPostCates);
}
public IEnumerable<NavigationItemViewModel> BuildNavigation(IEnumerable<MovieCategory> allCates, IEnumerable<PostCategory> allPostCates)
{
if (allCates == null || !allCates.Any())
{
return new List<NavigationItemViewModel>();
}
var roots = allCates.Where(x => !x.ParentId.HasValue).OrderBy(x => x.DisplayOrder).ToList();
if (!roots.Any())
{
return new List<NavigationItemViewModel>();
}
var listResult = new List<NavigationItemViewModel>();
foreach (var root in roots)
{
var children = allCates.Where(x => x.ParentId.Equals(root.Id)).OrderBy(x => x.DisplayOrder).ToList();
listResult.Add(new NavigationItemViewModel
{
Id = root.Id.ToString(),
ParentId = root.ParentId?.ToString(),
Name = root.Name,
Url = string.Format(UrlConsts.MoviesByCategory, root.Slug),
Children = BuildMovieChildren(allCates, children)
});
}
return listResult;
}
private IEnumerable<NavigationItemViewModel> BuildMovieChildren(IEnumerable<MovieCategory> allCates, IEnumerable<MovieCategory> children)
{
if (children == null || !children.Any())
{
return new List<NavigationItemViewModel>();
}
var listResult = new List<NavigationItemViewModel>();
foreach (var child in children)
{
var subChildren = allCates.Where(x => x.ParentId.Equals(child.Id)).OrderBy(x => x.DisplayOrder).ToList();
listResult.Add(new NavigationItemViewModel
{
Id = child.Id.ToString(),
ParentId = child.ParentId?.ToString(),
Name = child.Name,
Url = string.Format(UrlConsts.MoviesByCategory, child.Slug),
Children = BuildMovieChildren(allCates, subChildren)
});
}
return listResult;
}
private IEnumerable<NavigationItemViewModel> BuildPostChildren(IEnumerable<PostCategory> allCates, IEnumerable<PostCategory> children)
{
if (children == null || !children.Any())
{
return new List<NavigationItemViewModel>();
}
var listResult = new List<NavigationItemViewModel>();
foreach (var child in children)
{
var subChildren = allCates.Where(x => x.ParentId.Equals(child.Id)).OrderBy(x => x.SortOrder).ToList();
listResult.Add(new NavigationItemViewModel
{
Id = child.Id.ToString(),
ParentId = child.ParentId?.ToString(),
Name = child.Name,
Url = string.Format(UrlConsts.PostByCategory, child.Slug),
Children = BuildPostChildren(allCates, subChildren)
});
}
return listResult;
}
}
Khi đã có danh sách item theo dạng cha con, chúng ta cần hiển thị ra UI, ở đây mình dùng 1 cái ViewComponent tên là Navigation trong Controllers/Components/NavigationViewComponent.cs:
public class NavigationViewComponent : ViewComponent
{
private readonly INavigationService _navigationService;
public NavigationViewComponent(INavigationService navigationService)
{
_navigationService = navigationService;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var navItems = await _navigationService.GetNavigationItemsAsync();
return View(navItems);
}
}
Và hiển thị ở View trong Shared/Components/Navigation/Default.cshtml như sau:
@model IEnumerable<NavigationItemViewModel>
@foreach (var item in Model)
{
<li class="header-menu-item">
<a title="@item.Name" href="@item.Url" class="header-menu-link category"
id="@string.Format("menu-{0}-item", item.Id)">
<span>@item.Name</span>
@if (item.HasChildren)
{
<i class="far fa-angle-down arrow" v-on:click.prevent.stop="onToggleSubmenu('@item.Id')"></i>
}
</a>
@if (item.HasChildren)
{
<ul id="@string.Format("menu-{0}-submenu", item.Id)" class="submenu">
@foreach (var child in item.Children)
{
@await Html.PartialAsync("_SubMenuItem", child)
}
</ul>
}
</li>
}
Chúng ta cần tạo 1 Partial View tên là _SubMenuItem, đây là Partial view sẽ đệ quy khi một menu có danh sách menu con trong nó, đệ quy đến khi không có danh sách menu con trong nó nữa mới thôi. Nội dung file _SubMenuItem.cshtml nằm trong thư mục Shared:
@model NavigationItemViewModel;
<li>
<a title="@Model.Name" href="@Model.Url"
id="@string.Format("menu-{0}-item", Model.Id)">
<span>@Model.Name</span>
@if (Model.HasChildren)
{
<i class="far fa-angle-right arrow" v-on:click.prevent.stop="onToggleSubmenu('@Model.Id')"></i>
}
</a>
@if (Model.HasChildren)
{
<ul id="@string.Format("menu-{0}-submenu", Model.Id)" class="submenu submenu-2">
@foreach (var child in Model.Children)
{
@await Html.PartialAsync("_SubMenuItem", child)
}
</ul>
}
</li>
Ở đây có một điều là, khi lặp đến một item trong danh sách NavigationItem thì cứ check nếu bản ghi hiện tại HasChildren = true thì sẽ tiếp tục gọi _SubMenuItem partial view và truyền bản ghi con của nó vào, cứ như vậy. Khi nào Model.HasChildren = false thì sẽ dừng lại.
Tác giả: Bạch Ngọc Toàn
Chú ý: Tất cả các bài viết trên TEDU.COM.VN đều thuộc bản quyền TEDU, yêu cầu dẫn nguồn khi trích lại trên website khác.
Tags:
Bài viết liên quan
Tìm hiểu về ABP Framework, một framework mã nguồn mở trên .NET
ABP Framework là một framework hoàn chỉnh để xây dựng ứng dụng web hiện đại bằng cách áp dụng các best practice và tiêu chuẩn trong phát triển phần mềm.
Đọc thêm
Tìm lỗi ứng dụng ASP.NET Core trên Windows Server sử dụng stdout log
Một số lỗi trong lúc khởi động hệ thống ASP.NET Core trên Windows Server mà chúng ta chưa rõ nguyên nhân tại sao ứng dụng không hoạt động. Vậy bạn có thể dùng đểns stdout để chẩn đoán lỗi.
Đọc thêmVòng đời của Dependency Injection: Transient, Singleton và Scoped
Hiểu về vòng đời của các service được tạo sử dụng Dependency Injection là rất quan trọng trước khi sử dụng chúng.
Đọc thêm
Cơ chế Dependency Injection trong ASP.NET Core
Dependency Injection giờ đã trở thành thành phần chính thức mặc định của ASP.NET Core.
Đọc thêm
Unobtrusive Client Validation trong ASP.NET Corre
Trong bài viết này chúng ta sẽ thực hiện validation phía client sử dụng Javascript.
Đọc thêm
Validation Tag Helper trong ASP.NET Core
ASP.NET cung cấp các tag helper liên quan đến hiển thị validation message cho người dùng.
Đọc thêm
Model Validation trong ASP.NET Core
Bài viết này chúng ta sẽ học về cơ chế Model Validation.
Đọc thêm
Cơ chế Model Binding: Truyền dữ liệu từ View lên Controller
Trong cơ chế Model Binding của ASP.NET Core chúng ta sẽ học cách làm sao để truyền dữ liệu từ View lên Controller.
Đọc thêm
Environment Tag Helper trong ASP.NET Core
Environment tag helper hỗ trợ tạo ra các nội dung phụ thuộc vào biến quy định môi trường trong ASP.NET Core.
Đọc thêm
Input Tag Helper trong ASP.NET Core
Input Tag Helper tạo ra phần tử HTML tương ứng với thuộc tính của Model là input.
Đọc thêm