ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


Using the CodeDOM

by Nick Harrison
02/03/2003

Introduction

One of the promises of .NET is that the language used is secondary to the framework. The classes in the CodeDom namespace really drive this point home. Using CodeDom, we build a tree, or graph, populated with objects from the System.CodeDom namespace, and after the tree is populated, we use the CodeProvider object provided by every .NET language to convert the tree into code in that language. This makes switching languages as simple as switching the CodeProvider used at the end.

Imagine some of the possibilities:

Related Reading

.NET Framework Essentials
By Thuan L. Thai, Hoang Lam

Background

The System.CodeDom namespace includes objects for representing, in a language-independent fashion, most language structures. Each language-specific CodeProvider has the responsibility of dealing with that language's subtle nuances. For example, the CodeConditionStatement includes a collection of TrueStatements, a collection of FalseStatements, and a Condition attribute, but does not worry about whether an "end if" or curly braces are needed. The CodeProviders work this out. This layer of abstraction allows us to structure code to be generated and then output it any .NET language without getting bogged down in the details of the language being generated. This abstraction also makes it easier to structure code programmatically. For example, we can add parameters to the Parameters collection of the method being generated as we discover that we need them, without interfering with the flow of the code already generated.

Basics

Most of the objects we will be using are found in the System.CodeDom namespace. The additional objects will be located in language-specific namespaces such as the Microsoft.CSharp namespace, the Microsoft.VisualBasic namespace, and the Microsoft.JScript and Microsoft.VJSharp namespaces. Each of the language-specific namespaces includes the respective CodeProviders. Finally, the System.CodeDom.Complier namespace will define the interface ICodeGenerator, which will be used to output the generated code to a TextWriter object.

If all we wanted to produce was code snippets for an add-in or macro, we would use the CodeGenerator to generate code from a Statement, Expression, Type, etc. If, on the other hand, we intended to generate an entire file, we would start with a CodeNameSpace object. In this example, we will start with a namespace and demonstrate how to add imports, declare a class, declare a method, declare a variable, implement a loop structure, and index an array. In the end, we will combine these various samples to produce the most beloved of all programs.

Initialize a Namespace

We can use a function similar to this to initialize our namespace.


private CodeNameSpace InitializeNameSpace(string Name) 
{
  // Initialize the CodeNameSpace variable specifying the name of
  // the namespace
  CodeNameSpace CurrentNameSpace = new CodeNamespace (Name);

  // Add the specified Name spaces to the collection of namespaces
  // to import.   The CodeProviders will handle figuring out how
  // name spaces are imported in their respective languages.
  CurrentNameSpace.Imports.Add (new CodeNamespaceImport("System"));
  CurrentNameSpace.Imports.Add (new CodeNamespaceImport("System.Text"));

  return CurrentNameSpace;
}

This code will define a new namespace and import the System and System.Text namespaces.

Create a Class

We can use a function similar to this to declare a new class:


private CodeTypeDeclaration CreateClass (string Name)
{
  // Create a new CodeTypeDeclaration object specifying the name of
  // the class to be created.   
  CodeTypeDeclaration ctd = new CodeTypeDeclaration (Name);

  // Specify that this CodeType is a class as opossed to an enum or a
  // struct
  ctd.IsClass = true;

  // Specify that this class is public
  ctd.Attributes = MemberAttributes.Public;

  // Return our freshly created class
  return ctd; 
}

This function will create a new class with the specified name ready to be populated with methods, properties, events, etc.

Create a Method

We can use a function similar to this to declare a new method:


private CodeEntryPointMethod CreateMethod()
{
  // Declare a new CodeEntryPointMethod
  CodeEntryPointMethod method = new CodeEntryPointMethod();

  // Specify that this method will be both static and public
  method.Attributes = MemberAttributes.Public |
  MemberAttributes.Static;
  
  // Return the freshly created method
  return method;
}

For this example, we created a CodeEntryPointMethod. This object is similar to the CodeMemberMethod object, except that the CodeProviders will ensure that the EntryPoint object will be called as the entry in the class, such as Sub Main or void main, etc. For the CodeEntryPointMethod, a name of Main is assumed; for CodeMemberMethod, you must specify the name.

Declare a Variable

We can use a function similar to this to declare a variable.


private CodeVariableDeclarationStatement 
            DeclareVariables(System.Type DataType,
                             string Name)
{
  // Get a CodeTypeReference for the Type 
  // of the variable we are about to
  // create.   This will allow us not to 
  // have to get bogged down in the
  // language specific details of specifying 
  // data types.
  CodeTypeReference tr = new CodeTypeReference (DataType );

  // The CodeVariableDeclarationStatement 
  // will allow us to not have to
  // worry about such details as whether 
  // the Data Type or the variable name
  // comes first or whether or not a key 
  // word such  as Dim is required.
  CodeVariableDeclarationStatement Declaration = 
             new CodeVariableDeclarationStatement(tr, Name);
  
  // The CodeObjectCreateExpression handles 
  // all of the details for calling
  // constructors.   In most cases, this 
  // will be new, but sometimes it is New.
  // At any rate, we don't want to  have to 
  // worry about such details.
  CodeObjectCreateExpression  newStatement = new
  CodeObjectCreateExpression ();
  
  // Here we specify the object whose 
  // constructor we want to invoke.
  newStatement.CreateType = tr;
  
  // Here we specify that variable will be 
  // initialized by calling its constructor.
  Declaration.InitExpression = newStatement;
  return Declaration;
}

The individual .NET languages can have their own names for the data types that all map to common .NET data types. For example, in C# the data type would be int. In VB.NET, the same data type would be Integer. The common .NET type is System.Int32. The CodeTypeReference object goes directly to the common .NET data type, and then the language specific Code providers can use the more common language-specific name.

Initializing an Array

We can use a function similar to this to initialize an array.


private void InitializeArray (string Name, 
                              params char[] Characters )
{
  // Get a TypeReference for the Character 
  // array that was passed in
  // so that we can duplicate this data 
  // type in our generated code.
  CodeTypeReference tr = new CodeTypeReference (Characters.GetType());
  
  // Declare an array that matches our local array
  CodeVariableDeclarationStatement Declaration =
    new CodeVariableDeclarationStatement (tr, Name);
  
  // The CodePrimitiveExpression object is used to 
  // represent "primitive" or value data types such 
  // as char, int, double, etc.    We will use
  // an array of these primitive expressions to 
  // initialize the array we are declaring.
  CodePrimitiveExpression[] cpe = new 
    CodePrimitiveExpression[Characters.Length];
  
  // Loop through our local array of characters, 
  // creating the objects
  // for our array of CodePrimitiveExpressions
  for (int i = 0; i < Name.Length ; i++)
  {
    // Each CodePrimitiveExpression will have a language
    // independant representation of a character
    cpe[i] = new CodePrimitiveExpression (Characters[i]);
  }

  // The CodeArrayCreateExpression will handle calling 
  // the default constructor for the data type in the 
  // array.   Because we are also passing in the array of
  // CodePrimitiveExpressions, we won't need to specify 
  // the size of the array, and
  // every value in the array will have its initial value.
  CodeArrayCreateExpression array = new 
    CodeArrayCreateExpression(tr, cpe);
  
  // Specify that this CodeArrayCreateExpression will 
  // initialize our array variable declartion
  Declaration.InitExpression = array;
  
  return Declaration;
}

Implement a Loop Structure

We can use a function similar to this to build a loop structure.


private CodeIterationStatement CreateLoop(string LoopControlVariableName)
{
  // Declare a new variable that will be 
  // the loop control variable
  CodeVariableDeclarationStatement Declaration;

  // Declare a CodeIterationStatement which 
  // will house all of the loop logic
  CodeIterationStatement forloop = new CodeIterationStatement();
  
  // As an alternate method of specifying the 
  // data type for a variable
  // dynamically declared, we can use the typeof 
  // function to get the Type object
  // for a datatype without having to use a 
  // variable of that type.
  Declaration = new CodeVariableDeclarationStatement(typeof  (int),
    LoopControlVariableName);
  
  // Specify a very simple initialization expression 
  // of simply setting the new variable to zero.
  Declaration.InitExpression = new CodeSnippetExpression ("0");
  
  // Specify that this newly declared variable will 
  // be used to initialize the loop
  forloop.InitStatement = Declaration;
  
  // The CodeAssignStatement is used to handle 
  // assignment statements. The constructor we are 
  // using here expects two expressions, the first will
  // be on the left of the assignment.   The second 
  // will be on the right of the assigment.   Alternately, 
  // we could also call the default constructor and then
  // set the left and rigth properties explicitely.
  CodeAssignStatement assignment = new CodeAssignStatement( 
    new CodeVariableReferenceExpression(LoopControlVariableName), 
    new CodeSnippetExpression (LoopControlVariableName + " + 1" ));
  
  // Specify that we will use the assignment statement to iterate
  // through the loop.
  forloop.IncrementStatement = assignment;
  
  // Specify that the loop should end when 
  // the loop control variable exceeds the
  // number of characters in the array.
  forloop.TestExpression = new CodeSnippetExpression
    (LoopControlVariableName + " < Characters.Length");
  
  return forloop;
}

You'll note that we specified the data type information for the loop control variable using the typeof function to get the Type object directly, instead of declaring a CodeTypeReference object. This is just another constructor for the CodeVariableDeclartionStatement. There are a total of seven different constructors available.

Index an Array

We can use a function similar to this to index an array.


private CodeArrayIndexerExpression 
  CreateArrayIndex(string ArrayName, string IndexValue  )
{
  // Declare a new CodeArrayIndexerExpression
  CodeArrayIndexerExpression index = new CodeArrayIndexerExpression ();

  // The Indices property is a collection to support indexing into a
  // muli-dimensioanl array.   Here we are only interested in a simple
  // single dimensional array.
  index.Indices.Add ( new  CodeVariableReferenceExpression (IndexValue));
  
  // The TargetObject specifies the name of the array to be indexed
  index.TargetObject = new CodeSnippetExpression (ArrayName);
  
  return index;
}

The CodeArrayIndexerExpression object handles the various differences in the way that arrays are indexed. Specifically, in C#, arrays would be indexed like ArrayName[IndexValue], but in VB.Net, arrays would be indexed like ArrayName(IndexValue). This object allows us to ignore these differences and focus on details such as which array is being indexed and where in the array are we wanting to go.

Bringing it All Together (Hello World!)

All of the functions defined earlier can be added to a class and initialized in a constructor similar to this:


public CodeDomProvider()
{         
  CurrentNameSpace = InitializeNameSpace("ComputerSpeaks");
  CodeTypeDeclaration ctd = CreateClass ("HelloWorld");
  
  // Add the class to the namespace
  CurrentNameSpace.Types.Add (ctd);
  CodeEntryPointMethod mtd = CreateMethod();
  
  // Add the method to the class
  ctd.Members.Add (mtd);
  CodeVariableDeclarationStatement VariableDeclaration = 
    DeclareVariables (typeof (StringBuilder), "sbMessage");
  
  // Add the variable declaration to the method 
  mtd.Statements.Add (VariableDeclaration);
  CodeVariableDeclarationStatement array = InitializeArray
    ("Characters", 'H', 'E', 'L', 'L', 'O', ' ', 
    'W', 'O', 'R', 'L', 'D');
  
  // Add the initialized array to the method.
  mtd.Statements.Add (array);
  CodeIterationStatement loop = CreateLoop("intCharacterIndex");
  
  // Add the loop to the method
  mtd.Statements.Add (loop);
  
  // Build an index into the initialized array.
  CodeArrayIndexerExpression index = CreateArrayIndex("Characters",
    "intCharacterIndex");
  
  // Add a statement that will invoke the "Append" mehtod of the
  // sbMessage object passing a parameter of the array index
  // result.
  loop.Statements.Add (new CodeMethodInvokeExpression (
    new CodeSnippetExpression ("sbMessage"),"Append", 
    index));
  
  // After the loop finishes, print out the result of
  // all of the appends to the sbMessage object.
  mtd.Statements.Add (new CodeSnippetExpression
    ("Console.WriteLine (sbMessage.ToString())"));
}

The end result of such a constructor is a fully populated CodeDom tree but no code. Everything done so far has been completely independent of the target language. The code produced will be exposed as properties.

Exposing the Generated Code

Once the CodeDom tree is populated, producing code in any .NET language is relatively straightforward. Each language includes a CodeProvider object with a CreateGenerator method that will return an object implementing the ICodeGenerator interface. This interface defines all of the methods for generating code and allows us to write a helper function that will simplify writing the properties. The properties will only have to worry about passing the appropriate CodeGenerator to our GenerateCode helper function. The GenerateCode method will handle setting up a suitable TextWriter into which the code will be produced, and returning the resulting text to the property as a string.


private string GenerateCode (ICodeGenerator CodeGenerator)
{
  // The CodeGeneratorOptions object allows us to specify
  // various formatting settings that will be used 
  // by the generator.
  CodeGeneratorOptions cop = new CodeGeneratorOptions();

  // Here we specify that the curley braces should start 
  // on the line following the opening of the block
  cop.BracingStyle = "C";
  
  // Here we specify that each block should be 
  // indented by 2 spaces
  cop.IndentString = "  ";
  
  // The GenerateCodeFromNamepsace method expects to be 
  // passed a TextWriter that will hold the code being 
  // produced.    This could be a StreamWriter,
  // a StringWriter, or an IndentedTextWriter.   
  // A StreamWriter can be used to output the code to 
  // a file.    A StringWriter can be bound to a StringBuilder
  // which can be referenced as a local variable.
  // Here we will bind a StringWriter to the StringBuilder sbCode.
  StringBuilder sbCode  = new StringBuilder();
  StringWriter sw  = new StringWriter(sbCode);
  
  // Generate the Code!
  CodeGenerator.GenerateCodeFromNamespace(CurrentNameSpace, sw,cop);
  
  return sbCode.ToString();
}

Using this helper function, the language specific properties are fairly straightforward.


public string VBCode 
{
  get 
  {
    VBCodeProvider provider =  new VBCodeProvider ();
    ICodeGenerator codeGen = provider.CreateGenerator ();
    return GenerateCode (codeGen);
  }

}
public string JScriptCode
{
  get 
  {
    JScriptCodeProvider provider = new JScriptCodeProvider ();
    ICodeGenerator codeGen = provider.CreateGenerator ();
    return GenerateCode(codeGen);
  }

}

public string JSharpCode 
{
  get 
  {
    VJSharpCodeProvider provider = new VJSharpCodeProvider ();
    ICodeGenerator codeGen = provider.CreateGenerator ();
    return GenerateCode (codeGen);
  }

}

public string CSharpCode 
{
  get 
  {
    CSharpCodeProvider provider = new CSharpCodeProvider();
    ICodeGenerator codeGen = provider.CreateGenerator ();
    return GeneratorCode (codeGen);
  }

}

Displaying the Generated Code

For demonstration purposes, we will use a simple .aspx file with four labels, one for each of the languages being produced.


<table width="800" border="1">
  <tr>
    <th>VB.NET Code</th>
  </tr>
  <tr >
    <td>
      <asp:Label ID="vbCode" Runat="server" CssClass="code">
	  </asp:Label>
    </td>
  </tr>
  <tr>
    <th>
      C# Code</th></tr>
  <tr>
    <td><asp:Label ID="csharpcode" Runat="server" CssClass="code">
	</asp:Label></td>
  </tr>
  <tr>
    <th>J# Code</th></tr>
  <tr >
    <td>
      <asp:Label ID="JSharpCode" Runat="server" CssClass="code">
	  </asp:Label>
    </td>
  </tr>
  <tr>
    <th>JScript.NET</th>
  </tr>
  <tr>
    <td><asp:Label ID="JScriptCode" Runat="server" CssClass="code">
	</asp:Label></td>
  </tr>
</table>

In the code behind, we will instantiate an instance of the CodeDomProvider class that we created earlier and then set the value of the code properties to the Text properties of the corresponding labels on the .aspx page. To make the generated code look a little nicer on the web page, we will do some very simple formatting to replace new lines with an HTML line break, and little spaces with the HTML entity for a non breaking space.


private string FormatCode (string CodeToFormat)
{
  string FormattedCode = Regex.Replace (CodeToFormat, "\n", "<br>");
  FormattedCode = Regex.Replace (FormattedCode,  "  " , "&nbsp;");
  FormattedCode = Regex.Replace (FormattedCode, ",", ", ");
  return FormattedCode;
}

Now we simply assign the generated code to the corresponding Labels in the .asp page.

private void Page_Load(object sender, System.EventArgs e)
{

  HelloWorld.CodeDomProvider  codegen = new HelloWorld.CodeDomProvider ();
  vbCode.Text = FormatCode (codegen.VBCode);
  csharpcode.Text = FormatCode (codegen.CSharpCode);
  JScriptCode.Text = FormatCode (codegen.JScriptCode);
  JSharpCode.Text = FormatCode (codegen.JSharpCode);
  Page.EnableViewState = false;
}

The output should look similiar to this:

VB.NET Code



Imports System 
Imports System.Text 

Namespace HelloWorld 
   
  Public Class Hello_World 
     
    Public Shared Sub Main() 
      Dim sbMessage As System.Text.StringBuilder = _
                         New System.Text.StringBuilder 
      Dim Characters() As Char = New Char() {_
           Microsoft.VisualBasic.ChrW(72),  _
           Microsoft.VisualBasic.ChrW(69),  _
           Microsoft.VisualBasic.ChrW(76),  _
           Microsoft.VisualBasic.ChrW(76),  _
           Microsoft.VisualBasic.ChrW(79),  _
           Microsoft.VisualBasic.ChrW(32),  _
           Microsoft.VisualBasic.ChrW(87),  _
           Microsoft.VisualBasic.ChrW(79),  _
           Microsoft.VisualBasic.ChrW(82),  _
           Microsoft.VisualBasic.ChrW(76),  _
           Microsoft.VisualBasic.ChrW(68)} 
      Dim intCharacterIndex As Integer = 0 
      Do While intCharacterIndex < Characters.Length 
        sbMessage.Append(Characters(intCharacterIndex)) 
        intCharacterIndex = intCharacterIndex + 1 
      Loop 
      Console.WriteLine (sbMessage.ToString()) 
    End Sub 
  End Class 
End Namespace 

C# Code


namespace  HelloWorld 
{ 
    using  System; 
    using  System.Text; 
    
    
    public  class  Hello_World 
    { 
        
        public  static  void  Main() 
        { 
            System.Text.StringBuilder  sbMessage  =  new  
			     System.Text.StringBuilder(); 
            char[]  Characters  =  new  char[]  { 
                    'H', 
                    'E', 
                    'L', 
                    'L', 
                    'O', 
                    '  ', 
                    'W', 
                    'O', 
                    'R', 
                    'L', 
                    'D'}; 
            for  (int  intCharacterIndex  =  0;   
			      intCharacterIndex  <  Characters.Length;
                  intCharacterIndex  =  intCharacterIndex  +  1) 
            { 
                sbMessage.Append(Characters[intCharacterIndex]); 
            } 
            Console.WriteLine  (sbMessage.ToString()); 
        } 
    } 
} 

J# Code


package  HelloWorld; 
import  System.*; 
import  System.Text.*; 


public  class  Hello_World 
{ 
    
    public  static  void  main(String[]  args) 
    { 
        System.Text.StringBuilder  sbMessage  =  new  
		      System.Text.StringBuilder(); 
        char[]  Characters  =  new  char[] 
        { 
                'H', 
                'E', 
                'L', 
                'L', 
                'O', 
                '  ', 
                'W', 
                'O', 
                'R', 
                'L', 
                'D'} 
        ; 
        for  (int  intCharacterIndex  =  0;   
		      intCharacterIndex  <  Characters.Length;
              intCharacterIndex  =  intCharacterIndex  +  1) 
        { 
            sbMessage.Append(Characters[intCharacterIndex]); 
        } 
        Console.WriteLine  (sbMessage.ToString()); 
    } 
} 

JScript.NET Code


//@cc_on 
//@set @debug(off) 

import System; 
import System.Text; 

package HelloWorld 
{ 
   
  public class Hello_World 
  { 
     
    public static function Main() 
    { 
      var sbMessage : System.Text.StringBuilder = 
	        new System.Text.StringBuilder(); 
      var Characters : char[] = 
	        ['H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D']; 
      for (var intCharacterIndex : int = 0; 
      ; intCharacterIndex < Characters.Length; 
	  intCharacterIndex = intCharacterIndex + 1) 
      { 
        sbMessage.Append(Characters[intCharacterIndex]); 
      } 
      Console.WriteLine (sbMessage.ToString()); 
    } 
  } 
} 
HelloWorld.Hello_World.Main(); 

Conclusion

The CodeDom drives home the point that in .NET, the language is not nearly as important as the framework. This article demonstrated how to use some of the more common objects on the CodeDom that will probably be used in nearly every application of the CodeDom. The possibilities for structured code generation is really limited only by your imagination. I look forward to hearing some of the ways you are using the CodeDom.

Nick Harrison UNIX-programmer-turned-.NET-advocate currently working in Charlotte, North Carolina using .NET to solve interesting problems in the mortgage industry.


Return to ONDotnet.com

Copyright © 2009 O'Reilly Media, Inc.