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 |
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.
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.
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.
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.
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.
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.
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;
}
|
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.
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.
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.
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);
}
}
|
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, " " , " ");
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:
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
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());
}
}
}
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());
}
}
//@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();
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.