Introduction
I like using the PropertyGrid
as a way for the user to specify various options when using my programs, but I got tired of constantly having to change the class that stored those options. I wanted an easy way to change what properties the grid would show without me having to change anything in a class structure.
Since the PropertyGrid
automatically shows the properties of an object and the property's value, the idea came to me that I could dynamically create a type at runtime using the classes contained in Reflection.Emit
, so that's what I did.
Implementation
I needed a good way to store all the information needed to create each property. I did this with the Setting
object. Setting
objects store the property name, the initial value of the property, a description and category to be shown in the PropertyGrid
, and an EventHandler
so you can catch when the value of the property changes.
Secondly, I wanted to create an easy storage device for my Setting
objects. This is what the Settings
object is for. It is essentially a wrapper around a HashTable
that does the casting for you. The Settings
object is what makes the CustomPropertyGrid
come to life.
By setting the Settings
property of the CustomPropertyGrid
object to your own Settings
object, an internal type will be created having the properties that were specified as Setting
objects. The type will have an internal HashTable
to make the emitted code easy to write. For instance, if you have the following code:
Settings settings = new Settings();
settings["GridColor"]=new Setting(
myGrid.Color,
"The color of the lines used to draw the grid",
"Grid");
the type that is created will have a property written like so:
[Description("The color of the lines used to draw the grid")]
[Category("Grid")]
public Color GridColor
{
get{return (Color)myHash["GridColor"];}
set{myHash["GridColor"]=value;}
}
Emitting the bytecode
Doing this was fairly tricky, as this was a learning experience for me. The procedure consisted of writing a small class having the property signature I desired and using ildasm and peverify to examine the bytecodes. From that, I gathered enough information to emit code that works. Working with objects is easy, as there is a bytecode to load and store objects no problem, but value types are another matter. To store a value type, you must box it, and then you can treat it like an object. To retrieve a value type, you must unbox it (naturally) and then you have to load it based on what type it is. If it is a numeric type, there are different bytecodes to load int
s, char
s, double
s, float
s, etc. If you have a value type that is not a numeric type (like a Color
, or an enum
) then it's just a matter of loading the type of the object. Figuring out how to load the value types was the hardest part of this project for me to figure out.
private void emitProperty(
TypeBuilder tb,
FieldBuilder hash,
Setting s,
string name)
{
PropertyBuilder pb = tb.DefineProperty(
name,
PropertyAttributes.None,
s.Value.GetType(),
new Type[] {});
Type objType = s.Value.GetType();
MethodBuilder getMethod = tb.DefineMethod(
"get_"+name,
MethodAttributes.Public,
objType,
new Type[]{});
ILGenerator ilg = getMethod.GetILGenerator();
ilg.DeclareLocal(objType);
ilg.Emit(OpCodes.Ldarg_0);
ilg.Emit(OpCodes.Ldfld,hash);
ilg.Emit(OpCodes.Ldstr,name);
ilg.EmitCall(OpCodes.Callvirt,
typeof(Hashtable).GetMethod("get_Item"),
null);
if(objType.IsValueType)
{
ilg.Emit(OpCodes.Unbox,objType);
if(typeHash[objType]!=null)
ilg.Emit((OpCode)typeHash[objType]);
else ilg.Emit(OpCodes.Ldobj,objType);
}
else
ilg.Emit(OpCodes.Castclass,objType);
ilg.Emit(OpCodes.Stloc_0);
ilg.Emit(OpCodes.Br_S,(byte)0);
ilg.Emit(OpCodes.Ldloc_0);
ilg.Emit(OpCodes.Ret);
MethodBuilder setMethod = tb.DefineMethod(
"set_"+name,
MethodAttributes.Public,
null,
new Type[]{objType});
ilg = setMethod.GetILGenerator();
ilg.Emit(OpCodes.Ldarg_0);
ilg.Emit(OpCodes.Ldfld,hash);
ilg.Emit(OpCodes.Ldstr,name);
ilg.Emit(OpCodes.Ldarg_1);
if(objType.IsValueType)
ilg.Emit(OpCodes.Box,objType);
ilg.EmitCall(OpCodes.Callvirt,
typeof(Hashtable).GetMethod("set_Item"),
null);
ilg.Emit(OpCodes.Ret);
pb.SetGetMethod(getMethod);
pb.SetSetMethod(setMethod);
}
Once all the properties are emitted, I use reflection to create an object of that type and set the SelectedObject
property to this new object. When the form is shown, all the properties and their values are in a categorized fashion.
There is another custom property in the CustomPropertyGrid
called InstantUpdate
. By default, InstantUpdate
is on and what it does is, when a value is changed in the PropertyGrid
, an event is fired, the CustomPropertyGrid
catches the event and passes it along to the event in the specific Setting
object. The demo program included shows three different ways of handling these events. If you do not wish these events to be fired, just set the InstantUpdate
property to false
. Also, if you do not construct a Setting
with an EventHandler
, the event will not be fired regardless of the value of InstantUpdate
.
History
Update: Minor text changes/grammar.