Class Templates

Bring the Power of Templates to Your .NET Applications with the CodeDOM Namespace

Adam J. Steinert

Code download available at:CodeDOM.exe(169 KB)

This article assumes you're familiar with C# and Reflection

Level of Difficulty123

SUMMARY

In the .NET Framework, the CodeDOM object model can represent code in a variety of languages. This article examines how source code templates written with the Framework's System.CodeDom and System.CodeDom.Compiler namespaces allow developers to create reusable boilerplate source code that can be shared between projects. Components designed via templates improve productivity and shorten development time.

Here C++-style classes and templates are simulated and code is generated in multiple languages through the creation of CodeDOM object graphs. Compiling object graphs and formatting output code are also explained.

Contents

Creating Object Graphs
Using the Object Graph
Generating Source Code
Formatting the Output
Compiling Object Graphs
Compiler Options
Class Templates and the CodeDOM
Creating the Application
Creating the Template
Conclusion

The CodeDOM is used in the Microsoft® .NET Framework to represent source code documents in a language-neutral manner. CodeDOM object graphs can be manipulated and used to generate source code and even compiled assemblies for any number of uses. The System.CodeDom namespaces contain upwards of 70 type definitions that represent characteristic entities of a typical high-level, object-oriented language such as C#, Visual Basic® .NET, Eiffel, or Java. The System.CodeDom.Compiler namespace defines a framework of abstract classes for processing CodeDOM object graphs, and any language that wants to support the CodeDOM may do so through the implementation of the CodeDomProvider base class.

The CodeDOM plays an important behind-the-scenes role in the .NET Framework and Visual Studio® .NET. It's also used by ASP.NET to generate executable code, and in fact it is necessary for a language to implement a CodeDomProvider in order to be used in ASP.NET pages. The CodeDOM is also used for Web Services Description Language proxy generation and in the designers and code wizards in Visual Studio .NET. A number of languages implement CodeDomProvider classes and thus can be used to process CodeDOM object graphs. As you might expect, C# and Visual Basic .NET implement this through the CSharpCodeProvider and VBCodeProvider classes defined in the Microsoft.CSharp and Microsoft.VisualBasic namespaces, respectively. Additionally, there is a JScriptCodeProvider in the Microsoft.JScript namespace, and Visual J#™ .NET exposes the VJSharpCodeProvider in the Microsoft.VJSharp namespace. In addition, a number of Microsoft .NET language partners have, or are in the process of implementing, CodeDomProvider classes as part of their language integration with .NET. Some of these include Eiffel from Interactive Software Engineering, ActivePERL, developed by ActiveState, and Fujitsu COBOL.

After a brief look into the CodeDOM, some practical uses for the object model become apparent. One that I find most beneficial—and which is the main focus of this article—is using the CodeDOM to create templates for common and reusable source code. This is not a new concept. A number of books, articles, language features, and code-generating wizards discuss the topic. Regardless, I feel compelled to extol the virtues and benefits of using the CodeDOM to this end. By taking some time to learn the relatively simple (albeit large and cumbersome) object model, it is possible to create templates using the CodeDOM. Additionally, because a number of languages have implemented CodeDomProviders, the templates can be used to generate code in these languages, extending the usefulness of your templates to other programmers who don't work in your language.

In this article, I will walk through the creation of a CodeDOM object graph that is a template for a simple Windows®-based application. My first of two C# example applications incorporates this code. I will discuss using the object graph to generate source and compiled code and even create and execute the application in memory, and in multiple languages. After covering the basics, my second example will explore using the CodeDOM to mimic C++-style class templates.

Creating Object Graphs

I'll begin by looking at the creation of an object graph template with the CodeDOM to describe some of its central classes and how they represent code. Creating your first object graph with the CodeDOM is akin to writing code in a new language. It is structured in such a way that many object-oriented concepts are represented. As such, to describe its use I'll start with a twist on the epitome of programming examples: Hello World. The HelloGen project in the code download (see the link at the top of this article) is the basis for this examination, and the TemplateConstructor class is responsible for creating the object graph.

When creating templates, it often helps to have an example of the code you are creating as a reference. Figure 1 is the complete C# source that will serve as a reference for the construction of the object graph for the application. There is nothing extraordinary about the application itself. It consists of two classes, HelloApplication, which instantiates and shows the form, and HelloWorldForm, which defines the Windows Form that will be displayed to the user. As you will see later, the template will be modified at run time to replace the TextBox txtSourceCode's Text property when the form is displayed.

Figure 1 Hello World Template Reference

namespace HelloWorld { using System; using System.Drawing; using System.Windows.Forms; public class HelloWorldForm : System.Windows.Forms.Form { private System.Windows.Forms.TextBox txtSourceCode; public HelloWorldForm() { this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); this.Text = "Hello, World!"; this.Size = new System.Drawing.Size(700, 350); txtSourceCode = new System.Windows.Forms.TextBox(); this.Controls.Add(txtSourceCode); txtSourceCode.Multiline = true; txtSourceCode.ScrollBars = ScrollBars.Both; txtSourceCode.Top = 10; txtSourceCode.Width = 10; txtSourceCode.Size = new System.Drawing.Size(690, 300); txtSourceCode.Anchor = (AnchorStyles.Bottom | (AnchorStyles.Top | (AnchorStyles.Left | AnchorStyles.Right))); txtSourceCode.Text = "[Little Recursive Box]"; } } public class HelloApplication { public static void Main() { HelloWorldForm hwApp; hwApp = new HelloWorldForm(); hwApp.ShowDialog(); } } }

The TemplateConstructor class is where the entire object graph for Hello World is created, beginning with the call to GenerateCCU:

public CodeCompileUnit GenerateCCU() { CodeCompileUnit hwCompileUnit = new CodeCompileUnit(); CodeNamespace hwNamespace = BuildNamespace(); CodeTypeDeclaration hwFormClass = BuildFormClass(); hwClassRef = hwFormClass; CodeTypeDeclaration hwAppClass = BuildAppClass(); hwCompileUnit.Namespaces.Add(hwNamespace); hwNamespace.Types.Add(hwFormClass); hwNamespace.Types.Add(hwAppClass); return hwCompileUnit; }

The CodeCompileUnit class that is exposed in the System.CodeDom namespace is at the root of the Hello World object graph. This class is representative of a project or an assembly, and hwCompileUnit represents the HelloWorld assembly. The CodeCompileUnit is the object that is used by code providers and code generators and all of the code included in a CodeCompileUnit compiles to a single assembly.

Three of the public properties that the CodeCompileUnit exposes are of particular importance. The Namespaces property holds a collection of CodeNamespace objects which, as you would expect, represent namespaces within the assembly. The Referenced Assemblies property is a string collection representing the names of assemblies that the CodeCompileUnit references, and the AssemblyCustomAttributes property is a CodeAttributeDeclarationCollection representing the attributes applied to the assembly.

Populating the CodeCompileUnit means adding the CodeNamespace objects that will compose the assembly to the Namespaces collection hwCompileUnit. As you have likely deciphered by looking at the previous code for the GenerateCCU function, the CodeTypeDeclaration class represents classes within a namespace. The hwFormClass and hwAppClass objects represent the HelloWorld and AppClass classes in Figure 1, respectively, and completing the basic structure of the Hello World application is simply a matter of defining these CodeTypeDeclaration objects and adding them to the CodeNamespace hwNamespace. In order to make this as clear as possible I have moved the code for creating each of these objects into separate functions.

The CodeNamespace object is trivial; creating and defining one takes no more than a few lines of code:

private CodeNamespace BuildNamespace() { CodeNamespace hwNamespace = new CodeNamespace(); hwNamespace.Name = "HelloWorld"; hwNamespace.Imports.Add(new CodeNamespaceImport("System")); hwNamespace.Imports.Add(new CodeNamespaceImport("System.Drawing")); hwNamespace.Imports.Add(new CodeNamespaceImport("System.Windows.Forms")); return hwNamespace; }

The BuildNamespace function creates and returns a representation of the HelloWorld namespace. The Imports collection is a collection of CodeNamespaceImport objects that represents the using statement in C# and the Imports statement in Visual Basic .NET. The Namespace property of the CodeNamespaceImport class is the only property of note, and it is set internally by the constructor in this example.

As I mentioned before, the CodeTypeDeclaration class is used to represent classes in the CodeDOM; however, it may also be used to represent enumerations, interfaces, and structs. The type you are representing is noted by setting the appropriate public property—IsClass, IsEnum, IsInterface, or IsStruct—to True. The BuildFormClass function is responsible for creating the CodeTypeDeclaration for the HelloWorld class, as shown in Figure 2.

Figure 2 Creating the HelloWorldForm Class

private CodeTypeDeclaration BuildFormClass() { CodeTypeDeclaration FormClass = new CodeTypeDeclaration(); CodeEntryPointMethod MainMethod = new CodeEntryPointMethod(); // Set up Class FormClass.Name = "HelloWorldForm"; FormClass.IsClass = true; FormClass.BaseTypes.Add(new CodeTypeReference( typeof(System.Windows.Forms.Form))); FormClass.Attributes = MemberAttributes.Public; // set up members FormClass.Members.Add(new CodeMemberField( typeof(System.Windows.Forms.TextBox), "txtSourceCode")); // set up constructor FormClass.Members.Add(BuildFormConstructor()); return FormClass; }

As you can see from the C# source in Figure 1, the HelloWorldForm class represents the visible element of the application. To represent it in the object graph, I create a new CodeTypeDeclaration to represent the class, giving it the name HelloWorldForm and setting its IsClass property to True. Since the HelloWorld class is a form that derives from System.Windows.Form, I add a CodeTypeReference object to the BaseTypes collection, which represents the base Form class.

BuildFormClass also demonstrates how to represent an object's type information in the CodeDOM. The CodeTypeReference class represents a scalar or array type in the CodeDOM and can be passed as an argument to most CodeDOM constructors and methods that require a type to be defined. There are four overloads for its constructor; the first two are for scalar datatypes, which take either a String or a System.Type argument. The safest way to go, of course, is to use the System.Type overload; however, this may not always be convenient or even possible depending on the type of code you are modeling. Regardless, it is recommended by the documentation (and is good coding practice) to use the fully qualified name of the data type you want to represent. The third and fourth overloads let you specify an array type and take either a String or a CodeTypeReference as parameters. The second parameter for both is an integer that represents the array rank. Consequently, most of the CodeDOM classes that take a CodeTypeReference as a parameter also have overloads that take a String or System.Type.

To complete the description of the HelloWorldForm class, I set the Attributes property. Attributes is a bit field that represents the access modifiers for the classes which are specified in the MemberAttributes enumeration, and thus multiple modifiers can be applied through the bitwise OR operator. Next, I need to add the declaration of the TextBox and the class constructor to the HelloWorldForm class through the Members collection of FormClass. This collection will contain the objects that derive from CodeTypeMember and represent entities that are declared within a Type, including constructors, member variables, and events, to name just a few. While this constructor overload sets the Type and Name properties of the member, there is also an Attributes property that can set its modifiers, as well as an InitExpression member that sets the initial value if desired. The constructor for the class is generated in the BuildFormConstructor function; however, I will continue by first examining the BuildAppClass function and representing the HelloApplication class in the CodeDOM before examining the constructor.

Figure 3 shows the source code for the BuildAppClass function. There are three new concepts in this class: representing the initial method in an assembly, setting the value of variables, and invoking methods on an object. The CodeEntryPointMethod object is responsible for representing the method where execution begins in an assembly. It inherits most of its properties for the CodeTypeMember class and adds an additional ReturnType property to represent the return type of the function.

Figure 3 Creating the HelloApplication Class

private CodeTypeDeclaration BuildAppClass() { String TypeString = "HelloWorldForm"; if(UseFullNamespace) { TypeString = "HelloWorld." + TypeString; } CodeTypeDeclaration hwAppClass = new CodeTypeDeclaration("HelloApplication"); CodeEntryPointMethod MainMethod = new CodeEntryPointMethod(); hwAppClass.IsClass = true; hwAppClass.Attributes = MemberAttributes.Public; MainMethod.Attributes = MemberAttributes.Public | MemberAttributes.Static; MainMethod.ReturnType = new CodeTypeReference(typeof(void)); CodeExpression[] prmRun = { new CodeObjectCreateExpression( "HelloWorld", new CodeExpression[0]) }; MainMethod.Statements.Add(new CodeVariableDeclarationStatement( new CodeTypeReference(TypeString), "hwApp")); CodeExpression[] hwParams = new CodeExpression[0]; MainMethod.Statements.Add(new CodeAssignStatement( new CodeVariableReferenceExpression("hwApp"), new CodeObjectCreateExpression(TypeString, hwParams))); MainMethod.Statements.Add( new CodeMethodInvokeExpression( new CodeVariableReferenceExpression("hwApp"), "ShowDialog", hwParams)); hwAppClass.Members.Add(MainMethod); return hwAppClass; }

CodeStatementCollections enable you to assemble groups of individual code statements to form the bodies of higher-level code constructs, such as methods, properties and try...catch blocks. In Figure 3, the CodeEntryPointMethod.Statements collection represents the code statements that collectively form the body of the Main function. Two other such examples are the CodeTryCatchFinallyStatement.TryStatements and CodeConditionStatement.TrueStatements, which are used to represent blocks of code in a try block and the True block of an if statement, respectively. Building the Main method simply requires adding objects derived from the CodeStatement class to the Statements collection.

After adding the declaration for the HelloWorldForm class, you must represent its assignment to a new instance of that class. This is done using the CodeAssignStatement class, which takes two arguments that derive from the CodeExpression class. As you can guess, this is the abstract base class for representing expressions in the CodeDOM. The two parameters represent the left-hand and the right-hand sides of the assignment. For this particular assignment statement, I reference the variable representing the HelloWorldForm class with the CodeVariableReferenceExpression class, passing in the variable name as the argument to the constructor. The creation expression is represented by the CodeObjectCreateExpression class, which takes the type of the object to create and the list of parameters to be used in the creation. This list of parameters is an array of CodeExpression objects and cannot be null.

As I have done in this example, you can send in a zero-length array to represent a call to the constructor with no arguments. Finally, the CodeMethodInvokeExpression class represents a method invocation. Note that if I wanted to invoke a static method of a class, I would have replaced the CodeVariableReferenceExpression with a CodeTypeReferenceExpression object.

By now you should be getting a pretty good grasp of the way that code is represented in the CodeDOM. There are only a few new System.CodeDom classes presented in the BuildFormConstructor function, partially represented in Figure 4, which is the final part of the template to investigate. This function's main job is to create the representation of the constructor which takes care of all of the property assignments needed to display the form correctly.

Figure 4 BuildFormConstructor Excerpt

private CodeConstructor BuildFormConstructor() { CodeConstructor hwConstructor = new CodeConstructor(); CodeThisReferenceExpression oThis = new CodeThisReferenceExpression(); CodeStatementCollection stCol = new CodeStatementCollection(); hwConstructor.Attributes = MemberAttributes.Public; ••• prmSizeArgs[0] = new CodePrimitiveExpression(700); prmSizeArgs[1] = new CodePrimitiveExpression(350); stCol.Add(new CodeAssignStatement( new CodePropertyReferenceExpression(oThis, "Size"), new CodeObjectCreateExpression(typeof(System.Drawing.Size), prmSizeArgs))); ••• CodeTypeReferenceExpression oAnchor = new CodeTypeReferenceExpression("AnchorStyles"); CodeBinaryOperatorExpression AnchorStyles = new CodeBinaryOperatorExpression( new CodeFieldReferenceExpression(oAnchor, "Bottom"), CodeBinaryOperatorType.BitwiseOr, new CodeBinaryOperatorExpression( new CodeFieldReferenceExpression(oAnchor, "Top"), CodeBinaryOperatorType.BitwiseOr, new CodeBinaryOperatorExpression( new CodeFieldReferenceExpression(oAnchor, "Left"), CodeBinaryOperatorType.BitwiseOr, new CodeFieldReferenceExpression(oAnchor, "Right")))); ••• hwConstructor.Statements.AddRange(stCol); return hwConstructor; }

Again, classes derived from CodeStatement are added to the Statements property of the CodeConstructor class much as they were to the CodeEntryPointMethod described previously. Note the use of the CodePrimitiveExpression class to represent primitives such as numbers and strings in the CodeDOM. Representing the Anchor property assignment of the TextBox named source in Figure 4 is cumbersome. In C#, the assignment is straightforward; however, the CodeDOM's CodeBinaryOperatorExpression takes only three arguments: the left and right sides of the expressions represented by CodeExpression classes and a CodeBinaryOperatorType enumeration to symbolize the operation. Therefore, it takes a composition of three of these objects to represent this in the CodeDOM. While admittedly cumbersome, it's not difficult.

Using the Object Graph

Creating the object graph for the Hello World application template serves the necessary evil of introducing some important classes in the object model and shows how they fit together to represent working code. Now let's look at how you can use that template to produce code. The System.CodeDom.Compiler namespace is the target for this examination. It defines three interfaces and a number of abstract base classes usable by language vendors as references for operating on CodeDOM object graphs to produce source and compiled assemblies. It can also be used to parse existing source code into CodeDOM object graphs. The HelloGen sample application gives you a framework to look into some of the properties of these classes as they pertain to individual languages and to make comparisons between them.

Figure 5 shows the main form of the application, which is composed of two sections. The top tab control allows you to set various options on the code compiler and code generator being used, as well as to pick the target language when compiling the HelloWorld application. The bottom tab control contains output source from each of the languages that are made available through the application's source code. By default, the downloadable source code is set to generate code and assemblies in C#, Visual Basic .NET, and JScript®. If you want to explore generating code in other languages that implement CodeProviders, there are comments in the source code explaining how to add these to the application. While writing this article, I explored creating the template in a number of additional languages, including ISE's Eiffel, Visual J# .NET, Mondrian, and Fujitsu COBOL.

Figure 5 Main Form Generator

Generating Source Code

The ICodeGenerator interface of the System.CodeDom.Compiler namespace defines the methods used to generate code from a CodeDOM object graph, and the language-specific implementations of CodeDomProvider expose the CreateCodeGenerator function to return this interface. Clicking the Generate Preview button on the HelloGen main form initiates the preview process by calling the GeneratePreview method. This method begins by getting a CodeGeneratorOptions object that defines customizable parameters for the code generator output, which I will discuss next. Following this, it executes the loop in Figure 6, generating code from the object graph for each language.

Figure 6 Generating Preview Code

// Create our source Graph for each language foreach(LanguageItem Lang in cboTargetLanguage.Items) { if(Lang.LanguageID == Language.JScript) { HelloWorldApp.UseFullNamespace = true; } else { HelloWorldApp.UseFullNamespace = false; } hwCompileUnit = HelloWorldApp.GenerateCCU(); hwGenerator = GetLanguageGenerator(Lang.LanguageID); CodeText = new StringWriter(); hwGenerator.GenerateCodeFromCompileUnit(hwCompileUnit, CodeText, GeneratorOptions); ((TextBox)PreviewText[PT_PREFIX + Lang.LanguagePreviewID]).Text = CodeText.ToString(); CodeText.Close(); }

The first step is to create the compile unit for the HelloWorld application by calling the GenerateCCU method of the AppConstructor class. While it would have been preferable to do this before the loop, the way CodeDOM handles type information appears to be a stumbling block. The problem is referencing the HelloWorldForm type in the BuildAppClass function (see Figure 3) in the following statement:

MainMethod.Statements.Add(new CodeVariableDeclarationStatement( new CodeTypeReference(TypeString), "hwApp"));

Here, TypeString is a string representing the HelloWorldForm type, but the CodeTypeReference object doesn't recognize the HelloWorldForm type, so it simply emits the string value of TypeString. For C# and Visual Basic .NET this must be "HelloWorldForm", however, JScript .NET requires it to be "HelloWorld.HelloWorldForm". I have added the UseFullNamespace flag in order to resolve this issue and I have created a new CodeCompileUnit on each iteration of the loop.

In creating templates with the CodeDOM, it is important to be wary of such shortcomings and handle them appropriately. In this case, I'm simply setting the UseFullNamespace of the AppCreator object to true when generating code for JScript .NET and leaving it out for the rest. In many cases, you can use the Supports method of the ICodeGenerator interface to ensure the appropriate language capabilities of the generator you intend to use. This method takes a GeneratorSupport enumeration value and returns a Boolean, indicating whether the language supports that particular construct. The enumeration covers a number of useful language features and constructs, but is by no means exhaustive. Figure 7 lists the GeneratorSupport enumeration values, which are self-descriptive.

Figure 7 GeneratorSupport Enumeration Values

ArraysOfArrays MultiDimensionalArrays
AssemblyAttributes MultipleInterfaceMembers
ChainedConstructorArguments NestedTypes
ComplexExpressions ParameterAttributes
DeclareDelegates PublicStaticMembers
DeclareEnums ReferenceParameters
DeclareEvents ReturnTypeAttributes
DeclareInterfaces StaticConstructors
DeclareValueTypes TryCatchStatements
EntryPointMethod Win32Resources

Once you have a reference to the CodeCompileUnit, it's time to get hold of the code generator for the desired language. The GetLanguageGenerator function instantiates a new instance of a language-specific code provider class derived from CodeDomProvider and invokes its CreateGenerator method to return the ICodeGenerator interface for the specified language. The following example demonstrates this for C#:

case Language.CSharp: return new CSharpCodeProvider().CreateGenerator();

To create the language-specific source code, I invoke the GenerateCodeFromCompileUnit method (see Figure 6) of the ICodeGenerator interface returned by the CreateGenerator function. This takes as arguments the CodeCompileUnit representing my object graph, a StringWriter which will be filled with the source code, and the Generator options, which may be used to modify the style of the code created.

The output is displayed on the appropriate tab for quick comparison. At this point, the code from any preview window could be cut and pasted into a text file and compiled; however, later in this article I will explore the facilities in the HelloGen sample application to do this using objects in the System.CodeDom.Compiler namespace. By running the example, you can compare the C#, Visual Basic .NET, and JScript .NET output that was all compiled from the same template. Additionally, Figure 8 shows source generated for the HelloWorldForm class by a beta of ISE's EiffelCodeProvider. This code is a departure from the syntax of the other languages in the example, which tend to be quite similar, and underscores the language neutrality and flexibility of the CodeDOM.

Figure 8 Eiffel Source for HelloWorldForm

class HELLO_WORLD_FORM inherit FORM create a_ctor feature -- Initialization a_ctor is do set_auto_scale_base_size ( create {SIZE}.make_from_width_and_height (5, 13)) set_text (("Hello, World!").to_cil) set_size (create {SIZE}.make_from_width_and_height (700, 350)) create source.make get_controls.add (source) txtSourceCode.set_multiline (True) txtSourceCode.set_scroll_bars (both) txtSourceCode.set_top (10) txtSourceCode.set_width (10) txtSourceCode.set_size ( create {SIZE}.make_from_width_and_height (690, 300)) txtSourceCode.set_anchor (bottom or top or left or right) txtSourceCode.set_text (("[Little Recursive Box]").to_cil) end feature -- Access txtSourceCode: TEXT_BOX end -- HELLOWORLDFORM

Formatting the Output

Earlier, I mentioned that the CodeGeneratorOptions class allows you to modify properties of the generated source, and the Generator Options tab provides controls that allow you to modify these properties. On the call to GetGeneratorOptions, a new object is created and its properties are set. There are four such properties: BlankLinesBetweenMembers, BracingStyle, ElseOnClosing, and IndentString. The first is a Boolean, indicating whether the generator inserts blank lines between code elements such as member variables and classes. BracingStyle can be set to either "Block" or "c". BlockStyle appends opening braces to the same line as the code construct they encapsulate; otherwise, they are placed on the line immediately following the declaration. Similarly, ElseOnClosing defines the placement of else, catch, and finally statements. Finally, the IndentString property is used to set the sequence of characters for each indentation level, usually best kept as spaces.

Compiling Object Graphs

While the straight generation of source code is generally as far as you would go in creating templates, my discussion of the System.CodeDom.Compiler namespace would be lacking if I didn't cover the ICodeCompiler interface. This interface defines methods for compiling a CodeDOM object graph to intermediate language. The HelloGen application's Create Application button triggers the compilation of the object graph to the language selected in the Target Language dropdown of the Application Output tab, which also contains textboxes to specify the target location of both the code and the executable. The Compiler Parameters tab allows you to set customizable properties of the code compiler, which I will describe later.

The GenerateProgram method of the example application does the bulk of the work preparing and compiling the code. Again, I begin by generating the CodeCompileUnit for the HelloWorld application. In this case, I'll need both a code generator and a code compiler, so I'll retrieve a reference to those next. The GetLanguageCompiler function is almost identical to the GetLanguageGenerator function described earlier with the exception that it returns an ICodeCompiler interface through a call to CreateCompiler rather than CreateGenerator. The next step is to update hwCompileUnit with the source for the application. For the purposes of the preview application, AppConstructor.GenerateCCU creates an object graph that sets the TextBox txtSourceText's Text property to "[Little Recursive Box]" by default. To stress the cross-language capabilities of the template, I'd like that text to be replaced by the language-specific source code for the Hello World app being generated. I do this by first generating the source in the currently selected language with hwGenerator and replacing the CodePrimitiveExpression through the CreateExtendedCompileUnit method (see Figure 9). After regenerating the compile unit with this new language-specific information, its source is written to a text file, and I prepare to compile the code.

Figure 9 CreateExtendedCompileUnit

private void CreateExtendedCompileUnit(ICodeGenerator hwGenerator, ref CodeCompileUnit hwCompileUnit, CodeGeneratorOptions GeneratorOptions, TemplateConstructor HelloWorldApp) { StringWriter CodeText = new StringWriter(); // Generate code with text placeholder hwGenerator.GenerateCodeFromCompileUnit(hwCompileUnit, CodeText, GeneratorOptions); HelloWorldApp.SourceText = new CodePrimitiveExpression(CodeText.ToString()); hwCompileUnit = HelloWorldApp.GenerateCCU(); }

Compiler Options

Just as ICodeGenerator.CreateCompileUnit takes a CodeGeneratorOptions object to modify code generation, the CompileAssemblyFromDom method accepts a CompilerParameters object that defines parameters for compiling that code. Some of these options can be set on the Compiler Parameters tab of the application and set on the object when it is created through the GetCompilerParameters function. The CompilerOptions takes a string of parameters to send to the compiler, simulating command-line execution. GenerateExecutable is a Boolean property that tells the compiler whether it should compile an executable or a DLL. Although it is not visible through the UI, there is a ReferencedAssemblies property, which is a collection of strings representing names of the assemblies referenced in the CodeCompileUnit. Since HelloWorld is a Windows Forms app, I have added references to System.Drawing.dll and System.Windows.Forms.dll. Although System.dll is referenced by default, I added it to the source to be complete. The output of the compile can be set to debug or release using the IncludeDebugInformation property, and the TreatWarningsAsErrors flag can be set to tell the compiler with what seriousness it should treat warnings in the code.

The CodeCompileUnit is compiled through a call to ICodeCompiler.CompileAssemblyFromDom with a CompilerParameters object and the CodeCompileUnit in question:

hwResults = hwCompiler.CompileAssemblyFromDom(hwCompilerParameters, hwCompileUnit);

The hwResults object is of type CompilerResults, which is defined in the System.CodeDom.Compiler namespace. This class contains a number of useful properties for retrieving information about the compile process, especially through the Errors collection, which will hold information on any issues that caused the compile process to fail. If you choose to compile the application to a file, the PathToAssembly property will reflect the location of the newly created file, while the CompiledAssembly property gets a reference to the in-memory Assembly object, if applicable. HelloGen tests the GenerateInMemory property of hwCompilerParameters and, if True, uses reflection to execute the program in the ExecuteCompiledAssembly function.

Class Templates and the CodeDOM

With this knowledge of the CodeDOM in hand, let's put it to good use in a real-world example. One of the things that the C# and Visual Basic .NET languages lack is support for parametric polymorphism, commonly referred to as generics. One such example of this is class templates in C++. Class templates allow you to define classes in which certain instances of types within the class are generalized until the class is declared in code. Upon declaration the class is given a type, and at compile time a class is created for each distinct type used in the application. While different compilers can optimize this process to a degree by factoring commonalities in the class, this general description suffices for the purposes of this article. Classically, perhaps the best and certainly the most noteworthy candidates for this type of an implementation are collection classes because they are general purpose in nature.

By creating templates for these classes, you benefit from having their implementation in one place; modifying the template changes the implementation for all instances. Since they are strongly typed, you don't need to use extra code casting variables when using them, which leads to cleaner code and fewer errors. One way of reaping some of the benefits of class templates in languages without generics is, of course, to use variables of type Object to achieve the results of a localized source base and universal usability. The drawback here is in type safety and in a reliance on descriptive variable names and code comments to convey type information to the programmer.

Generics will probably be coming to the C# language in a future release, and the current proposed implementation looks promising, but until then a useful way to build classes that offers some of the advantages of generics would be helpful. This can be accomplished by creating templates using the CodeDOM. By creating classes that define object graphs for your collections or other template based classes, you benefit from having the implementation in one place. If you want to change something, you need only make modifications to the template class; however, you will need to generate the source again for each type you want to represent.

Additionally, so that changes will not be lost, you must be careful that any template classes that you generated previously have not been modified. While this is a serious disadvantage, it is one shared by other template-based class generators. You do, however, gain type safety and cleaner code from reduced casting. The additional advantage to a CodeDOM implementation is the potential to generate these classes in a number of languages for which there are CodeProviders, assuming that the template has been designed within the constraints of those languages.

Creating the Application

NetTemplateGenerator is a general-purpose application for generating source code from CodeDOM object graph templates. These templates are set up in such a way that they can accept parameters that will change the properties of the object graphs, which in turn changes the properties of the code generated from them. If these parameters represent the types of certain variables within the object graph, then I am in effect mimicking the behavior of C++ class templates. The NetTemplateGenerator application itself is responsible only for generating source code from CodeCompileUnit objects and providing the user with a way to modify the parameters to the template. The TypedHashtableProvider project exposes an example template, the CTHTGenerator class, which provides an object graph representing a simple Hashtable whose Key and Value types can be modified through NetTemplateGenerator. Figure 10 shows the app after loading the TypedHashtableProvider assembly and modifying the parameters to the template.

Figure 10 Template Generator

Figure 10** Template Generator **

Assemblies are loaded from the Source Assembly tab on the main form of the application. The default source assembly points to the TypedHashtableProvider.dll assembly, which contains the CTHTGenerator class for creating the CodeCompileUnit for this example. Clicking the Load Assembly button loads the chosen source assembly into memory and uses reflection to browse the classes in that assembly, as shown in the code snippet from PopulateClassesCBO in Figure 11.

Figure 11 Populating the Generator Class ComboBox

foreach(Type curType in types) { GeneratorMethods CurGenMethods = new GeneratorMethods(); if(curType.IsClass) { CurGenMethods.CCUGeneratorMethod = curType.GetMethod("GenerateCCU"); CurGenMethods.QueryVarsMethod = curType.GetMethod("QueryVariables"); CurGenMethods.SetVarsMethod = curType.GetMethod("SetVariables"); CurGenMethods.GetNameMethod = curType.GetMethod("GetName"); if(CurGenMethods.IsValid()) { CurGenMethods.ClassName = curType.FullName; if(CurGenMethods.GetNameMethod != null) { CurGenMethods.FriendlyName = GetProviderName(CurGenMethods); } cboClasses.Items.Add(CurGenMethods); bClassFound = true; } } }

The GeneratorMethods class defined in the NetTemplateGenerator holds references to MethodInfo objects in the class; this class can be used later to invoke those methods to set and retrieve information on the template to be generated. The IsValid class checks the three MethodInfo objects to ensure that they conform to the expected interface. If so, the object is added to the Generator Class combobox, making it available to the application as a new template that may be generated.

If one or more acceptable classes are found in the assembly, you will notice that the Parameters panel is filled with replaceable parameters for the currently selected template (see Figure 10). The OnChange event of the combobox calls CreateTypeControls, which uses reflection and the GeneratorMethods class referenced by the currently selected combobox in order to invoke the QueryVars method of the selected class:

object AppInstance = GeneratorAssembly.CreateInstance( curMethods.ClassName); GenClassTypesCol = (NameValueCollection) curMethods.QueryVarsMethod.Invoke(AppInstance, null);

This method returns a NameValueTypeCollection class that contains the names of the replaceable parameters and, optionally, default values for those parameters. The remainder of the method dynamically creates the TextBox controls for entry on the form.

The TypedHashtable template allows you to substitute values for the namespace, the type name of the class itself, and the type names of the values to be stored in the Hashtable and the associated keys. Since the types are being represented as strings, get in the habit of using the fully qualified name of the type to ensure it is represented correctly by the CodeDOM. In addition to replacing the type names with suitable values, you can modify the output parameters using the Class Output tab. Here, you select the language to output the source and a location to store the file—I've borrowed the GeneratorOptions group box and associated code from the HelloGen sample application.

Code for the class is generated in the GenerateCode method of the TemplateGenerator class. The method starts by creating an instance of the currently selected class of the loaded assembly and updating the GenClassTypesCol with the user-specified values. These values are then passed back to the generating class as an argument to SetVars, and the CodeCompileUnit is generated:

args[0] = GenClassTypesCol; curMethods.SetVarsMethod.Invoke(AppInstance, args); ccu = (CodeCompileUnit)curMethods.CCUGeneratorMethod.Invoke( AppInstance, null);

Using the CodeCompileUnit, the source code is generated as preview text and a text file, much the same as it was in the GeneratePreview method of the HelloGen sample:

codePreview = new StringWriter(); ntGenerator = GetLanguageGenerator( (Language)cboTargetLanguage.SelectedIndex); ntGenerator.GenerateCodeFromCompileUnit( ccu, codePreview, ntGeneratorOptions); codeFile = new StreamWriter(txtSourceDirectory.Text); codeFile.Write(codePreview.ToString()); txtPreview.Text = codePreview.ToString();

Creating the Template

The TypedHashtableProvider project implements the CTHTGenerator, which is responsible for generating the CodeCompileUnit for the simple Hashtable template based on the user-selected parameters. The private member variable TemplateVariables is a NameValueCollection that stores this information and is passed between this assembly and the NetTemplateGenerator assembly via the QueryVariables and SetVariables functions:

public NameValueCollection QueryVariables() { return TemplateVariables; } public void SetVariables(NameValueCollection varCollection) { TemplateVariables = varCollection; }

The CTHTGenerator constructor initializes this collection to values that describe the type expected, and the name for each item is used on the form to describe the parameters (see Figure 10).

Any class in an assembly designed for use by NetTemplateGenerator must implement three methods: QueryVariables, SetVariables, and GenerateCCU. QueryVariables simply returns the NameValueCollection of parameters to be set by the user. SetVariables takes one argument of type NameValueCollection, and this sets the internal collection to the values that the user selected. Finally, GenerateCCU generates the object graph using the user-supplied parameters and returns a CodeCompileUnit to the caller.

There is no magic involved in dynamically setting the types in the object graph. As you can see in Figure 12, I use the String overloads of the CodeDOM object constructors to set the type to the appropriate value from the NameValueCollection object.

Figure 12 Replacing Type Information in Templates

CodeMemberProperty DefaultProperty = new CodeMemberProperty(); CodeExpression[] HashIndex = new CodeExpression[1] { new CodeArgumentReferenceExpression("Key") }; CodeIndexerExpression InternalDefaultReference = new CodeIndexerExpression(InternalHash, HashIndex); DefaultProperty.Name = "Item"; DefaultProperty.Attributes = MemberAttributes.Public; DefaultProperty.Parameters.Add(new CodeParameterDeclarationExpression( TemplateVariables[T_KEYTYPE], "Key")); DefaultProperty.Type = new CodeTypeReference(TemplateVariables[T_VALUETYPE]); // Get Statement DefaultProperty.GetStatements.Add(new CodeMethodReturnStatement( new CodeCastExpression(TemplateVariables[T_VALUETYPE], InternalDefaultReference))); // Set Statement DefaultProperty.SetStatements.Add( new CodeAssignStatement(InternalDefaultReference, new CodePropertySetValueReferenceExpression())); TypedHash.Members.Add(DefaultProperty);

I've chosen this section of code to illustrate the type replacement and to highlight the representation of default properties in the CodeDOM. The trick to creating this effect is giving the CodeMemberProperty object the special name "Item" and adding a single parameter to the Parameters collection. Aside from defining the default property, browsing the code download for the CTHTGenerator class will reveal that the template includes the following methods, which are a small subset of those available in the System.Collections.Hashtable class: Add, ContainsKey, ContainsValue, and Remove. There are a number of properties and methods that I have not added to the template, and you may want to experiment with the CodeDOM by implementing these.

Conclusion

The NetTemplateGen sample application serves as a good introductory example of generating template-based classes using the CodeDOM, but there are any number of extensions and improvements that could be made. A nice addition would be the ability to restrict the types that you could choose when setting individual parameters. For instance, it wouldn't make sense to generate a template class that used String variables as operands to mathematical functions. This could be done by passing the parameters and their values via XML, using XML Schemas to verify the types. Another excellent modification would be to create a Visual Studio .NET add-in that incorporated this functionality into the IDE.

For related articles see:
Generative Programming: Modern Techniques to Automate Repetitive Programming Tasks

For background information see:
CodeDOM Quick Reference
System.CodeDom Namespace

Adam J. Steinertis a consultant for Tara Software, a Division of Yahara Software LLC in Madison, WI. He can be reached at adams@tarasoftware.com.