(Post 30/03/2007) 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.
Giao diện
của MyReflector |
|
ĐÔ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.
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?
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.
Sơ đồ các khối mã thành phần của assembly |
|
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.
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
(theo PC World VN) |