65.9K
CodeProject 正在变化。 阅读更多。
Home

快速生成和使用动态类

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.64/5 (9投票s)

2010年9月16日

CPOL

4分钟阅读

viewsIcon

65439

downloadIcon

2811

快速展示如何创建和使用动态类型/类

引言

本文旨在快速展示如何创建和使用动态类型/类。

我所说的“动态类”是什么?

偶尔,我想动态地向一个类添加属性或方法,并生成一个新的 Type,使其能够被实例化并让应用程序的其他部分使用它。

新的趋势是将 UI 控件绑定到包含数据的类,并轻松地呈现数据。WPF 和 Silverlight 广泛使用这种绑定,通过采用 MVVM 模式,开发人员可以创建一个非常通用的 View (UI),然后以编程方式操作数据 (Model),然后将两者绑定并呈现数据(“轻松地”)。

什么时候使用“动态类”?

假设一个查询引擎,根据查询提供一个包含 5 列的表,而总共有 20 列存在——为什么是 5 列?——仅仅因为 20 列不适合在一个页面上显示。 例如:一个音乐 CD 数据库,它将作曲家、作品、指挥、管弦乐队和独奏家作为响应表呈现。数据库还包括制造商、序列号、录音年份、价格等等。

查询本身返回所有 20 列(Model),但代码只用 5 列填充要呈现的数据类(ViewModel),然后我将其绑定到视图并呈现它。DataGrid——开箱即用(WPF/Silverlight)——能够消化绑定的类并输出一个网格,该网格为绑定类中的每个 public 属性显示一列。

当用户查询价格低于 20 美元的 CD 时,显示一个额外的价格列会很好;如果查询是关于录音年份,等等,也是一样。

如果我可以动态地向要呈现的数据类添加属性,那就太好了,这样当我将其绑定到数据网格视图时,它会自动为我生成所需的视图——“动态类”。

如何创建动态类?

实际上,我创建了一个动态类型并实例化它。

如何创建动态类型?

以下代码展示了如何使用反射来创建一个新的类型。这里有几点值得注意:

  • 我创建了一个从现有类型派生的类型,因为这将使在后面的代码中引用新类型更容易。
  • 即使我不使用新类型的名称,我也必须提供一个唯一的名称。
  • 验证该属性是否已存在。

以下方法接受 2 个名称(新类型的名称、新属性的名称)和 2 个类型(新属性的类型、现有基类型),并完成生成带有请求属性的(基)派生类型所需的所有繁重工作。

public static Type CreateMyNewType
   (string newTypeName, string propertyName, Type propertyType, Type baseClassType)
{
    // create a dynamic assembly and module 
    AssemblyBuilder assemblyBldr =
    Thread.GetDomain().DefineDynamicAssembly(new AssemblyName("tmpAssembly"), 
	AssemblyBuilderAccess.Run);
    ModuleBuilder moduleBldr = assemblyBldr.DefineDynamicModule("tmpModule");

    // create a new type builder
    TypeBuilder typeBldr = moduleBldr.DefineType
	(newTypeName, TypeAttributes.Public | TypeAttributes.Class, baseClassType);

    // Generate a private field for the property
    FieldBuilder fldBldr = typeBldr.DefineField
	("_" + propertyName, propertyType, FieldAttributes.Private);
    // Generate a public property
    PropertyBuilder prptyBldr = 
                typeBldr.DefineProperty(propertyName, 
		PropertyAttributes.None, propertyType, new Type[] { propertyType });
    // The property set and property get methods need the following attributes:
    MethodAttributes GetSetAttr = MethodAttributes.Public | MethodAttributes.HideBySig;
    // Define the "get" accessor method for newly created private field.
    MethodBuilder currGetPropMthdBldr = 
                typeBldr.DefineMethod("get_value", GetSetAttr, propertyType, null);

    // Intermediate Language stuff... as per Microsoft
    ILGenerator currGetIL = currGetPropMthdBldr.GetILGenerator();
    currGetIL.Emit(OpCodes.Ldarg_0);
    currGetIL.Emit(OpCodes.Ldfld, fldBldr);
    currGetIL.Emit(OpCodes.Ret);

    // Define the "set" accessor method for the newly created private field.
    MethodBuilder currSetPropMthdBldr = typeBldr.DefineMethod
	("set_value", GetSetAttr, null, new Type[] { propertyType });

    // More Intermediate Language stuff...
    ILGenerator currSetIL = currSetPropMthdBldr.GetILGenerator();
    currSetIL.Emit(OpCodes.Ldarg_0);
    currSetIL.Emit(OpCodes.Ldarg_1);
    currSetIL.Emit(OpCodes.Stfld, fldBldr);
    currSetIL.Emit(OpCodes.Ret);
        // Assign the two methods created above to the PropertyBuilder's Set and Get
    prptyBldr.SetGetMethod(currGetPropMthdBldr);
    prptyBldr.SetSetMethod(currSetPropMthdBldr);
        // Generate (and deliver) my type
    return typeBldr.CreateType();
}

如何使用动态类?

让这个新的动态类型从现有类型派生的好处之一是,我可以使用现有类型来保存动态类型,或者使用“var”关键字来定义变量。

但是这样做不会让我利用新属性:编译器会报错,因为编译器不知道新类型。因此,我可以做的一件事是使用新关键字“dynamic”声明保存新类型的变量,从而使编译器更易于接受。这样做将检查推迟到运行时,此时新类型具有请求的属性。

Class NotDynamic
{
    public string Composer {get; set;}
    public string Composition {get; set;}
}
Type  dynamicType = CreateMyNewType(“newTypeName”, 
	“Cost”, typeof(decimal), typeof(NotDynamic));
//use this variable for existing properties
NotDynamic newClassBase =  Activator.CreateInstance(dynamicType);
newClass.Composer = “Beethoven”;
//use this variable for dynamically created properties.
dynamic newClass = newClassBase;
newClass.Cost = 19.50;

如何让它变得更好?

为了使其更灵活,我需要不止一个属性,并且还能够动态地填充动态生成的属性。另一个改进可能是创建一些泛型方法。

为此,我引入了一个包含字符串-类型键值对的字典(属性名称、属性类型)。该字典由查询引擎填充(当用户选择条件时)。相同的字典被传递给填充动态类的代码,以便它知道要填充哪些属性。为了使这个方案有效,必须保持一定的命名约定。

public static Type CreateMyNewType
    (string newTypeName, Dictionary<string> dict, Type baseClassType)
        {
            bool noNewProperties = true;
            // create a dynamic assembly and module 
            AssemblyBuilder assemblyBldr = Thread.GetDomain().DefineDynamicAssembly
			(new AssemblyName("tmpAssembly"), AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBldr = assemblyBldr.DefineDynamicModule("tmpModule");

            // create a new type builder
            TypeBuilder typeBldr = moduleBldr.DefineType
	    (newTypeName, TypeAttributes.Public | TypeAttributes.Class, baseClassType);

            // Loop over the attributes that will be used as the 
            // properties names in my new type
            string propertyName = null;
            Type propertyType = null;
            var baseClassObj = Activator.CreateInstance(baseClassType);
            foreach (var word in dict)
            {
                propertyName = word.Key;
                propertyType = word.Value;

                //is it already in the base class?
                var src_pi = baseClassObj.GetType().GetProperty(propertyName);
                if (src_pi != null)
                {
                    continue;
                }

                // Generate a private field for the property
                FieldBuilder fldBldr = typeBldr.DefineField
		("_" + propertyName, propertyType, FieldAttributes.Private);
                // Generate a public property
                PropertyBuilder prptyBldr = typeBldr.DefineProperty
			(propertyName, PropertyAttributes.None, propertyType,
                            new Type[] {propertyType});
                // The property set and property get methods need the 
                // following attributes:
                MethodAttributes GetSetAttr = MethodAttributes.Public | 
						MethodAttributes.HideBySig;
                // Define the "get" accessor method for newly created private field.
                MethodBuilder currGetPropMthdBldr = typeBldr.DefineMethod
				("get_value", GetSetAttr, propertyType, null);

                // Intermediate Language stuff... as per Microsoft
                ILGenerator currGetIL = currGetPropMthdBldr.GetILGenerator();
                currGetIL.Emit(OpCodes.Ldarg_0);
                currGetIL.Emit(OpCodes.Ldfld, fldBldr);
                currGetIL.Emit(OpCodes.Ret);

                // Define the "set" accessor method for the newly created private field.
                MethodBuilder currSetPropMthdBldr = typeBldr.DefineMethod
			("set_value", GetSetAttr, null, new Type[] {propertyType});

                // More Intermediate Language stuff...
                ILGenerator currSetIL = currSetPropMthdBldr.GetILGenerator();
                currSetIL.Emit(OpCodes.Ldarg_0);
                currSetIL.Emit(OpCodes.Ldarg_1);
                currSetIL.Emit(OpCodes.Stfld, fldBldr);
                currSetIL.Emit(OpCodes.Ret);

                // Assign the two methods created above to the 
                // PropertyBuilder's Set and Get
                prptyBldr.SetGetMethod(currGetPropMthdBldr);
                prptyBldr.SetSetMethod(currSetPropMthdBldr);
                noNewProperties = false; //I added at least one property
            }

            if (noNewProperties == true)
            {
                return baseClassType; //deliver the base class
            }
            // Generate (and deliver) my type
            return typeBldr.CreateType();
        }

以下代码说明了如何使用上述方法以及如何使用相同的字典来填充新的类型/类。

protected Dictionary<string> ExtendedColumnsDict = null;
        
        public void start(XElement dynamicQuery)
        {
            ExtendedColumnsDict = ExtractFromXelement(dynamicQuery);
        }
        
        protected override ClassicP CreateClassicP
		(dynamic src, Dictionary<string> extendedColumnsDict)
        {
            //create the dynamic type & class based on the dictionary - 
            //hold the extended class in a variable of the base-type
            ClassicP classicEx = DynamicFactory.CreateClass
			<classicp>(GetExtendedType<classicp>(extendedColumnsDict));
            //fill in the base-type's properties
            classicEx.Composer = src.ComposerName;
            classicEx.Composition = src.CompositionName;
            classicEx.Orquestra = src.Orquestra;
            classicEx.Conductor = src.Conductor;
            classicEx.SubCategory = src.SubCategory;
            classicEx.Maker = src.Make;
            classicEx.Details = src;
            classicEx.Id = src.RecordingId;
            classicEx.CdBoxId = src.CD_BoxId;
            //fill in the dynamically created properties
            SetExtendedProperties(classicEx as dynamic, src, extendedColumnsDict);
            return classicEx;
        }

        //generic method that will extend dynamically the type T. 
        //keeping a global variable that holds the dynamic type
        //makes sure i create type only once 
        //(the class on the other hand has to be instantiated for each data-row
        protected Type GetExtendedType<t>
	(Dictionary<string> extendedColumnsDict) where T : class
        {
            if (ExtendedType == null)
            {
                ExtendedType = DynamicFactory.ExtendTheType<t>(ExtendedColumnsDict);
            }
            return ExtendedType;
        }
        
        //generic method that enumerates the dictionary and 
        //populate the dynamic class with values from the source
        //class that contains all the 20 columns.
        //there is an assumption here that the newly created properties 
        //have the same name as the original ones
        //in the source class.
        //the dynamic class (destination) is passed-in using the 
        //keyword dynamic in order defer the GetType()
        //operation until runtime when the dynamic type is available.
        protected void SetExtendedProperties
	<t>(dynamic dest, T src, Dictionary<string,> extendedPropsDict)
        {
            foreach (var word in extendedPropsDict)
            {
                var src_pi = src.GetType().GetProperty(word.Key);
                var dest_pi = dest.GetType().GetProperty(word.Key) as PropertyInfo;
                var val = src_pi.GetValue(src, null);
                //format the data based on its type
                if (val is DateTime)
                {
                    dest_pi.SetValue(dest, ((DateTime) val).ToShortDateString(), null);
                }
                else if (val is decimal)
                {
                    dest_pi.SetValue(dest, ((decimal) val).ToString("C"), null);
                }
                else
                {
                    dest_pi.SetValue(dest, val, null);
                }
            }
        }

关于示例源代码的说明

该示例使用了 DevExpress 的免费 DataGrid。 Microsoft 的网格在使用动态类时表现不如预期,而 Telerik 和 DevExpress 控件按预期处理动态属性。

另一方面,由于某些原因,DevExpress 控件没有按预期响应 NotifyPropertyChanged 事件,因此我需要将 DataContext 作为提交按钮的一部分进行分配。 Microsoft 和 Telerik 控件都正确地响应了该事件。

历史

  • 2010 年 9 月 12 日 初稿
© . All rights reserved.