背景:
我在一个项目中遇到这样一件事。一开始用户对要编辑的数据
没有多少要求,于是我用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);
}
}
}
这段代码有很详细的注释,我就不废话了。
代码下载:demo.zip
后话:
这里提供的代码,可以随意修改使用。只是希望各位将自己的心得,收获和困扰放到这里与大家分享。