Thursday, September 21, 2006

How to examine code and write a class with EnvDTE

Further to my experiments with the Guidance Automation Toolkit, I've been playing with generating code with my custom guidance package. Looking at the Service Factory GAT that's been released by the Patterns and Practices group, they use three different techniques for code generation; T4 templates, EnvDTE and CodeDom. If they use all three, I wondered which one I should be using. I've previously used the CodeDom in other projects and although it's very powerfull, you use it to generate the syntactic structure of the code and can then generate C#, VB or whatever, it is really long winded. T4 templates are at the opposite end of the spectrum, a bit like asp for code generation, you simply write a template of the code you want to generate and put code between <# #> marks that the template engine runs. The problem with it at the moment is that they are really new and the tools are there yet. There's no intellisense or code coloring for it and debugging isn't easy either.

So I decided to have a look at the EnvDTE Visual Studio automation class library for code generation. A lot of the GAT stuff seems to be built around it, so it's a natural fit for code generation duties. Unfortunately the documentation isn't that great, and this little demo of how to navigate a code file and write a class took much longer than it should have. But here it is, It gets the current visual studio environment and enumerates though all the projects and project items. It then examines itself outputting all the code elements and finally writes a new class inside its own namespace. If you try this out, make sure you name the file it's in 'HowToUseCodeModelSpike.cs'.

using System;
using NUnit.Framework;
using EnvDTE;
using EnvDTE80;

namespace Mike.Tests
{
    [TestFixture]
    public class DteSpike
    {
        [Test]
        public void HowToUseCodeModelSpike()
        {
            // get the DTE reference...
            DTE2 dte2 = (EnvDTE80.DTE2)System.Runtime.InteropServices.Marshal.GetActiveObject("VisualStudio.DTE.8.0");

            // get the solution
            Solution solution = dte2.Solution;
            Console.WriteLine(solution.FullName);

            // get all the projects
            foreach(Project project in solution.Projects)
            {
                Console.WriteLine("\t{0}", project.FullName);

                // get all the items in each project
                foreach(ProjectItem item in project.ProjectItems)
                {
                    Console.WriteLine("\t\t{0}", item.Name);

                    // find this file and examine it
                    if(item.Name == "HowToUseCodeModelSpike.cs")
                    {
                        ExamineItem(item);
                    }
                }
            }
        }

        // examine an item
        private void ExamineItem(ProjectItem item)
        {
            FileCodeModel2 model = (FileCodeModel2)item.FileCodeModel;
            foreach(CodeElement codeElement in model.CodeElements)
            {
                ExamineCodeElement(codeElement, 3);
            }
        }

        // recursively examine code elements
        private void ExamineCodeElement(CodeElement codeElement, int tabs)
        {
            tabs++;
            try
            {
                Console.WriteLine(new string('\t', tabs) + "{0} {1}", 
                    codeElement.Name, codeElement.Kind.ToString());

                // if this is a namespace, add a class to it.
                if(codeElement.Kind == vsCMElement.vsCMElementNamespace)
                {
                    AddClassToNamespace((CodeNamespace)codeElement);
                }

                foreach(CodeElement childElement in codeElement.Children)
                {
                    ExamineCodeElement(childElement, tabs);
                }
            }
            catch
            {
                Console.WriteLine(new string('\t', tabs) + "codeElement without name: {0}", codeElement.Kind.ToString());
            }
        }

        // add a class to the given namespace
        private void AddClassToNamespace(CodeNamespace ns)
        {
            // add a class
            CodeClass2 chess = (CodeClass2)ns.AddClass("Chess", -1, null, null, vsCMAccess.vsCMAccessPublic);
            
            // add a function with a parameter and a comment
            CodeFunction2 move = (CodeFunction2)chess.AddFunction("Move", vsCMFunction.vsCMFunctionFunction, "int", -1, vsCMAccess.vsCMAccessPublic, null);
            move.AddParameter("IsOK", "bool", -1);
            move.Comment = "This is the move function";

            // add some text to the body of the function
            EditPoint2 editPoint = (EditPoint2)move.GetStartPoint(vsCMPart.vsCMPartBody).CreateEditPoint();
            editPoint.Indent(null, 0);
            editPoint.Insert("int a = 1;");
            editPoint.InsertNewLine(1);
            editPoint.Indent(null, 3);
            editPoint.Insert("int b = 3;");
            editPoint.InsertNewLine(2);
            editPoint.Indent(null, 3);
            editPoint.Insert("return a + b; //");
        }
    }
}

13 comments:

Anonymous said...

You rock! Thanks for the starting point. Wish I found this post 6 hours ago!

Anonymous said...

In the examineitem method, you might want to add a check to see if the model is null before entering loop.

FileCodeModel2 model = (FileCodeModel2)item.FileCodeModel;

Mike Hadlow said...

Thanks Jay, you're welcome. And thanks for the tip :)

VSTASupport said...

Thanks,
This is helpful information for VSTA integrators

c.helder said...

Thanks for your help. I miss how to create a DTE element. The code is the following:
System.Type t = System.Type.GetTypeFromProgID("VisualStudio.DTE.8.0", true); //Note: don't put this call in a try block
object obj = System.Activator.CreateInstance(t, true);
EnvDTE80.DTE2 dte = (EnvDTE80.DTE2)obj;

Ram said...

Very useful article. I was just wondering if you know how can I programmatically read the project and items info for a solution on the disk (not the one loaded into visual studio now) using this technique? I am using similar technique with an Open(solution Name) method, but it works only occasionally and throws exception most of the time.. I appreciate any help from you.

Ram said...

Surprising the looping through the projects works most of the time, if I am debugging the program. But if I run it, it throws exception most of the time. But again, I am using the terms 'most' here as I have seen it working one or two times.!

Anonymous said...

BTW, this won't work as expected (sometimes) when you've got multiple instances of VS running. You have to go look through the running object table to get the instance you expect. Its a big pain in the ass.

Anonymous said...

BTW, this won't work as expected (sometimes) when you've got multiple instances of VS running. You have to go look through the running object table to get the instance you expect. Its a big pain in the ass.

ImaginaryDevelopment said...
This comment has been removed by the author.
ImaginaryDevelopment said...

var serviceProvider = Host as IServiceProvider;
if (serviceProvider != null) {
Dte = serviceProvider.GetService(typeof(SDTE)) as DTE;
}

helps with a few of the issues.

Williame Rocha said...

codeElement.Name' threw an exception of type 'System.Runtime.InteropServices.COMException

someone got the same error?

Im using Windows 7 64 bits and Visual Studio 2010 Professional Edition

thanks!

Anonymous said...

The loop

foreach(Project project in solution.Projects)

helps most of the time, but in large solutions you can have a whole tree of Folder ProjectItems containing other folders... containing projects containing folders .....

So a recursive approach may be necessary if you want it to work on all solutions.