PropertyGrid,DefaultValueAttribute,动态对象和枚举
注意:尽管我并不完全反对使用更高版本的答案,但我正在使用.Net 1.1.
我正在PropertyGrid中显示一些动态生成的对象。这些对象具有数字,文本和枚举属性。目前,我在设置枚举的默认值时遇到问题,因此它们在列表中不会总是显示为粗体。枚举本身也是动态生成的,并且除了默认值外,似乎工作正常。
首先,我想说明在引起错误的情况下如何生成枚举。第一行使用自定义类来查询数据库。只需用DataAdapter替换此行,或者使用数据库值填充DataSet的首选方法即可。我正在使用第1列中的字符串值来创建我的枚举。
private Type GetNewObjectType(string field, ModuleBuilder module, DatabaseAccess da) //Query the database. System.Data.DataSet ds = da.QueryDB(query); EnumBuilder eb = module.DefineEnum(field, TypeAttributes.Public, typeof(int)); for(int i = 0; i < ds.Tables[0].Rows.Count; i++) { if(ds.Tables[0].Rows[i][1] != DBNull.Value) { string text = Convert.ToString(ds.Tables[0].Rows[i][1]); eb.DefineLiteral(text, i); } } return eb.CreateType();
现在介绍如何创建类型。这很大程度上基于此处提供的示例代码。本质上,将pFeature视为数据库行。我们遍历各列,并使用列名作为新的属性名,并使用列值作为默认值;至少这是目标。
// create a dynamic assembly and module AssemblyName assemblyName = new AssemblyName(); assemblyName.Name = "tmpAssembly"; AssemblyBuilder assemblyBuilder = System.Threading.Thread.GetDomain().DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); ModuleBuilder module = assemblyBuilder.DefineDynamicModule("tmpModule"); // create a new type builder TypeBuilder typeBuilder = module.DefineType("BindableRowCellCollection", TypeAttributes.Public | TypeAttributes.Class); // Loop over the attributes that will be used as the properties names in out new type for(int i = 0; i < pFeature.Fields.FieldCount; i++) { string propertyName = pFeature.Fields.get_Field(i).Name; object val = pFeature.get_Value(i); Type type = GetNewObjectType(propertyName, module, da); // Generate a private field FieldBuilder field = typeBuilder.DefineField("_" + propertyName, type, FieldAttributes.Private); // Generate a public property PropertyBuilder property = typeBuilder.DefineProperty(propertyName, PropertyAttributes.None, type, new Type[0]); //Create the custom attribute to set the description. Type[] ctorParams = new Type[] { typeof(string) }; ConstructorInfo classCtorInfo = typeof(DescriptionAttribute).GetConstructor(ctorParams); CustomAttributeBuilder myCABuilder = new CustomAttributeBuilder( classCtorInfo, new object[] { "This is the long description of this property." }); property.SetCustomAttribute(myCABuilder); //Set the default value. ctorParams = new Type[] { type }; classCtorInfo = typeof(DefaultValueAttribute).GetConstructor(ctorParams); if(type.IsEnum) { //val contains the text version of the enum. Parse it to the enumeration value. object o = Enum.Parse(type, val.ToString(), true); myCABuilder = new CustomAttributeBuilder( classCtorInfo, new object[] { o }); } else { myCABuilder = new CustomAttributeBuilder( classCtorInfo, new object[] { val }); } property.SetCustomAttribute(myCABuilder); // The property set and property get methods require a special set of attributes: MethodAttributes GetSetAttr = MethodAttributes.Public | MethodAttributes.HideBySig; // Define the "get" accessor method for current private field. MethodBuilder currGetPropMthdBldr = typeBuilder.DefineMethod("get_value", GetSetAttr, type, Type.EmptyTypes); // Intermediate Language stuff... ILGenerator currGetIL = currGetPropMthdBldr.GetILGenerator(); currGetIL.Emit(OpCodes.Ldarg_0); currGetIL.Emit(OpCodes.Ldfld, field); currGetIL.Emit(OpCodes.Ret); // Define the "set" accessor method for current private field. MethodBuilder currSetPropMthdBldr = typeBuilder.DefineMethod("set_value", GetSetAttr, null, new Type[] { type }); // Again some Intermediate Language stuff... ILGenerator currSetIL = currSetPropMthdBldr.GetILGenerator(); currSetIL.Emit(OpCodes.Ldarg_0); currSetIL.Emit(OpCodes.Ldarg_1); currSetIL.Emit(OpCodes.Stfld, field); currSetIL.Emit(OpCodes.Ret); // Last, we must map the two methods created above to our PropertyBuilder to // their corresponding behaviors, "get" and "set" respectively. property.SetGetMethod(currGetPropMthdBldr); property.SetSetMethod(currSetPropMthdBldr); } // Generate our type Type generatedType = typeBuilder.CreateType();
最后,我们使用该类型创建它的实例并加载默认值,以便以后可以使用PropertiesGrid显示它。
// Now we have our type. Let's create an instance from it: object generatedObject = Activator.CreateInstance(generatedType); // Loop over all the generated properties, and assign the default values PropertyInfo[] properties = generatedType.GetProperties(); PropertyDescriptorCollection props = TypeDescriptor.GetProperties(generatedType); for(int i = 0; i < properties.Length; i++) { string field = properties[i].Name; DefaultValueAttribute dva = (DefaultValueAttribute)props[field].Attributes[typeof(DefaultValueAttribute)]; object o = dva.Value; Type pType = properties[i].PropertyType; if(pType.IsEnum) { o = Enum.Parse(pType, o.ToString(), true); } else { o = Convert.ChangeType(o, pType); } properties[i].SetValue(generatedObject, o, null); } return generatedObject;
但是,当我们尝试获取枚举的默认值时,这会导致错误。未设置DefaultValueAttribute dva,因此在尝试使用它时会导致异常。
如果我们更改此代码段:
if(type.IsEnum) { object o = Enum.Parse(type, val.ToString(), true); myCABuilder = new CustomAttributeBuilder( classCtorInfo, new object[] { o }); }
对此:
if(type.IsEnum) { myCABuilder = new CustomAttributeBuilder( classCtorInfo, new object[] { 0 }); }
获取DefaultValueAttribute dva没问题;但是,该字段然后在PropertiesGrid中以粗体显示,因为它与默认值不匹配。
任何人都可以弄清楚为什么在将默认值设置为生成的枚举时无法获得DefaultValueAttribute吗?我们可能会猜到,我仍然是反射的新手,所以这对我来说是相当新的。
谢谢。
更新:作为对alabamasucks.blogspot的回应,使用ShouldSerialize当然可以解决我的问题。我能够使用普通的类创建方法;但是,我不确定如何对生成的类型执行此操作。据我所知,我将需要使用MethodBuilder并生成IL以检查该字段是否等于默认值。听起来很简单。我想用IL代码表示:
public bool ShouldSerializepropertyName() { return (field != val); }
我可以使用ildasm.exe从类似的代码中获取IL代码,但是我有几个问题。如何在IL代码中使用val变量?在我的示例中,我使用了一个值为0的int。
IL_0000: ldc.i4.s 0 IL_0002: stloc.0 IL_0003: ldloc.0 IL_0004: ldarg.0 IL_0005: ldfld int32 TestNamespace.TestClass::field IL_000a: ceq IL_000c: ldc.i4.0 IL_000d: ceq IL_000f: stloc.1 IL_0010: br.s IL_0012 IL_0012: ldloc.1 IL_0013: ret
这肯定会变得棘手,因为IL对于每种类型都有不同的加载命令。当前,我使用整数,双精度型,字符串和枚举,因此代码必须根据类型自适应。
有谁知道如何做到这一点?还是我朝着错误的方向前进?
解决方案
回答
我不确定如何使该属性起作用,但是还有另一种选择可能会更容易。
除了检查DefaultValueAttribute,PropertyGrid还使用反射来查找名为" ShouldSerializeProperty Name"的方法,其中[Property Name]是所讨论属性的名称。如果属性设置为非默认值,则此方法应返回布尔值true,否则返回false。使用反射创建返回正确值然后修复属性的方法可能会更容易。
回答
我们应该尝试使用带有字符串和Type参数的DefaultValueAttribute,并传入字符串枚举值(val.ToString)和枚举的类型。