通信人家园
标题:
动态类型与PropertyGrid
[查看完整版帖子]
[打印本页]
时间:
2010-12-23 10:44
作者:
xiaotao_yu
标题:
动态类型与PropertyGrid
背景
:
我在一个项目中遇到这样一件事。一开始用户对要编辑的数据
没有多少要求,于是我用PropertyGrid来提供编辑界面,我的开
发被大大简化了。但是用户使用了一段时间后提出,所有对象的
属性个数必须可以动态增减,甚至在运行中。虽然他们再次表示
增减的个数不会超过5个,但是这次我选择不相信他们了,我需要
一个具有一定弹性的设计。于是每个对象会自带一个Dictionary保
存属性。我再提供配置文件来描述每种对象的属性表。到目前为止
一切OK。但是当我将这个对象赋给PropertyGrid时问题来了,这个
控件竟然不允许手动增减属性,她只接受对象的public property!
问题
:
如上所述
1、我们有一个对象,对象要编辑的属性不是它自身的property,
而是保存在一个Dictionary里;
2、对象属性的编辑界面PropertyGrid只接受public property;
3、我不想自己开发编辑界面;
分析
:
根据需求我们不难看出真正困扰我的其实是第3条,而这一条是
我自己强加的,用户对编辑界面的唯一要求就是简单,
PropertyGrid他们认可,Label加TextBox他们也认可,在这方面他
们其实是很可爱的。
我现在可以放弃我自己的需求实现一个Label加TextBox的对话框
,也可以用DataGridView。其实只要放弃PropertyGrid我有很多选
择。但是我喜欢PropertyGrid,他在这个项目中正合适,而且我自
己很难实现出它的效果。
其实说白了,我们只要把Dictionary中间的键值对变成对象的
property就行了,所有的矛盾一下归结为:如何创建一个对象,它
的property都来自Dictionary中间的键值对。这时我的脑海中闪出
一个单词“Emit”。
Emit是一个很强大的功能,但是不太好用(大概这是一个铁律:
强大的都不好驾驭),我也就没有认真学习过。现在机会来了。
我在这里只简单介绍一下Emit的使用,感兴趣的可以深入学习,最
好能把学习成果发布到这里与大家分享。
Emit简介
:
Emit是一种允许代码在运行时创建并执行代码的功能,用它创建
的代码功能有限制,但是执行效率与编译后的代码无异。提醒一下
,想使用Emit需要对DotNet的内存模型有一定的了解,更重要的是
需要了解IL的知识。下面我用一个例子说明使用Emit的大致流程,
一个HelloWorld程序;)
(这段代码来自项目exam/exam1)
Code
using
System;
using
System.Collections.Generic;
using
System.Text;
using
System.Reflection.Emit;
using
System.Reflection;
namespace
demo1
{
class
Program
{
static
void
Main(
string
[] args)
{
//
创建程序集“DynamicAssembly”
AssemblyBuilder assemblyBuilder
=
AppDomain.CurrentDomain.DefineDynamicAssembly(
new
AssemblyName(
"
DynamicAssembly
"
)
//
, AssemblyBuilderAccess.Run
//
该程序集只用作运行,你还可以创建可保存的程序集
);
//
创建模块“DynamicModule”
ModuleBuilder moduleBuilder
=
assemblyBuilder.DefineDynamicModule(
"
DynamicModule
"
);
//
创建类型“DynamicClass”
TypeBuilder typeBuilder
=
moduleBuilder.DefineType(
"
DynamicClass
"
, TypeAttributes.Public
|
TypeAttributes.Class
//
相当于 public class DynamicClass
,
null
//
基类
,
null
//
接口
);
//
为类型增加一个方法“Greet”
MethodBuilder methodBuilder
=
typeBuilder.DefineMethod(
"
Greet
"
, MethodAttributes.Public
);
//
获得ILGenerator对象,该对象用来为方法注入IL语言
ILGenerator g
=
methodBuilder.GetILGenerator();
//
将字符串"HelloWorld"放到堆栈顶端
g.Emit(OpCodes.Ldstr,
"
HelloWorld
"
);
//
调用Console.WriteLine打印堆栈顶端的值
g.Emit(OpCodes.Call,
typeof
(Console).GetMethod(
"
WriteLine
"
, BindingFlags.Public
|
BindingFlags.Static
,
null
,
new
Type[] {
typeof
(
string
) }
,
null
));
//
return
g.Emit(OpCodes.Ret);
//
创建动态类型
Type type
=
typeBuilder.CreateType();
//
实例化类型
object
target
=
Activator.CreateInstance(type);
//
获得Greet方法的描述
MethodInfo mi
=
type.GetMethod(
"
Greet
"
);
//
调用方法
mi.Invoke(target,
null
);
}
}
}
上面的代码先创建了一个程序集“DynamicAssembly”,然后给程序集增加
一个模块“DynamicModule”,最后在模块中创建类型“DynamicClass”。这是
使用Emit的经典步骤。在得到类型以后真正的工作才开始。上面的代码只
是为类型定义了一个函数“Greet”,然后用ILGenerator来为函数注入代码:
Console.WriteLine("HelloWorld")。
好了,Emit就介绍到这里。下面我们来看Emit技术实现我们的动态属性对象。
动态属性对象实现
:
我们需要的是一个可以把一系列键值对转换为一个类型的public property的
工具,说白了就是一个函数。
关于这个动态类型,我们得仔细考虑:
1)它的public property都是来自输入的键值对;
2)每个property名称都是键的名称;
3)每个property都有get和set函数;
4)我们需要在set函数里面发出修改前事件(prevSet)和修改完成事件(postSet)。
最后一条又是我加的,因为我需要在属性值被修改前对新值进行检验,修改
后提示有关界面(也为实现诸如MVP之类的模式提供支持)。
这里的代码进行了简化,我们只处理属性值是字符串的情况。也没有(UITypeEditor)。
这样做是为了不分散大家注意力。
我们已经知道要创建一个什么样的动态类型了,现在来看代码:
(这段代码来自项目exam2/utilities.cs)
Code
using
System.Collections.Generic;
using
System;
using
System.Reflection.Emit;
using
System.Reflection;
namespace
demo2
{
///
<summary>
///
描述一个属性容器,属性对象会实现这个接口
///
</summary>
public
interface
IPropertyContainer
{
///
<summary>
///
获得属性名称列表
///
</summary>
IEnumerable
<
string
>
PropertyNames {
get
; }
///
<summary>
///
设置或获得属性值
///
</summary>
///
<param name="key">
属性名
</param>
///
<returns></returns>
object
this
[
string
key] {
get
;
set
; }
}
///
<summary>
///
工具类
///
</summary>
public
static
class
ObjectWrappeerBuilder
{
///
<summary>
///
设置动作执行前调用
///
</summary>
///
<param name="name">
属性名
</param>
///
<param name="value">
属性值
</param>
///
<returns>
返回true才执行设置,否则不执行
</returns>
public
delegate
bool
PrevSet(
string
name,
object
value);
///
<summary>
///
设置动作成功后代用
///
</summary>
///
<param name="name">
属性名
</param>
///
<param name="value">
属性值
</param>
public
delegate
void
PostSet(
string
name,
object
value);
///
<summary>
///
创建动态对象
///
</summary>
///
<param name="propObj">
源对象
</param>
///
<param name="prevSet">
设置前置动作
</param>
///
<param name="postSet">
设置后置动作
</param>
///
<returns>
返回动态对象
</returns>
public
static
object
Build(IPropertyContainer propObj, PrevSet prevSet, PostSet postSet)
{
//
创建一个动态程序集“DynamicAssembly”
AssemblyBuilder assemblyBuilder
=
AppDomain.CurrentDomain.DefineDynamicAssembly(
new
AssemblyName(
"
DynamicAssembly
"
)
, AssemblyBuilderAccess.Run);
//
为程序集增加一个模块“DynamicModule”
ModuleBuilder moduleBuilder
=
assemblyBuilder.DefineDynamicModule(
"
DynamicModule
"
);
//
创建一个动态类型“”
TypeBuilder typeBuilder
=
moduleBuilder.DefineType(propObj.GetType().Name
+
"
PropertiesWrapper
"
, TypeAttributes.NotPublic
|
TypeAttributes.Class
//
这里设置类型的属性:非public 的class类型
,
typeof
(
object
)
//
这里设置基本类为object,因为需要调用基类构造函数
,
null
);
//
为类型增加一个成员变量“element”,IPropertyContainer
FieldBuilder eleFB
=
typeBuilder.DefineField(
"
element
"
, propObj.GetType()
, System.Reflection.FieldAttributes.Private);
//
为类型增加一个成员变量“prevSet”,PrevSet
FieldBuilder prevSetFB
=
typeBuilder.DefineField(
"
prevSet
"
, prevSet.GetType()
, System.Reflection.FieldAttributes.Private);
//
为类型增加一个成员变量“postSet”,PostSet
FieldBuilder postSetFB
=
typeBuilder.DefineField(
"
postSet
"
, postSet.GetType()
, System.Reflection.FieldAttributes.Private);
//
将键值对添加成类型的property
foreach
(
string
key
in
propObj.PropertyNames)
{
BuildProperty(propObj, typeBuilder, prevSet, prevSetFB, postSet
, postSetFB, eleFB, key, propObj[key]);
}
//
获得类型数组,一般在各种反射函数中
Type[] argTypes
=
new
Type[] { propObj.GetType(), prevSet.GetType(), postSet.GetType() };
//
定义构造函数,我们要用这种方式将外部值传给动态类型
ConstructorBuilder defCtorBuilder
=
typeBuilder.DefineConstructor(MethodAttributes.Public
, CallingConventions.Standard
, argTypes
//
我们刚定义的类型数组在这里描述了构造函数的参数列表
);
//
获得构造函数的ILGenerator
ILGenerator cilg
=
defCtorBuilder.GetILGenerator();
//
获得基类构造函数
ConstructorInfo objCtor
=
Type.GetType(
"
System.Object
"
).GetConstructor(
new
Type[
0
]);
//
下面的IL代码可以翻译成:
/*
* ctor(IPropertyContainer propObj, PrevSet prevSet, PostSet postSet)
* :base()
* {
* this.element = propObj;
* this.prevSet = prevSet;
* this.postSet = postSet;
* }
*
*/
cilg.Emit(OpCodes.Ldarg_0);
cilg.Emit(OpCodes.Call, objCtor);
cilg.Emit(OpCodes.Ldarg_0);
cilg.Emit(OpCodes.Ldarg_1);
cilg.Emit(OpCodes.Stfld, eleFB);
cilg.Emit(OpCodes.Ldarg_0);
cilg.Emit(OpCodes.Ldarg_2);
cilg.Emit(OpCodes.Stfld, prevSetFB);
cilg.Emit(OpCodes.Ldarg_0);
cilg.Emit(OpCodes.Ldarg_3);
cilg.Emit(OpCodes.Stfld, postSetFB);
cilg.Emit(OpCodes.Ret);
//
创建类型
Type type
=
typeBuilder.CreateType();
//
创建实例
return
Activator.CreateInstance(type,
new
object
[] { propObj, prevSet, postSet },
null
);
}
///
<summary>
///
这是一个辅助函数,将键值对变成一个property
///
</summary>
///
<param name="propObj">
源对象
</param>
///
<param name="typeBuilder">
类型描述
</param>
///
<param name="prevSet">
调用前事件代理
</param>
///
<param name="prevSetFB">
调用前事件成员变量描述
</param>
///
<param name="postSet">
调用后事件代理
</param>
///
<param name="postSetFB">
调用后事件成员变量描述
</param>
///
<param name="eleFB">
源对象成员变量描述
</param>
///
<param name="key">
键
</param>
///
<param name="val">
值
</param>
private
static
void
BuildProperty(IPropertyContainer propObj, TypeBuilder typeBuilder
, PrevSet prevSet, FieldBuilder prevSetFB, PostSet postSet, FieldBuilder postSetFB
, FieldBuilder eleFB,
string
key,
object
val)
{
//
定义一个property
PropertyBuilder propBuilder
=
typeBuilder.DefineProperty(key
, System.Reflection.PropertyAttributes.None, val.GetType(),
null
);
//
定义一个函数用作property的get函数
MethodBuilder getMethodBuilder
=
typeBuilder.DefineMethod(
"
get_
"
+
key,
MethodAttributes.Public,
val.GetType(),
new
Type[] { });
//
获得该函数ILGenerator
ILGenerator ilg
=
getMethodBuilder.GetILGenerator();
//
获得IPropertyContainer的property: this[string key],的描述
PropertyInfo pi
=
typeof
(IPropertyContainer).GetProperty(
"
Item
"
,
new
Type[] {
typeof
(
string
) });
//
下面的IL代码可以翻译成:
/*
* get
* {
* return element[key];
* }
*
*/
ilg.Emit(OpCodes.Ldarg_0);
ilg.Emit(OpCodes.Ldfld, eleFB);
ilg.Emit(OpCodes.Ldstr, key);
ilg.EmitCall(OpCodes.Call, pi.GetGetMethod(),
null
);
ilg.Emit(OpCodes.Ret);
//
将该函数设置为property的get函数
propBuilder.SetGetMethod(getMethodBuilder);
//
定义一个函数用作property的set函数
MethodBuilder setMethodBuilder
=
typeBuilder.DefineMethod(
"
set_
"
+
key,
MethodAttributes.Public,
null
,
new
Type[] { val.GetType() });
//
获得该函数ILGenerator
ilg
=
setMethodBuilder.GetILGenerator();
//
下面的IL代码可以翻译成:
/*
* set
* {
* if(this.prevSet(value))
* {
* this.element[key] = value;
* this.postSet(value);
* }
* }
*
* 特别说明一下,大家会发现经常出现一个ilg.Emit(OpCodes.Ldarg_0);的调用
* 其实这是在将this指针放到堆栈顶端,因为非静态的成员函数的第一个参数其实
* 就是this指针,因此只要你需要引用成员都需要先调用这段代码。如果有Python
* 的开发经验对这个就不难理解了。
*/
System.Reflection.Emit.Label get_out
=
ilg.DefineLabel();
///
/调用前置函数,判断是否允许设置
ilg.Emit(OpCodes.Ldarg_0);
ilg.Emit(OpCodes.Ldfld, prevSetFB);
ilg.Emit(OpCodes.Ldstr, key);
ilg.Emit(OpCodes.Ldarg_1);
ilg.Emit(OpCodes.Call, prevSet.GetType().GetMethod(
"
Invoke
"
));
ilg.Emit(OpCodes.Brfalse, get_out);
//
如果为false跳转至get_out
//
调用设置函数
ilg.Emit(OpCodes.Ldarg_0);
ilg.Emit(OpCodes.Ldfld, eleFB);
ilg.Emit(OpCodes.Ldstr, key);
ilg.Emit(OpCodes.Ldarg_1);
ilg.EmitCall(OpCodes.Callvirt, pi.GetSetMethod(),
null
);
//
调用后置函数
ilg.Emit(OpCodes.Ldarg_0);
ilg.Emit(OpCodes.Ldfld, postSetFB);
ilg.Emit(OpCodes.Ldstr, key);
ilg.Emit(OpCodes.Ldarg_1);
ilg.Emit(OpCodes.Call, postSet.GetType().GetMethod(
"
Invoke
"
));
ilg.MarkLabel(get_out);
ilg.Emit(OpCodes.Ret);
//
将该函数设置为property的set函数
propBuilder.SetSetMethod(setMethodBuilder);
}
}
}
通信人家园 (https://www.txrjy.com/)
Powered by C114