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; //"); } } }
You rock! Thanks for the starting point. Wish I found this post 6 hours ago!
ReplyDeleteIn the examineitem method, you might want to add a check to see if the model is null before entering loop.
ReplyDeleteFileCodeModel2 model = (FileCodeModel2)item.FileCodeModel;
Thanks Jay, you're welcome. And thanks for the tip :)
ReplyDeleteThanks,
ReplyDeleteThis is helpful information for VSTA integrators
Thanks for your help. I miss how to create a DTE element. The code is the following:
ReplyDeleteSystem.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;
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.
ReplyDeleteSurprising 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.!
ReplyDeleteBTW, 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.
ReplyDeleteBTW, 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.
ReplyDeleteThis comment has been removed by the author.
ReplyDeletevar serviceProvider = Host as IServiceProvider;
ReplyDeleteif (serviceProvider != null) {
Dte = serviceProvider.GetService(typeof(SDTE)) as DTE;
}
helps with a few of the issues.
codeElement.Name' threw an exception of type 'System.Runtime.InteropServices.COMException
ReplyDeletesomeone got the same error?
Im using Windows 7 64 bits and Visual Studio 2010 Professional Edition
thanks!
The loop
ReplyDeleteforeach(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.