generate _net code in any language using codedom

12

Click here to load reader

Upload: garyo

Post on 29-Oct-2015

28 views

Category:

Documents


3 download

DESCRIPTION

Generate _NET Code in Any Language Using CodeDOM

TRANSCRIPT

Page 1: Generate _NET Code in Any Language Using CodeDOM

Generate .NET Code in Any Language Using CodeDOM

The CodeDOM namespace contains classes that abstract the idea of code. After defining code in this abstract manner, you can use a language-specific CodeProvider class to generate code in any .NET language from that single abstract CodeDOM representation.

by A. Russell Jones

he .NET framework is replete with nooks and crannies of functionality that are amazingly powerful, but sorely

underdocumented. One of those lesser-known capabilities lies in the CodeDOM namespace. This namespace is

the .NET language equivalent of the HtmlTextWriter and XmlTextWriter classes, but rather than using the classes to

write HTML or XML, you use the classes and methods in the CodeDOM namespace to write .NET code. One of the

most interesting things about CodeDOM is that it's completely language-agnostic. The CodeDOM namespace

abstracts most (not all—at least, not yet) types of code operations into an object model. You create object instances

that represent namespaces, classes, fields, methods, properties, parameters, references to various types, and so

forth.

Each .NET language has a special code provider class. For VB.NET, the code provider class is

Microsoft.VisualBasic.VBCodeProvider. For C#, the class is Microsoft.CSharp.CSharpCodeProvider. Each provider

gives you access to an object that implements the ICodeGenerator interface. The interface defines several

GenerateCodeFrom... methods, one of which is GenerateCodeFromNamespace, which writes all the code for a

namespace. Table 1 shows the high-level process to generate a namespace containing a class.

Table 1: The high-level process for generating a namespace containing a class with CodeDOM. You can refer to this

process at any time by clicking the sidebar Generic CodeDOM Process.

1. Create a namespace.

2. Import namespaces, such as System.Text or System.Drawing.

3. Create a class and add it to the namespace.

4.Create member fields for the class, defining the name and type for each field.

5. Create methods and properties

6.Populate each method or property with code by adding statements to it.

7. Add the method or property to the class.

8. Create an appropriate code provider object.

9.Use the code provider's CreateGenerator method to obtain a CodeGenerator object.

10.Call one of the generator's GenerateCodeFrom... methods to emit the code.

The CodeDOM namespace has a large number of classes, which can be a little intimidating at first. The best way to

learn them is to dive in and create a class.

Page 2: Generate _NET Code in Any Language Using CodeDOM

Generate a Simple Class (Steps 1-5)

In this section you'll build a simple class that:

holds a single value

knows how to print that value to the Output window

Create a new Windows Forms application and add the statement Imports System.CodeDOM to the top of the class

module. Add a button named btnSimpleClass to the form. You'll define and generate the class code in that button's

Click event handler.

To create the code, I'll step through the process steps from above, in detail:

1. Create a namespace

The CodeNamespace class represents a namespace.

' create a namespace

ns = New CodeNamespace("Simple")

A CodeNamespace is a top-level object in the code object model. The CodeNamespace exposes an Imports property

that returns a CodeNamespaceImportCollection collection containing the namespaces you want to import. Add each

namespace using the collection's Add method

2. Import namespaces

ns.Imports.Add(New CodeNamespaceImport _

("System"))

ns.Imports.Add(New CodeNamespaceImport _

("System.Diagnostics"))

ns.Imports.Add(New CodeNamespaceImport _

("System.Text"))

The CodeNamespace object exposes a Types property that returns a CodeTypeDeclarationCollection collection

containing the classes (CodeTypeDeclaration objects) in that namespace. When you first create the CodeNamespace,

its Types collection is empty. The Types collection holds classes, interfaces, structures, or enumerations, all of

which are "types." You add each type to the collection using the Add method. For this example, create one class and

add it to the namespace you created in step one.

3. Create a class and add it to the namespace

Use the CodeTypeDeclaration class to create a new class type. (You would also use the CodeTypeDeclaration to

create a new interface, structure, or enumeration type.) One overloaded constructor accepts the name of the new

type as a parameter.

' create a new class named "SimpleClass"

Dim aClass As New CodeTypeDeclaration _

("SimpleClass")

' add the class to the namespace's

' Types collection

ns.Types.Add(aClass)

4. Create member fields for the class, defining the name and type for each field

Each CodeTypeDeclaration object has a Members property that is an instance of the CodeTypeMemberCollection

class. As you probably expect by now, the collection accepts instances of the CodeTypeMember class. A

CodeTypeMember is a base class that represents a field, a method (Sub or Function), a constructor, a property, or a

nested type, collectively termed members of the class. Use the appropriate derived class to represent each member.

For example, to create a new field, use the CodeMemberField class. To add a new method, use the

CodeMemberMethod class, etc.

Page 3: Generate _NET Code in Any Language Using CodeDOM

For this example, create one private Integer field. Note that "Integer" isn't a valid type; use System.Int32 instead.

Because the class imports the System namespace, you can use simply "Int32" and everything works properly.

' add an Integer field named "mNumber"

Dim aField As New CodeMemberField _

("Int32", "mNumber")

' make it private

aField.Attributes = MemberAttributes.Private

' add it to the class Members collection.

aClass.Members.Add(aField)

5. Create methods and properties

Create the methods and properties as new CodeTypeMembers, using the appropriate derived class. Each

CodeTypeMember has properties that let you specify the member's scope (Attributes property), parameters

(Parameters property), return type ReturnType property, and contained code (Statements property). Members that

may hold more than one value are exposed as collection types. For example, the Attributes, Parameters, and

Statements properties are all collection types. You populate these by creating type instances, and using each

collection's Add method to add them to the collection. The following code creates the SimpleClass constructor.

' create a public class constructor

Dim aClassConstructor As New CodeConstructor()

' make it public

aClassConstructor.Attributes = _

MemberAttributes.Public

' the constructor accepts one Integer

' parameter named "aNumber"

aClassConstructor.Parameters.Add( _

New CodeParameterDeclarationExpression( _

New CodeTypeReference("Integer"), "aNumber"))

' assign the value of "aNumber" to the

' mNumber field. The following code

' will output "Me.mNumber = aNumber" in VB.NET

' or "this.mNumber = aNumber" in C#

aClassConstructor.Statements.Add( _

New CodeAssignStatement( _

New CodeFieldReferenceExpression( _

New CodeThisReferenceExpression(), _

"Number"), _

New CodeArgumentReferenceExpression _

("aNumber")))

' add the constructor to the class

aClass.Members.Add(aClassConstructor)

When you generate all the CodeDOM code you've seen thus far using the VBCodeGenerator class, the result is:

' generator output in VB.NET

Imports System

Imports System.Diagnostics

Imports System.Text

Namespace Simple

Public Class SimpleClass

Page 4: Generate _NET Code in Any Language Using CodeDOM

Private mNumber As Int32

Public Sub New(ByVal aNumber As [Integer])

MyBase.New

Me.Number = aNumber

End Sub

Compare the constructor code to the output. Note that the VB code generator understands VB.NET syntax well

enough to perform some tasks automatically. For example, creating a constructor generates a Sub New

automatically. The End Sub is also automatic. In fact, the code generator creates all End <type> statements (or end

brackets if you generate C#) automatically. The CodeThisReferenceExpression in the preceding code is the same as

writing Me in VB.NET or this in C#.

Creating a constructor and creating a method are essentially the same process, although the CodeDOM classes you

need differ slightly. The following code creates a public read-only property named "Number."

' create a public readonly property

' named "Number" that returns an Integer

' create a CodeMemberProperty object

' to represent the property

Dim p As New CodeMemberProperty()

' give the new property a name

p.Name = "Number"

' make it public

p.Attributes = MemberAttributes.Public

p.Type = New CodeTypeReference("Int32")

p.HasGet = True

p.HasSet = False

Pay attention to the HasGet and HasSet properties—you'll read more about them in the next section.

Generate a Simple Class (Steps 6-10)

Now that you have a namespace, a class, and a property, you need to add code statements to make the property do

something.

6. Populate each method or property with code by adding statements to it

Member types such as methods and constructors have only one Statements collection, but properties have two: a

GetStatements collection and a SetStatements collection. In this class you need to add only one statement to the

SimpleClass.Number property—a Get statement that returns the value of the mNumber field. (To create a Return

statement, use the CodeMethodReturnStatement class.) Add the new Get statement to the CodeMemberProperty's

GetStatements collection.

' create a statement for the Property Get block

' which will be "Return Me.mNumber"

p.GetStatements.Add( _

New CodeMethodReturnStatement( _

New CodeFieldReferenceExpression( _

New CodeThisReferenceExpression(), "mNumber")))

That completes the Number property. The VB-generated output for the new property is:

Page 5: Generate _NET Code in Any Language Using CodeDOM

Public Overridable ReadOnly Property _

Number As Int32

Get

Return Me.mNumber

End Get

End Property

Going back to the HasGet and HasSet properties, I was surprised that they were read-write. I expected them to be

read-only. In fact, they act a bit strange. Here's how the code generator responds when you set (or don't set) those

properties.

You don't have to set the CodeMemberProperty class's HasGet and HasSet properties. If you comment out the two

lines that assign values to the Boolean HasGet and HasSet properties (see the code at the end of step 5), you'll get

exactly the same output as shown above; in other words, the code generator is smart enough to "know" whether to

generate a read-only or read-write property based on the statements in its GetStatements and SetStatements

collections. So if you don't set the properties, the emitted code is exactly what you'd expect—creating a property with

a Get block but not a Set block causes the code generator to emit a read-only property, while creating both a Get and

a Set block causes the generator to emit a read-write property.

You can force the generator to output a Set block even if the SetStatements collection is empty by setting the HasSet

property to True. When you do that, the generator emits an empty Set block that looks like this:

Set

End Set

The reverse of the preceding statement is not true. If you populate the GetStatements collection, the generator emits

a Get block even if you explicitly set the HasGet property to False. Although I didn't test the Set, I expect it acts the

same way; populating the collection overrides a HasSet value of False.

Adding a Function or Sub (a void function in C#) method is similar to creating a constructor or a property; you can

see why Microsoft abstracted constructors, methods, and properties and other member types into subclasses of a

single CodeTypeMembers base class. The following code adds a public method named PrintNumber that prints the

value of the read-only Number property using the System.Diagnostics.Debug class's WriteLine method.

' create a public method named "PrintNumber"

Dim aMethod As New CodeMemberMethod()

aMethod.Name = "PrintNumber"

aMethod.Attributes = MemberAttributes.Public

' this will be a Sub method (a void

' function in C#) because the

' ReturnType is Nothing (null)

aMethod.ReturnType = _

New CodeTypeReference("Nothing")

' create the statement to print the number

aMethod.Statements.Add( _

New CodeMethodInvokeExpression( _

New CodeSnippetExpression( _

"System.Diagnostics.Debug"), _

"WriteLine", _

New CodePropertyReferenceExpression( _

New CodeThisReferenceExpression(), _

"Number")))

The code that creates the single statement for the PrintNumber method is the most interesting part of the preceding

code. The CodeDOM contains special classes that represent many types of statements, but when there's no specific

class for a statement type (in other words, the statement is not a Return statement, not a variable assignment, not a

method invocation, etc.), you use the CodeSnippetExpression class to hold the statement. The code generator emits

Page 6: Generate _NET Code in Any Language Using CodeDOM

the contents of CodeSnippetExpression objects literally; in other words, you can output anything you like by putting

the code in a CodeSnippetExpression. The VB-generated output for the new property is:

Public Overridable Sub PrintNumber()

System.Diagnostics.Debug.WriteLine(Me.Number)

End Sub

The property is now complete. You need to add it to the class's Members collection.

7. Add the method or property to the class

After creating the method or property, add it to the class's Members collection.

' add the method to the class

aClass.Members.Add(aMethod)

Adding the property completes the code. The CodeDOM code you've written so far contains a complete but

abstracted representation of a namespace (the CodeNamespace object) that contains one CodeTypeDeclaration

object representing a class. The class has one private Int32 field (mNumber), one public read-only property (Number),

and one public method (PrintNumber).

The CodeDOM representation is language-independent; you can use the abstracted form as a template for generating

compilable code in any .NET-compliant language. To do that, you must first create an instance of the appropriate

code provider for the language you want to generate. Remember that each .NET language has a special code provider

class that gives you access to an object that implements the ICodeGenerator interface, which you can use to

generate language-specific code. In step 8, I'll show you the code to create both VB.NET and C# versions of the

Simple namespace.

8. Create an appropriate code provider object

' To generate VB.NET code

' create a new VBCodeProvider instance

Dim VbProvider As New VBCodeProvider()

' To generate C# code

Dim CSProvider As New _

Microsoft.CSharp.CSharpCodeProvider()

Note: Code providers are language-specific, so you won't find them in any of the System namespaces. The VBCodeProvider class is in the Microsoft.VisualBasic namespace, and the CSProvider is in the Microsoft.CSharp namespace.

9. Use the code provider's CreateGenerator method to obtain an ICodeGenerator object

Each code provider has a CreateGenerator method. The method returns an object that implements the

ICodeGenerator interface. The ICodeGenerator has a number of GenerateFrom... methods that generate code, such

as GenerateCodeFromNamespace, GenerateCodeFromExpression, GenerateCodeFromCompileUnit,

GenerateCodeFromStatement, and GenerateCodeFromType. Use whichever one meets your needs.

' Generate VB.NET code

' create a new VBCodeProvider instance

Dim VbProvider As New VBCodeProvider()

' get a Generator object

Dim codeGen As ICodeGenerator = _

VbProvider.CreateGenerator()

' generate the code

Dim s As String = getCode(ns, codeGen)

Page 7: Generate _NET Code in Any Language Using CodeDOM

' write the results to a text field

Me.txtResult.Text = "VB.NET CODE:" & _

vbCrLf & vbCrLf

Me.txtResult.AppendText(s)

' Repeat the sequence to generate C# code

' but with the CSProvider instead.

Dim CSProvider As New _

Microsoft.CSharp.CSharpCodeProvider()

' get a Generator object

Dim codeGen As ICodeGenerator = _

VbProvider.CreateGenerator()

' generate the code

Dim s As String = getCode(ns, codeGen)

' write the results to the Output Window

Me.txtResult.AppendText("C# CODE:" & _

vbCrLf & vbCrLf)

Me.txtResult.AppendText(s)

The preceding code passes the ICodeGenerator instance to the getCode function, which calls the

ICodeGenerator.GenerateCodeFromNamespace method. In the next step, you'll see what this method does.

10. Call one of the generator's GenerateCodeFrom... methods to emit the code.

In this case, because you want to generate a complete namespace, the most appropriate method to use is

GenerateCodeFromNamespace. The method accepts:

a CodeNamespace parameter containing the abstract representation of the namespace to generate

a TextWriter instance into which the generator writes the emitted code

a CodeGeneratorOptions instance containing properties that control various options for generating code.

One such option is the IndentString property, which specifies how many spaces the generator uses to indent

code, used in the getCode method shown below.

Private Function getCode( _

ByVal CodeGenerator As ICodeGenerator) As String

' create a CodeGeneratorOptions instance

Dim options As New CodeGeneratorOptions()

' set the indentation level of the

' emitted code

options.IndentString = Space(3)

' create a StringWriter

Dim sb As New StringBuilder()

Dim sw As StringWriter = New StringWriter(sb)

' generate the code

CodeGenerator.GenerateCodeFromNamespace( _

ns, sw, options)

' return the result string

Return sb.ToString()

End Function

Figure 1 shows a form containing the completed code in VB.NET.

Page 8: Generate _NET Code in Any Language Using CodeDOM

Figure 1: When you click the "Create a Simple Class" button, the sample form generates the namespace and class code from the CodeDOM representation and displays it in the TextBox.

Listing 1 shows the output of the completed process in VB.NET, while Listing 2 shows the output in C#.

It's a significant amount of work to write the CodeDOM code to emit even a simple class; however, the potential

benefits are enormous. Once written and debugged, you can guarantee that the emitted code will be error-free. You

should also recognize that CodeDOM isn't limited to VB.NET and C#; a .NET language developer can implement a

code provider and implement ICodeGenerator to emit code in any language. Therefore, the CodeDOM code you write

today is very likely to generate code in other languages in the future—even though those languages may not even

exist yet!

Simplify the Process

The preceding code shows the linear inline process to create the CodeDOM representation of a class; however, you

can simplify the process considerably by wrapping up the code to create various types, members, and statements in

easier-to-use methods. For example, here are three wrapper methods that simplify adding member fields to a class,

creating a class constructor, and assigning a value to a member field.

Wrapper method to create a member field

' create a member field

Private Function CreateField( _

ByVal fieldType As Type, _

ByVal fieldname As String, _

ByVal scope As MemberAttributes)

Dim cmf As New CodeMemberField(fieldType, _

fieldname)

cmf.Attributes = scope

Return cmf

End Function

Wrapper method to create a constructor

' create a constructor

Private Function CreateConstructor(_

ByVal aClass As CodeTypeDeclaration, _

ByVal scope As MemberAttributes, _

ByVal params As

CodeParameterDeclarationExpressionCollection, _

ByVal Statements As CodeStatementCollection) _

As CodeConstructor

Page 9: Generate _NET Code in Any Language Using CodeDOM

' create a class constructor with the

' appropriate scope attribute

Dim aClassConstructor As New CodeConstructor()

aClassConstructor.Attributes = scope

' assign parameters

If Not params Is Nothing Then

aClassConstructor.Parameters.AddRange(params)

End If

' add statements

If Not Statements Is Nothing Then

aClassConstructor.Statements.AddRange _

(Statements)

End If

Return aClassConstructor

End Function

Wrapper method to assign a vaue to a member field

' assign a value to a member field

Private Function CreateFieldAssignment( _

ByVal aClass As CodeTypeDeclaration, _

ByVal assignTo As String, _

ByVal assignFrom As String)

Return New CodeAssignStatement( _

New CodeFieldReferenceExpression( _

New CodeThisReferenceExpression(), _

assignTo), New

CodeArgumentReferenceExpression _

(assignFrom))

End Function

Even with only these few wrapper methods, you can see that the process to create a new namespace containing a

class with a private member field, a constructor with one parameter, and a field assignment is considerably simpler

than writing the CodeDOM code directly. Here's an example that uses the wrapper methods rather than the in-line

method shown in the first part of this article.

Private Sub btnAutomate_Click(_

ByVal sender As System.Object, _

ByVal e As System.EventArgs) _

Handles btnAutomate.Click

Dim VbProvider As New VBCodeProvider()

Dim aClass As New CodeTypeDeclaration()

' create a public class

aClass.Name = "AutoGen"

' add a private field

aClass.Members.Add(CreateField( _

GetType(String), "myPrivateVariable", _

MemberAttributes.Private))

' create a parameters collection

Dim params As New _

CodeParameterDeclarationExpressionCollection()

Page 10: Generate _NET Code in Any Language Using CodeDOM

' add a string parameter

params.Add(New _

CodeParameterDeclarationExpression( _

GetType(String), "someValue"))

' create a statements collection

Dim statements As New CodeStatementCollection()

' add a field assignment statement

statements.Add(CreateFieldAssignment(aClass, _

"myPrivateVariable", "someValue"))

' create the class constructor

aClass.Members.Add( _

CreateConstructor( _

aClass, MemberAttributes.Public, _

params, statements))

' generate the code in VB.NET

Dim sb As New StringBuilder()

Dim sw As StringWriter = New StringWriter(sb)

Dim codeGen As ICodeGenerator =

VbProvider.CreateGenerator()

codeGen.GenerateCodeFromType(aClass, sw, Nothing)

' show the results

Me.txtResult.Text = "VB.NET CODE " & _

vbCrLf & vbCrLf

Me.txtResult.AppendText(sb.ToString() & _

vbCrLf & vbCrLf)

' generate the code in C#

sb = New StringBuilder()

sw = New StringWriter(sb)

Dim CSProvider As New _

Microsoft.CSharp.CSharpCodeProvider()

codeGen = CSProvider.CreateGenerator()

codeGen.GenerateCodeFromType(aClass, sw, Nothing)

Me.txtResult.AppendText("C# CODE " & _

vbCrLf & vbCrLf)

Me.txtResult.AppendText(sb.ToString())

End Sub

This time, the code creates a public class named AutoGen with a private String member field named

myPrivateVariable. The AutoGen class creates a constructor that accepts a String parameter, and assigns the

parameter value to the private member field. When a user clicks the "Automate CodeDOM" button on the sample form

to run the preceding method, you'll see this code in the txtResult TextBox (see Figure 2):

Page 11: Generate _NET Code in Any Language Using CodeDOM

Figure 2: When you click the "Automate CodeDOM" button, the sample form generates the code for the AutoGen class and displays it in the TextBox.

VB.NET CODE

Public Class AutoGen

Private myPrivateVariable As String

Public Sub New(ByVal someValue As String)

MyBase.New

Me.myPrivateVariable = someValue

End Sub

End Class

C# CODE

public class AutoGen {

private string myPrivateVariable;

public AutoGen(string someValue) {

this.myPrivateVariable = someValue;

}

}

Note that this example uses a different GenerateCodeFrom... method. This example contains only a class, not a

complete namespace; therefore, the code calls the generator's GenerateCodeFromType method, rather than the

GenerateCodeFromNamespace method used in the first example.

What's the Point?

You can use these techniques to write wizards to reduce repetitive coding tasks, to generate GUI code, or to emit

code customized by users. For example, Microsoft uses the CodeDOM in Visual Studio to create abstract

representations of typed DataSet classes that use the field names and types from a table or query specified by a

developer. They then emit the code for the typed DataSet in the same language that the user selected for the project.

Finally, CodeDOM functionality isn't limited to generating source code; it also contains methods to compile code, so

you can compile your generated code to an assembly at runtime. The combination provides a powerful way to create,

compile, and execute customized code created at runtime as well as design time.

A. Russell Jones is DevX's Executive Editor. He's a former reptile keeper and professional musician who now

composes computer applications. His most recent books are Mastering ASP.NET with VB.NET and Mastering

ASP.NET with Visual C# (both published by Sybex). Reach him by e-mail at [email protected].

B.

DevX is a division of Jupitermedia Corporation © Copyright 2007 Jupitermedia Corporation. All Rights Reserved.