Friday, January 21, 2005
« It's all in the wording... | Main | Sql Server - Remote Directory Browsing »

On the last post, I had introduced the concept of creating an IDE in which types and their instances are generated dynamically.  For a full discussion on the rationale behind the application is in the previous post, along with how properties were designed and organized.  This follow-up discussion, however, is focused on taking the semi-abstract notion of what a property is and converting that into a real type at runtime and its associated instances.

Each of the dynamically created types within the system is similar to other, related types.  For this reason, it was a good idea to define a base class.  This enables us to create a collection or array that is strongly typed to contain instances of each of the disparate types.  I decided to create this type manually in the assembly, vs. creating it dynamically so that I could utilize the strong typing within my code.  I called this base type PropertySetBase.  For consistency purposes (as well as some other more motivating reasons that I'll discuss in another article), it was decided that this base class contain no properties except for a 'Name' property.  Now, I had a special case here in which the names were predefined and not assigned within the source .ascx file.  Were that not the case, I would not even consider having the Name property there...the base class would be nothing but a blank, abstract class.  My class resembles:

public abstract class PropertySetBase {
   private string _name;

   public PropertySetBase(string name) {
      _name = name;
   }

   [ParenthesizePropertyName(true)]
   [Description(“The control identifier.  This property is readonly.“)]
   public string Name {
      get { return _name; }
   }
}

At this point, it now became necessary to take the IToolPropertyDefn instances and convert them into actual, runtime classes.  One of the most powerful, yet perhaps under-utilized aspects of the .NET framework is the ability the runtime has to generate types dynamically via the classes contained within the System.Reflection.Emit namespaces.  Probably because doing so requires knowledge of Intermediate Language (IL).

In order to generate the classes at runtime, I created a builder class.  Recall that all of the properties for a given dynamic type are defined within the ToolPropertyDefns class as flyweight objects.  Therefore, my builder could, given a tool id, resolve to the set of properties pertaining to the type and (if it's not yet created) generate the type, create an instance, and return it.  Once the type is defined, the definition is cached in a Hashtable for fast access later on, when another instance of the same type is requested.  A snippet:

internal sealed class PropertySetBuilder {
   private static Hashtable _types = new Hashtable();

   internal static PropertySetBase Build(Tools toolId) {
     
// establish the property definitions array
      IToolPropertyDefn[] props = ToolPropertyDefns.GetDefn(toolId);

     
// check to see if the type has been defined previously, if not create it
      Type type = _types[toolId] as Type;
      if ( null == type ) {
         // ...logic here to create the type
      }
   }
}

Now, in order to create definitions for these types dynamically we use various classes within the System.Reflection.Emit namespace such as AssemblyBuilder, ModuleBuilder, TypeBuilder, et al.  This functionality I decided to isolate into some private helper methods, additionally, I had to define the instances privately in the PropertySetBuilder class.

private AssemblyBuilder _asmBuilder;
private ModuleBuilder _modBuilder;

private static AssemblyBuilder getAsmBuilder() {
   if ( null == _asmBuilder ) {
      AppDomain domain = Thread.GetDomain();
      AssemblyName asmName = new AssemblyName();
      asmName.Name = “DynTypes“;
      _asmBuilder = domain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
   }
   return _asmBuilder;
}

private static ModuleBuilder getModBuilder() {
   if ( null == _modBuilder ) {
      AssemblyBuilder asmBuilder = getAsmBuilder();
      _modBuilder = asmBuilder.DefineDynamicModule(“DynTypeModule“);
   }
   return _modBuilder;
}

private static TypeBuilder getTypeBuilder(string name) {
   ModuleBuilder modBuilder = getModBuilder();
   return modBuilder.DefineType(name + “PropertySet“, TypeAttributes.Class | TypeAttributes.Public, typeof(PropertySetBase));
}

These methods will create a new AppDomain, with a dynamically generated assembly, with a dynamic module into which the types will be defined.  The assembly is given permission enough to run but cannot be persisted to disc.  Additionally, the TypeBuilder defines that the types it creates will be public classes that derive from PropertySetBase.

Once this framework is established, we can go through the motions of taking our IToolPropertyDefn instances and converting them into actual, runtime properties.  Therefore, let's take our first code example above and flesh-out the if {...} block.

if ( null == type ) {
   TypeBuilder tb = getTypeBuilder(toolId.ToString());
   createCtor(tb);

   if ( null != props && props.Length > 0 ) {
      foreach ( IToolPropertyDefn defn in props ) {
         addProperty(tb, defn);
      }
   }
   type = tb.CreateType();

   // cache off the newly created type
   _types[toolId] = type;
}

The following procedures establish how to create the class constructor (so instances can be created) as well as the property/method implementation:

private static Type[] _ctorParamsString = new Type[] { typeof(string) };

private static void createCtor(TypeBuilder tb) {
   ConstructorInfo baseCtor = (typeof(PropertySetBase)).GetConstructor(_ctorParamsString);
   ConstructorBuilder ctor = tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, _ctorParamsString);
   ILGenerator il = ctor.GetILGenerator();
   il.Emit(OpCodes.Ldarg_0);         // push the 'this' pointer
   il.Emit(OpCodes.Ldarg_1);         // push the string param
   il.Emit(OpCodes.Call, baseCtor);  // call the base ctor
   il.Emit(OpCodes.Ret);             // return - if omitted we get an InvalidProgramException
}

private static void addProperty(TypeBuilder tb, IToolPropertyDefn defn) {
   FieldBuilder fb = tb.DefineField(defn.PrivateName, defn.DataType, FieldAttributes.Private);
   PropertyBuilder pb = tb.DefineProperty(defn.Name, PropertyAttributes.HasDefault, defn.DataType, Type.EmptyTypes);

   // establish the description attribute
   ConstructorInfo descCi = typeof(DescriptionAttribute).GetConstructor(_ctorParamsString);
   CustomAttributeBuilder descCab = new CustomAttributeBuilder(descCi, new object[] { defn.Description });
   pb.SetCustomAttribute(descCab);

   // establish the default value attribute
   ConstructorInfo defCi = typeof(DefaultValueAttribute).GetConstructor(new Type[] { defn.DataType });
   CustomAttributeBuilder defCab = new CustomAttributeBuilder(defCi, new object[] { defn.DefaultValue });
   pb.SetCustomAttribute(defCab);

   // define the getter and setter methods
   ILGenerator il;
   if ( ( defn.Mode & PropertyMode.Read ) > 0 ) {
      MethodBuilder getter = tb.DefineMethod(“get“ + defn.Name, MethodAttributes.Public, defn.DataType, Type.EmptyTypes);
      il = getter.GetILGenerator();
      il.Emit(OpCodes.Ldarg_0);
      il.Emit(OpCodes.Ldfld, fb);  // load the private field into the return value
      il.Emit(OpCodes.Ret);
      pb.SetGetMethod(getter);
   }

   if ( ( defn.Mode & PropertyMode.Write ) > 0 ) {
      MethodBuilder setter = tb.DefineMethod(“set“ + defn.Name, MethodAttributes.Public, null, new Type[] { defn.DataType });
      il = setter.GetILGenerator();
      il.Emit(OpCodes.Ldarg_0);
      il.Emit(OpCodes.Ldarg_1);
      il.Emit(OpCodes.Stfld, fb);  // assign the string to the private field
      il.Emit(OpCodes.Ret);
      pb.SetSetMethod(setter);
   }
}

That's about all there is to it - at least for a really simple property with an underlying field.  Data gets assigned to and read from the underlying field.  The last remaining step is to create the instance from the type and return it from the Build() method created above.  The following code takes the Type as defined, calls the constructor on it, and returns the instance:

internal static PropertySetBase Build(Tools toolId) {
  
// ... code to create the type as described above
  
   ConstructorInfo ci = type.GetConstructor(_ctorParamsString);
   PropertySetBase ret = ci.Invoke(new object[] { ToolHelper.GetToolName(toolId) }) as PropertySetBase;

   // iterate through the properties and assign the default values
   // note, we cannot necessarily assign the value via the property because not all properties allow
   // writing (e.g. they are readonly).  therefore, reflect to the underlying field we created
   // and assign the value directly to it.
   if ( null != props && props.Length > 0 ) {
      foreach ( IToolPropertyDefn defn in props ) {
         FieldInfo fi = type.GetField(defn.PrivateName, BindingFlags.NonPublic | BindingFlags.Instance);
         fi.SetValue(ret, defn.DefaultValue);
      }
   }

   return ret;

}

As you can see, it's pretty easy and straightforward.  With this object suite and structure, I'm not working with an extremely diverse range of objects, but it's working remarkably well.  In the next discussion, we'll talk about how to programmatically assign values to the properties and how to reverse engineer the instances into their ASP.NET code form reliably.

Friday, January 21, 2005 9:39:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback