• Thứ Ba, 21/02/2006 15:02 (GMT+7)

    Khảo sát động assembly và kiểu với System.Reflection

    Trong bài báo này, tôi sẽ giới thiệu với các bạn về không gian kiểu (namespace) System.Reflection cũng như cách dùng nó để khảo sát động (reflection) assembly. Đây là một tính năng vừa hay vừa mạnh mà .NET cung cấp cho lập trình viên! Nó giúp ta có thể nắm bắt thông tin về assembly, lớp, đối tượng, hàm, sự kiện... cũng như điều chỉnh thông tin về đối tượng ngay trong khi chạy chương trình.

    ĐÔI NÉT VỀ ASSEMBLY

    Theo định nghĩa của MSDN, assembly là một khối xây dựng nên một trình ứng dụng chạy trên nền CLR; khối này có thể sử dụng lại, có thể có nhiều phiên bản và có chứa siêu thông tin (meta) tự mô tả. Mỗi khi bạn biên dịch một dự án (project) thành một tập .dll hay .exe, bạn tạo ra một assembly.

    Hình 1: Sơ đồ các khối mã thành phần của assembly

     
    Một assembly có thể chứa một hay nhiều môđun. Mỗi môđun có thể được biên dịch thành một tập tin riêng lẻ (tạo ra assembly loại đa tệp (multi-file assembly)) hay nhiều môđun được biên dịch thành cùng một tập tin (tạo ra assembly loại đơn tệp (single-file assembly)). Mỗi môđun có thể chứa một hay nhiều kiểu (type). Mỗi kiểu lại có thể chứa một hay nhiều thành viên (member) thuộc một trong các loại sau: hàm tạo (constructor), hàm chức năng (method), hàm thuộc tính (property), trường (field), sự kiện (event). Cũng như kiểu, mỗi thành viên lại có các thuộc tính (attribute) như public, private, static... Riêng hàm tạo, hàm chức năng, hàm thuộc tính, hàm sự kiện còn có thể có các tham biến (parameter), kiểu dữ liệu trả về và biến cục bộ (hình 1).

    Khi thi hành, assembly sẽ được tải vào miền ứng dụng (application domain). Miền ứng dụng là đơn vị quản lý của CLR (Common Language Runtime). Trong một miền ứng dụng có thể có một hay nhiều assembly cùng hoạt động. Miền ứng dụng khác với tiến trình (process) và tiểu trình (thread), vốn là hai loại đơn vị quản lý của hệ điều hành. Nhiều miền ứng dụng có thể chạy trong cùng một tiến trình, nhiều tiểu trình có thể cùng chạy trong một miền ứng dụng. Tại mỗi thời điểm, một tiểu trình phải chạy trong một miền ứng dụng, nhưng ở các thời điểm khác nhau, một tiểu trình vẫn có thể chạy ở các miền ứng dụng khác nhau.

    Mỗi assembly có thể thuộc loại dùng riêng (private) hay dùng chung (shared). Loại dùng riêng được lưu trong thư mục chứa trình ứng dụng hay thư mục con của thư mục đó. Loại dùng chung được lưu trong vùng đệm GAC, một vùng hệ thống được tự động cài đặt khi bạn cài CLR lên máy; thông thường đó là thư mục c:\Windows\Assembly hay c:\Winnt\Assembly.

    Assembly có thể có một tên duy nhất (strong name) hay một tên khả trùng (weak name). Một tên khả trùng chứa thông tin để nhận dạng assembly như tên, số phiên bản, thông tin văn hóa. Một tên duy nhất còn có thêm một khóa công khai và chữ kí số hóa. Một assembly dùng chung phải có một tên duy nhất. Mặc định Visual Studio .NET tạo ra assembly có tên khả trùng. Nếu bạn muốn tạo assembly có tên duy nhất, bạn có thể sử dụng tiện ích Strong Name (sn.exe) và Assembly Linker (Al.exe) của .NET Framework SDK hoặc là cung cấp thông tin về tên duy nhất cho trình biên dịch bằng cách sử dụng các thuộc tính như AssemblyKeyNameAttribute và AssemblyKeyFileAttribute.

    GIỚI THIỆU VỀ KHÔNG GIAN KIỂU SYSTEM. REFLECTION

    Cơ chế khảo sát động là một nét đáng chú ý của .NET. Thông qua cơ chế này, chương trình có thể thu thập và xử lí thông tin mô tả (metadata) của chính mình. Chẳng những chương trình có thể khảo sát thông tin về các assembly, kiểu, thành viên của kiểu, các đối tượng đang tồn tại trong bộ nhớ mà chương trình còn có thể gọi các hàm chức năng, lấy hay đặt giá trị thuộc tính,... của những đối tượng đó. Sử dụng Reflection, ta có thể tạo ra các trình duyệt kiểu hay các trình soạn thảo thuộc tính.

    Các API giúp thực hiện cơ chế khảo sát kiểu đều nằm trong không gian kiểu System.Reflection. Bảng sau giới thiệu một số lớp trong không gian này:

     

    Lớp

       

    Công dụng

     
     

    Assembly

       

    Định nghĩa một assembly.

     
     

    AssemblyFlagsAttribute

       

    Chỉ định assembly có hỗ trợ việc nhiều phiên bản cùng chạy trên cùng máy, trong cùng tiến trình hay trong cùng miền ứng dụng không. Không thể kế thừa lớp này.

     
     

    AssemblyName

       

    Mô tả thông tin nhận dạng một assembly.

     
     

    ConstructorInfo

       

    Giúp nắm bắt thông tin về thuộc tính của hàm tạo cũng như thông tin mô tả hàm tạo.

     
     

    EventInfo

       

    Giúp nắm bắt thông tin về thuộc tính của sự kiện cũng như thông tin mô tả sự kiện.

     
     

    FieldInfo

       

    Giúp nắm bắt thông tin về thuộc tính của trường cũng như thông tin mô tả trường.

     
     

    MemberInfo

       

    Giúp nắm bắt thông tin về thuộc tính của thành viên cũng như thông tin mô tả thành viên.

     
     

    MethodBase

       

    Cung cấp thông tin về các hàm chức năng và hàm tạo. Đây là lớp cha của các lớp MethodInfo và ConstructorInfo.

     
     

    MethodInfo

       

    Giúp nắm bắt thông tin về thuộc tính của hàm chức năng cũng như thông tin mô tả hàm chức năng.

     
     

    Module

       

    Giúp khảo sát kiểu đối với môđun.

     
     

    ParameterInfo

       

    Giúp nắm bắt thông tin về thuộc tính của tham biến cũng như thông tin mô tả tham biến.

     
     

    PropertyInfo

       

    Giúp nắm bắt thông tin về thuộc tính của hàm thuộc tính cũng như thông tin mô tả hàm thuộc tính.

     
     

    ReflectionTypeLoadException

       

    Hàm chức năng Module.GetTypes() sẽ phát ra lỗi này khi không tải được lớp nào đó trong mô-đun chỉ định. Không thể thừa kế lớp này.

     
     

    TargetException

       

    Lỗi này được phát ra khi chương trình cố triệu gọi một hàm không hợp lệ.

     

     

    TargetInvocationException

     

     

    Lỗi này do hàm chức năng bị triệu gọi thông qua cơ chế Reflection phát ra. Không thể thừa kế lớp này.

     

     

    TargetParameterCountException

       

    Lỗi này được phát ra khi số tham số truyền cho một hàm bị gọi động không đúng với số tham biến đã khai báo của nó. Không thể thừa kế lớp này.

     

    Sau đây chúng ta sẽ lần lượt tìm hiểu làm thế nào dùng System.Reflection để khảo sát assembly và kiểu chứa trong assembly cũng như gọi động các thành viên của một đối tượng.

    1. Khảo sát assembly và kiểu

    Đầu tiên, chúng ta cần tạo ra một đối tượng kiểu Assembly đại diện cho assembly mà ta muốn khảo sát. Có thể dùng một trong các cách sau:

    Nếu bạn muốn khảo sát các assembly dùng trong chương trình, sử dụng câu lệnh:

    Assembly[] assArr = System.AppDomain.CurrentDomain.GetAssemblies()

    Ở đây, System.AppDomain.CurrentDomain chỉ miền ứng dụng đang thi hành câu lệnh này và GetAssemblies() sẽ trả về một mảng các assembly có trong miền đó.
    Nếu bạn muốn khảo sát một assembly khác thì gọi hàm LoadFrom() hay Load(). Khi dùng LoadFrom(), bạn cần chỉ ra đầy đủ đường dẫn tới assembly dùng riêng cần tải vào bộ nhớ. Khi dùng Load(), bạn cần chỉ ra tên của assembly dùng chung theo dạng "Tên, Version=..., Culture=..., PublicKeyToken=... ". Dòng lệnh sau sẽ tải assembly Myasm.exe nằm ở thư mục gốc đĩa C vào bộ nhớ:

    Assembly ass = Assembly.LoadFrom("C:\Myasm.exe")

    Còn các dòng lệnh sau sẽ tải assembly System.dll ở GAC:

    String s = "System, Version = 1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"

    Assembly ass = Assembly.Load(s);

    Sau khi tạo xong đối tượng assembly, ta có thể bắt đầu khảo sát.

    Để truy xuất các thông tin về assembly, ta sử dụng các thành viên của lớp Assembly. Đoạn mã sau sẽ lần lượt hiển thị tên và vị trí trên máy của các assembly trong mảng assArr:

    foreach (Assembly a in assArr)

    Console.WriteLine(a.FullName + " nằm ở " + a.Location);

    Để khảo sát kiểu chứa trong assembly, ta có thể trực tiếp gọi:

    Type typ = ass.GetType("Tên kiểu cần khảo sát");

    hay

    Type[] typArr = ass.GetTypes();

    Hoặc tạo ra một đối tượng Module trước:

    Module mod = ass.GetModule("Tên mô-đun cần khảo sát");

    hay

    Module[] modArr = ass.GetModules();

    rồi gọi GetType() hay GetTypes() của đối tượng môđun để truy xuất một kiểu nào đó hay tất cả các kiểu trong môđun:

    Type typ = mod.GetType("Tên kiểu cần khảo sát");

    hay

    Type[] typArr = mod.GetTypes();

    Type ở đây có thể là kiểu lớp (class), kiểu giao diện (inteface), hay kiểu liệt kê (enumeration). Muốn biết đối tượng Type của bạn thuộc kiểu nào, hãy gọi hàm thuộc tính IsClass, IsInterface, IsEnum để kiểm tra. Ví dụ:

    Console.WriteLine(typ.FullName + (typ.IsClass ? " là kiểu lớp" : " không phải kiểu lớp"));// Nếu typ là một lớp thì thông báo "là kiểu lớp". Nếu không thì báo " không phải kiểu lớp".

    Cuối cùng, để khảo sát các thành viên của một kiểu, ta có thể tạo ra biến hay mảng đối tượng kiểu MemberInfo, ConstructorInfo, MethodInfo, EventInfo, PropertyInfo, FieldInfo... bằng cách gọi các hàm chức năng của đối tượng Type như: GetMember hay GetMembers, GetConstructor hay GetConstructors, GetMethod hay GetMethods, GetEvent hay GetEvents... Ví dụ:

    PropertyInfo[] propArr = typ.GetProperties();

    foreach (PropertyInfo p in propArr){

    String s = "";

    s = "Thuộc tính " + p.Name;

    s += (p.CanRead ? " (có thể đọc)" : " ");

    s += (p.CanWrite ? " (có thể ghi)" : " ");

    s += "được khai báo trong kiểu " + p.DeclaringType;

    Console.WriteLine(s);

    }

    Nhằm minh họa những gì vừa nói về System.Reflection, tôi xin đưa ra một trình ứng dụng nhỏ MyReflector dùng để liệt kê danh sách môđun, kiểu và thành viên trong một assembly nào đó. Giao diện của MyReflector như ở hình 2, gồm các thành phần sau:

     

    Vùng

     

     

    Kiểu

     

     

    Công dụng

     

     

    txtOpenedAssembly

     

     

    TextBox

     

     

    Hiển thị tên và thông tin nhận dạng của assembly cần khảo sát kiểu

     

     

    btnOpenAssembly

     

     

    Button

     

     

    Dùng để mở assembly cần khảo sát kiểu (thông qua một đối tượng OpenFileDialog), sử dụng hàm Assembly.LoadFrom()

     

     

    btnOpenAssembly2

     

     

    Button

     

     

    Dùng để mở assembly cần khảo sát kiểu (bằng cách gõ thông tin nhận dạng assembly vào vùng txtOpenedAssembly), sử dụng hàm Assembly.Load()

     

     

    lstModules

     

     

    ListBox

     

     

    Hiển thị danh sách môđun trong assembly được mở

     

     

    lstTypes

     

     

    ListBox

     

     

    Hiển thị danh sách kiểu trong môđun được chọn trong vùng lstModules

     

     

    lstMembers

     

     

    ListBox

     

     

    Hiển thị danh sách thành viên trong kiểu được chọn trong vùng lstTypes

     

    Cách thức hoạt động của MyReflector như sau: Đầu tiên, bạn chọn 1 trong 2 nút Mở assembly để mở ra assembly cần khảo sát. Nếu bạn chọn (LoadFrom), hộp thoại Open sẽ hiện ra cho bạn chỉ định assembly cần mở. Nếu bạn chọn (Load), chương trình sẽ mở assembly dựa trên nội dung ở TextBox txtOpenedAssembly; nội dung này phải có dạng như đã đề cập ở trên (Tên, Version=..., Culture=..., PublicKeyToken=...). Tiếp theo, bạn chọn một môđun nào đấy trong danh sách môđun để xem các kiểu của nó, rồi chọn một kiểu tùy ý trong danh sách kiểu để xem các thành viên tương ứng. Quá đơn giản, phải không?

    Hình 2- Giao diện của MyReflector


    Và đoạn mã thực hiện cũng đơn giản không kém. Để mở assembly chỉ định từ hộp thoại Open, ta dùng lệnh:
    private Assembly openedAssembly = null;

    ...

    openedAssembly = Assembly.LoadFrom(openFileDialog.FileName);

    Còn để mở assembly dựa vào tên đầy đủ của nó (ghi trong TextBox txtOpenedAssembly), gọi:
    openedAssembly = Assembly.Load(txtOpenedAssembly.Text);

    Kế đến, ta dùng đoạn mã sau để đếm và liệt kê tên các môđun có trong assembly được mở:

    private Module[] moduleList = null;

    ...

    moduleList = openedAssembly.GetModules();

    lstModules.Items.Clear();

    lblModules.Text = "Danh sách mô-đun: " + moduleList.Length.ToString();

    foreach (Module m in moduleList)

    {

    lstModules.Items.Add(m.ToString());

    }

    Chúng ta cũng dùng đoạn mã tương tự để đếm và liệt kê tên các kiểu có trong 1 môđun được chọn nào đó:

    private Module selectedModule = null;

    private Type[] typeList = null;
    ...

    selectedModule = moduleList[0];

    typeList = selectedModule.GetTypes();

    lstTypes.Items.Clear();

    lblTypes.Text = "Danh sách kiểu: " + typeList.Length.ToString();

    foreach (Type t in typeList)

    {

    lstTypes.Items.Add(t.ToString());

    }

    Phần mã vừa trình bày có hơi khác với mã nguồn chương trình mẫu (Reflector.zip) tải về từ website www.pcworld.com.vn.

    2. Gọi động một thành viên

    Thông thường, khi chúng ta muốn truy cập một thành viên nào đó, chẳng hạn hàm ToString(), của một đối tượng obj, chúng ta phải ghi đúng tên của thành viên đó trước lúc biên dịch theo dạng obj.ToString(). Với System.Reflection và System.Type, chúng ta có thể gọi hàm ToString() của đối tượng obj trong khi chạy chương trình theo dạng sau:

    t.InvokeMember("ToString", ..., ..., obj, ...); // t là một đối tượng kiểu System.Type

    thậm chí bạn có thể thay "ToString" bằng "tostring", "TOSTRING", "tOstring",...

    .NET cung cấp khả năng gọi động thành viên thông qua các biến thể của hàm InvokeMember() của lớp Type. Ở đây, chúng ta chỉ tìm hiểu biến thể sau:

    public object InvokeMember(

    string name, BindingFlags invokeAttr, Binder binder, object tartget, object[] args

    );

    trong đó:

    - name (kiểu string) là tên của thành viên cần gọi động. Nếu name bằng chuỗi rỗng ("") hay null thì thành viên mặc định sẽ được gọi.

    - invokeAttr (kiểu liệt kê System.Reflection.BindingFlags) là một mặt nạ bit tạo ra từ các cờ thông tin chỉ định cách thức tìm kiếm và sử dụng thành viên cần gọi. invokeAttr bằng null tương đương với BindingFlags.Public | BindingFlags.Instance. Dưới đây là một số cờ BindingFlags thông dụng:

    Nếu muốn lấy giá trị trả về của thành viên, dùng cờ BindingFlags.Instance hay BindingFlags.Static. Ngoài ra, nếu thành viên thuộc loại static, dùng cờ BindingFlags.Static; ngược lại, dùng cờ BindingFlags.Instance.

    Nếu thành viên cần gọi thuộc loại public, dùng cờ BindingFlags.Public. Ngược lại, dùng cờ BindingFlags.NonPublic. Nếu bạn không cần quan tâm thành viên đó có thuộc loại public hay không thì dùng cả hai cờ (sử dụng toán tử OR (|)).

    Nếu thành viên cần gọi thuộc loại static và có thể được thừa kế từ các lớp khác, dùng cờ BindingFlags.FlattenHierarchy.

    Nếu không muốn quan tâm đến cách viết hoa hay thường của tên thành viên, dùng cờ BindingFlags.IgnoreCase.

    Nếu chỉ muốn dùng các thành viên được khai báo trong kiểu, bỏ qua các thành viên kế thừa, dùng cờ BindingFlags.DeclaredOnly.

    Nếu muốn gọi hàm tạo thì đặt tham số name bằng null hay chuỗi rỗng và dùng cờ BindingFlags.CreateInstance.

    Nếu muốn gọi các hàm chức năng khác thì dùng cờ InvokeMethod.

    Nếu muốn lấy giá trị của trường hay hàm thuộc tính thì dùng cờ GetField hay GetProperty.

    Nếu muốn đặt giá trị cho trường hay hàm thuộc tính thì dùng cờ SetField hay SetProperty.

    (Không dùng cùng lúc 2 cờ trở lên trong các cờ CreateInstance, InvokeMethod, GetField, SetField, GetProperty, SetProperty)

    - binder (kiểu Binder) chỉ định cách chọn thành viên để gọi từ danh sách thành viên tìm được. binder cũng chỉ định cách chuyển kiểu và gọi động thành viên. Thông thường, chúng ta đặt binder bằng null để sử dụng đối tượng DefaultBinder của hệ thống. Nếu bạn không thích null, bạn phải tạo ra một lớp con của binder rồi truyền một đối tượng của lớp này làm binder.

    - target (kiểu Object) là đối tượng có thành viên cần gọi.

    - args (kiểu Object[]) là mảng chứa các tham số cần truyền cho thành viên cần gọi.

    - Hàm InvokeMember() trả về một đối tượng kiểu Object chứa giá trị trả về của thành viên được gọi. Nếu cần sử dụng giá trị này, bạn nhớ chuyển kiểu thích hợp cho nó.

    Chú ý, việc gọi hàm InvokeMember() có thể gây ra một số lỗi như:

       

    Loại lỗi

     

     

    Điều kiện gây ra lỗi

     
       

    ArgumentException

     

     

    invokeAttr chứa cờ CreateInstance kết hợp với InvokeMethod, GetField, SetField, GetProperty, SetProperty
    hay invokeAttr chứa cờ GetField và SetField
    hay invokeAttr chứa cờ GetProperty và SetProperty
    hay invokeAttr chứa cờ InvokeMethod và SetField hay SetProperty
    hay args là mảng nhiều chiều
    hay invokeAttr chứa cờ SetField và args có hơn một phần tử
    (hay một số điều kiện khác)

     
       

    MissingFieldException

     

     

    Không tìm thấy trường hay thuộc tính

     
       

    TargetException

     

     

    Không thể gọi thành viên chỉ định đối với target

     
       

    AmbiguousMatchException

     

     

    Có nhiều hơn một hàm chức năng thỏa điều kiện gọi

     

    Ngoài ra, để gọi được một hàm chức năng thì số tham biến (parameter) trong phần khai báo hàm phải bằng với số tham số (argument) truyền trong mảng args (trừ trường hợp hàm có định nghĩa tham số mặc định) và kiểu của các tham số phải có thể chuyển được thành kiểu của tham biến.

    Và đây là ví dụ minh họa cách dùng InvokeMember().

    Giả sử ta có lớp sau :

    class MyClass{

    private int Field1 = 0;

    public MyClass(){Console.WriteLine("MyClass");}

    public int Prop1{

    set {Field1 = value;}

    get { return Field1;}

    }

    public String ToString(){return Field1.ToString();}

    }

    Bây giờ ta muốn sử dụng lớp này. Thay vì dùng các câu lệnh bình thường như dưới đây:

    MyClass obj = new MyClass();

    Console.WriteLine("Kiểu của obj: " + obj.GetType().ToString()) ;

    obj.Prop1 = 10;

    Console.WriteLine("Prop1 = " + obj.Prop1);

    Console.WriteLine("ToString = " + obj.ToString());

    Ta có thể dùng:

    // Tạo một đối tượng kiểu Type đại diện cho kiểu MyClass

    Type t = typeof(MyClass) ;

    // Tạo mảng tham số cần truyền cho hàm tạo MyClass().

    Object[] args = new Object[]{} ;

    // Dùng InvokeMember() để gọi hàm tạo. Ở đây, vì số tham biến của hàm tạo là 0 nên có thể thay args bằng null. Ngoài ra, tham biến target cũng nên đặt bằng null.

    Object obj = t.InvokeMember(null, BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.CreateInstance, null, null, args);

    Console.WriteLine("Kiểu của obj: " + obj.GetType().ToString()) ;

    // Đặt thuộc tính Prop1 bằng 10

    t.InvokeMember("Prop1", BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty, null, obj, new Object[]{10});

    // Lấy giá trị của thuộc tính Prop1 gán cho biến v

    int v = (int) t.InvokeMember("Prop1", BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty, null, obj, null);

    Console.WriteLine("Prop1 = " + v);

    // Gọi hàm ToString (mà không cần chú ý viết hoa hay viết thường ) và gán giá trị trả về cho biến s

    String s = (String) t.InvokeMember("tostring", BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Nonpublic | BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.IgnoreCase, null, obj, null);

    Console.WriteLine("ToString = " + s);

    Ngoài ra, ta còn có thể:

    // Đặt giá trị 5 cho trường Field1. Hãy chú ý là trường này thuộc loại private, tức là bình thường ta không thể gọi obj.Field1 = 5;

    t.InvokeMember("Field1", BindingFlags.DeclaredOnly | BindingFlags.Nonpublic | BindingFlags.Instance | BindingFlags.SetField, null, obj, new Object[]{5});

    // Lấy giá trị của trường Field1 gán vào biến v

    v = (int) t.InvokeMember("Field1", BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Nonpublic | BindingFlags.Instance | BindingFlags.GetField, null, obj, null);

    Console.WriteLine("Field1 = " + v);

    Như bạn đã thấy, tuy cách gọi động thành viên phải tốn khá nhiều dòng mã so với cách gọi bình thường nhưng nó có lợi thế là rất linh hoạt: chẳng những có thể gọi thành viên bằng tên (thông qua một biến chuỗi), không phân biệt hoa hay thường, mà còn có thể gọi cả các thành viên private. Không những thế, cách thức gọi động tuy khác nhau đối với các loại thành viên khác nhau nhưng có cấu trúc hoàn toàn xác định, có thể lập ra mẫu gọi tổng quát, áp dụng được cho bất kì kiểu nào, thành viên nào. Do vậy, việc vận dụng tính năng gọi động sẽ giúp chương trình của bạn cho phép người dùng thoải mái chọn hàm muốn thi hành, nạp tham số cần thiết... hay giúp bạn kiểm soát chính chương trình của mình.

    3. Ví dụ minh họa

    Ở phần 1, chúng ta đã tạo một trình duyệt kiểu đơn giản; ở phần 2, chúng ta cũng đã thử gọi động thành viên. Nhưng những ví dụ đó có thể vẫn chưa thuyết phục lắm. Cho nên, tôi sẽ minh họa thêm bằng một trình ứng dụng nữa: PropertyEditor. Đây là chương trình mô phỏng tính năng của cửa sổ Properties quen thuộc trong Visual Studio hay các IDE khác. Mời các bạn xem giao diện của nó trên hình 3.

    Cách hoạt động của chương trình chắc các bạn cũng đã đoán ra: khi nhấn vào nút Form trên ToolboxForm thì một DesignForm mới sẽ xuất hiện, khi nhấn vào những nút còn lại thì điều khiển (control) tương ứng sẽ được tạo ra trên DesignForm đang được chọn. Cuối cùng, khi nhấn vào một điều khiển trên DesignForm hay chính DesignForm thì thông tin về các thuộc tính tương ứng sẽ hiện ra trên PropertySheetForm (ở đây, tôi chỉ cho hiện các thuộc tính public kiểu string và int). Nếu muốn, bạn có thể sửa giá trị những thuộc tính của đối tượng trực quan đang được chọn thông qua PropertySheetForm này bằng cách nhấn 2 lần lên giá trị cần sửa và gõ vào giá trị mới.

     

    Hình 3- Giao diện của PropertyEditor

    Dựa vào phần mô tả trên, có thể thấy PropertyEditor cần thực hiện được 3 tính năng chính là tạo một điều khiển theo tên chỉ định, xem thuộc tính của đối tượng chỉ định và chỉnh sửa giá trị thuộc tính của đối tượng chỉ định. Bây giờ, chúng ta bắt đầu tìm hiểu phần mã thực thi các tính năng đó. Ở đây chỉ tập trung vào những đoạn mã liên quan đến System.Reflection. Phần còn lại mời các bạn xem thêm trong mã nguồn.

    Như bạn thấy trên hình, chương trình của chúng ta gồm 4 nhóm form chính là ManagerForm, ToolboxForm, PropertySheetForm và DesignForm, trong đó ManagerForm là form chủ chứa tất cả các form còn lại. Ngoài việc lưu tham chiếu đến các đối tượng ToolboxForm, PropertySheetForm và DesignForm, ManagerForm còn phải kiểm soát thông tin về DesignForm cũng như điều khiển hiện hành. Sau đây là một số biến quan trọng của ManagerForm:

    // Các biến sau lần lượt trỏ đến form thiết kế hiện hành, mảng các form thiết kế đã tạo, điều khiển đang được chọn.

    internal Form curForm = null;

    internal ArrayList frmList = new ArrayList();

    internal Control curControl = null;

    // Các biến sau lần lượt trỏ đến form hộp công cụ, form bảng thuộc tính của chương trình.

    internal ToolboxForm toolbox = null;

    internal PropertySheetForm propertySheet = null;

    // Biến này trỏ đến assembly System.Windows.Forms.dll. Cần phải tải assembly này vào bộ nhớ mới có thể tạo động các điều khiển.

    internal Assembly myAsm = null;

    Trong 6 biến vừa nêu, chỉ có myAsm cần được giải thích thêm. Sở dĩ ta dùng biến này là vì: Các điều khiển mà chương trình của chúng ta cần tạo ra đều thuộc không gian kiểu System.Windows.Forms; không gian này lại nằm trong assembly System.Windows.Forms.dll. Do đó, chúng ta phải tải assembly này vào bộ nhớ trước và lưu tham chiếu đến nó để về sau sử dụng:

    myAsm = Assembly.Load("System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");

    Kể từ bây giờ, dựa vào myAsm, chúng ta có thể tạo động bất kì điều khiển nào có trong System.Windows.Forms, chứ không chỉ có Button và Label. Xem hàm createNewControl() của ManagerForm bạn sẽ rõ:

    public void createNewControl(string ControlType)

    {

    ...

    // Từ ControlType (dạng viết gọn) tạo ra dạng đầy đủ của tên kiểu. Riêng trường hợp ControlType = "Form" thì tạo trực tiếp phông thiết kế.

    string s;

    switch (ControlType)

    {

    case "Form":

    curForm = new DesignForm(this);

    ...

    return;

    default:

    if (curForm == null || curForm.IsDisposed) return;

    s = "System.Windows.Forms." + ControlType;

    break;

    }

    // Tạo ra đối tượng obj có kiểu chỉ định.

    Type t = myAsm.GetType(s);

    Object[] args = new Object[]{};

    Object obj = t.InvokeMember(null, BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.CreateInstance, null, null, args);

    ...

    }

    Như vậy, để tạo một ComboBox hay ListBox, TreeView..., bạn chỉ đơn giản gọi:

    createNewControl("ComboBox");

    createNewControl("ListBox");

    createNewControl("TreeView");

    Ngoài createNewControl(), ManagerForm còn định nghĩa updateControl() dùng để chỉnh sửa giá trị thuộc tính của một điều khiển nào đó.

    public void updateControl(Control whichControl, string whichProperty, object whichValue)

    {

    Type t = whichControl.GetType();

    PropertyInfo pi = t.GetProperty(whichProperty);

    switch (pi.PropertyType.ToString())

    {

    case "System.String":

    pi.SetValue(whichControl, Convert.ToString(whichValue), null);

    // Câu lệnh sau tương đương với câu lệnh liền trên:

    // t.InvokeMember(whichProperty, BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.Public, null, whichControl, new object[]{whichValue});

    break;

    case "System.Int32":

    pi.SetValue(whichControl, Convert.ToInt32(whichValue),null);

    // Các câu lệnh sau tương đương với câu lệnh liền trên

    // int i = Convert.ToInt32(whichValue);

    // t.InvokeMember(whichProperty, BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.Public, null, whichControl, new object[]{i});

    break;

    }

    }

    Hiện tại, đoạn mã trên chỉ áp dụng cho những thuộc tính mang giá trị kiểu string và int. Bạn có thể tự mở rộng để áp dụng cho những thuộc tính mang giá trị kiểu khác.

    Thế là tạm xong phần của ManagerForm. Bây giờ tới PropertySheetForm, form này đơn giản hơn ManagerForm nhiều. Phần cốt lõi của nó là 3 hàm chức năng sau:

    // Hàm này nhận vào một đối tượng và trả về một mảng 2 chiều mô tả một bảng dữ liệu có 3 cột: tên thuộc tính, giá trị thuộc tính và kiểu của thuộc tính.

    public string[][] getProperties(object whichObject) {...}

    // Hàm này nhận vào một mảng chuỗi hai chiều (các phần tử 0 của những mảng con 1 chiều tạo ra nội dung mảng Items của ListView, các phần tử từ thứ 2 trở đi là nội dung mảng SubItems của phần tử tương ứng trong mảng Items) và trả về một đối tượng ListView.

    public ListView createListView(string[][] itemArr) {...}

    // Hàm này nhận vào một đối tượng và cập nhật giá trị các thuộc tính của đối tượng này lên bảng thuộc tính.

    public void updateSheet(object whichObject) {...}

    Mỗi khi curForm hay curControl thay đổi thì ManagerForm sẽ gọi updateSheet() để cập nhật bảng thuộc tính. Đến phiên mình, updateSheet() sẽ gọi getProperties() và createListView() để tạo lại danh sách thuộc tính thích hợp.

    Cuối cùng, cần nói qua về hàm addButton() của ToolboxForm. Hàm này không sử dụng đến tính năng của System.Reflection mà chỉ dùng để tạo thêm nút lệnh vào hộp công cụ. Bạn truyền cho hàm một chuỗi cho biết tên kiểu của điều khiển sẽ được tạo ra khi nhắp vào nút lệnh này. Hàm xử lí sự kiện của nút lệnh đó sẽ truyền chuỗi trên đến createNewControl() của ManagerForm.

    Trong hàm tạo của ToolboxForm, tôi chỉ dùng 3 lệnh sau:

    addButton("Form");

    addButton("Button");

    addButton("Label");

    Do đó, trên hộp công cụ trong hình 3 chỉ có 3 nút lệnh. Nếu thích bổ sung thêm các nút dùng để tạo CheckBox, ComboBox, DataGrid..., bạn chỉ cần thêm:

    addButton("CheckBox");

    addButton("ComboBox");

    addButton("DataGrid");

    Chương trình InvokeMemberDemo và PropertyEditor dùng để minh họa cho phần này có trong tệp Reflector.zip (tải về từ www.pcworld.com.vn). Bạn cần dùng lệnh Set As Startup Project khi lựa chọn chương trình muốn chạy.

    Hi vọng những gì trình bày trong bài báo này giúp các bạn cảm nhận được phần nào sức mạnh của cơ chế khảo sát động trong .NET. Chúc các bạn sẽ tìm thêm được nhiều điều thú vị với System.Reflection.

    Nguyên Phương
    Email:
    hungphung@hcm.fpt.vn

    ID: A0602_103
    File đính kèm