Bài 96: (ASP.NET Core MVC) Xây dựng ứng dụng mẫu - Các bài Post của Blog (phần 3)

Ngày đăng: 12/30/2022 11:16:58 AM

Xây dựng Model bài Post

Model biểu diễn bài Post, mỗi bài Post đó có thể nằm trong một hoặc nhiều Category như sau:

Models/PostBase.cs

 public class PostBase
 {
     [Key]
     public int PostId {set; get;}
 
     [Required(ErrorMessage = "Phải có tiêu đề bài viết")]
     [Display(Name = "Tiêu đề")]
     [StringLength(160, MinimumLength = 5, ErrorMessage = "{0} dài {1} đến {2}")]
     public string Title {set; get;}
 
     [Display(Name = "Mô tả ngắn")]
     public string Description {set; get;}
 
     [Display(Name="Chuỗi định danh (url)", Prompt = "Nhập hoặc để trống tự phát sinh theo Title")]
     [Required(ErrorMessage = "Phải thiết lập chuỗi URL")]
     [StringLength(160, MinimumLength = 5, ErrorMessage = "{0} dài {1} đến {2}")]
     [RegularExpression(@"^[a-z0-9-]*$", ErrorMessage = "Chỉ dùng các ký tự [a-z0-9-]")]
     public string Slug {set; get;}
 
     [Display(Name = "Nội dung")]
     public string Content {set; get;}
 
     [Display(Name = "Xuất bản")]
     public bool Published {set; get;}
 
     public List<PostCategory>  PostCategories { get; set; }
 
 }
 

Models/Post.cs

 [Table("Post")]
 public class Post : PostBase
 {
 
     [Required]
     [Display(Name = "Tác giả")]
     public string AuthorId {set; get;}
     [ForeignKey("AuthorId")]
     [Display(Name = "Tác giả")]
     public AppUser Author {set; get;}
 
     [Display(Name = "Ngày tạo")]
     public DateTime DateCreated {set; get;}
 
     [Display(Name = "Ngày cập nhật")]
     public DateTime DateUpdated {set; get;}
 }
 

Để tạo ra quan hệ nhiều - nhiều giữa Post và Category thực hiện tạo ra bảng PostCategory với Model như sau:

 public class PostCategory
 {
     public int PostID {set; get;}
 
     public int CategoryID {set; get;}
 
     [ForeignKey("PostID")]
     public Post Post {set; get;}
 
     [ForeignKey("CategoryID")]
     public Category Category {set; get;}
 }
 

Cập nhật vào AppDbContext

 public class AppDbContext : IdentityDbContext<AppUser> {
 
     public DbSet<Category> Categories {set; get;}
     public DbSet<Post> Posts {set; get;}
     public DbSet<PostCategory> PostCategories {set; get;}
 
     public AppDbContext (DbContextOptions<AppDbContext> options) : base (options) { }
 
     protected override void OnModelCreating (ModelBuilder builder) {
 
         // ...
 
         // Tạo key của bảng là sự kết hợp PostID, CategoryID, qua đó
         // tạo quan hệ many to many giữa Post và Category
         builder.Entity<PostCategory>().HasKey(p => new {p.PostID, p.CategoryID});
 
     }
 
 }
 

Thực hiện lệnh tạo Migration và cập nhật database

 dotnet ef migrations add AddPost
 dotnet ef database update AddPost
 

Chú ý, nếu muốn hủy cập nhật thì thực hiện, cập nhật về phiên bản Migration trước (ví dụ AddCategory), sau đó xóa bản Migration hiện tại

 dotnet ef database update AddCategory
 dotnet ef migrations remove
 

Tạo các Controller Post với các chức năng CRUD

Tạo Controller tên PostController, với các Action và View mặc định, để từ đó sửa các chức năng

 dotnet aspnet-codegenerator controller -name PostController -m mvcblog.Models.Post -dc mvcblog.Data.AppDbContext -outDir  Areas/Admin/Controllers -l _Layout

Sau lệnh này nó tạo ra Controller PostController tại Areas/Admin/Controllers/PostController.cs, và các file View tại thư mục Areas/Admin/Views/Post

Bạn hãy thêm thuộc tính [Authorize] cho Controller, đăng nhập và kiểm tra tại địa chỉ /admin/post

Do code phát sinh mặc định chưa đảm bảo hoạt động chính xác trên dữ liệu phức tạp mong muốn, giờ tiến hành cập nhật tùy biến từng chức năng về quản lý các bài viết Create, Edit, Update ...

Inject dịch vụ UserManager

Do trong quá trình tạo bài viết (post), chỉnh sửa, cập nhật ... cần có thông tin về User, nên ta tiến hành Inject dịch vụ UserManager vào controller bằng cách sửa

 public class PostController : Controller
 {
     private readonly AppDbContext _context;
 
     private readonly UserManager<AppUser> _usermanager;
 
     public PostController(AppDbContext context, UserManager<AppUser> usermanager)
     {
         _context = context;
         _usermanager = usermanager;
     }
     ...
 

Theo cách tương tự bạn có thể Inject dịch vụ Logger ILogger<PostController>

Tùy biến Action - Create tạo các bài Post

Cập nhật Action Create (cho get và post) như sau:

     [Area ("Admin")]
     [Authorize]
     public class PostController : Controller {
 
         /...
 
         [BindProperty]
         public int[] selectedCategories { set; get; }
 
         // GET: Admin/Post/Create
         public async Task Create () {
 
             // Thông tin về User tạo Post
             var user = await _usermanager.GetUserAsync (User);
             ViewData["userpost"] = $"{user.UserName} {user.FullName}";
 
             // Danh mục chọn để đăng bài Post
             var categories = await _context.Categories.ToListAsync ();
             ViewData["categories"] = new MultiSelectList (categories, "Id", "Title");
             return View ();
 
         }
 
 
         // POST: Admin/Post/Create
         [HttpPost]
         [ValidateAntiForgeryToken]
         public async Task Create ([Bind ("PostId,Title,Description,Slug,Content,Published")] PostBase post) {
 
             var user = await _usermanager.GetUserAsync (User);
             ViewData["userpost"] = $"{user.UserName} {user.FullName}";
 
             // Phát sinh Slug theo Title
             if (ModelState["Slug"].ValidationState == ModelValidationState.Invalid) {
                 post.Slug = Utils.GenerateSlug (post.Title);
                 ModelState.SetModelValue ("Slug", new ValueProviderResult (post.Slug));
                 // Thiết lập và kiểm tra lại Model
                 ModelState.Clear();
                 TryValidateModel (post);
             }
 
 
             if (selectedCategories.Length == 0) {
                 ModelState.AddModelError (String.Empty, "Phải ít nhất một chuyên mục");
             }
 
             bool SlugExisted = await _context.Posts.Where(p => p.Slug == post.Slug).AnyAsync();
             if (SlugExisted)
             {
                 ModelState.AddModelError(nameof(post.Slug), "Slug đã có trong Database");
             }
 
             if (ModelState.IsValid) {
                 //Tạo Post
                 var newpost = new Post () {
                     AuthorId = user.Id,
                     Title = post.Title,
                     Slug = post.Slug,
                     Content = post.Content,
                     Description = post.Description,
                     Published = post.Published,
                     DateCreated = DateTime.Now,
                     DateUpdated = DateTime.Now
                 };
                 _context.Add (newpost);
                 await _context.SaveChangesAsync ();
 
                 // Chèn thông tin về PostCategory của bài Post
                 foreach (var selectedCategory in selectedCategories) {
                     _context.Add (new PostCategory () { PostID = newpost.PostId, CategoryID = selectedCategory });
                 }
                 await _context.SaveChangesAsync ();
 
                 return RedirectToAction (nameof (Index));
             }
 
             var categories = await _context.Categories.ToListAsync ();
             ViewData["categories"] = new MultiSelectList (categories, "Id", "Title", selectedCategories);
             return View (post);
         }
 
       /..
 }
 

Cả hai Action này sử dụng View - Create.cshtml có nội dung

 @model mvcblog.Models.PostBase
 
 @{
     ViewData["Title"] = "Tạo mới bài viết";
     Layout = "_Layout";
 }
 
 <h1>Tạo bài viết</h1>
 <hr />
 <div class="row">
     <div class="col-md-8">
         <form asp-action="Create">
             <div asp-validation-summary="All" class="text-danger"></div>
 
 
             <div class="form-group">
                 <label class="control-label">Chọn danh mục</label>
                 @Html.ListBox("selectedCategories", ViewBag.categories, 
                     new {@class="w-100", id = "selectedCategories"})
             </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="Description" class="control-label"></label>
                 <textarea asp-for="Description" class="form-control"></textarea>
                 <span asp-validation-for="Description" 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">
                 <label asp-for="Content" class="control-label"></label>
                 <textarea asp-for="Content" class="form-control"></textarea>
                 <span asp-validation-for="Content" class="text-danger"></span>
             </div>
             <div class="form-group form-check">
                 <label class="form-check-label">
                     <input class="form-check-input" asp-for="Published" /> 
                     @Html.DisplayNameFor(model => model.Published)
                 </label>
             </div>
             <div class="form-group">Tác giả: <strong>@ViewBag.userpost</strong></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>
 
 
 @section Scripts
 {
     @await Html.PartialAsync("_Summernote", new {height = 200, selector = "#Content"})
     
     <script src="~/lib/multiple-select/multiple-select.min.js"></script>
     <link rel="stylesheet" href="~/lib/multiple-select/multiple-select.min.css" />
     <script>
           $('#selectedCategories').multipleSelect({
                 selectAll: false,
                 keepOpen: false,
                 isOpen: false
             });
     </script>
 
 }
 

Một số lưu ý về code sử dụng trong chức năng tạo bài viết ở trên

Mỗi bài Post có thể nằm trong một hoặc vài Category, bảng Post và Category có mối quan hệ nhiều nhiều. Bảng trung gian đó là PostCategory với hai trường dữ liệu PostID và CategoryID

Trong trang tạo có cho phép lựa chọn các Category của Post.

mvcblog

Để thực hiện chức năng này - có dùng thuộc tính là mảng chứa các CategoryID của bài Post

 [BindProperty]
 public int[] selectedCategories { set; get; }
 

Để tạo phần tử HTML chọn các Category sẽ sử dụng MultiSelectList và gửi nó đến View

 var categories = await _context.Categories.ToListAsync ();
 ViewData["categories"] = new MultiSelectList (categories, "Id", "Title");
 

Tại View dựng phần tử Select bằng

 <div class="form-group">
     <label class="control-label">Chọn danh mục</label>
     @Html.ListBox("selectedCategories", ViewBag.categories, 
         new {@class="w-100", id = "selectedCategories"})
 </div>

Phần tử HTML này có tên selectedCategories bind với Controller. Có thể tích hợp thư viện JS - multiple-select (Xem phần cuối Role trong Identity) Bạn cần tải về thư viện JS tại multiple-select, sau đó ở Create.cshtml, phần cuối có section tích hợp thư viện và kích hoạt nó cho phần tử selectedCategories

 @section Scripts
 {
     @await Html.PartialAsync("_Summernote", new {height = 200, selector = "#Content"})
     <script src="~/lib/multiple-select/multiple-select.min.js"></script>
     <link rel="stylesheet" href="~/lib/multiple-select/multiple-select.min.css" />
     <script>
           $('#selectedCategories').multipleSelect({
               selectAll: false,
               keepOpen: false,
               isOpen: false
           });
     </script>
 
 }

Trong đoạn section trên cũng kích hoạt Summernote cho phần tử Content để soạn thảo HTML (xem Tích hợp Summernote vào ASP.NET )

Trong dữ liệu Post có trường Slug, có thể dùng như là một định danh đến bài viết - sau này có thể dùng để tạo Url, khi tạo nếu không nhập Slug thì nó sinh ra từ Title (ví dụ nếu Title là "Bài viết" thì phát sinh Slug là "bai-viet", đoạn code thực hiện chức năng đó là:

 // Phát sinh Slug theo Title nếu Slug không được nhập
 if (ModelState["Slug"].ValidationState == ModelValidationState.Invalid) {
     post.Slug = Utils.GenerateSlug (post.Title);
     ModelState.SetModelValue ("Slug", new ValueProviderResult (post.Slug));
     // Thiết lập và kiểm tra lại Model
     ModelState.Clear();
     TryValidateModel (post);
 }
 

Utils.GenerateSlug là phương thức tĩnh, chuyển đổi Title thành Url thân thiện, phương thức này được xây dựng như sau: mã nguồn Utils.GenerateSlug

Như vậy đã hoàn thành chức năng tạo bài viết, hãy tạo một số bài viết mẫu của bạn

mvcblog

Tùy biến Index - Trang danh sách bài viết

mvcblog

Action Index trong Controller được sửa lại như sau:

 public const int ITEMS_PER_PAGE = 4;
 // GET: Admin/Post
 public async Task<IActionResult> Index ([Bind(Prefix="page")]int pageNumber) {
 
     if (pageNumber == 0)
         pageNumber = 1;
 
 
     var listPosts = _context.Posts
         .Include (p => p.Author)
         .Include (p => p.PostCategories)
         .ThenInclude (c => c.Category)
         .OrderByDescending(p => p.DateCreated);
 
     _logger.LogInformation(pageNumber.ToString());
 
     // Lấy tổng số dòng dữ liệu
     var totalItems = listPosts.Count ();
     // Tính số trang hiện thị (mỗi trang hiện thị ITEMS_PER_PAGE mục)
     int totalPages = (int) Math.Ceiling ((double) totalItems / ITEMS_PER_PAGE);
 
     if (pageNumber > totalPages)
         return RedirectToAction(nameof(PostController.Index), new {page = totalPages});
 
 
     var posts = await listPosts
                     .Skip (ITEMS_PER_PAGE * (pageNumber - 1))
                     .Take (ITEMS_PER_PAGE)
                     .ToListAsync();
 
     // return View (await listPosts.ToListAsync());
     ViewData["pageNumber"] = pageNumber;
     ViewData["totalPages"] = totalPages;
 
     return View (posts.AsEnumerable());
 }
 

View Index.cshtml có nội dung như sau:

 @model IEnumerable<mvcblog.Models.Post>
 
 @{
     ViewData["Title"] = "Index";
     Layout = "_Layout";
 }
 
 <h1>Danh mục</h1>
 
 <p>
     <a asp-action="Create">Tạo bài viết mới</a>
 </p>
 <table class="table">
     <thead>
         <tr>
             <th>
                 @Html.DisplayNameFor(model => model.Title)
             </th>
 
             <th>
                 @Html.DisplayNameFor(model => model.AuthorId)
             </th>
             <th>
                 Ngày tạo <br/>
                 Cập nhật
             </th>
             <th>
                 @Html.DisplayNameFor(model => model.Published)
             </th>
             <th>Chuyên mục</th>
             <th></th>
         </tr>
     </thead>
     <tbody>
 @foreach (var item in Model) {
         <tr>
             <td>
                 
                 <a title="xem chi tiết" asp-action="Details" asp-route-id="@item.PostId">
                     <strong>@Html.DisplayFor(modelItem => item.Title)</strong>
                 </a>
             </td>
             <td>
                 @Html.DisplayFor(modelItem => item.Author.UserName)
             </td>
             <td>
                 @item.DateCreated.ToShortDateString()
                 <br>
                 @item.DateUpdated.ToShortDateString()
             </td>
 
             <td>
                 @Html.DisplayFor(modelItem => item.Published)
             </td>
             <td>
                 @Html.Raw(string.Join("<br>",
                     item.PostCategories
                     .Select(p => p.Category)
                     .ToList()
                     .Select(c => $"<i>{c.Title}</i>")))
             </td>
 
             <td>
                 <a asp-action="Edit" asp-route-id="@item.PostId">Sửa</a> |
                 <a asp-action="Delete" asp-route-id="@item.PostId">Xóa</a>
             </td>
         </tr>
 }
     </tbody>
 </table>
 
 
 @{
 
     Func<int?,string> generateUrl = (int? _pagenumber)  => {
         return Url.ActionLink("Index", null, new {page = _pagenumber});
     };
 
     var datapaging = new {
         currentPage = ViewBag.pageNumber,
         countPages  = ViewBag.totalPages,
         generateUrl =  generateUrl
     };
 
 }
 <partial name="_Paging" model="@datapaging" />

Một số điểm chú ý của code trên

Có tích hợp Paging (xem Partial phân trang)

Khi truy vấn lấy danh sách các Post, mới mỗi Model Post cũng tải luôn Author và danh sách các Categories của Post, nên thực hiện LINQ đó như sau:

 var listPosts = _context.Posts
     .Include (p => p.Author)                // Tải Author
     .Include (p => p.PostCategories)        // Tải các PostCategory
     .ThenInclude (c => c.Category)          // Mỗi PostCateogry tải luôn Categtory
     .OrderByDescending(p => p.DateCreated);
 

Danh sách các Post chuyển đến View để hiện thị chỉ lấy từng trang một, do đó thực hiện tiếp LINQ

 var posts = await listPosts
                 .Skip (ITEMS_PER_PAGE * (pageNumber - 1))       // Bỏ qua các trang trước
                 .Take (ITEMS_PER_PAGE)                          // Lấy số phần tử của trang hiện tại
                 .ToListAsync();
 

posts này sẽ chuyển đến View để hiện thị

Trong Index.cshtml, phần tích hợp Paging thực hiện tương tự ở các hướng dẫn khác. Lưu ý ở đây có cột hiện thị tên các danh mục của bài Post, các tên này được lấy ra bằng LINQ như sau:

 @Html.Raw(string.Join("<br>",
     item.PostCategories
     .Select(p => p.Category)
     .ToList()
     .Select(c => $"<i>{c.Title}</i>")))
 

Trang cập nhật bài viết

Action Edit, tương ứng với View Edit.cshtml có chức năng cập nhật nội dung bài viết có sẵn, xây dựng chức năng này với code như sau:

 // GET: Admin/Post/Edit/5
 public async Task<IActionResult> Edit (int? id) {
     if (id == null) {
         return NotFound ();
     }
 
     // var post = await _context.Posts.FindAsync (id);
     var post = await _context.Posts.Where (p => p.PostId == id)
         .Include (p => p.Author)
         .Include (p => p.PostCategories)
         .ThenInclude (c => c.Category).FirstOrDefaultAsync ();
     if (post == null) {
         return NotFound ();
     }
 
     ViewData["userpost"] = $"{post.Author.UserName} {post.Author.FullName}";
     ViewData["datecreate"] = post.DateCreated.ToShortDateString ();
 
     // Danh mục chọn
     var selectedCates = post.PostCategories.Select (c => c.CategoryID).ToArray ();
     var categories = await _context.Categories.ToListAsync ();
     ViewData["categories"] = new MultiSelectList (categories, "Id", "Title", selectedCates);
 
     return View (post);
 }
 
 // POST: Admin/Post/Edit/5
 // To protect from overposting attacks, enable the specific properties you want to bind to, for
 // more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
 [HttpPost]
 [ValidateAntiForgeryToken]
 public async Task<IActionResult> Edit (int id, [Bind ("PostId,Title,Description,Slug,Content")] PostBase post) {
 
     if (id != post.PostId) {
         return NotFound ();
     }
 
 
 
     // Phát sinh Slug theo Title
     if (ModelState["Slug"].ValidationState == ModelValidationState.Invalid) {
         post.Slug = Utils.GenerateSlug (post.Title);
         ModelState.SetModelValue ("Slug", new ValueProviderResult (post.Slug));
         // Thiết lập và kiểm tra lại Model
         ModelState.Clear ();
         TryValidateModel (post);
     }
 
     if (selectedCategories.Length == 0) {
         ModelState.AddModelError (String.Empty, "Phải ít nhất một chuyên mục");
     }
 
     bool SlugExisted = await _context.Posts.Where (p => p.Slug == post.Slug && p.PostId != post.PostId).AnyAsync();
     if (SlugExisted) {
         ModelState.AddModelError (nameof (post.Slug), "Slug đã có trong Database");
     }
 
     if (ModelState.IsValid) {
 
         // Lấy nội dung từ DB
         var postUpdate = await _context.Posts.Where (p => p.PostId == id)
             .Include (p => p.PostCategories)
             .ThenInclude (c => c.Category).FirstOrDefaultAsync ();
         if (postUpdate == null) {
             return NotFound ();
         }
 
         // Cập nhật nội dung mới
         postUpdate.Title = post.Title;
         postUpdate.Description = post.Description;
         postUpdate.Content = post.Content;
         postUpdate.Slug = post.Slug;
         postUpdate.DateUpdated = DateTime.Now;
 
         // Các danh mục không có trong selectedCategories
         var listcateremove = postUpdate.PostCategories
                                        .Where(p => !selectedCategories.Contains(p.CategoryID))
                                        .ToList();
         listcateremove.ForEach(c => postUpdate.PostCategories.Remove(c));
 
         // Các ID category chưa có trong postUpdate.PostCategories
         var listCateAdd = selectedCategories
                             .Where(
                                 id => !postUpdate.PostCategories.Where(c => c.CategoryID == id).Any()
                             ).ToList();
 
         listCateAdd.ForEach(id => {
             postUpdate.PostCategories.Add(new PostCategory() {
                 PostID = postUpdate.PostId,
                 CategoryID = id
             });
         });
 
         try {
 
             _context.Update (postUpdate);
 
             await _context.SaveChangesAsync();
         } catch (DbUpdateConcurrencyException) {
             if (!PostExists (post.PostId)) {
                 return NotFound ();
             } else {
                 throw;
             }
         }
         return RedirectToAction (nameof (Index));
     }
 
     var categories = await _context.Categories.ToListAsync ();
     ViewData["categories"] = new MultiSelectList (categories, "Id", "Title", selectedCategories);
     return View (post);
 }
 
 

Edit.cshtml

 @model mvcblog.Models.PostBase
 
 @{
     ViewData["Title"] = "Edit";
     Layout = "_Layout";
 }
 
 <h1>Edit</h1>
 
 <h4>Cập nhật bài viết</h4>
 <hr />
 <div class="row">
     <div class="col-md-8">
         <form asp-action="Edit">
             <div asp-validation-summary="All" class="text-danger"></div>
 
             <input type="hidden" asp-for="PostId" />
             <div class="form-group">
                 <label class="control-label">Chọn danh mục</label>
                 @Html.ListBox("selectedCategories", ViewBag.categories, 
                     new {@class="w-100", id = "selectedCategories"})
             </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="Description" class="control-label"></label>
                 <textarea asp-for="Description" class="form-control"></textarea>
                 <span asp-validation-for="Description" 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">
                 <label asp-for="Content" class="control-label"></label>
                 <textarea asp-for="Content" class="form-control"></textarea>
                 <span asp-validation-for="Content" class="text-danger"></span>
             </div>
             <div class="form-group form-check">
                 <label class="form-check-label">
                     <input class="form-check-input" asp-for="Published" /> @Html.DisplayNameFor(model => model.Published)
                 </label>
             </div>
             <div class="form-group">Tác giả: <strong>@ViewBag.userpost</strong></div>
             <div class="form-group">Ngày tạo: <strong>@ViewBag.datecreate</strong></div>
             
             <div class="form-group">
                 <input type="submit" value="Cập nhật" class="btn btn-primary" />
             </div>
         </form>
     </div>
 </div>
 
 <div>
     <a asp-action="Index">Quay lại danh sách</a>
 </div>
 
 @section Scripts
 {
     @await Html.PartialAsync("_Summernote", new {height = 200, selector = "#Content"})
     
     <script src="~/lib/multiple-select/multiple-select.min.js"></script>
     <link rel="stylesheet" href="~/lib/multiple-select/multiple-select.min.css" />
     <script>
           $('#selectedCategories').multipleSelect({
                 selectAll: false,
                 keepOpen: false,
                 isOpen: false
             });
     </script>
 
 }

Như vậy đến đây có đủ chức năng về quản lý các bài viết Post

Mã nguồn tham khảo ASP_NET_CORE/mvcblog, hoặc tải về bản bài này ex068-post

Nguồn tin: