PropertyGrid, DefaultValueAttribute, динамический объект и перечисления - PullRequest
2 голосов
/ 16 сентября 2008

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

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

Ответы [ 2 ]

3 голосов
/ 17 сентября 2008

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

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

2 голосов
/ 14 октября 2008

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

...