Article source code: codedom_1.zip
Hopefully over the following weeks these articles will, if nothing else, bring the possibility of generating code with .NET to your attention. More than that though I hope you will actually find a use for The CodeDOM and follow these articles to generate code and complied assemblies. Along the way I'll try to highlight some pitfalls that I have become aware of though being part of a company that has VSIP membership.
The code samples will be in VB, the terminology in the most part will be VB orientated too. The generated code will be in both languages though, demonstrating the main use of the CodeDOM. Sorry C# developers, I thought it would make a change!
Requirements
- Visual Studio.NET Professional, or Enterprise Architect Edition
- VB.NET or C#.NET Programming Knowledge
CodeDOM Overview
Before reading this document you may want to brush up on some of the concepts and terminology found at the following sites:
System.CodeDom Namespace
CodeDOM Quick Reference
This week is simply an introduction, as I've already said hopefully you will find a use for the CodeDOM which will make any following articles more useful.
Introduction to the Code DOM
To start with I will explain some of the advantages of the CodeDOM and times where I can imagine it being of use.
We'll then go on to explaining the first steps into creating simple hieratical objects know as CodeDOM graphs, or CodeDOM trees, that can be taken and built into assemblies or simply used to output source code as shown in the following examples.
The main CodeDOM advantage is language independent code generation.
p>
I've found the CodeDOM useful in projects I been involved in where tools have been created which generate repetitive code, allowing some of the laborious nature of coding an extensible database program to be alleviated. Whenever I create a database program I usually have to create many maintenance screens that have a one-to-one relationship with certain database tables. These are required to allow users after delivery to add maybe another category to certain sections of a program etc. Time is best spent working on the more complicated screens and a tool can be created that "churns out" these similar screens in seconds, and also allows a single point of call when an alteration is required, and therefore re-generation is needed.
Another is the ability to create, compile and execute an assembly at runtime, and in utilising that advantage I have also seen the CodeDOM used quite differently to the first example when used to speed file access. An assembly created on the fly can be populated from maybe an XML configuration file; it is then far faster then to interrogate the assembly for consequent requests.
Lets get started.
Download Test Application
We will gradually build our examples up so that we can generate a class that utilises as many code structure types as I feel necessary to allow you to understand the CodeDOM.
The example I'll be using is a very simplistic bank account class with properties:
- Account Holder
- Account Number
- Current Balance (Read only)
A function that transfers money to another account and returns a success value will be included.
The example will raise an event when the balance goes below zero. The Account class implements an interface called IAccount
. The class also has a constructor that allows initiation of the class with an account number and starting balance.
One you've downloaded the sample the first point of interest is the main form frmTest
.
From frmTest
you'll be able to build up the sample class step-by-step. The buttons on this main form basically call the functions that I will go on to explain in a moment. The return objects from the functions are then passed to the CodeWriter
class.
You'll see that I've repeated a lot of function calls on the click events; this is just to ease understanding of the example. I also apologise for the order that members of the class appear; to the best of my knowledge you cannot correct this without having a file output and using the CodeLinePragma
object (please contact me if you know any different)?
The example output language defaults to VB but by using the language combo you can change this to C#, showing the beauty of the CodeDOM.
The actual code for IAccount
doesn't exist in my example it's simply an imaginary interface that has been referenced for demonstration purposes.
CodeWriter Class
This class will be expanded on through out the course of the articles but for now its purpose is to take CodeDOM objects placed in a hierarchical order of namespace, class, members and return code back to us as a string in our chosen language.
The shared/static class (doesn't require instantiation) contains the public property Language
, this allows the choice of output code language.
Also there's only one function at the moment call Write that allows a parameter of type CodeNamespace
. This parameter contains the whole code hierarchy.
Inside the Write
function is a select statement that chooses between output language (in this case VB or C#), the code then creates either a VBCodeProvider
or a CSharpCodeProvider
object. The provider objects then are used to create a Generator
.
The Providers allow access to code generators and compilers for the relevant language.
The rest of the functionality populates a StringBuilder
from a StringWriter
stream that is passed to the Generator's GenerateCodeFromNameSpace
method, and the contents of the StringBuilder
is returned (this being the actual code).
Invoking 'Generate Class' and the CreateClass Function
Next we'll take a look line-by-line at the first function.
Private Sub cmdGenerateClass_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles cmdGenerateClass.Click
Dim NewClass As CodeTypeDeclaration = CreateClass
Dim CodeNamespace As New CodeNamespace("Bank")
CodeNamespace.Types.Add(NewClass)
txtcode.Text = CodeWriter.Write(CodeNamespace)
End Sub
The first line of the code runs the function CreateClass
.
The simple job of CreateClass
is to create a class (no surprise there then), by either calling the VB or C# class creation functions. This is the only time in my examples at present that we really need to do something a little different depending on the output language.
Next the returned class object is added to a Namespace object, CodeNamespace
.
The namespace is then passed to the CodeWriter
class.
First we will take a look at the function CreateVBClass
.
Private Function CreateVBClass() As CodeTypeDeclaration
Dim AccountClass As New CodeTypeDeclaration()
With AccountClass
.IsClass = True
.Name = "BankAccount"
.Members.Add(New CodeSnippetTypeMember("Implements IAccount"))
.Comments.Add(New CodeCommentStatement( _
"This VB Class has been generated by the CodeDOM"))
.Comments.Add(New CodeCommentStatement(""))
End With
Return AccountClass
End Function
I've tried to comment my code as much as I can, trying to make as readable as possible (it's probably the most commenting I've done for a while!), but I've left the comments out of the article code segments, they can be found in the actual download.
Firstly we create a CodeTypeDeclaration
object state that we wish it to signify a Class, as this property also allows Enum, Interface or Structure to be chosen. We then name the class accordingly.
Next we add some comments and the base types. We are implementing an interface called IAccount
, to do this is a little different in VB than C#. In the C# we wouldn't need to inherit from Object but we need to do this in VB so that when we add our further basetypes the CodeDOM knows that they must be interfaces as VB can only inherit from on base class.
Finally we return the class object to the calling function.
Looking at CreateCSharpClass
we can see the subtle differences required by C#.
Private Function CreateCSharpClass() As CodeTypeDeclaration
Dim AccountClass As New CodeTypeDeclaration()
With AccountClass
.IsClass = True
.Name = "BankAccount"
.BaseTypes.Add(New CodeTypeReference("IAccount"))
.Comments.Add(New CodeCommentStatement( _
"This C# Class has been generated by the CodeDOM"))
.Comments.Add(New CodeCommentStatement(""))
.Members.Add(New CodeEntryPointMethod())
End With
Return AccountClass
End Function
The first of these differences is the possible need for an Entry Point
method. This is achieved by creating a CodeMemberMethod
object (as shown else where in this article).
The second is the ability to utilise the BaseTypes
collection to add a reference to the interface, IAccount
, that the class implements.
Other than those two additions the code is identical for either of the two languages. The class creation is the only time that I have had to make a differentiation between the two output languages within the whole example.
Invoking 'With Fields' and the AddFields Function.
To add field members to the class creation, we have just looked at, requires the following code which calls the AddFields
function creating a CodeMemeberFieldObject
.
NewClass.Members.AddRange(AddFields)
Beneath is the code for the AddFields
function
Private Function AddFields() As CodeMemberField()
Dim AccountHolderField As New CodeMemberField()
With AccountHolderField
.Name = "mAccountHolder"
.Type = New CodeTypeReference(GetType(String))
.Attributes = MemberAttributes.Private
End With
Dim AccountNumberField As New CodeMemberField()
With AccountNumberField
.Name = "mAccountNumber"
.Type = New CodeTypeReference(GetType(Int32))
End With
Dim StartBalanceField As New CodeMemberField()
With StartBalanceField
.Name = "mStartBalance"
.Type = New CodeTypeReference(GetType(Decimal))
End With
Dim CurrentBalance As New CodeMemberField()
With CurrentBalance
.Name = "mCurrentBalance"
.Type = New CodeTypeReference(GetType(Decimal))
End With
Return New CodeMemberField() {AccountHolderField, AccountNumberField, _
StartBalanceField, CurrentBalance}
End Function
The account class has three private fields, mAccountHolderField
, mAccountNumberField
, mStartBalanceField
, mCurrentBalance
. The function of these fields is to store the state of there corresponding properties or in the case of the mStartBalanceField
it's job is simply to store the initial value passed into the constructor.
For each of the fields a CodeMemberField
object is created and properties Name and Type are set, also it is necessary to set the scope of the field using the Attributes
property.
Finally a CodeMemberField
array is passed back to the calling code to be added to our class.
Invoking 'With Properties' and the AddProperties Function.
Firstly we need to add the function call -
NewClass.Members.AddRange(AddProperties)
Then looking at the AddProperties
function, we see it's one of the larger functions in this example, it mostly just repeats the same code for each of the three properties so we'll just take a look at defining the AccountHolderProperty
property –
Dim AccountHolderProperty As New CodeMemberProperty()
Dim AccountInterface As New CodeTypeReference("IAccount")
With AccountHolderProperty
.Name = "AccountHolder"
.Type = New CodeTypeReference(GetType(String))
.Attributes = MemberAttributes.Public
.HasGet = True
.HasSet = True
.ImplementationTypes.Add(AccountInterface)
Dim AccountHolderReference As New CodeFieldReferenceExpression()
AccountHolderReference.FieldName = "mAccountHolder"
.GetStatements.Add(New CodeMethodReturnStatement( _
AccountHolderReference))
Dim AccountHolderAssignment As New CodeAssignStatement()
AccountHolderAssignment.Left = AccountHolderReference
AccountHolderAssignment.Right = New CodeArgumentReferenceExpression( _
"value")
.SetStatements.Add(AccountHolderAssignment)
AccountHolderAssignment = Nothing
End With
To start with we create a CodeMemberProperty
called 'AccountHolderProperty' to be returned from this function along with the other properties.
Next we create a new instance of a CodeTypeReference
, the type we will be referenceing is IAccount
as the properties of this class will be implementing members of this interface. As I mentioned earlier I haven't actually gone into defining the structure of IAccount
as it's really just for demonstration purposes as it could just confuse matters?
Now we have the property instance, we need to set a few details namely, Type, Attributes (Scope), HasGet (Has a Get accessor), HasSet (has a set accessor), Name and the any types that are implemented (in this case we add the reference to IAccount
as mentioned before).
Before we return the Property from this function we need to build the the Get and Set accessors.
First the Get, in which we need to create a reference to the private field member mAccountHolder
, using the CodeDOM object, CodeFieldReferenceExpression
.
Now we have a refrence to the field, we need to return it from the function so we use a CodeMethodReturnStatement
object, passing the field reference in as the required parameter. The return statement is then taken and added to the Statements
collection of the Get
accessor. Our generated code will now return the value of the private field, which is exactly what we want.
Now the Set
accessor, we'd like to just create a pretty standard set that takes the value assigned to the property and set mAccountHolder
to be equal to that value. So to do this we are going to create a CodeAssignStatement
, this object has two important properties, they are the left and right hand side of the assignment statement.
For the left hand side we can reuse or reference to the private field, mAccountHolder
.
The right hand side requires us to create a CodeArgumentReferenceExpression
to the default 'value' property of a Set
accessor (well in VB anyway).
The last step of property creation is to add the return statement to the GetStatement's Statement
collection, and the value Assignment statement to the SetStatement's Statement
collection.
Invoking 'With Constructor' and the CreateConstructor Function.
Dim Constructor As New CodeConstructor()
With Constructor
.Parameters.Add(New CodeParameterDeclarationExpression(GetType( _
Int32), "accountNumber"))
.Parameters.Add(New CodeParameterDeclarationExpression(GetType( _
Decimal), "startBalance"))
Dim FieldReference As New CodeFieldReferenceExpression()
Dim ArgumentRef As New CodeArgumentReferenceExpression()
ArgumentRef.ParameterName = .Parameters(0).Name
FieldReference.FieldName = "mAccountNumber"
Dim AccountNumberAssign As New CodeAssignStatement(FieldReference, _
ArgumentRef)
.Statements.Add(AccountNumberAssign)
FieldReference = New CodeFieldReferenceExpression()
ArgumentRef = New CodeArgumentReferenceExpression()
ArgumentRef.ParameterName = .Parameters(1).Name
FieldReference.FieldName = "mStartBalance"
Dim StartBalanceAssign As New CodeAssignStatement(FieldReference, _
ArgumentRef)
FieldReference = Nothing
ArgumentRef = Nothing
.Statements.Add(StartBalanceAssign)
StartBalanceAssign = Nothing
End With
Return Constructor
In order to create a Constructor we need the aptly named CodeDOM CodeConstructor
object. The statements within the Constructors will take the two defined parameters and assign them to their corresponding private field members. In order to understand the code I will just take you through one of the assignments.
To start with then we need to define the parameters, we could have used AddRange
but I've added them one at a time for simplicity. The parameters are accountNumber
(Int32) and startBalance
(Decimal ).
We have created a CodeAssignStatement
before but we'll go through it again.
The first step in this case is to create a CodeArgumentReferenceExpression
(ArgumentRef) that refers to the constructors' parameter accountNumber
. Next using a CodeFieldReferenceExpression
object we need to make reference to the field mAccountNumber.
Using a CodeAssignStatement
we assign these two objects to each other and then add the assignment to the constructors' Statements
collection.
This process is then repeated for each parameter. The Constructor is then returned to the calling code and passed to the CodeWriter
class.
Invoking 'With Function' and the AddFunction Function
At this stage we are not including any event demonstration code, notice the overloaded version of the AddFunction
, so I've removed that section for now from the following code:
Private Function AddFunction( _
ByVal includeEvent As Boolean) As CodeMemberMethod
Dim FunctionMember As New CodeMemberMethod()
With FunctionMember
.Name = "TransferFunds"
.Statements.Add(New CodeCommentStatement( _
"Put the body of the function here that commits the transfer."))
.Attributes = MemberAttributes.Public
.Statements.Add(New CodeMethodReturnStatement( _
New CodeSnippetExpression("true")))
.ReturnType = New CodeTypeReference(GetType(Boolean))
End With
Return FunctionMember
End Function
Once a CodeMemberMethod
has been created, (a CodeMemberMethod
defines both a sub or a function, the differentiation is made by the ReturnType
property), we need to set a few basic properties such as Name and Attributes (scope) and the ReturnType (Boolean).
We also have two Statements
to add to the function, the first is just a comment using the CodeCommentStatement
object, that states any futher function code should go here, in this case the code would action the transfer of money, if we were going into that much detail. The second is the return statement, not surprisingly using the CodeDOM CodeMethodReturnStatement
object, and a CodeSnippetExpression
(which allows us to add any text we wish) containing the return value of 'True'.
The CodeMemberMethod
is then returned from this function.
Invoking 'With Event and the AddEvent Function.
There are three parts to adding the demonstration of events. Invoking the OnOverDrawn
sub which raises the Event OnOverDrawn
(the addition to the previous function that calls the OnOverDrawn
sub). Having a protected
sub that raises the event means that if the class is inherited from a different action can be taken and also any logic required around raising the event can be placed within the sub which means there is only one section of code to maintain/change and all the calling code will benefit from the change. Finally the definition of the OverDrawn Event
.
If we look at these three steps in a sensible order we should start with the definition of the Event:
Private Function AddEvent() As CodeMemberEvent
Dim OverDrawnEvent As New CodeMemberEvent()
With OverDrawnEvent
.Name = "OverDrawn"
.Attributes = MemberAttributes.Public
.Type = New CodeTypeReference(GetType(EventHandler))
End With
Return OverDrawnEvent
End Function
This time we use a CodeMemberEvent
object and set the name and Attributes
properties. The type of the Event will be an EventHandler
, this is set by creating a CodeTypeReference
object.
Then we just return the CodeMemberEvent
back to the calling code.
Lets look at the definition of the OnOverDrawn
sub:
Private Function AddOnOverDrawnSub() As CodeMemberMethod
Dim OnOverDrawnSub As New CodeMemberMethod()
With OnOverDrawnSub
.Attributes = MemberAttributes.FamilyOrAssembly
.Name = "OnOverDrawn"
.Parameters.Add(New CodeParameterDeclarationExpression(GetType( _
EventArgs), "e"))
Dim OverDrawnEventRef As New CodeEventReferenceExpression( _
New CodeThisReferenceExpression(), "OverDrawn")
Dim OverDrawnEventInvoke As New CodeDelegateInvokeExpression()
With OverDrawnEventInvoke
.TargetObject = OverDrawnEventRef
.Parameters.Add(New CodeThisReferenceExpression())
.Parameters.Add(New CodeVariableReferenceExpression("e"))
End With
.Statements.Add(OverDrawnEventInvoke)
OverDrawnEventInvoke = Nothing
OverDrawnEventRef = Nothing
End With
Return OnOverDrawnSub
End Function
As with AddFunction
we have created a CodeMemberMethod
but this time with no return type and the Attributes property is set to FamilyOrAssembly
meaning it is protected. This method accepts a parameter called e
and is of type EventArgs
as you can see this is achieved by the use of a CodeParameterDeclarationExpression
object. We then need to reference the Event OverDrawn
using a CodeEventReferenceExpression
. This Event reference is added to the Target Property of the CodeDelegateInvokeExpression
object, also parameters of Me
and e
are added to the object. This Delegate invocation is added to the methods Statement
collection and the Sub is passed back to the calling code.
Here is the extra section of AddFunction
that is added by the overloaded AddFunction with a parameter value of True -
Dim CodeConditionStatement As New CodeConditionStatement()
With CodeConditionStatement
.Condition = New CodeBinaryOperatorExpression( _
New CodeFieldReferenceExpression( _
New CodeThisReferenceExpression(), "mCurrentBalance"), _
CodeBinaryOperatorType.LessThan, _
New CodePrimitiveExpression(0))
Dim OnOverDrawnMethodInvoke As New CodeMethodInvokeExpression()
OnOverDrawnMethodInvoke.Method = New CodeMethodReferenceExpression( _
New CodeThisReferenceExpression(), "OnOverDrawn")
OnOverDrawnMethodInvoke.Parameters.Add(New CodeSnippetExpression( _
"System.EventArgs.Empty"))
.TrueStatements.Add(OnOverDrawnMethodInvoke)
End With
.Statements.Add(CodeConditionStatement)
We need to create a CodeConditionStatement
that will decide if the OnOverDrawn
sub should be called. The condition needs to contain a CodeBinaryOperatorExpression
that checks to see if the field reference to mCurrentBalance
is less than zero (I've used CodePrimitiveExpression
to include the integer value of '0' in the comparison).
Next we need to create the Statement
to be added to the True part of the comparison (we have no need to define a False statement). This will mean building a CodeMethodInvokeExpression
object. The Method
property of the CodeMethodInvokeExpression
will reference the method OnOverDrawn
. Also we need to pass the required parameter into the method, we'll do this with the following line of code -
CodeSnippetExpression("System.EventArgs.Empty"))
The condition is now complete and is added to the functions Statement
collection.
The explanation of the Event calling code means that all sections of the test application have now been covered.
I hope that this small demonstration helps illustrate how to utilise much of the functionality included in the CodeDOM to achieve the code structures that you will undoubtedly require in any CodeDOM projects you embark on yourselves.
To conclude this section of building CodeDOM graphs it has to be said that there is a lot of work required to build event the simplest of elements such as an event of property. But in order to achieve a totally language independent representation of the code you wish to output it is obvious that this is something we have to put up with.
The examples, I feel so far, haven't been complicated but have covered a lot of ground.
Next we'll go one to look a list of the limitations I have experienced so far within the CodeDOM.
Limitations
Below I have listed (in no particular order), limitations that I have encounted in version 1 of the .NET Framework (and in some cases 1.1).Where ever posible I have tied to add workarounds.
Limitation | Workaround |
Adding Comments other than atop methods, classes or namespaces is a known limitation. | Add a CodeSnippetTypeMember containing the literal text for the comment to the Members collection (in the appropriate order to appear in the proper location). The drawback of this approach is that the comment syntax will not be translated across languages. Build your graph dynamically to substitute comment snippets using language-specific syntax if robust multiple language support for comments between type members is important. |
IParser is not implemented or throws errors for both C# or VB | No workaround is needed, as this certainly does not stop you from compiling. The limitation is that you have to ensure the code that you try to compile is statically correct rather than allowing IParser to do the checking for you. |
Different languages need specific elements sometimes | A Case statement to insert extra code for certain languages such as Start Point in C# (as mentioned in this articles example). |
Using the Alias directive | Nested namespaces |
The CodeDom namespaces contain classes to conceptually represent most programming constructs. Examples of these include: declarations, statements, iterations, arrays, casts, comments, error handling, and others. However, there are limitations to the current implementation of the CodeDOM, which will remain until Microsoft updates the CodeDom namespaces | To represent constructs not presently supported in CodeDOM you can use the "Snippet" classes. These classes allow you to insert literal lines of source code into a compile unit. The classes available for handling snippets include: the CodeSnippetCompileUnit , CodeSnippetExpression , CodeSnippetStatement , and CodeSnippetTypeMember classes. You can use the snippet classes as a generic "catch all" for implementing presently unsupported programming constructs in the CodeDom. |
Variable/Field declaration list (int i,j,k;) | Change to individual variable declarations. Really to keep code totally readable this is a preferred method anyway. |
Pointer type | Jagged array type (array of arrays). |
|
Also:
- Unsafe modifier not supported.
- ReadOnly modifier not supported.
- Volatile modifier not supported.
- Add and Remove accessors for Event not supported.
Useful links
An example of taking C# code and building a CodeDOM tree.
Microsoft .NET CodeDom Technology - Part 1
Microsoft .NET CodeDom Technology - Part 2
If you do not need source code, you can use the classes in System.Reflection.Emit This namespace is designed to be used by script engines and compilers to generate MSIL code or complete assemblies on disk.
Next Time, Part 2: Introduction to the Code Model