Using the CodeDOM
by Nick Harrison02/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:
- Query the metadata about your stored procedures to build a class to handle parameter binding.
- Query the manifest of an assembly to build a class to unit-test every function.
- Build standard boilerplate code for templates in every language your group uses.
- Write sample code once, and allow the reader to view it in any language they choose.
- Define your own template grammar and parse it to produce code in any language.
- Generate code in a language that you are not familiar with, but are trying to learn, to compare to a language that you already know.
|
Related Reading
.NET Framework Essentials |
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;
}

