- Tạo danh mục Category cho Blog
- Tạo chức năng CRUD cho Category
- Chức năng tạo mới Category
- Giao diện Category ở dạng cây phân cấp
- Giao diện Category ở dạng cây để chọn mục cha
Bài thực hành này tiếp tục trên ví dụ cũ mvcblog
:
Model Binding, Validation, Html Form trong ASP.NET MVC,
tải mã nguồn về mở ra tiếp tục phát triển
ex068-binding-validation
Bắt đầu từ bài thực hành này sẽ tiến hành phát triển ví dụ, tạo một trang Blog.
Định nghĩa Model Category
Model Category
tương ứng là bảng Category trong CSDL SQL Server, lưu trữ thông
tinh các danh mục của Blog, mỗi danh mục có thể là danh mục gốc hoặc
là danh mục con của một danh mục khác,
ta sẽ xây dựng Model này như sau: (Tham khảo
Tạo model trong EF)
Models/Category.cs
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace mvcblog.Models { [Table("Category")] public class Category { [Key] public int Id { get; set; } // Category cha (FKey) [Display(Name = "Danh mục cha")] public int? ParentCategoryId { get; set; } // Tiều đề Category [Required(ErrorMessage = "Phải có tên danh mục")] [StringLength(100, MinimumLength = 3, ErrorMessage = "{0} dài {1} đến {2}")] [Display(Name = "Tên danh mục")] public string Title { get; set; } // Nội dung, thông tin chi tiết về Category [DataType(DataType.Text)] [Display(Name = "Nội dung danh mục")] public string Content { set; get; } //chuỗi Url [Required(ErrorMessage = "Phải tạo url")] [StringLength(100, MinimumLength = 3, ErrorMessage = "{0} dài {1} đến {2}")] [RegularExpression(@"^[a-z0-9-]*$", ErrorMessage = "Chỉ dùng các ký tự [a-z0-9-]")] [Display(Name = "Url hiện thị")] public string Slug { set; get; } // Các Category con public ICollection<Category> CategoryChildren { get; set; } [ForeignKey("ParentCategoryId")] [Display(Name = "Danh mục cha")] public Category ParentCategory { set; get; } } }
Cập nhật DbContext của ứng dụng
Ứng dụng này đang định nghĩa một DbConntext là AppDbContext
, thêm Model trên vào
bằng cách thêm thuộc tính vào lớp AppDbContext
:
public DbSet<Category> Categories {set; get;}
Cập nhật Database bằng EF Migration
Sử dụng kỹ thuật
EF Migration để cập nhật, trước tiên thiết lập khi bảng Category trong DB được tạo
có thực hiện thêm tạo Index cho cột slug
, mở AppDbContext
thêm vào nội dung sau trong phương thức OnModelCreating
(Tham khảo EF dùng Fluent API):
// Tạo Index cho cột Slug bảng Category builder.Entity<Category>(entity => { entity.HasIndex(p => p.Slug); });
Thực hiện lệnh sau tạo một Migration đặt tên là AddCategory
dotnet ef migrations add AddCategory
Sau đó thực hiện cập nhật database
dotnet ef database update AddCategory
Nếu sau cập nhật cần sửa đổi thì có thể xóa Migration và thực hiện lại quy trình trên, xóa Migration cuối bằng cách
dotnet ef database update Migration-trước dotnet ef migrations remove
Tạo CRUD cho Category
Bạn có thể sử dụng công cụ dotnet
aspnetcodegenerator, tự động phát sinh ra Controller
với các Action/View đáp ứng các chức năng Create, Read, Update, Delete cho Category. Sau
khi phát sinh, sẽ chỉnh sửa trên mã nguồn này cho nhanh chóng, hãy thực hiện lệnh sau:
dotnet aspnet-codegenerator controller -name CategoryController -m mvcblog.Models.Category -dc mvcblog.Data.AppDbContext -outDir Areas/Admin/Controllers -l _Layout
Lệnh trên sẽ phát sinh cho chúng ta Controller với tên là Category
, các tham số đó là:
-name Category
tạo controller đặt tên là Category-m mvcblog.Models.Category
chỉ ra lớp Model sẽ phát sinh CRUD-dc mvcblog.Data.AppDbContext
chỉ ra lớp DbContext-outDir Areas/Admin/Controllers
nơi lưu code Controller (đặt trong Area có tên Blog)-l _Layout
view phát sinh sử dụng layout là_Layout
Sau lệnh trên nó tạo ra Controller Category
trong Area Admin
, các View
lưu ở thư mục Areas/Admin/Views/Category
, để các View tự động nạp thư viện TagHelper
hãy tạo ra file Areas/Admin/Views/Category/_ViewImports.cshtml
với nội dung
@using mvcblog @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Như vậy đã tạo ra Controller và các View, bạn có thể truy cập /admin/category/
để hiện thị
trang Index, là danh sách các Category - Từ trang này có thể tạo mới, sửa đổi, xóa Category
Giờ sẽ mở Controller và các file View ra để tùy biến
Tùy biến chức năng tạo Category
Chức năng này thực hiện qua 2 Action Create
(một cho get và một cho post) và file view
là Create.cshtml
, sửa lại Action Create trong CategoryController
như sau:
// GET: Blog/Category/Create public async Task<IActionResult> Create() { // ViewData["ParentId"] = new SelectList(_context.Categories, "Id", "Slug"); var listcategory = await _context.Categories.ToListAsync(); listcategory.Insert(0, new Category() { Title = "Không có danh mục cha", Id = -1 }); ViewData["ParentId"] = new SelectList(listcategory, "Id", "Title", -1); return View(); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Create([Bind("Id,ParentId,Title,Content,Slug")] Category category) { if (ModelState.IsValid) { if (category.ParentId.Value == -1) category.ParentId = null; _context.Add(category); await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } // ViewData["ParentId"] = new SelectList(_context.Categories, "Id", "Slug", category.ParentId); var listcategory = await _context.Categories.ToListAsync(); listcategory.Insert(0, new Category() { Title = "Không có danh mục cha", Id = -1 }); ViewData["ParentId"] = new SelectList(listcategory, "Id", "Title", category.ParentId); return View(category); }
Trang view Index.cshtml có nội dung:
@model mvcblog.Models.Category @{ ViewData["Title"] = "DANH MỤC"; Layout = "_Layout"; } <h1>@ViewData["Title"]</h1> <h4>Tạo danh mục Blog</h4> <hr /> <div class="row"> <div class="col-md-4"> <form asp-action="Create"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="ParentId" class="control-label"></label> <select asp-for="ParentId" class ="form-control" asp-items="ViewBag.ParentId"></select> </div> <div class="form-group"> <label asp-for="Title" class="control-label"></label> <input asp-for="Title" class="form-control" /> <span asp-validation-for="Title" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Content" class="control-label"></label> <input asp-for="Content" class="form-control" /> <span asp-validation-for="Content" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Slug" class="control-label"></label> <input asp-for="Slug" class="form-control" /> <span asp-validation-for="Slug" class="text-danger"></span> </div> <div class="form-group"> <input type="submit" value="Tạo mới" class="btn btn-primary" /> </div> </form> </div> </div> <div> <a asp-action="Index">Quay lại danh sách</a> </div>
Để ý khi tạo Category có phát sinh phần tử HTML Select để chọn ID danh mục cha nếu cần,
để làm điều đó đã sử dụng đoạn mã tạo ra SelectList
rồi chuyển nó tới View thông qua
ViewData, thể hiện qua đoạn mã với diễn giải như sau:
// Đọc các danh mục từ Db var listcategory = await _context.Categories.ToListAsync(); // Chèn vào đầu một danh mục tạm thời với ID = -1 để chọn cho trường hợp thiết lập không // Danh mục cha listcategory.Insert(0, new Category() { Title = "Không có danh mục cha", Id = -1 }); // Tạo SelectList để dựng Select HTML, với các mục trong listcategory, giá trị khi chọn // là trường ID, hiện thị thì là trường Title - mặc định chọn ID = -1 (không có mục cha) ViewData["ParentId"] = new SelectList(listcategory, "Id", "Title", -1);
Tạo View thì nó dựng HTML để chọn phần tử cha bằng select TagHelper
Tạo phần tử HTML select tương ứng cho ParentID của Model, các mục chọn có nguồn từ Controller truyền
tới là ViewBag.ParentId
<select asp-for="ParentId" class ="form-control" asp-items="ViewBag.ParentId"></select>
Các chức năng Delete, Edit, Detail tùy biến một cách tương tự. Kết quả sẽ có đủ chức năng để quản lý danh mục
Hiện thị danh mục ở dạng cây
Ở giao diện trang Index liệt kê các danh mục theo từng dòng của bảng, các danh mục xuất hiện không có sự phân cấp - không nhóm lại làm cho việc quan sát khó khăn.
Ta sẽ tùy biến hiện thị như ở dạng cây thư mục, các Category cùng cha sẽ xuất hiện ở một nhóm, và tiêu đề sẽ thụt vào theo cấp của nó.
Trước tiên, sẽ dùng EF truy vấn lấy ra các Category gốc (không có cha), nhưng nó cũng tải luôn
tất cả các Category chon trong nó, nghĩa là nạp thông tin cho thuộc tính CategoryChildren
của lớp Category
Sửa Action Index trong Controller Category như sau:
public async Task<IActionResult> Index() { var qr = (from c in _context.Categories select c) .Include(c => c.ParentCategory) // load parent category .Include(c => c.CategoryChildren); // load child category var categories = (await qr.ToListAsync()) .Where(c => c.ParentCategory == null) .ToList(); return View(categories); }
Với truy vấn LINQ trên, nó đã tải toàn bộ danh mục, bạn có thể duyệt đệ quy từ mục cha, đến các mục con. Danh mục lưu trong items được chuyển đến View (file Index.cshtml) - Tại đây sẽ trình bày thành bảng mỗi dòng hiện thị một Category. Ta sẽ tạo ra một phương thức trong View để tạo HTML các dòng này (phần tử tr). View sẽ được gọi đệ quy để render các phần tử con có thụt vào đầu dòng:
@using mvcblog.Models.Category @model IEnumerable<Category> @{ ViewData["Title"] = "Danh mục blog"; } <h1>@ViewData["Title"]</h1> <p> <a asp-action="Create">Tạo danh mục mới</a> </p> <table class="table"> <thead> <tr> <th> @Html.DisplayNameFor(model => model.Title) </th> <th> @Html.DisplayNameFor(model => model.ParentCategory) </th> <th></th> </tr> </thead> <tbody> @foreach (var item in Model) { int level = 0; await RenderCategory(item, level); } </tbody> </table> @{ async Task RenderCategory(Category item, int level) { string prefix = String.Concat(Enumerable.Repeat(" ", level)); <tr> <td> @Html.Raw(prefix) @Html.DisplayFor(modelItem => item.Title) </td> <td> @Html.DisplayFor(modelItem => item.ParentCategory.Title) </td> <td> <a asp-action="Edit" asp-route-id="@item.Id">Sửa</a> | <a asp-action="Details" asp-route-id="@item.Id">Chi tiết</a> | <a asp-action="Delete" asp-route-id="@item.Id">Xóa</a> </td> </tr> if (item.CategoryChildren?.Count > 0) { foreach (var cCategory in item.CategoryChildren) { await RenderCategory(cCategory, level + 1); } } } }
Trong View trên, nó tạo dòng của Table cho một Category, nếu Category đó các danh mục con thì đã gọi đệ quy đến chính RenderCategory để tạo các dòng của danh mục con, với cấp tăng thêm một (tương ứng với số khoảng trắng tạo thụt đầu dòng).
Hiện thị danh mục ở dạng cây khi chọn mục cha
Khi tạo danh mục mới, hoặc cập nhật (Edit), phần tử HTML Select để chọn mục cha, bạn có thể xây dựng nó hiện thị ở cấu trúc phân cấp để dễ nhìn, thực hiện như sau:
Đầu tiên xây dựng phương thức trả về các danh mục được nhóm theo mục cha, duyệt đệ quy danh mục đó và thay đổi tiểu đề bằng cách chèn thêm tiền tố là chuỗi các ký tự "-" có độ dài theo cấp của Category
async Task<IEnumerable<Category>> GetItemsSelectCategorie() { var items = await _context.Categories .Include(c => c.CategoryChildren) .Where(c => c.ParentCategory == null) .ToListAsync(); List<Category> resultitems = new List<Category>() { new Category() { Id = -1, Title = "Không có danh mục cha" } }; Action<List<Category>, int> _ChangeTitleCategory = null; Action<List<Category>, int> ChangeTitleCategory = (items, level) => { string prefix = String.Concat(Enumerable.Repeat("—", level)); foreach (var item in items) { item.Title = prefix + " " + item.Title; resultitems.Add(item); if ((item.CategoryChildren != null) && (item.CategoryChildren.Count > 0)) { _ChangeTitleCategory(item.CategoryChildren.ToList(), level + 1); } } }; _ChangeTitleCategory = ChangeTitleCategory; ChangeTitleCategory(items, 0); return resultitems; }
Lúc này trong các Action: Edit, Create thay ViewData["ParentId"]
gán bằng
ViewData["ParentId"] = new SelectList (await GetItemsSelectCategorie(), "Id", "Title", category.ParentId);
Kết quả
Cũng chú ý, dùng các kỹ thuật xác thực quyền để thiết lập những User có quyền truy cập vào Controller này
Mã nguồn tham khảo ASP_NET_CORE/mvcblog, hoặc tải về bản bài này ex068-category