C# cơ bản .NET Core

Bài thực hành này tiếp tục trên ví dụ cũ mvcblog: Tích hợp Entity Framework và Identity, tải mã nguồn về mở ra tiếp tục phát triển ex068-identity

Về binding model, validation model đã từng thực hành đối với Razor Page, như tại: Model Binding Razor Page. Trong các phần này áp dụng lại trong mô hình MVC tại các Controller

Model Binding - Ánh xạ dữ liệu

Khi các Controller làm việc với dữ liệu, nó sẽ tiếp nhận thông tin gửi đến từ Http Request. Như các dữ liệu trong Route, trong chuỗi query của Url, trong trường thông tin Form. Những dữ liệu đều có dạng theo cặp key/value (tên / giá trị dạng chuỗi)

Việc lấy dữ liệu từ nguồn gửi đến, căn cứ vào tên, thực hiện convert sang kiểu dữ liệu phù hợp và gán vào các thuộc tính của Controller, hay các tham số của phương thức Action gọi là binding (ánh xạ dữ liệu).

Hãy xét ví dụ sau: đây là một Action trong Controller

[HttpGet]
[Route("testviewpost/{Postid?}/")]
public JsonResult testviewpost(int postid, bool viewall) {

    return Json(new {
        postid = postid,
        viewall = viewall
    });
}

Action trên trả về nội dung Json, khi truy cập địa chỉ /testviewpost/12/?viewall=true, thấy nội dung trả về là:

{"postid":12,"viewall":true}

Bạn thấy hai tham số của Action testviewpost được thiết lập giá trị. Nó đã thực hiện qua các bước khi Action được gọi:

  • Tìm từ nguồn Http Request (route, query, form data) dữ liệu có tên giống tên tham số postid, đã tìm thấy trong route data có postid = "12"
  • Chuyển kiểu (convert) từ chuỗi "12" sang kiểu int (kiểu của tham số)
  • Tiếp tục tìm từ nguồn xem có dữ liệu với tên viewall, đã thấy viewall = "true"
  • Chuyển kiểu sang bool
  • Khi hệ thống gọi Action, hai giá trị tìm được trên sẽ chuyển đến cho Action thông qua tham số phương thức testviewpost(12, true)

Khi hệ thống tìm từ nguồn đến, nếu không có nó sẽ thiết lập giá trị khởi tạo mặc định cho tham số của phương thức

Ví dụ trên đã thực hiện việc ánh xạ (binding) dữ liệu vào tham số của phương thức, là loại binding đơn giản. Ngoài ra còn có thể thực hiện binding phức tạp vào các thuộc tính public của Controller, chúng là model của Controller, lúc này quá trình binding hoặc validation (kiểm tra hợp lệ) bị lỗi nó sẽ lưu trong ControllerBase.ModelState, bạn có thể dùng ModelState.IsValid để kiểm tra xem ánh xạ dự liệu và kiểm tra có thành công hay không. Binding các thuộc tính của Controller thực hiện như sau

Sử dụng BindProperty trong ASP.NET MVC

Để nguồn dữ liệu đến qua Http Request (Từ trường dữ liệu Form, route data, url query, file upload), tự động convert kiểu và gán cho thuộc tính của Controller, thì thuộc tính đó cần sử dụng thuộc tính bổ sung [BindProperty], Ví dụ:

public class LearnAspController : Controller
{
    [BindProperty]
    public string StudentName {set; get;}
    ...

Thuộc tính StudentName được thiết lập binding, lúc này nếu hệ thống MVC triệu gọi Controller (các Action của Controller) thì nó sẽ tìm trong nguồn đến xem có dữ liệu nào mà có tên (key, không phân biệt in hoa, thường) trùng tên thuộc tính, nếu có nó tự động convert giá trị và gán cho thuộc tính. Mặc định nó không binding khi truy vấn bằng phương thức get, do vậy nếu muốn thực hiện binding với HTTP Get thì cần chỉ rõ trong [BindingProperty], ví dụ:

public class LearnAspController : Controller
{
    [BindProperty(SupportsGet=true)]
    public string StudentName {set; get;}

Chỉ rõ nguồn khi binding dữ liệu

Trong nguồn đến Http Request nó có thể chia nhỏ gồm dữ liệu từ: url query, form data, route data, file upload, Http Request Body, Header của Http Request

Khi bạn muốn chỉ rõ dữ liệu lấy từ nguồn nào thì bạn sử dụng các thuộc tính thiết lập binding tương ứng gồm:

  • [FromQuery] dữ liệu trích xuất từ Url query (/abc/?key=value)
  • [FromRoute] dữ liệu lấy từ giá trị trong tham số của Route
  • [FromForm] dữ liệu lấy từ Form
  • [FromBody] dữ liệu lấy từ Body của Http Request
  • [FromHeader] dữ liệu lấy từ Header của Http Request

Ví dụ, thuộc tính sau được binding từ nguồn là Form Submit đến

[FromForm]
public string StudentName {set; get;}

Name trong [BindingProperty], [FromQuery], [FromRoute] ...

Như trên đã nói, khi binding nó tìm trong nguồn đến dữ liệu cùng tên với thuộc tính, từ đó lấy giá trị dữ liệu tương ứng. Tuy nhiên, nếu muốn binding dữ liệu mà mà tên ở nguồn đến và tên thuộc tính Controller khác nhau thì dùng đến thuộc tính Name trong [BindingProperty], [FromQuery], [FromRoute] ...

[FromQuery(Name = "hovaten")]
public string StudentName {set; get;}

Đoạn code trên thực hiện binding dữ liệu trong nguồn đến vào thuộc tính StudentName có key là hovaten

Binding dữ liệu phức tạp

Asp.net ngoài khả năng convert, chuyển kiểu sang các kiểu dữ liệu đơn giản như Boolean, Double ... Ta còn có thể binding những dữ liệu phức tạp là những lớp.

Ví dụ, có lớp sau:

public class Student {
    public int ID {set; get;}
    public string StudentName {set; get;}
    public string Email {set; get;}
}

Để Binding dữ liệu vào lớp như trên thì: lớp đó phải có phương thức khởi tạo mặc định, có các thuộc tính lưu được dữ liệu vào ở chế độ public.

Lúc này bạn có thể khai báo và sử dụng lớp Student có thiết lập binding, ví dụ như:

[BindProperty]
public Student firststudent {set; get;}

Khi thực hiện binding dữ liệu phức tạp trên, hệ thống ASP.NET MVC khởi tạo firststudent (theo phương thức khởi tạo mặc định), sau đó từng thuộc tính của firststudent được binding từ nguồn đến với mẫu key từ nguồn đến có dạng prefix.property_name (tên key được tổ hợp từ tiền tố thuộc tính và tên thuộc tính).

Ở trên tiền tố sẽ là firststudent, vậy để nó sẽ tìm trong nguồn đến các key như "firststudent.ID", "firststudent.StudentName" ... để lấy giá trị binding.

Trong trường hợp prefix.property_name không tìm thấy trong nguồn đến nó sẽ thực hiện bước tìm tiếp theo, chỉ tìm theo tên thuộc tính property_name mà bỏ quả tiền tố, như tìm ID, StudentName ...

Khi các cơ chế binding mặc định không đáp ứng nhu cầu nào đó, bạn có thể xây dựng Binder riêng: Tạo Model Binder riêng, tương tự khi các Validation mặc định không đủ, bạn có thể thực hiện xây dựng validation riêng

Html Form và Validation trong ASP.NET MVC

Việc tạo các Form, phát sinh các phần tử HTML biểu diễn các thành phần của Model thực hiện hoàn toàn giống với trường hợp Razor Page HTML Form và Validation kiểm tra dữ liệu

Thực hiện lại ví dụ trong đường dẫn trên, ta sẽ thực hiện tạo Controller trong một Area có tên TestMvc, hãy thực hiện theo các bước sau:

Gõ lệnh tạo cấu trúc thư mục chuẩn cho Area với tên TestMvc

dotnet aspnet-codegenerator area TestMvc

Sau lệnh này nó tạo ra thư mục Areas/TestMvc và các thư mục con theo cấu trúc như Controllers, Views, Models, Data

Tạo ra một Controller đặt tên CustomerUpdate, tạo ra trong area vừa tạo, thực hiện lệnh sau:

dotnet aspnet-codegenerator controller -name CustomerUpdate -outDir Areas/TestMvc/Controllers

Nó tạo ra class CustomerUpdateController, lưu tại Areas/TestMvc/Controllers/CustomerUpdateController.cs

Để route tới Controller trong các Area thêm vào Startup.configure:

app.UseEndpoints (endpoints => {

    // Thêm vào đoạn code route tới Controller trong Area
    // Url sẽ là /Area/Controller/Action 
    endpoints.MapControllerRoute(
    name: "MyArea",
    pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
    /...
});

Như vậy nếu truy cập địa chỉ /testmvc/customerupdate/ thì sẽ thi hành Index của Controller trên

Cập nhật Areas/TestMvc/Controllers/CustomerUpdateController.cs nội dung sau:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Net.Http;

namespace mvcblog.Areas.TestMvc.Controllers {

    // Thiết lập Controller thuộc Area TestMvc
    [Area ("TestMvc")]
    public class CustomerUpdateController : Controller {

        // Tạo lớp Model CustomerInfo biểu diễn khách hàng được submit đến từ Form
        public class CustomerInfo {

            [Required (ErrorMessage = "Phải có tên")]
            [StringLength (20, MinimumLength = 3, ErrorMessage = "Chiều dài không chính xác")]
            [Display (Name = "TÊN KHÁCH")] // Label hiện thị
            public string Customername { set; get; }

            [Required (ErrorMessage = "Thiếu email")]
            [EmailAddress]
            [Display (Name = "EMAIL")]
            public string Email { set; get; }

            [Required (ErrorMessage = "Thiếu năm sinh")]
            [Display (Name = "NĂM SINH")]
            [Range (1970, 2000, ErrorMessage = "{0} phải trong khoảng {1} đến {2}")]
            public int? YearOfBirth { set; get; }
        }


        //  customerInfo được binding từ dữ liệu gửi đến
        [BindProperty]
        public CustomerInfo customerInfo { set; get; }

        [TempData]
        public string Message {set;get;}

        public IActionResult Index() {

            // Kiểm tra trạng thái hợp lệ của dữ liệu Model, khi form submit (post)
            if (ModelState.IsValid && Request.Method == HttpMethod.Post.Method)  {
                Message = "Dữ liệu gửi đến hợp lệ";
                Console.WriteLine(Request.Method);
            }
            else {
                Message = "Điền lại thông tin gửi đến";
            }
            ViewData["Message"] = Message;
            // Trả về ViewResult, gửi customerInfo là Model đến view Index.cshtml
            return View(customerInfo);
        }

    }
}

Trong code trên chú ý những điểm có chút khác biệt Controller so với Razor Page:

  • [Area ("TestMvc")] cho biết Controller thuộc Area nào (phải thiết lập để route làm việc đúng)
  • return View(customerInfo); truyền Model tới View, nó cũng cho biết đối tượng Model trong Action này là lớp nào (CustomerInfo), qua đó nó thược hiện kiểm tra hợp lệ dữ liệu của Model theo lớp tương ứng (truy cập ModelState.IsValid)
  • Request.Method == HttpMethod.Post.Method : Kiểm tra HttpMethod là Post

Khi Action chuyển dữ liệu (model) tới View, ở đây Action là Index thì View tương ứng là file Index.cshtml, nó tìm ưu tiên ở đường đẫn Areas/TestMvc/Views/CustomerUpdate/Index.cshtml, vậy bạn cần tạo thư mục chứa các View của của controller theo đường dẫn tương ứng trên cho controller Areas/TestMvc/Controllers/CustomerUpdateController.cs

Tạo code Areas/TestMvc/Views/CustomerUpdate/Index.cshtml

@using mvcblog.Areas.TestMvc.Controllers
@{
    ViewData["Title"] = "Customer";
    Layout = "_Layout";
}

@model CustomerUpdateController.CustomerInfo
<div class="alert alert-primary">@ViewData["Message"]</div>

@{
    var customer = Model;
    if (customer != null)
    {
        if (ViewContext.ModelState.IsValid) {
            <h3>Thông tin đã gửi</h3>
            <div class="card">
                <div class="card-body">
                    <p><strong>Tên: </strong> @customer.Customername</p>
                    <p><strong>Email: </strong> @customer.Email</p>
                    <p><strong>Năm sinh: </strong> @customer.YearOfBirth</p>
                </div>
            </div>
        }

    }
}  

<h2 class="display-4">Nhập thông tin</h2>
<form method="post" class="m-2 p-3 bg-light">
    <div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
        <label asp-for="@Model.Customername"></label>
        <input class="form-control" asp-for="@Model.Customername" />
        <span asp-validation-for="@Model.Customername" class="text-danger"></span>  
    </div>
    <div class="form-group">
        <label asp-for="@Model.Email"></label>
        <input class="form-control" asp-for="@Model.Email" />
        <span asp-validation-for="@Model.Email" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="@Model.YearOfBirth"></label>
        <input class="form-control" asp-for="@Model.YearOfBirth" />
        <span asp-validation-for="@Model.YearOfBirth" class="text-danger"></span>
    </div> 
    <div class="form-group">
        <button class="btn btn-danger" asp-action="Index">Gửi thông tin</button>
    </div>
</form>

Một số lưu ý trong code file index.cshtml trên

  • Tương tự .cshtml đã trình bày trong phần Razor Page, view ở đây có sử dụng các TagHelper trong Razor để phát sinh các phần tử HTML (liên kết, phần tử form ...), để có TagHelper thì cần nạp vào bằng chỉ thị
    @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers Ở đây, ta sẽ thiết lập tất các các .cshtml trong thư mục Areas/TestMvc/Views tự động thêm vào đoạn code trên, ta tạo ra file Areas/TestMvc/Views/_ViewImports.cshtml với nội dung
    @using mvcblog
    @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
    
  • Để truy cập tới ModelState thì dùng thuộc tính ViewContext.ModelState (khác với Razor page có ngay thuộc tính ModelState), các thiết lập, truy cập, inject, cú pháp khác giống với trường hợp đã trình bày ở Razor Page

Kiểm tra thử

mvc blog

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


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