views:

1824

answers:

2

Note: I am using .Net 1.1, although I am not completely against answer that use higher versions.

I am displaying some dynamically generated objects in a PropertyGrid. These objects have numeric, text, and enumeration properties. Currently I am having issues setting the default value for the enumerations so that they don't always appear bold in the list. The enumerations themselves are also dynamically generated and appear to work fine with the exception of the default value.

First, I would like to show how I generate the enumerations in the case that it is causing the error. The first line uses a custom class to query the database. Simply replace this line with a DataAdapter or your preferred method of filling a DataSet with Database values. I am using the string values in column 1 to create my enumeration.

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();

Now on to how the type is created. This is largely based of the sample code provided here. Essentially, think of pFeature as a database row. We loop through the columns and use the column name as the new property name and use the column value as the default value; that is the goal at least.

// 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();

Finally, we use that type to create an instance of it and load in the default values so we can later display it using the 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;

However, this causes an error when we try to get the default value for an enumeration. The DefaultValueAttribute dva does not get set and thus causes an exception when we try to use it.

If we change this code segment:

 if(type.IsEnum)
 {
  object o = Enum.Parse(type, val.ToString(), true);
  myCABuilder = new CustomAttributeBuilder(
   classCtorInfo,
   new object[] { o });
 }

to this:

 if(type.IsEnum)
 {
  myCABuilder = new CustomAttributeBuilder(
   classCtorInfo,
   new object[] { 0 });
 }

There are no problems getting the DefaultValueAttribute dva; however, the field is then bolded in the PropertiesGrid because it does not match the default value.

Can anyone figure out why I cannot get the DefaultValueAttribute when I set the default value to my generated enumeration? As you can probably guess, I am still new to Reflection, so this is all pretty new to me.

Thanks.

Update: In response to alabamasucks.blogspot, using ShouldSerialize would certainly solve my problem. I was able to create the method using a normal class; however, I am unsure on how to do this for a generated type. From what I can figure out, I would need to use MethodBuilder and generate the IL to check if the field is equal to the default value. Sounds simple enough. I want to represent this in IL code:

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

I was able to get the IL code using ildasm.exe from similar code, but I have a couple of questions. How do I use the val variable in the IL code? In my example, I used a int with the value of 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

This certainly can get tricky because IL has a different load command for each type. Currently, I use ints, doubles, strings, and enumerations, so the code will have to be adaptive based on the type.

Does anyone have an idea how to do this? Or am I heading in the wrong direction?

+2  A: 

I'm not sure how to get the attribute to work, but there is another option that may be easier.

In addition to checking for the DefaultValueAttribute, the PropertyGrid also uses reflection to look for a method named "ShouldSerializeProperty Name", where [Property Name] is the name of the property in question. This method should return a boolean that is true if the property is set to a non-default value and false otherwise. It would probably be easier for you to use reflection to create a method that returns the correct value then to fix up the attribute.

alabamasucks
ShouldSerialize[Property Name] is an intersting tidbit. Is this in the "hidden features" wiki?
Neil N
It's documented on MSDN at http://msdn.microsoft.com/en-us/library/53b8022e(VS.71,classic).aspx. Obviously, that doens't makes it easy to discover on your own unless you happen to search for the right thing.
alabamasucks
+1  A: 

You should try it with the DefaultValueAttribute taking a String and a Type parameter, passing in the string enum value (val.ToString), and the type of your enum.

csgero