Chèn Custom Html Attributes Trong ASP.NET MVC

Trong khi làm việc với asp.net mvc, chúng ta thường hay chèn custom attribute cho các thẻ html vì chúng đóng vai trò như là metadata giúp cho các thư viện javascript ở phía client có thể hoạt động. Ví dụ nếu chúng ta sử dụng thư viện knockout.js chúng ta sẽ chèn custom attribute có tên là data-bind. Trong bài viết này, tôi trình bày những cách khả thi để có thể thực hiện nhiệm vụ nói trên.

Giả sử chúng ta có một view chứa form cho phép user điền thông tin như first name, last name, email, address, v.v…. Sử dụng Razor view và object model, chúng ta sẽ định nghĩa view như sau:

@model Models.UserInfo

@using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
    <div class="line">
        @Html.LabelFor(m => m.FirstName)
        @Html.TextBoxFor(m => m.FirstName);
    </div>
    <div class="line">
        @Html.LabelFor(m => m.LastName)
        @Html.TextBoxFor(m => m.LastName)
    </div>
    <div class="line">
        @Html.LabelFor(m => m.Email)
        @Html.TextBoxFor(m => m.Email)
    </div>
    <div class="line">
        @Html.LabelFor(m=>m.Address)
        @Html.TextBoxFor(m=>m.Address)
    </div>
    <div class="line">
        @Html.LabelFor(m=>m.About)
        @Html.TextAreaFor(m=>m.About)
    </div>
    <div class="line">
        <input type="submit" value="Sumbit"/>
    </div>
}

Bây giờ, giả sử chúng ta cần chèn thêm custom attribute tên là required với giá trị “true” cho các field first name, last name, và email có trong form. Cách đơn giản nhất và dễ sử dụng nhất là developer tự chèn custom attribute bằng tay. Nếu bạn đang sử dụng các hàm html helper thì chúng đều có thể nhận thêm một đối số thứ ba là một object chứa các custom attribute bạn muốn chèn vào. Trong trường hợp của ví dụ trên, form có thể viết lại như sau:

@model Models.UserInfo

@using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
    <div class="line">
        @Html.LabelFor(m => m.FirstName)
        @Html.TextBoxFor(m => m.FirstName, new { required = true });

    </div>

    <div class="line">
        @Html.LabelFor(m => m.LastName)
        @Html.TextBoxFor(m => m.LastName, new { required = true })
    </div>

    <div class="line">
        @Html.LabelFor(m => m.Email)
        @Html.TextBoxFor(m => m.Email, new { required = true })
    </div>

    <div class="line">
        @Html.LabelFor(m => m.Address)
        @Html.TextBoxFor(m => m.Address)
    </div>

    <div class="line">
        @Html.LabelFor(m => m.About)
        @Html.TextAreaFor(m => m.About)
    </div>

    <div class="line">
        <input type="submit" value="Sumbit"/>
    </div>
}

Nhược điểm của cách này là với mỗi field cần chèn custom attribute bạn đều phải viết thêm code giống hệt nhau. Nếu bạn có 10 nơi mà field này xuất hiện thì bạn sẽ phải viết 10 lần code giống nhau. Tệ hơn, nếu bạn có 3 hay 4 field cần chèn cùng một loại custom attribute và mỗi field xuất hiện khoảng 10 nơi trong project của bạn thì số lần bạn phải viết code tăng lên đáng kể. Và chuyện gì xảy ra khi bạn muốn thay đổi giá trị hay tên của custom attribute mà bạn vừa viết. Bạn phải sửa ở tất cả mọi nơi mà nó xuất hiện.

Một nhược điểm nữa của cách sử dụng này là nếu bạn muốn chèn một custom attribute mà tên của nó có dấu gạch ngang ví dụ như data-bind thì bạn sẽ phải viết code như thế nào? Bạn không thể viết là new { required = true, data-bind = "" } vì tên của property trong ngôn ngữ C# không cho phép dấu gạch ngang. Bạn vẫn có thể chèn được nếu bạn thay dấu gạch ngang bằng dấu gạch dưới và Razor đủ thông minh để render thành data-bind cho bạn. Nhưng rõ ràng, đây giống như là một “tricky” mà không phải ai mới sử dụng đều biết.

Rõ ràng khi bạn cần sử dụng custom attribute ở nhiều nơi trong project của bạn thì cách sử dụng html helper không phải là giải pháp hoàn hảo. May mắn, asp.net mvc cung cấp một cách tiếp cận khác cho vấn đề của chúng ta mà được gọi là html template. Trong các hàm html helper có hai hàm khá đặc biệt là Html.DisplayFor() và Html.EditorFor(). Hai hàm này sử dụng html template để render nội dung html của object model. Mặc định, nếu chúng ta dùng Html.DisplayFor() với giá trị truyền vào kiểu string thì nó sẽ render thành chuỗi text còn nếu sử dụng Html.EditorFor() thì nó sẽ render thành thẻ input với type là kiểu text. Tuy nhiên chúng ta có thể cung cấp html template của riêng chúng ta và trong trường hợp đó thì Html.DisplayFor() cũng như Html.EditorFor() sẽ sử dụng template mà chúng ta cung cấp. Sử dụng tính năng này để chèn thêm custom attribute trong template của chúng ta và nếu sau này nếu có phải thêm hay xóa bớt custom attribute thì chúng ta chỉ phải edit lại template mà thôi.

Trở lại ví dụ của chúng ta, để sử dụng html template đầu tiên ta cần biết class UserInfo được định nghĩa như sau:

public class UserInfo
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string Email { get; set; }

    public string Address { get; set; }

    public string About { get; set; }
}

Vì các field FirstName, LastName và Email là kiểu String nên chúng ta sẽ tạo ra một template cho kiểu String trong project của chúng ta. Đầu tiên tạo thư mục tên là EditorTemplates nằm trong thư mục Shared của project. Trong thư mục EditorTemplates tạo một partial view tên là String.cshtml. Tên của template phải trùng với tên kiểu dữ liệu mà trong trường hợp này là String. Trong String.cshtml chúng ta định nghĩa template của chúng ta như sau:

@model String

@Html.TextBoxFor(m => m, new { required = true })

Khi đó, form view của chúng ta có thể viết lại như sau:

@model Models.UserInfo

@using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
    <div class="line">
        @Html.LabelFor(m => m.FirstName)
        @Html.EditorFor(m => m.FirstName)
    </div>

    <div class="line">
        @Html.LabelFor(m => m.LastName)
        @Html.EditorFor(m => m.LastName)
    </div>

    <div class="line">
        @Html.LabelFor(m => m.Email)
        @Html.EditorFor(m => m.Email)
    </div>

    <div class="line">
        @Html.LabelFor(m => m.Address)
        @Html.TextBoxFor(m => m.Address)
    </div>

    <div class="line">
        @Html.LabelFor(m => m.About)
        @Html.TextAreaFor(m => m.About)
    </div>

    <div class="line">
        <input type="submit" value="Sumbit"/>
    </div>
}

Giải pháp html template có một hạn chế là custom attribute bị hard-code trong template. Nếu field email cần chèn thêm một attribute khác ví dụ như regular-expression thì nó không còn sử dụng được template vừa định nghĩa ở trên nữa. Chúng ta cần một giải pháp bao quát hơn. Giải pháp đó có thể được tóm tắt trong 2 bước. Đầu tiên, chúng ta apply .NET attribute lên trên object model để đăng kí với framework những metadata cần thiết. Sau đó, dựa trên metadata đã đăng kí chúng ta sinh ra các attribute tương ứng.

Những .NET attribute giúp đăng kí metadata với asp.net mvc phải implement interface IMetadataAware. Chúng ta có thể sử dụng attribute được build sẵn là AdditionalMetadataAttribute tuy nhiên attribute này chỉ hỗ trợ đăng kí mỗi lần một cặp key, value mà thôi. Giả sử chúng ta cần chèn thêm attribute data-bind cho các field first name, last name. Chúng ta sẽ tạo một class có tên là DataBindAttribute như sau:

namespace Models
{
    public class DataBindAttribute : Attribute, IMetadataAware
    {

        private readonly string _bindingsValue;

        public DataBindAttribute(paramsstring[] bindings)
        {
            string temp = string.Empty;
            Array.ForEach(bindings, b => temp += b + ",");
            _bindingsValue = temp;
        }

        public void OnMetadataCreated(ModelMetadata metadata)
        {
            metadata.AdditionalValues[DataBindHelper.BindingKey] = _bindingsValue;
        }
    }

    public static class DataBindHelper
    {
        public const string BindingKey = "data-bind";
        public static string GetBindingValue(thisHtmlHelper html,  ModelMetadata metadata)
        {
            object value;
            metadata.AdditionalValues.TryGetValue(BindingKey, out value);
            return (string)value;
        }
    }
}

Tiếp theo, trong object model chúng ta apply attribute này lên các property FirstName, LastName

public class UserInfo
{

    [DataBind("value:firstName", "focus:validateFirstName")]
    publicstring FirstName { get; set; }

    [DataBind("value:lastName", "focus:validateLastName")]
    public string LastName { get; set; }

    public string Email { get; set; }

    public string Address { get; set; }

    public string About { get; set; }
}

Cuối cùng trong template String.cshtml đã được tạo ra trước đây, chúng ta sẽ viết lại code để get thông tin metadata từ ViewContext và render attribute tương ứng.

@model String
@using Models;
@{
    var attributes = newRouteValueDictionary {
        {"required", true}
    };

    var bindingValue = Html.GetBindingValue(ViewContext.ViewData.ModelMetadata);
    if (!string.IsNullOrEmpty(bindingValue))
    {
        attributes.Add(DataBindHelper.BindingKey, bindingValue);
    }
}
@Html.TextBoxFor(m => m, attributes);

Code của form vẫn không thay đổi và html trả về từ server giờ đây sẽ có thêm attribute data-bind trên thẻ input của các field first name và last name trong khi trên field email chỉ có attribute required mà thôi. Cách sử dụng metadata để render custom attribute cũng là cách mà asp.net mvc sử dụng đằng sau tính năng validation ở client thông qua các attribute Data Annotation.

Để tổng kết lại, chúng ta có 3 cách chèn custom attributes:

Sử dụng các hàm html helper thông thường
Sử dụng html template
Sử dụng metadata kết hợp với html template

Việc sử dụng cách nào phụ thuộc vào ngữ cảnh cũng như mức độ phức tạp của project.

Nguồn: http://nguyenvutuan.blogspot.com