May 112010
 

This is the third part of a series covering code generators in Visual Studio 2008. Part I and part II of this series cover how to create the project and all the necessary steps for writing a code generator.

The series is divided into four parts:

Part I  – creating a Visual Studio Package

Part II  – creating and registering a code generator

Part III (this one) – generating code & debugging

Part IV – creating the setup project and deploying the package

Generating the code and debugging

So far we’ve created a Visual Studio integration package and a skeleton code generator. The next step will be to actually generate some code.

Before we do that, we must decide on the file format and the output.

For simplicity, let’s assume we will  have plain text files which define constants. The first line will have the full class name (including namespace):

MyNamespace.MyClass
Red=1
Blue=2
Pink=3

The output for the above example could be:

namespace MyNamespace
{
	public class MyClass
	{
		public const int Red = 1;
		public const int Blue = 2;
		public const int Pink = 3;
	}
}

First we need to modify the GetDefaultExtension() method and make it return a file extension. I like the idea of making it clear when the code is auto generated by a designer, so we’ll return “.designer.cs” as the extension.

Second we need to parse the input file. To make this blog post shorter, I will skip over the parsing details, but you can check the source zip for how we parse the file.

To generate the code we will use CodeDom which makes things easier. Right now the class should look like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TextTemplating.VSHost;
using Microsoft.VisualStudio.Shell;
using VSLangProj80;
using System.Diagnostics;
using System.IO;
using System.CodeDom.Compiler;
using System.CodeDom;

namespace MyCompany.MyCodeGenerator
{
    [ComVisible(true)]
    [Guid(GuidList.guidMyCodeGenerator)]
    [CodeGeneratorRegistration(typeof(MyCodeGenerator), ".myfile", vsContextGuids.vsContextGuidVCSProject, GeneratesDesignTimeSource = true, GeneratorRegKeyName = ".myfile")]
    public class MyCodeGenerator : BaseCodeGeneratorWithSite
    {
        protected override byte[] GenerateCode(string inputFileName, string inputFileContent)
        {
            var provider = CodeDomProvider.CreateProvider("C#");

            try
            {
                var parser = new ConstantsDefinitionParser(inputFileContent);
                var definition = parser.Parse();

                var compileUnit = GenerateCode(definition);
                using (StringWriter writer = new StringWriter())
                {
                    CodeGeneratorOptions options = GetCodeOptions();

                    provider.GenerateCodeFromCompileUnit(compileUnit, writer, options);
                    //Generate the code
                    writer.Flush();

                    return GetContentBytes(writer);
                }
            }
            catch (Exception e)
            {
                HanldeException(e);

                //Returning null signifies that generation has failed
                return null;
            }
        }

        private CodeCompileUnit GenerateCode(ConstantsDefinition definition)
        {
            throw new NotSupportedException();
        }

        private static CodeGeneratorOptions GetCodeOptions()
        {
            CodeGeneratorOptions options = new CodeGeneratorOptions();
            options.BlankLinesBetweenMembers = true;
            options.BracingStyle = "C";
            return options;
        }

        private void HanldeException(Exception e)
        {
            StackTrace exceptionStackTrace = new StackTrace(e, true);
            StackFrame exceptionStackFrame = exceptionStackTrace.GetFrame(-2);
            int lineNumber = exceptionStackFrame.GetFileLineNumber();
            int column = exceptionStackFrame.GetFileColumnNumber();
            base.GeneratorErrorCallback(false, 4, e.ToString(), lineNumber, column);
        }

        public override string GetDefaultExtension()
        {
            return ".designer.cs";
        }

        private static byte[] GetContentBytes(StringWriter writer)
        {
            //Get the Encoding used by the writer. We're getting the WindowsCodePage encoding,
            //which may not work with all languages
            Encoding enc = Encoding.GetEncoding(writer.Encoding.WindowsCodePage);

            //Get the preamble (byte-order mark) for our encoding
            byte[] preamble = enc.GetPreamble();
            int preambleLength = preamble.Length;

            //Convert the writer contents to a byte array
            byte[] body = enc.GetBytes(writer.ToString());

            //Prepend the preamble to body (store result in resized preamble array)
            Array.Resize<byte>(ref preamble, preambleLength + body.Length);
            Array.Copy(body, 0, preamble, preambleLength, body.Length);

            //Return the combined byte array
            return preamble;
        }

    }
}

Most of the code is self-explanatory. The GetContentBytes method is used to convert the string to it’s binary equivalent and to prepend (if necessary) the byte order mark to it.

The whole code generation magic will happen in the GenerateCode(definition) method:

 private CodeCompileUnit GenerateCode(ConstantsDefinition definition)
{
	throw new NotSupportedException();
}

Although CodeDom makes it rather easy to generate code, there is a library called LinqToCodedom on CodePlex that provides a whole set of extension methods and classes that help creating dynamic code.

The GenerateCode method needs to create a namespace, add a class definition to it and then add definitions for each of the defined constants. As you can see below, the code is really easy to make and to understand, but a bit verbose if the classes are complex:

        private CodeCompileUnit GenerateCode(ConstantsDefinition definition)
        {
            var generator = new CodeDomGenerator();

            // create the namespace in which the class resides
            var codeNamespace = generator.AddNamespace(definition.Namespace);

            // create the class definition and set it's attributes
            var classDefinition = codeNamespace.AddClass(definition.ClassName);
            classDefinition.IsClass = true;
            classDefinition.TypeAttributes = System.Reflection.TypeAttributes.Public;

            AddConstantDefinitions(definition, classDefinition);

            return generator.GetCompileUnit(CodeDomGenerator.Language.CSharp);
        }

        private static void AddConstantDefinitions(ConstantsDefinition definition, CodeTypeDeclaration classDefinition)
        {
            for (var i = 0; i < definition.Constants.Count; i++)
            {
                var constantName = definition.Constants.GetKey(i);
                var constantValue = definition.Constants.GetValues(i)[0];

                var constant = new CodeMemberField(typeof(int), constantName);
                constant.Attributes = MemberAttributes.Public | MemberAttributes.Const;

                constant.InitExpression = new CodePrimitiveExpression(Convert.ToInt32(constantValue));

                classDefinition.Members.Add(constant);
            }
        }

Debugging the code

Fortunately debugging code generators is very easy due to a feature of Visual Studio called the “experimental hive”. Basically there is a copy of the related registry keys which can be used by VisualStudio when debugging instead of the main registry (to prevent hard to diagnose and fix problems). The Visual Studio SDK comes with a command line tool called ‘Reset the Microsoft Visual Studio 2008 Experimental hive’ which, as its name implies, resets this copy of the registry. So if something goes wrong, you can easily go back to the initial state of the experimental hive registry.

If you compile and run the code, a new Visual Studio window will appear. Create a new C# project (a ClassLibrary or Console project) and add a new text file to it. Name the file test.myfile and paste the definition into it. The moment you are saving the file, an automated file is created. If you’ve used the definition above, the generated file should look like this:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.4927
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace MyNamespace
{

    public class MyClass
    {

        public const int Red = 1;

        public const int Blue = 2;

        public const int Pink = 3;
    }
}

Stay tuned for part 4 of this series in which I go into creating a setup package and deploying the generator.

Downloads

 Easy creation of code generators in Visual Studio 2008 (part 3 of 4)