C# Cơ bản .NET Core §1 Cài đặt, chương trình C# đầu tiên §2 Biến, kiểu dữ liệu và nhập/xuất §3 Toán tử số học và gán §4 So sánh, logic và lệnh if, switch §5 Vòng lặp for, while §6 Phương thức - Method §7 Phương thức - Delegate §8 Lớp - Class §9 Namespace §10 Partial, Nested §11 Kiểu giá trị, tham chiếu §12 Kiểu vô danh và dynamic §13 Biểu thức lambda §14 Event §15 Hàm hủy - Quá tải toán tử - thành viên tĩnh - indexer §16 null và nullable §17 Mảng §18 Chuỗi ký tự §19 Tính kế thừa §20 Phương thức khởi tạo §21 Tính đa hình - abstract - interface §22 Struct và Enum §23 Ngoại lệ Exeption §24 IDisposable - using §25 File cơ bản §26 FileStream §27 Generic §28 Collection - List §29 SortedList §30 Queue / Stack §31 Linkedlist §32 Dictionary - HashSet §33 Phương thức mở rộng §34 ObservableCollection §35 LINQ §36 (Multithreading) async - bất đồng bộ §37 Type §38 Attribute Annotation §39 DI Dependency Injection §40 (Multithreading) Parallel §41 Thư viện lớp §42 (Networking) HttpClient §43 (Networking) HttpMessageHandler §44 (Networking) HttpListener §45 (Networking) Tcp TcpListenerr/TcpClient §46 (ADO.NET) SqlConnection §47 (ADO.NET) SqlCommand §48 (EF Core) Tổng quan §49 (EF Core) Tạo Model §50 (EF Core) Fluent API §51 (EF Core) Query §52 (EF Core) Scaffold §53 (EF Core) Migration §54 (ASP.NET CORE) Hello World! §55 (ASP.NET CORE) Middleware §56 (ASP.NET CORE) Map - Request - Response §57 (ASP.NET CORE) IServiceCollection - MapWhen §58 (ASP.NET CORE) Session - ISession §59 (ASP.NET CORE) Configuration §60 (ASP.NET CORE) Gửi Mail §61 (ASP.NET CORE) SASS/SCSS §62 (ASP.NET CORE) LibMan §63 (ASP.NET RAZOR) Khởi tạo và Route §64 (ASP.NET RAZOR) Cú pháp Razor §65 (ASP.NET RAZOR) Layout trong ASP.NET Core §66 (ASP.NET RAZOR) Partial §67 (ASP.NET RAZOR) ViewComponent §68 (ASP.NET RAZOR) TagHelper §69 (ASP.NET RAZOR) PageModel §70 (ASP.NET RAZOR) Model Binding §71 (ASP.NET RAZOR) HTML Form, Validation §72 (ASP.NET RAZOR) Upload File §73 (ASP.NET RAZOR) HtmlHelper §74 (ASP.NET RAZOR) Entity Framework §75 (ASP.NET RAZOR) Paging §76 (ASP.NET RAZOR) Identity (1) - Register, Login, Logout §77 (ASP.NET RAZOR) Identity (2) Lockout, Reset Password §78 (ASP.NET RAZOR) Identity (3) Google Login §79 (ASP.NET RAZOR) Identity (4) Facebook Login §80 (ASP.NET RAZOR) Identity (5) profile, password, email ... §81 (ASP.NET RAZOR) Identity (6) Role §82 (ASP.NET RAZOR) Identity (7) Role-based Authorization §83 (ASP.NET RAZOR) Identity (8) RoleClaim §84 (ASP.NET RAZOR) Identity (9) Authorization Handler §85 (ASP.NET RAZOR) IAuthorizationService §86 (ASP.NET MVC) Controller - View §87 (ASP.NET MVC) Route §88 (ASP.NET MVC) EF, Identity §89 (ASP.NET MVC) Binding, Validation §90 (ASP.NET MVC) Xây dựng Website(1) §91 (ASP.NET MVC) Xây dựng Website(2) §92 (ASP.NET MVC) Xây dựng Website(3) §93 (ASP.NET MVC) Xây dựng Website(4) §94 (ASP.NET MVC) Giỏ hàng - Cart (5) §95 (ASP.NET MVC) elFinder (5) §96 (ASP.NET MVC) SB Admin (6)

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")] // Model  tương ứng với bảng Category
    public class Category {

        [Key]
        public int Id { get; set; }

        // Category cha (FKey)
        [Display(Name = "Danh mục cha")]
        public int? ParentId { 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("ParentId")]
        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:

    // GET: Admin/Category
    public async Task<IActionResult> Index () {

    var items =  _context.Categories
        .Include (c => c.CategoryChildren)   // <-- Nạp các Category con
        .AsEnumerable()
        .Where (c => c.ParentCategory == null)
        .ToList();

    }

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 ViewComponent để tạo HTML các dòng này (phần tử tr). ViewComponent này sẽ đặt tên là RowTreeCategory

Vậy file Index.cshtml sẽ như sau:

@model IEnumerable<mvcblog.Models.Category>

@{
    ViewData["Title"] = "Danh sách các chuyên mục Blog";
    Layout = "_Layout";
}

<h1>@ViewData["Title"]</h1>

<p>
    <a asp-action="Create">Tạo chuyên mục mới</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Slug)
            </th>
       
            <th></th>
        </tr>
    </thead>
    <tbody>

    @if (Model.Count() > 0)
        @await Component.InvokeAsync("RowTreeCategory", new { categories = Model, level = 0})
    </tbody>
</table>

Đoạn code gọi đến ViewComponent đó là

@await Component.InvokeAsync("RowTreeCategory", new { categories = Model, level = 0})

Nó truyền đến Component dữ liệu có danh sách các Category để từ đó nó dựng HTML là các dòng trong bảng

Về cách tạo và sử dụng ViewComponent xem tại: Tạo và sử dụng ViewComponent , ta sẽ tiến hành xây dựng ViewComponent RowTreeCategory như sau:

RowTreeCategory

RowTreeCategory sẽ lưu ở thư mục Areas/Admin/Views/Category/Components/RowTreeCategory với hai file là RowTreeCategory.csDefault.cshtml

RowTreeCategory.cs

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace MyViewComponent {

  [ViewComponent]
  public class RowTreeCategory : ViewComponent {
    public RowTreeCategory () {

    }
    // data là sữ liệu có cấu trúc
    // {
    //    categories - danh sách các Category
    //    level - cấp của các Category
    // }
    public IViewComponentResult Invoke (dynamic data) {
      return View(data);
    }
  }
}

Default.cshtml

@model dynamic
@{
    List<mvcblog.Models.Category> categories = Model.categories;
    int level = Model.level;  
    string prefix = String.Concat(Enumerable.Repeat("&nbsp;&nbsp;&nbsp;&nbsp;", level));

}
@if (categories.Count > 0)
{
     @foreach (var item in categories)
     {
          <tr>
            <td>
                @* @Html.DisplayFor(modelItem => item.Title) *@
                @Html.Raw(@prefix) @item.Title
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Slug)
            </td> 
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
          @if (item.CategoryChildren != null && item.CategoryChildren.Count > 0) {
            @await Component.InvokeAsync("RowTreeCategory", new { categories = item.CategoryChildren.ToList(), level = 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 RowTreeCategory để 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

Đăng ký theo dõi ủng hộ kênh