4

I am studying Visual Studio Extensibility. This code from MSDN creates a new C# solution containing a project with a class:

EnvDTE.DTE dte =   this.GetService(typeof(Microsoft.VisualStudio.Shell.Interop.SDTE)) as EnvDTE.DTE;
EnvDTE80.Solution2 solution = (EnvDTE80.Solution2)dte.Solution;
try {
    solution.Create(@"F:\Dev\Visual Studio 2013\Packages\Spikes\VSPNewSolution\Test\MySolution", "MySolution");

    string templatePath = solution.GetProjectTemplate("ConsoleApplication.zip", "CSharp");
    string projectPath = @"F:\Dev\Visual Studio 2013\Packages\Spikes\VSPNewSolution\\Test\MySolution\MyProject";

    /*
     * from MZTools site :
     * Once you have the template file name, you can add a project to the solution using the EnvDTE80.Solution.AddFromTemplate method.
     * Note: this method returns null (Nothing) rather than the EnvDTE.Project created, 
     * so you may need to locate the created project in the Solution.Projects collection. 
     * See PRB: Solution.AddXXX and ProjectItems.AddXXX methods return Nothing (null).
     */
    EnvDTE.Project project = solution.AddFromTemplate(templatePath, projectPath, "MyProject", false);

    EnvDTE.ProjectItem projectItem;
    String itemPath;

    // Point to the first project
    project = solution.Projects.Item(1); // try also "MyProject"

    VSLangProj.VSProject vsProject = (VSLangProj.VSProject)project.Object;
    vsProject.References.Add("NUnit.Framework");

    // Retrieve the path to the class template.
    itemPath = solution.GetProjectItemTemplate("Class.zip", "CSharp");

    //Create a new project item based on the template, in this case, a Class.
    projectItem = project.ProjectItems.AddFromTemplate(itemPath, "MyClass.cs");
}
catch (Exception ex) {
    System.Windows.Forms.MessageBox.Show("ERROR: " + ex.Message);
}  

I managed to add a reference to MyProject using VSLangProj .
So far, so good.
The resulting class is :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyProject
{
    class MyClass
    {
    }
}

What I did not found after a lot of googleing is a way to add a using directive in the class code ( using NUnit.Framework; in this case ).
The trivial way would be to write the line directly manipulating the class document.
Is there a way to do it programmatically using Visual Studio Extensibility ?

UPDATE

After some tries to get the CodeClass object for the created class, I tried the code posted in Finding a ProjectItem by type name via DTE with little changes. Here is the updated code :

EnvDTE.DTE dte = this.GetService(typeof(Microsoft.VisualStudio.Shell.Interop.SDTE)) as EnvDTE.DTE;
EnvDTE80.Solution2 solution = (EnvDTE80.Solution2)dte.Solution;
try {

    string solutionPath = @"F:\Dev\Visual Studio 2013\Packages\Spikes\VSPNewSolution\Test\MySolution";
    solution.Create(solutionPath, "MySolution");

    string templatePath = solution.GetProjectTemplate("ConsoleApplication.zip", "CSharp");
    string projectPath = @"F:\Dev\Visual Studio 2013\Packages\Spikes\VSPNewSolution\\Test\MySolution\MyProject";

    EnvDTE.Project project = solution.AddFromTemplate(templatePath, projectPath, "MyProject", false);

    EnvDTE.ProjectItem projectItem;
    String itemPath;

    foreach (EnvDTE.Project p in solution.Projects) {
        if (p.Name == "MyProject") {
            project = p;
            break;
        }
    }

    VSLangProj.VSProject vsProject = (VSLangProj.VSProject)project.Object;
    vsProject.References.Add("NUnit.Framework");

    itemPath = solution.GetProjectItemTemplate("Class.zip", "CSharp");
    projectItem = project.ProjectItems.AddFromTemplate(itemPath, "MyClass.cs");

    // I decided to save both, just in case
    solution.SaveAs(solutionPath + @"\MySolution.sln");
    project.Save();

    EnvDTE.CodeClass codeClass = FindClass(project, "MyClass.cs");

    // Display the source code for the class (from MSDN).

    if (codeClass != null) {
        EnvDTE.TextPoint start = codeClass.GetStartPoint(EnvDTE.vsCMPart.vsCMPartWhole);
        EnvDTE.TextPoint finish = codeClass.GetEndPoint(EnvDTE.vsCMPart.vsCMPartWhole);
        string src = start.CreateEditPoint().GetText(finish);
        System.Windows.Forms.MessageBox.Show(src, codeClass.FullName + "Source"); 
    }
}
catch (Exception ex) {
    System.Windows.Forms.MessageBox.Show("ERROR: " + ex.Message);
    }
}

private CodeClass FindClass(Project project, string className) {
    return FindClass(project.CodeModel.CodeElements, className);
}

private CodeClass FindClass(CodeElements elements, string className) {
    foreach (CodeElement element in elements) {
        if (element is CodeNamespace || element is CodeClass) {
            CodeClass c = element as CodeClass;
            if (c != null && c.Access == vsCMAccess.vsCMAccessPublic) {
                if (c.FullName == className)
                    return c;

                CodeClass subClass = FindClass(c.Members, className);
                if (subClass != null)
                    return subClass;
            }

            CodeNamespace ns = element as CodeNamespace;
            if (ns != null) {
                CodeClass cc = FindClass(ns.Members, className);
                if (cc != null)
                    return cc;
            }
        }
    }
    return null;
}

Well, it turns out that FindClass always returns null, because project.CodeModel.CodeElements.Count is zero. Duh ?

UPDATE 2
Well, please don't beat me.The original code had a surplus backslash in the projectPath variable.
This caused the project.CodeModel.CodeElements.Count to be zero.
Also, FindClass requires the class FullName without the extension and searches public classes only.
I corrected the code but still got null in return ( my own fault, I guess : I must have missed something ).
Anyway, FindClass searches the given class in all project CodeElements, including the classes in the project references.
This is an overkill in my case, since I am searching a class local to the project.
So I wrote a function that just does that.
Here it is :

public static CodeClass FindClassInProjectItems(Project project, string className) {
            CodeClass result = null;
            foreach (EnvDTE.ProjectItem pi in project.ProjectItems) {                
                if (pi.Name == className + ".cs") {
                    if (pi.FileCodeModel != null) {
                        foreach (EnvDTE.CodeElement ce in pi.FileCodeModel.CodeElements) {
                            if (ce is EnvDTE.CodeClass) {
                                result = ce as EnvDTE.CodeClass;
                                break;
                            }
                            else if (ce is EnvDTE.CodeNamespace) {
                                CodeNamespace ns = ce as CodeNamespace;

                                if (ns.Name == project.Name) {
                                    foreach (CodeElement sce in ns.Members) {
                                        if (sce is CodeClass && sce.Name == className) {
                                            result = sce as CodeClass;
                                            break;
                                        }
                                    }
                                }                                    
                            }
                        }
                    }
                }        
            }
            return result;
        } 

It works so I created a static ClassFinder class and added the function.
The next step was to retrieve the full class source code, including the using directives.
I found a sample on MSDN here, this is the crucial code :

// Display the source code for the class.
TextPoint start = cls.GetStartPoint(vsCMPart.vsCMPartWhole);
TextPoint finish = cls.GetEndPoint(vsCMPart.vsCMPartWhole);
string src = start.CreateEditPoint().GetText(finish);

Actually, the first line throws an exception.
So I tried all the members of vsCMPart enum : most of them throw an exception, except : vsCMPart.vsCMPartBody, vsCMPart.vsCMPartHeader, vsCMPart.vsCMPartNavigate and vsCMPart.vsCMPartWholeWithAttributes.
vsCMPart.vsCMPartHeader and vsCMPart.vsCMPartWholeWithAttributes return the same result ( at least in this case ),
while the others do not return the whole code.
To keep it short :

private void DisplayClassSource(CodeClass codeClass) {
    EnvDTE.TextPoint start = codeClass.GetStartPoint(vsCMPart.vsCMPartHeader);
    EnvDTE.TextPoint finish = codeClass.GetEndPoint();
    string source = start.CreateEditPoint().GetText(finish);        
    System.Windows.Forms.MessageBox.Show(source, codeClass.FullName + "Class source");
}   

private void DisplayNamespaceSource(CodeNamespace codeNamespace) {
    EnvDTE.TextPoint start = codeNamespace.GetStartPoint(EnvDTE.vsCMPart.vsCMPartWholeWithAttributes);
    EnvDTE.TextPoint finish = codeNamespace.GetEndPoint();
    string src = start.CreateEditPoint().GetText(finish);
    System.Windows.Forms.MessageBox.Show(src, codeNamespace.FullName + "Namespace source");
}  

If we want the source code as it appears in the IDE, including the using directives,
we must use the classCode.ProjectItem object :

    private void DisplayClassFullSource(CodeClass codeClass) {
        System.Text.StringBuilder sb = new System.Text.StringBuilder();
        foreach (CodeElement ce in codeClass.ProjectItem.FileCodeModel.CodeElements) {
            if (ce.Kind == vsCMElement.vsCMElementImportStmt) {
                // this is a using directive
                // ce.Name throws an exception here !
                sb.AppendLine(GetImportCodeLines(ce));
            }
            else if (ce.Kind == vsCMElement.vsCMElementNamespace) {
                sb.AppendLine();
                sb.AppendLine(GetNamespaceCodeLines(ce));
            }
        }

        System.Windows.Forms.MessageBox.Show(sb.ToString(), codeClass.FullName + "class source");
    }

    private static string GetImportCodeLines(CodeElement ce) {
        TextPoint start = ce.GetStartPoint(vsCMPart.vsCMPartWholeWithAttributes);
        TextPoint finish = ce.GetEndPoint(vsCMPart.vsCMPartWholeWithAttributes);
        return start.CreateEditPoint().GetText(finish);
    }

    private string GetNamespaceCodeLines(CodeElement ce) {
        EnvDTE.TextPoint start = ce.GetStartPoint(vsCMPart.vsCMPartWholeWithAttributes);
        //EnvDTE.TextPoint finish = codeClass.GetEndPoint(EnvDTE.vsCMPart.vsCMPartWhole); // ERROR : the method or operation is not implemented
        EnvDTE.TextPoint finish = ce.GetEndPoint();
        return start.CreateEditPoint().GetText(finish);
    }  

Now we are very near to the problem's solution. See my answer. (sorry if this looks like a novel)

Community
  • 1
  • 1
Jack Griffin
  • 1,228
  • 10
  • 17

1 Answers1

3

There is no direct way as far as I can tell to add a using directive to a CodeClass.
The only way I found is this :
It certainly needs improving but it works.
This code assumes that all using directives are all in a row, at the top of the class code.
It will not work properly if a using directive is present within the namespace, for instance.
It adds the given directive just after the last found.
It does not check the code to determine if the directive is already present.

    private void AddUsingDirectiveToClass(CodeClass codeClass, string directive) {
        CodeElement lastUsingDirective = null;

        foreach (CodeElement ce in codeClass.ProjectItem.FileCodeModel.CodeElements) {
            if (ce.Kind == vsCMElement.vsCMElementImportStmt) {
                // save it
                lastUsingDirective = ce;
            }
            else {
                if (lastUsingDirective != null) {
                    // insert given directive after the last one, on a new line
                    EditPoint insertPoint = lastUsingDirective.GetEndPoint().CreateEditPoint();
                    insertPoint.Insert("\r\nusing " + directive + ";");
                }
            }
        }
    }  

So, the final working code is

        EnvDTE.DTE dte = this.GetService(typeof(Microsoft.VisualStudio.Shell.Interop.SDTE)) as EnvDTE.DTE;
        EnvDTE80.Solution2 solution = (EnvDTE80.Solution2)dte.Solution;
        try {
            /*
             * NOTE while the MSDN sample states you must open an existing solution for the code to work,
             * it works also without opening a solution.
             */
            string solutionPath = @"F:\Dev\Visual Studio 2013\Packages\Spikes\VSPNewSolution\Test\MySolution";
            solution.Create(solutionPath, "MySolution");

            string templatePath = solution.GetProjectTemplate("ConsoleApplication.zip", "CSharp");
            string projectPath = solutionPath + @"\MyProject";

            /*
             * from MZTools site :
             * Once you have the template file name, you can add a project to the solution using the EnvDTE80.Solution.AddFromTemplate method.
             * Note: this method returns null (Nothing) rather than the EnvDTE.Project created, 
             * so you may need to locate the created project in the Solution.Projects collection. 
             * See PRB: Solution.AddXXX and ProjectItems.AddXXX methods return Nothing (null).
             */
            EnvDTE.Project project = solution.AddFromTemplate(templatePath, projectPath, "MyProject", false); 

            // the following code would do since there is only a single project
            //project = solution.Projects.Item(1); 

            // tried this :
            // project = solution.Projects.Item("MyProject"); 
            // but it throws an invalid argument exception

            // search project by name
            foreach (EnvDTE.Project p in solution.Projects) {
                if (p.Name == "MyProject") {
                    project = p;
                    break;
                }
            }

            // add a reference to NUnit
            VSLangProj.VSProject vsProject = (VSLangProj.VSProject)project.Object;
            vsProject.References.Add("NUnit.Framework");

            // Retrieve the path to the class template.
            string itemPath = solution.GetProjectItemTemplate("Class.zip", "CSharp");

            //Create a new project item based on the template, in this case, a Class.

            /*
             * Here we find the same problem as with solution.AddFromTemplate(...) ( see above )
             */
            EnvDTE.ProjectItem projectItem = project.ProjectItems.AddFromTemplate(itemPath, "MyClass.cs");

            solution.SaveAs(solutionPath + @"\MySolution.sln");                                
            project.Save();

            // retrieve the new class we just created
            EnvDTE.CodeClass codeClass = ClassFinder.FindClassInProjectItems(project, "MyClass");                               

            if (codeClass != null) {
                DisplayClassFullSource(codeClass);
                AddUsingDirectiveToClass(codeClass, "NUnit.Framework");
                project.Save();
                DisplayClassFullSource(codeClass);
            }


        }
        catch (Exception ex) {
            System.Windows.Forms.MessageBox.Show("ERROR: " + ex.Message);
        }            
    } 

P.S.
Before accepting this answer, I'll wait a while in case someone else posts a better solution.

EDIT
Time passed, no further posts on the subject. I marked this as the accepted answer.

Jack Griffin
  • 1,228
  • 10
  • 17