动态类型与PropertyGrid

来源:互联网 发布:java培训完好找工作吗 编辑:程序博客网 时间:2024/05/12 08:49
     背景:
   我在一个项目中遇到这样一件事。一开始用户对要编辑的数据
  没有多少要求,于是我用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)
  
  
  
  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)
  
  
  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);
   }
   }
  }
  
  
   这段代码有很详细的注释,我就不废话了。
  代码下载:
  http://files.cnblogs.com/sillyemperor/demo.zip
  后话:
   这里提供的代码,可以随意修改使用。只是希望各位将自己的心得,收获和困扰放到这里与大家分享。
原创粉丝点击