C# – Tạo và sử dụng Custom Attribute

Trong bài trước tôi đã giới thiệu về Attribute và cách sử dụng một số attribute cơ bản. Bài viết đó chỉ nhằm mục đích giới thiệu phần nào cách sử dụng attribute và vẫn chưa đủ để áp dụng trong nhiều trường hợp. Muốn hiểu rõ hơn về attribute trong .Net, bạn phải tự đặt câu hỏi “Họ đã làm điều đó như thế nào?”, bạn phải tự tạo ra cho mình một thứ tương tự và trải nghiệm việc sử dụng những đó để trả lời cho câu hỏi của mình.

Tạo lớp Attribute

Để tạo một attribute mới, bạn phải tạo một lớp kế thừa từ System.Attribute đồng thời sử dụng attribute AttributeUsage để khai báo các thông tin cần thiết.

Khi đặt tên cho attribute sắp tạo ra này, bạn nên tuân theo quy tắc của Microsoft là thêm hậu tố “Attribute” vào tên lớp. Bạn không cần phải lo lắng nếu như tên lớp quá dài, khi sử dụng IDE sẽ tự động bỏ phần “Attribute” phía sau đi, và bạn có thể sử dụng cả hai tên của attribute, ví dụ như Obsolete và ObsoleteAttribute là tương đương nhau.

Bây giờ ta thử tạo ra một attribute đơn giản để lưu tên tác giả và địa chỉ web, một ví dụ rất thực tế và cần thiết:

class DemoAttribute : System.Attribute

{

private string author;

public string Url { get; set; }

public DemoAttribute(string author)

{

this.author = author;

}

public override string ToString()

{

return String.Format("Author: {0}\nLocation: {1}", Author, Url);

}

}

Positional Parameter và Named Parameter

Bây giờ hãy tạo một lớp mới để test attribute này. Cách sử dụng tương tự như khi với các attribute của .Net.


[Demo("YinYang",Url="https://yinyangit.wordpress.com")]

class DemoClass

{

public void Print(string message)

{

Console.WriteLine(message);

}

}

Bạn có thể thấy khi gõ đến phần tham số thứ hai của attribute trên, chức năng IntelliSense hiển thị tooltip hướng dẫn như sau:

DemoAttribute.DemoAttribute(string author, Named Parameters…)

Rõ ràng lớp DemoAttribute ta chỉ định nghĩa duy nhất một constructor yêu cầu tham số author, nhưng khi sử dụng ở trên bạn có lại có thể gán thêm giá trị cho tất cả property miễn là chúng có thể truy xuất được để gán dữ liệu (ngoại trừ các static property).

Mặc dù thông tin này không quan trọng nhưng bạn cũng cần biết để phân biệt rõ ràng. Trong một attribute phân biệt 2 loại tham số, một loại là Positional Parameter và loại kia là Named Parameter. Bạn có thể nhận biết chúng qua các tham số sử dụng trong constructor của attribute, tham số nào được sử dụng thì chúng là Positional Parameter và chúng là bắt buộc mỗi khi sử dụng attribute, còn lại là Named Parameter và chúng là tùy chọn.

Vậy trong attribute trên thì Author là Positional Parameter và Url là Named Parameter.

Các attribute parameter chỉ cho phép bạn sử dụng các kiểu dữ liệu đơn giản sau (trích từ MSDN):

  • Simple types (bool, byte, char, short, int, long, float, and double)
  • string
  • System.Type
  • enums
  • object (The argument to an attribute parameter of type object must be a constant value of one of the above types.)
  • One-dimensional arrays of any of the above types

Để gán giá trị cho các Named Parameter, bạn chỉ cần gõ tên của nó theo sau là dấu “=” và cuối cùng là giá trị cần gán, như ví dụ ở trên là cách tôi gán địa chỉ cho tham số Url.

Sử dụng AttributeUsage Attribute

Như đã nói lúc đầu, sau khi tạo lớp bạn phải dùng attribute AttributeUsage để xác định các kiểu thành phần nào có thể sử dụng được attribute mà bạn tạo ra và một số thông tin liên quan khác. Mặc định nếu không dùng AttributeUsage này để chỉ rõ thì giá trị của nó sẽ là All, tức là attribute của bạn có thể được sử dụng bởi mọi loại thành phần như class, struct, method, enum,…

Khai báo của attribute này có dạng sau:

[AttributeUsage(

ValidOn,

AllowMultiple=allowmultiple,

Inherited=inherited

)]

Trong đó:

–       ValidOn: xác định thành phần nào có thể sử dụng được attribute. Giá trị này có kiểu enum AttributeTargets và có thể kết hợp nhiều giá trị với nhau bằng toán tử bit hoặc “|”. Mặc định là All.

–       AllowMultiple: Một giá trị bool, chỉ định rằng attribute có cho phép sử dụng nhiều lần trên một thành phần không. Mặc định là false, giá trị true hầu như không bao giờ được sử dụng vì bạn chỉ cần dùng attribute một lần duy nhất là đủ để khai báo các thông tin cần thiết cho thành phần mà nó được gắn vào.

–       Inherited: Một kiểu bool có giá trị mặc định là true. Tham số này xác định rằng attribute có thể được thừa kế từ một lớp cha mà nó đã được sử dụng không. Để hiểu rõ hơn tôi sẽ cung cấp một ví dụ nhỏ ở cuối bài.

Bây giờ là lúc bạn áp dụng những kiến thức vừa được trình bày, ở đây tôi muốn attribute DemoAttribute chỉ có thể được sử dụng bởi các class, struct và method. Các tham số AllowMultiple và Inherited không cần thay đổi. Vậy tôi gắn AttributeUsage vào lớp DemoAttribute như sau:


[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method)]

class DemoAttribute : System.Attribute

{

// […]

}

Truy xuất các attribute trong quá trình Runtime


Đây cũng là một kĩ thuật reflection mà tôi đã trình bày trong một bài trước đây. Bạn có thể dùng một trong hai cách sau để lấy về các Custom Attribute từ một kiểu dữ liệu nào đó (trong ví dụ này là lớp Program):

Attribute[] attributes = Attribute.GetCustomAttributes(typeof(Program));

object[] attributes = typeof(Program).GetCustomAttributes(true);

Cách đầu tiên thường được sử dụng hơn vì nó trả về một mảng Attribute và ta có thể sử dụng những thành phần của attribute mà không cần phải ép kiểu. Ngoài ra, bạn cũng có thể lấy các Custom Attribute từ các method, event, property,… bằng cách gọi phương thức GetCustomAttributes() tương ứng trong các đối tượng thuộc kiểu MemberInfo, MethodInfo, EventInfo,…

Ví dụ sau sẽ in ra thông tin của DemoAttribute mà ta gắn vào lớp Program, thông qua phương thức ToString():


[Demo("YinYang", Url = "https://yinyangit.wordpress.com")]

class Program

{

static void Main(string[] args)

{

Attribute[] attributes = Attribute.GetCustomAttributes(typeof(Program));

foreach (var attr in attributes)

{

Console.WriteLine(attr);

}

Console.Read();

}

}

Bạn hãy chạy thử và xem chương trình sẽ in ra đúng theo những gì mà ta đã viết trong phương thức ToString() của DemoAttribute.

Author: YinYang
Location: https://yinyangit.wordpress.com

Ví dụ về tham số Inherited của AttributeUsage

Đầu tiên bạn tạo ra hai lớp DemoClass1 và DemoClass2 thừa kế từ DemoClass1:


[Demo("DemoClass1")]

class DemoClass1

{

public virtual void Print(string message)

{

Console.WriteLine(message);

}

}

class DemoClass2:DemoClass1

{

public override void Print(string message)

{

base.Print(message);

}

}

Bạn có thể tự hỏi liệu DemoClass2 có sẵn DemoAttribute kế thừa từ DemoClass1 không. Điều này tùy thuộc vào giá trị của tham số Inherited mà bạn gán khi dùng AttributeUsage cho lớp DemoAttribute. Bây giờ để mặc định Inherited=true, bạn hãy chạy thử phương thức Main() của lớp Program sau:


class Program

{

static void Main(string[] args)

{

Attribute[] attributes = Attribute.GetCustomAttributes(typeof(DemoClass2));

foreach (var attr in attributes)

{

Console.WriteLine(attr);

}

Console.Read();

}

}

Kết quả xuất ra là:

Author: DemoClass1

Mặc dù bạn lấy các Custom Attribute từ DemoClass2 nhưng kết quả in ra lại cho thấy nó có giá trị của attribute mà bạn gán cho DemoClass1. Nguyên nhân là attribute này đã được kế thừa từ DemoClass1 sang DemoClass2.

Bây giờ bạn hãy sửa tham số Inherited=false và chạy lại chương trình, kết quả là màn hình Console không xuất hiện gì ngoài dấu nhắc lệnh.

Từ ví dụ trên bạn có thể suy ra được công dụng của tham số Inherited này của AttributeUsage. Việc để tham số này có giá trị mặc định sẽ không chính xác trong ví dụ về DemoAttribute này. Khi bạn tạo một lớp kế thừa những thông tin tác giả từ lớp cha, trong khi tác giả lại là một người khác, vậy việc cần làm khi sau khi viết một lớp kế thừa là gắn một DemoAttribute vào lớp con vừa được tạo ra nhằm cập nhật thông tin hợp lý.

https://yinyangit.wordpress.com

Advertisements

14 thoughts on “C# – Tạo và sử dụng Custom Attribute

  1. Mình tạo 1 lớp Attribute là InfoAttribute và 1 lớp Person
    Cụ thể:
    public class Person
    {
    [InfoPerson(DisplayColumn = “DisplayName”)]
    public string Name { get; set; }
    }
    Nhưng trong hàm void main(),
    var a = new Person();
    a.Name; thì được, còn a.InfoPerson thì không được. Vậy làm sao sử dụng được attribute InfoPerson?

    Phản hồi
  2. Xin lỗi vì bây giờ mới thấy comment này của bạn. Mình xin trả lời ngắn gọn vấn đề của bạn như sau:

    Để lấy custom attribute của bất kì thành phần nào trong class thì bạn ko cần tạo thể hiện cho class đó (vì chúng đã được khai báo trong class).

    Để lấy được các attribute cho một member của class, cụ thể là ví dụ trên, bạn có thể dùng overload Attribute.GetCustomAttribute() hoặc Attribute.GetCustomAttributes() với tham số là một đối tượng MemberInfo (System.Reflection).

    Ví dụ sau sẽ lấy các custom attribute của member Name trong lớp Person:

    Type personType = typeof(Person);
    			
    MemberInfo memInfo= personType.GetMember("Name")[0];
    
    // Lay cac attribute cua member Name
    Attribute[] attributes=Attribute.GetCustomAttributes(memInfo);
    
    foreach(Attribute attr in attributes)
    	Console.WriteLine(attr.GetType().Name +":"+ attr);
    

    Thân!

    Phản hồi
  3. Vậy có cách nào đăng ký Attributes mình vừa tạo không? Đăng ký 1 thuộc tính như của Microsoft đấy. Mình thấy cách dùng Reflection hơi thủ công và không thích hợp khi gọi lại thuộc tính đó nhiều lần.

    Phản hồi
  4. Custom attribute dùng để bổ sung thông tin và được gắn trong phần khai báo của class, property,… . Như bạn thấy dùng reflection để lấy các custom attribute không quá phức tạp. Tuy nhiên nếu bạn muốn sử dụng như property thì có thể sử dụng property. Chẳng hạn như thư viện bạn tạo ra có thể dùng các property hoặc method để xuất ra các thông tin như author, version,… như các ngôn ngữ lập trình trước đây.

    Phản hồi
  5. Đúng rồi đó. Ý mình nói là tạo thư viện riêng hẳn hoi.
    Đứng vai trò là người viết thư viện chương trình, bạn cần tạo ra các thư viện cần thiết và bổ sung chúng cho dự án thực tế.
    Người lập trình viên chỉ việc gọi nó lên như là 1 thư viện có sẵn.
    Mình chỉ mới có ý tưởng như thế, còn hiện thực thì vẫn chưa làm được.

    Phản hồi
    • 
      namespace Database
      {
          [MetadataTypeAttribute(typeof(RoleMetadata))]
          public partial class Role
          {
              
          }
          
          [DisplayName("Nhóm người dùng")]
          public class RoleMetadata
          {
              [Display(Name = "Tên nhóm")]
              public object RoleName { get; set; }        
      
              [Display(Name = "Người tạo")]
              [UIHint("UserById")]
              public object CreatedByUserId { get; set; }       
              
          }
      }
      

      Giả sử mình tạo 1 class là Role, và trong metadata, mình đặt 1 attribute là UIHinht(“UserById”).
      Mình làm trang web động, và dùng Reflection để gán dữ liệu cho object rồi lưu xuống database.
      Vấn đề mình gặp phải:
      – Khi vào trang List của bảng Role, CreatedByUserId sẽ dựa trên UIHint để tìm template UserById và lấy dữ liệu của bảng user để hiển thị (Ví dụ: CreatedByUserId là 1, vào bảng User tìm key = 1 và lấy DisplayName là YinYang).
      -Tạo trang Insert thông tin của bảng Role, CreatedByUserId sẽ dựa trên UIHint để lấy thông tin của User. Cái mình cần lưu là Tên người đang đăng nhập và tạo bảng này, ví dụ Án Bình Trọng đang đăng nhập, cần lấy key là 2 và gán vào CreatedByUserId.
      Vấn đề là:
      – Insert: dựa vào Tên đăng nhập, tìm key và gán giá trị vào cho CreatedByUserId
      – List: dựa vào key, tìm DisplayName trong bảng user và hiển thị.
      Mình nghĩ là nên xây dựng lớp InformationAttribute rồi ứng với mỗi trang sẽ có UIHint cho từng cái.
      Vấn đề là khi xây dựng thư viện Attribute, làm sao cấu hình để trên giao diện View, người lập trình chỉ cần gõ:
      Trang List:
      -string template = obj.Info.List.UIHint;
      Trang Insert:
      -string template = obj.Info.Insert.UIHint;
      Mình xin cảm ơn bạn trước.
      Từ lúc đọc blog của bạn, từ Reflection tới Attribute, mình đã nghĩ ra nhiều ý tưởng hay dựa trên cái Entity Framework để xây dựng trang web động theo kiểu động từ object trong trang luôn.

      Phản hồi
  6. Có phải bạn muốn cho phép người dùng thực thi một câu lệnh như C#:

    -string template = obj.Info.List.UIHint;

    Nếu vậy bạn có thể sử dụng đến kĩ thuật CodeDom.

    Để tạo ra các property động, bạn có thể dùng ExpandoObject, tuy nhiên trong vấn đề này có lẽ bạn nên xây dựng một lớp với các property tương ứng với dữ liệu cần lấy. Chẳng hạn ngoài việc gắn attribute UIHint, bạn cũng tạo thêm một property là UIHint có nhiệm vụ lấy dữ liệu của attribute đó.

    Có thể mình chưa hiểu đúng vấn đề của bạn, nếu bạn có thể cô đọng nội dung hơn thì sẽ dễ thấy rõ ràng vấn đề hơn.

    Thân!

    Phản hồi
  7. À, mình đang làm dự án nhỏ về Dynamic Data với MVC: Dynamic Data
    Mình ghi ở blog này 3 phần (hiểu sao ghi vậy, không phải tutorial nên bạn chịu khó đọc nhé).
    Mình giới thiệu sơ:
    Khi sử dụng Entity Framework, nó tạo metadata. Dựa vào Metadata, ta định nghĩa thêm các thuộc tính khác (DisplayName, UIHint, Order, HideIn…). Và khi đưa dữ liệu lên web, nếu là trang List:
    Duyệt các metadata, dùng Reflection để lấy dữ liệu.
    Vấn đề nảy sinh là trong bảng, ngoài những MetaColumn, còn có các MetaForeignKey và PrimaryKey. Nếu là ForeignKey phải lấy dữ liệu từ bảng khác để hiển thị. Nếu là PrimaryKey thì ẩn đi. Vấn đề là sẽ lấy dữ liệu cột nào trong bảng foreignkey.ParentTable????
    Khi Insert dữ liệu, có những cái người dùng nhập, có những cái lấy thông tin từ web, ví dụ lấy thông tin từ Session là ai đăng nhập, để lưu vào hệ thống người đó đã thao tác gì, ở bảng nào….
    Nếu dựa vào các attribute của MS xây dựng sẵn thì không đủ. Vì có 3 trang: List, Edit, Detail, mỗi trang có 1 UIHint riêng, không thể dùng chung được.
    Nên bài toán đặc ra, tạo 1 mảng 2 chiều chứa các UIHint và các Attribute khác.
    Các Attribute này được đăng ký trong Gobal.ascx khi ứng dụng bắt đầu chạy.

    Nhưng mình không biết cách xây dựng. Đây là 1 vấn đề thú vị. Mình tin chắc bạn sẽ làm được. Cám ơn bạn rất nhiều.

    Phản hồi
  8. Mặc dù chưa rõ hết ý của bạn nhưng theo mình thì trường hợp này có thể không cần thiết phải dùng attribute mà dựa vào việc thiết kế database. Ngoài ra cũng có thể truy vấn metadata được trong Entity Framework để lấy các thông tin cần thiết thông qua Metadata Workspace.

    Thực sự thì phần lập trình web mình chưa nghiên cứu nhiều, nếu được bạn có thể viết các ý tưởng dự án lên một bài riêng trên blog của bạn. Mình sẽ tham gia thảo luận và hi vọng có thể học được thêm nhiều kiến thức.

    p/s: Nhân tiện mình cũng thêm blog của bạn vào mục Blog Roll để tiện theo dõi.

    Phản hồi
  9. Mình muốn lấy giá trị của thuộc tính trong enum thì phải làm thế nào?
    Ví dụ:

     public class MathInfoAttribute : Attribute
        {
            public string DisplayName;
            public string Name;
        }
    public enum MathType: int
        {
            [MathInfo(DisplayName = "Hàm sin")]
            sin= 1,
            [CommonInfo(DisplayName = "Hàm cos")]
            cos= 2,
            [CommonInfo(DisplayName = "Hàm tg")]
            tg= 3,
            [CommonInfo(DisplayName = "Hàm cotg")]
            cotg= 4,       
        }
    

    Mình lấy được MathType.sin rồi, nhưng dùng mãi Reflection vẫn không lấy được DisplayName (hoặc Name) của sin?
    Mình xin cám ơn bạn trước.

    Phản hồi
  10. Tại comment #3 ở trên của mình, bạn cũng có thể thấy cách làm là dùng phương thức static Attribute.GetCustomAttributes() để lấy về 1 mảng các attribute. Sau khi có các attribute rồi, bạn chỉ cần lấy kiểu của attribute và sau đó là lấy các member, field, property của nó. Cụ thể với field hoặc property, bạn có thể lấy được giá trị từ một instance cụ thể bằng phương thức GetValue() như sau:

    Type t = typeof(MathType);
    
    MemberInfo memInfo = t.GetMember("sin")[0];
    
    Attribute[] attributes = Attribute.GetCustomAttributes(memInfo);
    
    foreach (Attribute attr in attributes)            
        Console.WriteLine(attr.GetType().GetField("DisplayName").GetValue(attr));
               
    
    Phản hồi

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s