Click here to Skip to main content
Click here to Skip to main content

CodeDom Assistant

By , 21 Sep 2007
 
Screenshot - codedomassistant.gif

Introduction

I find writing CodeDom code kind of like writing assembly. It's like cutting the grass with a pair of scissors or unloading a truck load of sand with a spoon. Its long winded, it can be done, but it is tedious. However, there are advantages in using CodeDom.

Now, it would be nice if you could whip up a class in VB or C# and generate the CodeDom code. CodeDom Assistant does this with the help of SharpDevelop's NRefactory Library in conjunction with writing our own CodeDomProvider to generate C# CodeDom code. The code you get will construct a CodeDom compileunit, and it will definitely be ugly. But what can you expect from a machine?

Background

Code Generation can be done using various techniques. In essence, code generation saves time, and enables you to create code using established patterns.

In the world of .NET, one technique is to use CodeDom. It allows you create a document object model of the code. This can by either compiled into an assembly or used to generate code. Every .NET language should have an implementation of a CodeDomProvider to generate code. The problem is that CodeDom does not implement every syntax construct of every language. In essence, it is a subset, but there are enough operations to emulate most language constructs.

Parsing C# or VB is going to be the toughest part, but luckily one of SharpDevelop's little libraries is NRefactory. This has the machinerary required to parse C# or VB code. The key component for us is the CodeDomVisitor class. Sadly, it is only a partial implementation. They implement enough to run SharpDevelop's form generation. I am not saying I have completed the implementation, rather, I have filled most of the holes in the CodeDomVisitor.

The second part is implementing a CodeDomCodeProvider. This will take a CodeDom compile unit and generate C# code that will create a CodeDom compile unit.

Alternatives To Unsupported CodeDom Constructs

When converting C# to CodeDom, certain constructs will not be able to be mapped directly into a CodeDom element. Rather we have to emulate the intent of the code. I am sure better transformations can be made, but these are sufficient for my needs. For example: int a = b++; is definitely not the same as the transformed CodeDom int a = (b = (b + 1));.

Not all constructs are going to be able to be transformed perfectly into CodeDom; they are merely sufficient for my needs. When I originally wrote the code in C# I did so with these limitations in mind. For example:

The foreach statement is handled using the for iterator and GetEnumerator(). Behold:

foreach (string s in mylist)
{
}

Can be constructed in CodeDom as:

for (System.Collections.IEnumerator _it1 = mylist.GetEnumerator(); 
    _it1.MoveNext(); )
{
    string s = ((string)_it1.Current);
}

The do statement is handled using the for iterator. For example:

do
{
}
while (expr);

Can be constructed in CodeDom as:

for (bool _do1 = true; _do; _do = expr)
{
}

The while statement is handled again with the for iterator. For example:

while (expr)
{
}

Can be constructed in CodeDom as

for (; expr; )
{
}

The continue and break statements are handled using the goto statements. For example:

for (int i = 0; i < 5; i++)
{
    if (i < 2)
        continue;

    if (i == 3)
        break;
}

Can be constructed in CodeDom as:

for (int i = 0; i < 5; i = i + 1)
{
    if (i < 2)
        goto continue1;

    if (i == 3)
        goto break1;

continue1:
}
break1:

Unary operators: i++; ++i; i--; --i; !; can be constructed using the binary operators: i = i + 1; etc..

The switch statement becomes a nested if statement.

switch(expr)
{
case label1:
    break;

case label2:
    break;

default:
}

Can be constructed in CodeDom as:

object _switch1 = expr;

if (_switch1.Equals(label1))
{
}
else
{
    if (_switch1.Equals(label2))
    {
    }
    else
    {
    }
}

The using(new a()) {} statement can be emulated with:

object _dispose1 = null;
try
{
    _dispose1 = new a();
}
finally
{
    if (((_dispose1 != null) 
       && (typeof(System.IDisposable).IsInstanceOfType(_dispose1) == true)))
    {
        (System.IDisposable)(_dispose1)).Dispose();
    }
}

Constructing CodeDom Using NRefactory

The generate function uses NRefactory parser. NRefactory supports C# and VB. I tend to use C#, and have not tested the VB. The OutputClass is essentially the output combobox selection. There are native C# and VB NRefactory output transformations, which are much more complete than CodeDom. For other languages we use the CodeDomVisitor which maps the NRefactory parser output to CodeDom. The CodeDom CodeCompileUnit is then passed to a CodeDomProvider which generates output.

void Generate(ICSharpCode.NRefactory.SupportedLanguage language, 
    TextReader inputstream, OutputClass output)
{
    ICSharpCode.NRefactory.IParser parser = ParserFactory.CreateParser(
        language, inputstream);
    parser.Parse();

    if (parser.Errors.Count > 0)
    {
        ICSharpCode.Core.ExceptionDialog dlg = 
             new ICSharpCode.Core.ExceptionDialog(
             null, "Error Parsing Input Code");
        dlg.ShowDialog();
        return;
    }

    if (output.CodeDomProvider != null)
    {
        CodeDomVisitor visit = new CodeDomVisitor();

        visit.VisitCompilationUnit(parser.CompilationUnit, null);

        // Remove Unsed Namespaces
        for (int i = visit.codeCompileUnit.Namespaces.Count - 1; i >= 0; i--)
        {
            if (visit.codeCompileUnit.Namespaces[i].Types.Count == 0)
            {
                visit.codeCompileUnit.Namespaces.RemoveAt(i);
            }
        }

        CodeGeneratorOptions codegenopt = new CodeGeneratorOptions();
        codegenopt.BlankLinesBetweenMembers = true;

        System.IO.StringWriter sw = new System.IO.StringWriter();

        output.CodeDomProvider.GenerateCodeFromCompileUnit(
            visit.codeCompileUnit, sw, codegenopt);

        this.scintillaOutput.Text = sw.ToString();

        sw.Close();
    }
    else
    {
        AbstractAstTransformer transformer = output.CreateTransformer();

        // do what SharpDevelop does...
        List<ISpecial> specials = 
            parser.Lexer.SpecialTracker.CurrentSpecials;
        if (language == SupportedLanguage.CSharp && 
            transformer is ToVBNetConvertVisitor)
        {
            PreprocessingDirective.CSharpToVB(specials);
        }
        else if (language == SupportedLanguage.VBNet && 
            transformer is ToCSharpConvertVisitor)
        {
            PreprocessingDirective.VBToCSharp(specials);
        }

        parser.CompilationUnit.AcceptVisitor(transformer, null);

        IOutputAstVisitor prettyprinter = output.CreatePrettyPrinter();

        using (SpecialNodesInserter.Install(specials, prettyprinter))
        {
            prettyprinter.VisitCompilationUnit(parser.CompilationUnit, null);
        }

        this.scintillaOutput.Text = prettyprinter.Text;
    }
}

The NRefactory CodeDomVisitor class needed to be updated to implement more C# constructs. I am sure I have have missed a few.

Generating C# Code CodeDom

All CodeDom compile units can be passed to any CodeDomProvider to generate code. So, we write a new CodeDom provider that will generate CodeDom code for us. The implementation is quite simple. The main job of the CodeDomCodeProvider is to create an ICodeGenerator.

public class CodeDomCodeProvider : System.CodeDom.Compiler.CodeDomProvider
{
    public CodeDomCodeProvider()
            : base()
        {
        }

    public override string FileExtension
    {
        get
        {
            return "cs";
        }
    }

    public override System.CodeDom.Compiler.ICodeGenerator CreateGenerator()
    {
        return new CodeGenerator();
    }

    public override System.CodeDom.Compiler.ICodeCompiler CreateCompiler()
    {
        return null;
    }
}

The CodeGenerator then traverses the CodeDom elements and outputs code that will reconstruct the tree when compiled.

Using Other CodeDomProviders

On initialisation CodeDom Assistant scans the GAC to find assemblies that have an implementation of CodeDomProvider. Since the output of the parsed C#/VB is CodeDom the output can be generated by any CodeDomProvider. So it could act as an effective language Convertor.

Points of Interest

You can only convert code that compiles. It is not very good at telling why it did not compile. The other issue I have found is that it may parse the file, but the CodeDom it produces is incomplete. This may cause the CodeDomProvider CodeGenerator to barf. Usually with some obscure error, like e value is null.

The output of CodeDomCodeProvider is ugly, but assists you in the construction of creating code generators with CodeDom. A lot of our code generation is done through the meta-data of a database. So this code would only be a starting point.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

raygilbert
Web Developer
Australia Australia
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralFantastic ToolmemberAkleinek25 Sep '08 - 3:54 
This tool is exectly what I am looking for. Cool | :cool:
 
I only have two suggestion for the next version:
1) Provide some comments in the generated CodeDomCodeProvider output. The orgin code would be usefull to find snippets in the output
2) Make the variables in the CodeDomCodeProvider output more unique. Like _method1_arg1 instead of _arg1. Addons to the orgin code can easier been made.
 
But still: Good job!!!
Generallogic bug in GenerateCodeTypeReference building string for arraymemberdrdandle15 Aug '08 - 7:20 
Hi,
 
I was reading in the following vb.net code (using CodeDomCodeProvider)
Class test
	Private Sub testArray()
		Dim intI() As Short
		ReDim Preserve intI(1)
		intI(0) = 1
		intI(1) = 2
	End Sub
End Class
 
The output that was messed up was (this is not all the output just the code of interest):
CodeMemberMethod _testArray_method1 = new CodeMemberMethod();
_testArray_method1.Attributes = MemberAttributes.Assembly|MemberAttributes.FamilyOrAssembly|MemberAttributes.Private;
_testArray_method1.Name = "testArray";
CodeVariableDeclarationStatement _decl1 = new CodeVariableDeclarationStatement();
_decl1.Name = "intI";
CodeTypeReference _System_Int16_type1 = new CodeTypeReference("System.Int16", 1);
CodeTypeReference _System_Int16_type2 = new CodeTypeReference("System.Int16");
.ArrayElementType.Add(_System_Int16_type2);
 
Notice the last line started with just a period.
 
I think I have fixed the problem by adding a line of code to:
 
string GenerateCodeTypeReference(string context, CodeTypeReference typeref, System.IO.TextWriter w, System.CodeDom.Compiler.CodeGeneratorOptions o)
 
The change I made prevents a recursive call if context is null or has a length of 0. Here is the entire changed routine.
 

 

 
string GenerateCodeTypeReference(string context, CodeTypeReference typeref, System.IO.TextWriter w, System.CodeDom.Compiler.CodeGeneratorOptions o)
{
    string name = GetNextVar(typeref.BaseType + "_type");
 
    CodeTypeReference elementType = typeref.ArrayElementType;
    if (elementType == null)
    {
        w.WriteLine(@"CodeTypeReference {0} = new CodeTypeReference(""{1}"");", name, typeref.BaseType);
    }
    else
    {
        w.WriteLine(@"CodeTypeReference {0} = new CodeTypeReference(""{1}"", {2});", name, typeref.BaseType, typeref.ArrayRank);
 
        if (context != null && context.Length > 0) //If statement added by RMD 2008_08_15 to prevent outputting if there is nothing to output
            GenerateCodeTypeReference(string.Format(@"{0}.ArrayElementType", context), typeref.ArrayElementType, w, o); 
    }
 
    if (context != null && context.Length > 0)
    {
        w.WriteLine(@"{0}.Add({1});", context, name);
        w.WriteLine();
    }
 
    return name;
}<pre> 

QuestionNot outputting eventsmemberdrdandle11 Aug '08 - 4:38 
Hi,
 
Thanks so much for writing this article, it has helped me out a ton.
 
I have found that events seem to be getting lost in the output code. Here is an example:
 
Input Code:
 
Option Strict Off
Option Explicit On
Imports Microsoft.VisualBasic.PowerPacks
Friend Class Form1
	Inherits System.Windows.Forms.Form
 
    Private Sub Command1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Command1.Click
        Me.Text1.Text = "button pressed"
    End Sub
End Class
 

 

Then output code with CodeDomCodeProvider (as you can see events are there):
// CodeDom Compile Unit
CodeCompileUnit _compileunit1 = new CodeCompileUnit();
 

//
// Namespace Global
//
CodeNamespace _Global_namespace1 = new CodeNamespace("Global");
 
// Imports
_Global_namespace1.Imports.Add(new CodeNamespaceImport("Microsoft.VisualBasic.PowerPacks"));
 
//
//
// class Form1
//
CodeTypeDeclaration _Form1_class1 = new CodeTypeDeclaration("Form1");
_Form1_class1.Attributes = MemberAttributes.Final|MemberAttributes.Assembly|MemberAttributes.FamilyOrAssembly|MemberAttributes.Private;
_Form1_class1.IsClass = true;
CodeTypeReference _System_Windows_Forms_Form_type1 = new CodeTypeReference("System.Windows.Forms.Form");
_Form1_class1.BaseTypes.Add(_System_Windows_Forms_Form_type1);
 

//
// Function Command1_Click(System.Object sender, System.EventArgs e)
//
CodeMemberMethod _Command1_Click_method1 = new CodeMemberMethod();
_Command1_Click_method1.Attributes = MemberAttributes.Assembly|MemberAttributes.FamilyOrAssembly|MemberAttributes.Private;
_Command1_Click_method1.Name = "Command1_Click";
CodeTypeReference _System_Object_type1 = new CodeTypeReference("System.Object");
CodeParameterDeclarationExpression _sender_arg1 = new CodeParameterDeclarationExpression(_System_Object_type1, "sender");
_sender_arg1.Direction = FieldDirection.In;
_sender_arg1.Name = "sender";
CodeTypeReference _System_Object_type2 = new CodeTypeReference("System.Object");
_sender_arg1.Type = _System_Object_type2;
_Command1_Click_method1.Parameters.Add(_sender_arg1);
 
CodeTypeReference _System_EventArgs_type1 = new CodeTypeReference("System.EventArgs");
CodeParameterDeclarationExpression _e_arg1 = new CodeParameterDeclarationExpression(_System_EventArgs_type1, "e");
_e_arg1.Direction = FieldDirection.In;
_e_arg1.Name = "e";
CodeTypeReference _System_EventArgs_type2 = new CodeTypeReference("System.EventArgs");
_e_arg1.Type = _System_EventArgs_type2;
_Command1_Click_method1.Parameters.Add(_e_arg1);
 
CodeAssignStatement _assign1 = new CodeAssignStatement();
CodePropertyReferenceExpression _prop1 = new CodePropertyReferenceExpression();
_prop1.PropertyName = "Text";
CodePropertyReferenceExpression _prop2 = new CodePropertyReferenceExpression();
_prop2.PropertyName = "Text1";
CodeThisReferenceExpression _this1 = new CodeThisReferenceExpression();
_prop2.TargetObject = _this1;
_prop1.TargetObject = _prop2;
_assign1.Left = _prop1;
CodePrimitiveExpression _value1 = new CodePrimitiveExpression();
_value1.Value = "button pressed";
_assign1.Right = _value1;
_Command1_Click_method1.Statements.Add(_assign1);
 
_Form1_class1.Members.Add(_Command1_Click_method1);
 
_Global_namespace1.Types.Add(_Form1_class1);
 
_compileunit1.Namespaces.Add(_Global_namespace1);
 

 

Test CodeDom output using the VBCodeProvider (no Handles keyword after the procedure declaration):
 
Option Strict Off
Option Explicit On
 
Imports Microsoft.VisualBasic.PowerPacks
 
Namespace [Global]
    
    Public Class Form1
        Inherits System.Windows.Forms.Form
        
        Private Sub Command1_Click(ByVal sender As Object, ByVal e As System.EventArgs)
            Me.Text1.Text = "button pressed"
        End Sub
    End Class
End Namespace
 

 

 
Does anyone have any ideas on how to get the output code to include the events? This might be something to do with the fact that C# wires up the events in the Designer.cs file instead of in the form file like vb does.
 
Thanks
 
Randy
GeneralUse your code:SampleCode.cs to test,Errormemberguaike15 Jun '08 - 16:28 
Could not load file or assembly 'file:///C:\CodeDom Assistant\CodeDomAssistant_Demo\script_44d3a227-aaf1-431b-a788-c14c939c8037.dll' or one of its dependencies. The system cannot find the file specified.
 
if i removed this method:
 
public void TestForEach()
{
List mylist = new List();
mylist.Add("test1");
mylist.Add("test2");
mylist.Add("test3");
 
foreach (string t in mylist)
{
if (t == "test2")
break;
}
}
it work fine,so I think it can't process the Generic Type

GeneralVistax64memberMember 456280616 Apr '08 - 0:51 
It does not run at all!
GeneralRe: Vistax64memberchprogmer14 Apr '11 - 5:03 
Personally, under Win7-64 bits and VS 2010 C# express, I had to:
1. Change the target from AnyCPU to x86 (if not, I have an error 193 '%1 is not a valid win32 DLL').
2. Update the sources of ScintillaNET.
Now it works fine.
Generallanguage convertormembersardarkhan27 Mar '08 - 20:39 
i need coding for language translator english to arabic in C# .Net if any body knows the coding for desired software please send me at my ID m.s.kworld81@gotmail.com , msardar999@yahoo.com. i will be vey thankful to you people helping me in my project
GeneralI looked everywhere for this!!!memberjbosltad8 Jan '08 - 10:36 
This is just what I wanted!!! I can't thank you enough. Please put this into the main code of SharpDevelop, so it will be easily found.
 
Thanks again.
Generalthis is amazingmemberdave.dolan28 Nov '07 - 10:21 
Your code can 'write' my compiler for me! This is great!
GeneralGreat Stuffmembersefstrat6 Nov '07 - 11:35 
I am writing a custom serializer to modify the CodeDom and your tool saved me a lot of frustration, many thanks!
GeneralNice articlel, but ...memberAdar Wesley1 Oct '07 - 9:35 
Hi,
 
Nice article showing how to use NRefactory.
 
However, the insentive for generating CodeDom code is not clear to me.
 
If I understand correctly, your motivation was to have (dynamic) code that will
generate the desired CodeCompileUnit at runtime. I see two easy solutions to this.
1. Your code that parses the C# already does exactly this. All you have to do is
save the original C# as data and run the same parsing code at runtime. The C#
code functions as a Template which is parsed at runtime and your code creates
CodeCompileUnit from it.
 
2. The other option I see is to use the fact that CodeCompileUnit is marked as
Serializable and just serialize it after the first parsing. At runtime you
can load the serialized data and Deserialize to create the CodeCompileUnit runtime
instance. (I have not tried it, but it should work.)
 
So, did I miss a hidden reason for generating the CodeDom code?
 

 
---
Adar Wesley
GeneralRe: Nice articlel, but ...memberdave.dolan28 Nov '07 - 10:31 
you can write code in one language, parse it to code dom and output it in any language that has a codedom provider, just one example
GeneralI know I'm doing something wrong....memberednrgc24 Sep '07 - 6:28 
I know I'm doing something wrong, but I'm getting an error whenever I try to test any codedom code. Any help is greatly appreciated.
 
Error Description:
 
Could not load file or assembly 'file:///C:\CodeDom Assistant\CodeDomAssistant_Demo\script_44d3a227-aaf1-431b-a788-c14c939c8037.dll' or one of its dependencies. The system cannot find the file specified.
at System.Reflection.Assembly.nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, Assembly locationHint, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, Boolean forIntrospection)
at System.Reflection.Assembly.InternalLoad(AssemblyName assemblyRef, Evidence assemblySecurity, StackCrawlMark& stackMark, Boolean forIntrospection)
at System.Reflection.Assembly.InternalLoadFrom(String assemblyFile, Evidence securityEvidence, Byte[] hashValue, AssemblyHashAlgorithm hashAlgorithm, Boolean forIntrospection, StackCrawlMark& stackMark)
at System.Reflection.Assembly.LoadFrom(String assemblyFile, Evidence securityEvidence)
at System.Activator.CreateInstanceFrom(String assemblyFile, String typeName, Boolean ignoreCase, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes, Evidence securityInfo)
at RemoteLoader.RemoteLoaderFactory.Create(String assemblyFile, String typeName, Object[] constructArgs) in c:\zips\C# Source\CodeDom Assistant\sln.CodeDomAssistant\RemoteLoader\RemoteLoaderFactory.cs:line 24
at RemoteLoader.RemoteLoaderFactory.Create(String assemblyFile, String typeName, Object[] constructArgs)
at CodeDomAssistant.CSharpSnippet.Execute(String method, Object[] args) in c:\zips\C# Source\CodeDom Assistant\sln.CodeDomAssistant\CodeDomAssistant\CSharpSnippet.cs:line 106
at CodeDomAssistant.FormCodeAssistant.CompileTestCodeDom
GeneralRe: I know I'm doing something wrong....memberraygilbert24 Sep '07 - 12:52 
To test the script I compile a temporary assembly on the fly. So error usually occurs when the C# has an incomplete conversion to CodeDom. I would then track down the conversion error and implement it the CodeDomOutputVisitor class.
GeneralRe: I know I'm doing something wrong....memberednrgc25 Sep '07 - 1:45 
It's happening with your sample file. So, I'm stumped.

GeneralExcellentmembermcory224 Sep '07 - 5:55 
I've been working on a code generator myself lately, an open source scripting language, and I wish I would've seen this a little sooner -- would've saved me a bit of hassle with some of the generation stuff. And you're completely right -- CodeDom is really nice, but extremely tedious.
 
One thing about the using(blah) {} "translation" -- why are you creating a plain object and then checking if it implements IDisposable, instead of an instance of a specific class? I'm sure if you're writing a system that's capable of parsing, it can most certainly determine if the class being used implements IDisposable and then just call Dispose(). And, if it's a pre-determined class, you'd already know ahead of time that you could (or could not) call Dispose().
 
It'd save about twenty lines of CodeDom generation Smile | :) (well, probably less, but definitely feels like more...)
GeneralRe: Excellentmemberraygilbert24 Sep '07 - 13:16 
Yes, you are right. I should have read the docs "The object provided to the using statement must implement the IDisposable interface". So I can cast it directly to IDisposable instead of object.
 
Thx
GeneralNice articlememberDaniel Grunwald22 Sep '07 - 9:04 
Great article!
 
Nice to see NRefactory used outside SharpDevelop.
 
Could you follow the steps in http://wiki.sharpdevelop.net/JoiningTheTeam.ashx[^] so that we can merge your changes to CodeDomVisitor into the official NRefactory release?
 
And you should use parser.Errors.ErrorOutput to get more information about parser errors.
The NRefactory error messages are not as nice as the C# compiler messages, but at least you get line/column numbers where the syntax error is.

 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 21 Sep 2007
Article Copyright 2007 by raygilbert
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid