2008-09-16 3 views
2

Примечание. Я использую .Net 1.1, хотя я не полностью против ответа, использующего более высокие версии.PropertyGrid, DefaultValueAttribute, динамический объект и перечисления

Я показываю некоторые динамически сгенерированные объекты в 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(); 

Теперь о том, как создается тип. Это в значительной степени основано на примере кода here. По существу, подумайте о 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; однако поле затем выделено жирным шрифтом в PropertyGrid, потому что оно не соответствует значению по умолчанию.

Может ли кто-нибудь понять, почему я не могу получить атрибут DefaultValueAttribute, когда я установил значение по умолчанию для моего сгенерированного перечисления? Как вы, наверное, догадались, я до сих пор новичок в Reflection, поэтому для меня это совсем не ново.

Спасибо.

Обновление: В ответ на alabamasucks.blogspot использование функции ShouldSerialize, безусловно, решит мою проблему. Я смог создать метод, используя обычный класс; однако я не уверен, как это сделать для сгенерированного типа. Из того, что я могу понять, мне нужно будет использовать MethodBuilder и сгенерировать IL, чтобы проверить, совпадает ли поле с значением по умолчанию. Звучит достаточно просто. Я хочу, чтобы представить это в IL код:

public bool ShouldSerializepropertyName() 
{ 
    return (field != val); 
} 

Я был в состоянии получить код IL, используя ildasm.exe из кода, но у меня есть несколько вопросов. Как использовать переменную val в коде IL? В моем примере я использовал Int со значением 0.

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 имеет другую команду загрузки для каждого типа.В настоящее время я использую ints, doubles, strings и перечисления, поэтому код должен быть адаптивным в зависимости от типа.

Есть ли у кого-нибудь идеи, как это сделать? Или я направляюсь в неправильном направлении?

ответ

3

Я не уверен, как заставить атрибут работать, но есть и другой вариант, который может быть проще.

В дополнение к проверке атрибута DefaultValueAttribute PropertyGrid также использует отражение для поиска метода с именем «ShouldSerializeProperty Name», где [Имя свойства] - это имя соответствующего свойства. Этот метод должен возвращать логическое значение, которое истинно, если для свойства установлено значение, отличное от значения по умолчанию, и false в противном случае. Вероятно, вам будет легче использовать отражение, чтобы создать метод, который возвращает правильное значение, а затем исправить атрибут.

+0

ShouldSerialize [Property Name] является интересным лакомым кусочком. Это в вики-страницах «скрытых функций»? –

+0

Он задокументирован в MSDN по адресу http://msdn.microsoft.com/en-us/library/53b8022e (VS.71,classic).aspx. Очевидно, что это не позволяет легко обнаружить самостоятельно, если вы не пытаетесь найти подходящую вещь. –

2

Вам следует попробовать его с помощью DefaultValueAttribute, взяв параметр String и Type, передав значение перечисления строки (val.ToString) и тип вашего перечисления.