C# cơ bản .NET Core

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("&nbsp;&nbsp;&nbsp;&nbsp;", 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


Đăng ký nhận bài viết mới