C# cơ bản .NET Core

Model Binding là gì?

Trong ứng dụng ASP.NET Core thì các trang Razor Page hoặc các Controller (MVC) cần các dữ liệu gửi bởi truy vấn, từ các thông tin gửi đến như chuỗi query của url, tham số route, dữ liệu post của Form ... ứng dụng có thể đọc được những dữ liệu này, bạn có thể hoàn toàn trích xuất được những dữ liệu này bằng cách sử dụng các phương thức cung cấp bởi đối tượng HttpRequest (thuộc tính Request của Controller hoặc của PageModel).

var data1 = Request.RouteValues["data1"];               // Lấy  dữ liệu từ tham số Route
var data2 = Request.Query["data2"];                     // Lấy dữ liệu từ query url
var data3 = Request.Form["data3"];                      // Lấy dữ liệu của Form gửi đến
var data4 = Request.Form.Files["data4"];                // Lấy dữ liệu files

Trong các thao tác lấy dữ liệu nhiều khi bạn cũng cần kiểm tra sự tồn tại của dữ liệu cần lấy trước, lấy được dữ liệu phải chuyển đổi, kiểm tra hợp lệ rồi mới có thể dữ dụng.

ASP.NET Core cung cấp một cơ chế giúp bạn tự động quá trình lấy dữ liệu trên gọi là Model Biding, nó hoạt động tự động qua mấy bước chính:

  • Lấy các dữ liệu từ các nguồn gửi đến (Route data, Form data, chuỗi Query, File tải lên)
  • Tiến hành chuyển và thiết lập dữ liệu đó cho Razor Page hoặc Controller (thiết lập vào tham số phương thức Handler, Action hay các thuộc tính public của lớp)
  • Chuyển đổi chuỗi dữ liệu nhận được thành các kiểu phù hợp với .NET

Hãy nhanh chóng tạo ra một dự án ứng dụng Web với Razor Pages để thực hành:

dotnet new webapp -o razor05.modelbinding

Mở thư mục razor05.modelbinding bằng VSC.

Hai kiểu Binding: tham số và thuộc tính

Khi thực hiện bind dữ liệu từ nguồn gửi đến có hai kiểu: binding vào tham số của phương thức (handler, action) và binding vào thuộc tính của Controller, Page Model. Để tìm hiểu kỹ hơn qua ví dụ, trong dữ án trên mở file Pages/Index.cshtml sửa code thành như sau:

@page "/order/{productid:int?}/"
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<form method="POST">
    <input type="text" name="username" placeholder="Nhập tên" />
    <input type="email" name="email" placeholder="Nhập email" />
    <button class="btn btn-danger" type="submit">Gửi</button>
</form>

Trang Razor này thiết lập để URL truy vấn phù hợp là /order/12/, /order/1/?color=Red ..., khi vào trang điền thông tin bấm vào Gửi thì nó chuyển thông - và thi hành Handler OnPost(), mở /Pages/Index.cshtml.cs và thêm vào Hander này:

// Handler gọi khi truy vấn bằng HTTP POST
public void OnPost()
{
    // Microsoft.AspNetCore.Http.Extensions -> GetDisplayUrl
    Console.WriteLine(Request.GetDisplayUrl());
    var req = Request;
}

Thử đặt breakpoint để Debug và truy cập vào địa chỉ /order/1/?color=Red điền thông tin rồi bấm vào gửi:

submit

Kết quả Debug như hình

Bạn thấy, dữ liệu do truy vấn gửi đến đều có thể đọc được bằng Request (HttpRequest):

  • HttpRequest.Query: lưu các dữ liệu phân tích từ chuỗi query url (color có giá trị Red)
  • HttpRequest.RouteValues: lưu các dữ liệu phân tích từ Route phù hợp với Url (productid có giá trị 1)
  • HttpRequest.Form: lưu giá trị tương ứng với các phần tử Form gửi đến (username là XuanThuLab và email là xuannthulab.net@gmail.com)

Parameter Binding - Binding vào tham số Handler, Action

Khi bạn khai báo các Handler của PageModel hoặc các Action của Controller, nếu nó có tham số thì APS.NET tự động tìm trong dữ liệu gửi đến xem có phần tử nào có tên cùng tên tham số này không, nếu có dữ liệu sẽ chuyển kiểu cùng kiểu tham số và gán giá trị cho tham số này.

Ví dụ, tạo OnGet handler trong Index.cshtml.cs như sau:

public void OnGet(int? productID, string color)
{
    Console.WriteLine($"ProductID: {productID}; color: {color}");
}

Khi truy cập với phươg thức get, đến các URl thì kết quả tương ứng là:

  • https://localhost:5001/order/1/?color=red ~ ProductID: 1; color: red
  • https://localhost:5001/order/10/ ~ ProductID: 1; color:
  • https://localhost:5001/order/ ~ ProductID: ; color:
  • ...

Bạn thấy dữ liệu đã tự động lấy từ nguồn gửi đến và thiết lập cho tham số của Handler, Action. Miễn là tên của tham số và tên nguồn dữ liệu giống nhau (không phân biệt chữ hoa thường ProductID, productID ... đều được). Nếu nguồn dữ liệu không có thì tham số là giá trị mặc định (int? mặc định là null, int mặc định 0, string mặc định null ...).

Property Binding - Binding vào thuộc tính của Model

Việc tự động lấy từ nguồn dữ liệu gửi đến thiết lập cho thuộc tính public của lớp Model (Controller, PageModel) đó là Property Binding. Việc này được thiết lập bằng cách sử dụng thuộc tính bổ sung [BindProperty] trên thuộc tính cần Binding.

Lưu ý: Chỉ các thuộc tính (thuộc tính trong lớp C# chứ không phải trường) của lớp với phạm vị truy cập là public mới sử dụng được Binding

BindProperty chỉ hoạt động trên thuộc tính lớp, nó có các tham số: (nguyên tắc sử dụng thuộc tính bổ sung xem ở Attribute Annotation )

  • Name chỉ ra chuỗi tên của nguồn dữ liệu, nếu thiếu lấy theo tên thuộc tính
  • SupportsGet kiểu bolean, mặc định là false - nó không bind khi truy cập là get, nếu true hỗ trợ bind ngay cả khi truy cập theo http get

Mở /Pages/Index.cshtml.cs chính sửa lớp Model để áp dụng thử:

/..
public class IndexModel : PageModel {

        // Binding Email từ dữ liệu từ nguồn tới có tên Email, email, emaIL ...
        [BindProperty]
        public string Email { get; set; }

        // Binding cho UserId từ nguồn gửi đến, dữ liệu nguồn có tên username
        [BindProperty (Name = "username")]
        public string UserId { set; get; }

        // Binding ProductID - thiết lập BINDING ngay cả khi truy cập là HTTP GÉT
        [BindProperty(SupportsGet=true)]
        public int ProductID { set; get; }

        // Binding Color
        [BindProperty]
        public string Color { set; get; }

    /..
}

Thêm vào Pages/Index.cshtml

<div class="card bg-success">
    <div class="card-body text-white">
        <p>Dữ liệu trong Model:</p>
        <ul>
            <li>ProductID: @Model.ProductID</li>
            <li>UserName: @Model.UserId</li>
            <li>Email: @Model.Email</li>
            <li>Color: @Model.Color</li>
        </ul>
    </div>
</div>

Truy cập địa chỉ https://localhost:5001/order/10/?color=red điền thông tin và POST thử:

Như vậy đã Binding thành công từ nguồn dữ liệu gửi đến (Form, Url, Route) vào thuộc tính của Model

[BindProperties] thiết lập Binding tất cả các thuộc tính

[BindProperties] tác dụng vào lớp (khai báo trước tên khai báo lớp) nó kích hoạt binding cho tất cả các thuộc tính (giúp cho bạn đỡ phải thiết lập nhiều lần, tuy nhiên các thuộc tính có những tùy chọn riêng thì vẫn có thể dùng [BindProperty]

[BindProperties] // Thiết lập này thì thiết lập binding cho các thuộc tính của lớp IndexModel
public class IndexModel : PageModel {

    /..

}

Nguồn Binding dữ liệu

Qua phần trên, bạn thấy dữ liệu gửi đến tự động chuyển gán cho các thuộc tính, tham số được lấy từ các nguồn

  • Các trường trong Form được submit đến
  • Nội dung của phần Body trong thông điệp HTTP gửi đến (áp dụng cho Controller)
  • Dữ liệu của Route
  • Dữ liệu trích xuất từ chuỗi truy vấn url (?key1=value1&key2=value2)
  • Các file upload

Mặc định là như vậy, tuy nhiên nhiều trường hợp bạn muốn một thuộc tính hoặc tham số phương thức Handler, Action sẽ binding dữ liệu từ một nguồn cụ thể chỉ định một cách tường minh nhằm đảm bảo chính xác. Trở lại ví dụ trên:

Nếu bạn truy vấn đến địa chỉ: https://localhost:5001/order/10/?color=red&productid=2, thì trong các nguồn đến bạn thấy - nguồn với key là productid có:

  • Trong Route: productid có giá trị 10
  • Trong URL Query: productid gó giá trị 2

Theo thứ tự trích xuất dữ liệu, thì thuộc tính ProductID sẽ gán giá trị 10 - vì binding từ Route trước. Trong trường hợp bạn muốn, chỉ định là sẽ lấy từ nguồn Query thì bạn cần dùng thuộc tính [FromQuery] thì lúc này ProductID sẽ có giá trị là 2

public class IndexModel : PageModel {
    /..
        [FromQuery]
        [BindProperty(SupportsGet=true)]
        public int ProductID { set; get; }
    /..
}

Để chỉ định rõ nguồn binding dùng các thuộc tính bổ sung:

  • [FromQuery] - lấy giá trị từ chuỗi query của URL
  • [FromRoute] - lấy giá trị từ Route
  • [FromForm] - lấy giá Form gửi đến
  • [FromBody] - lấy giá từ Body của Http Request
  • [FromHeader] - lấy giá từ Header của Http Request

[FromQuery], [FromRoute], [FromForm], [FromHeader] có thuộc tính Name để bạn thiết lập key của dữ liệu sẽ binding trong trường hợp key khác với tên thuộc tính. Ví dụ:

// Lấy dữ liệu từ Route, dữ liệu này có key abc để gán vào thuộc tính xyz
[FromRoute(Name="abc")]
public string xyz {set; get;}

Binding dữ liệu cho đối tượng phức tạp

Binding dữ liệu thực hiện như trên có thể áp dụng cho các loại cấu trúc dữ liệu phức tạp (một đối tượng lớp nhiều thuộc tính), ví dụ khai báo một lớp

public class Customer {
     public int CustomerID {set; get;}
     public string Email {set; get;}
     public string UserName {set; get;}
}

Lúc này trong Model nếu bạn khai báo:

[BindProperty(SupportsGet=true)]
public Customer customer {set; get;}

Thì thuộc tính customer được binding như bình thường. Tuy hiên để hiểu rõ cơ chế hoạt động bạn lưu ý:

  • Tìm các nguồn dữ liệu có giống tên thuộc tính để thực hiện binding, có nghĩa sẽ tìm customer.ID trong Form, trong Route, Query để binding vào ID của customer ...
  • Nếu trường hợp trên không thấy dữ liệu thì bắt đầu tìm đến nguồn không có tiền tố. Tìm các nguồn với key là ID để binding vào ID của customer

Bạn có thể thay đổi tiền tố khác với tên thuộc tính bằng: [Bind(Prefix = "Tiento")]

Thêm vào Index.cshtm để kiểm tra

@if(Model.customer != null) {
    <div class="card bg-success">
        <div class="card-body text-white">
            <p>Dữ liệu customer:</p>
            <ul>
                <li>CustomerID: @Model.customer.CustomerID</li>
                <li>UserName: @Model.customer.UserName</li>
                <li>Email: @Model.customer.Email</li>
            </ul>
        </div>
    </div>
}

Một số thuộc tính bổ sung có thể áp dụng binding dữ liệu phức tạp

[Bind] để chỉ định những thuộc tính của lớp sẽ thực hiện Binding

[Bind("Email", "UserName")] // chỉ binding 2 thuộc tính Email và UserName của lớp Customer
public class Customer {
   /...
}

[BindRequired] áp dụng cho một thuộc tính của lớp phức tạp, nó sẽ phát sinh trạng thái lỗi nếu không thực hiện binding cho thuộc tính này

public class Customer {
   /...
   [BindRequired]
   public int CustomerID {set; get;}

}

[BindNever] không thực hiện binding

public class Customer {
   /...
   [BindNever]
   public int CustomerID {set; get;}

}

Để tạo ra cơ chế Binding tùy chọn riêng có thể tham khảo Tạo ModelBinder

Mã nguồn tham khảo ASP_NET_CORE/razor05.modelbinding


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