Issue with visual studio template & directory creation
Asked Answered
T

6

32

I'm trying to make a Visual Studio (2010) template (multi-project). Everything seems good, except that the projects are being created in a sub-directory of the solution. This is not the behavior I'm looking for.

The zip file contains:

Folder1
+-- Project1
    +-- Project1.vstemplate
+-- Project2
    +-- Project2.vstemplate
myapplication.vstemplate

Here's my root template:

<VSTemplate Version="3.0.0" Type="ProjectGroup" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005">
    <TemplateData>
        <Name>My application</Name>
        <Description></Description>
        <Icon>Icon.ico</Icon>
        <ProjectType>CSharp</ProjectType>
  <RequiredFrameworkVersion>4.0</RequiredFrameworkVersion>
  <DefaultName>MyApplication</DefaultName>
  <CreateNewFolder>false</CreateNewFolder>
    </TemplateData>
    <TemplateContent>
        <ProjectCollection>
   <SolutionFolder Name="Folder1">
    <ProjectTemplateLink ProjectName="$safeprojectname$.Project1">Folder1\Project1\Project1.vstemplate</ProjectTemplateLink>
    <ProjectTemplateLink ProjectName="$safeprojectname$.Project2">Folder2\Project2\Project2.vstemplate</ProjectTemplateLink>
   </SolutionFolder>
        </ProjectCollection>
    </TemplateContent>
</VSTemplate>

And, when creating the solution using this template, I end up with directories like this:

Projects
+-- MyApplication1
    +-- MyApplication1 // I'd like to have NOT this directory
        +-- Folder1
            +-- Project1
            +-- Project2
    solution file

Any help?

EDIT:

It seems that modifying <CreateNewFolder>false</CreateNewFolder>, either to true or false, doesn't change anything.

Tantivy answered 7/10, 2010 at 14:37 Comment(4)
Fabian, I've faced the same problem. Were you able to find a solution for that without using WizardExtension?Lippmann
TBH, I don't remember. This is a very old question and I don't use this template stuff anymore.Tantivy
Thank you for the answer! I'll think about not using that template stuff as well:)Lippmann
Are there some new informations about this problem?Doura
H
9

To create solution at root level (not nest them in subfolder) you must create two templates: 1) ProjectGroup stub template with your wizard inside that will create new project at the end from your 2) Project template

use the following approach for that

1. Add template something like this

  <VSTemplate Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005" Type="ProjectGroup">
    <TemplateData>
      <Name>X Application</Name>
      <Description>X Shell.</Description>
      <ProjectType>CSharp</ProjectType>
      <Icon>__TemplateIcon.ico</Icon>
    </TemplateData>
    <TemplateContent>
    </TemplateContent>
    <WizardExtension>
    <Assembly>XWizard, Version=1.0.0.0, Culture=neutral</Assembly>
    <FullClassName>XWizard.FixRootFolderWizard</FullClassName>
    </WizardExtension>  
  </VSTemplate>

2. Add code to wizard

// creates new project at root level instead of subfolder.
public class FixRootFolderWizard : IWizard
{
    #region Fields

    private string defaultDestinationFolder_;
    private string templatePath_;
    private string desiredNamespace_;

    #endregion

    #region Public Methods
    ...
    public void RunFinished()
    {
        AddXProject(
            defaultDestinationFolder_,
            templatePath_,
            desiredNamespace_);
    }

    public void RunStarted(object automationObject,
        Dictionary<string, string> replacementsDictionary,
        WizardRunKind runKind, object[] customParams)
    {
        defaultDestinationFolder_ = replacementsDictionary["$destinationdirectory$"];
        templatePath_ = 
            Path.Combine(
                Path.GetDirectoryName((string)customParams[0]),
                @"Template\XSubProjectTemplateWizard.vstemplate");

         desiredNamespace_ = replacementsDictionary["$safeprojectname$"];

         string error;
         if (!ValidateNamespace(desiredNamespace_, out error))
         {
             controller_.ShowError("Entered namespace is invalid: {0}", error);
             controller_.CancelWizard();
         }
     }

     public bool ShouldAddProjectItem(string filePath)
     {
         return true;
     }

     #endregion
 }

 public void AddXProject(
     string defaultDestinationFolder,
     string templatePath,
     string desiredNamespace)
 {
     var dte2 = (DTE) System.Runtime.InteropServices.Marshal.GetActiveObject("VisualStudio.DTE.10.0");
     var solution = (EnvDTE100.Solution4) dte2.Solution;

     string destinationPath =
         Path.Combine(
             Path.GetDirectoryName(defaultDestinationFolder),
             "X");

     solution.AddFromTemplate(
         templatePath,
         destinationPath,
         desiredNamespace,
         false);
     Directory.Delete(defaultDestinationFolder);
}
Humphries answered 1/6, 2011 at 9:21 Comment(1)
But the AddXProject adds the projects to some sub folder and not the solution root folder itself...Pulchi
P
7

This is based on @drweb86 answer with some improvments and explanations.
Please notice few things:

  1. The real template with projects links is under some dummy folder since you can't have more than one root vstemplate. (Visual studio will not display your template at all at such condition).
  2. All the sub projects\templates have to be located under the real template file folder.
    Zip template internal structure example:

    RootTemplateFix.vstemplate
    -> Template Folder
       YourMultiTemplate.vstemplate
            -->Sub Project Folder 1
               SubProjectTemplate1.vstemplate
            -->Sub Project Folder 2
               SubProjectTemplate2.vstemplate
            ...
    
  3. On the root template wizard you can run your user selection form and add them into a static variable. Sub wizards can copy these Global Parameters into their private dictionary.

Example:

   public class WebAppRootWizard : IWizard
   {
    private EnvDTE._DTE _dte;
    private string _originalDestinationFolder;
    private string _solutionFolder;
    private string _realTemplatePath;
    private string _desiredNamespace;

    internal readonly static Dictionary<string, string> GlobalParameters = new Dictionary<string, string>();

    public void BeforeOpeningFile(ProjectItem projectItem)
    {
    }

    public void ProjectFinishedGenerating(Project project)
    {
    }

    public void ProjectItemFinishedGenerating(ProjectItem
        projectItem)
    {
    }

    public void RunFinished()
    {
        //Run the real template
        _dte.Solution.AddFromTemplate(
            _realTemplatePath,
            _solutionFolder,
            _desiredNamespace,
            false);

        //This is the old undesired folder
        ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(DeleteDummyDir), _originalDestinationFolder);
    }

    private void DeleteDummyDir(object oDir)
    {
        //Let the solution and dummy generated and exit...
        System.Threading.Thread.Sleep(2000);

        //Delete the original destination folder
        string dir = (string)oDir;
        if (!string.IsNullOrWhiteSpace(dir) && Directory.Exists(dir))
        {
            Directory.Delete(dir);
        }
    }

    public void RunStarted(object automationObject,
        Dictionary<string, string> replacementsDictionary,
        WizardRunKind runKind, object[] customParams)
    {
        try
        {
            this._dte = automationObject as EnvDTE._DTE;

            //Create the desired path and namespace to generate the project at
            string temlateFilePath = (string)customParams[0];
            string vsixFilePath = Path.GetDirectoryName(temlateFilePath);
            _originalDestinationFolder = replacementsDictionary["$destinationdirectory$"];
            _solutionFolder = replacementsDictionary["$solutiondirectory$"];
            _realTemplatePath = Path.Combine(
                vsixFilePath,
                @"Template\BNHPWebApplication.vstemplate");
            _desiredNamespace = replacementsDictionary["$safeprojectname$"];

            //Set Organization
            GlobalParameters.Add("$registeredorganization$", "My Organization");

            //User selections interface
            WebAppInstallationWizard inputForm = new WebAppInstallationWizard();
            if (inputForm.ShowDialog() == DialogResult.Cancel)
            {
                throw new WizardCancelledException("The user cancelled the template creation");
            }

            // Add user selection parameters.
            GlobalParameters.Add("$my_user_selection$",
                inputForm.Param1Value);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.ToString(), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }

    public bool ShouldAddProjectItem(string filePath)
    {
        return true;
    }
}    
  1. Notice that the original destination folder deletion is done via a different thread.
    The reason is that the solution is generated after your wizard ends and this destination folder will get recreated.
    By using ohter thread we assume that the solution and final destination folder will get created and only then we can safely delete this folder.
Pulchi answered 20/7, 2015 at 15:3 Comment(7)
This answer is AVESOME ! Make sure to pay very good attention to step 1 and 2. One root solution is needed ( that is a fake ) and another root solution in sub dir which is the one used to create the solution into a directory of your choice. I also had to put in a trap in RunFinished to make sure only to run it once. Just set a boolean on the first time. I couldn't get the other answers to run correctly.Patrickpatrilateral
"@drweb86" == "Siarhei Kuchuk"Quoin
I'm afraid I don't understand the syntax of your block of code explaining the folder structure. Are RootTemplateFix.vstemplate and Template Folder in the same directory? Or is Template Folder one level deeper?Hunan
@Pulchi it would be good if you could further clarify the steps you followed. The workflow is not clear enough from this answer.Scorecard
@alexlomba87 you create a dummy root template. It's wizard (The root wizard, parent) copies variables into the children templates. It can determine where they are created (destination) and knows their relative paths in the source. It solves this issue. Notice, That every child template can have it's wizard and this wizard can get variables from the parent wizard. Also, notice, that the source structure is deployed at the machine, does it's job and than getting deleted.Pulchi
@Hunan you create a zip with the dummy template as root, and other templates in a deeper level(!) in the same directory. This zip is extracted to the target path BUT is getting deleted at the end of the generation process. It's just let you solve the problem of the multiple templates and redirect them to the final destination you need them to get created at.Pulchi
Is there any way you can create a github repo that has this set up? When I do this it fails to copy any of the items in the "Template" folder because it isn't linked in the roottemplate.vstemplate file. All I end up with is the root template and the code in the wizard fails because none of the files are located in the extension directory.Gynecology
B
5

Another solution with using a Wizard alone:

    public void RunStarted(object automationObject, Dictionary<string, string> replacementsDictionary, WizardRunKind runKind, object[] customParams)
    {
        try
        {
            _dte = automationObject as DTE2;
            _destinationDirectory = replacementsDictionary["$destinationdirectory$"];
            _safeProjectName = replacementsDictionary["$safeprojectname$"];

            //Add custom parameters
        }
        catch (WizardCancelledException)
        {
            throw;
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex + Environment.NewLine + ex.StackTrace, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            throw new WizardCancelledException("Wizard Exception", ex);
        }
    }

    public void RunFinished()
    {
        if (!_destinationDirectory.EndsWith(_safeProjectName + Path.DirectorySeparatorChar + _safeProjectName))
            return;

        //The projects were created under a seperate folder -- lets fix it
        var projectsObjects = new List<Tuple<Project,Project>>();
        foreach (Project childProject in _dte.Solution.Projects)
        {
            if (string.IsNullOrEmpty(childProject.FileName)) //Solution Folder
            {
                projectsObjects.AddRange(from dynamic projectItem in childProject.ProjectItems select new Tuple<Project, Project>(childProject, projectItem.Object as Project));
            }
            else
            {
                projectsObjects.Add(new Tuple<Project, Project>(null, childProject));
            }
        }

        foreach (var projectObject in projectsObjects)
        {
            var projectBadPath = projectObject.Item2.FileName;
            var projectGoodPath = projectBadPath.Replace(
                _safeProjectName + Path.DirectorySeparatorChar + _safeProjectName + Path.DirectorySeparatorChar, 
                _safeProjectName + Path.DirectorySeparatorChar);

            _dte.Solution.Remove(projectObject.Item2);

            Directory.Move(Path.GetDirectoryName(projectBadPath), Path.GetDirectoryName(projectGoodPath));

            if (projectObject.Item1 != null) //Solution Folder
            {
                var solutionFolder = (SolutionFolder)projectObject.Item1.Object;
                solutionFolder.AddFromFile(projectGoodPath);
            }
            else
            {
                _dte.Solution.AddFromFile(projectGoodPath);
            }
        }

        ThreadPool.QueueUserWorkItem(dir =>
        {
            System.Threading.Thread.Sleep(2000);
            Directory.Delete(_destinationDirectory, true);
        }, _destinationDirectory);
    }

This supports one level of solution folder (if you want you can make my solution recursive to support every levels)

Make Sure to put the projects in the <ProjectCollection> tag in order of most referenced to least referenced. because of the removal and adding of projects.

Babineaux answered 17/12, 2015 at 13:29 Comment(0)
C
4

Multi-project templates are very tricky. I've found that the handling of $safeprojectname$ makes it almost impossible to create a multi-project template and have the namespace values replaced correctly. I've had to create a custom wizard which light up a new variable $saferootprojectname$ which is always the value that the user enters into the name for the new project.

In SideWaffle (which is a template pack with many templates) we have a couple multi-project templates. SideWaffle uses the TemplateBuilder NuGet package. TemplateBuilder has the wizards that you'll need for your multi-project template.

I have a 6 minute video on creating project templates with TemplateBuilder. For multi-project templates the process is a bit more cumbersome (but still much better than w/o TemplateBuilder. I have a sample multi-project template in the SideWaffle sources at https://github.com/ligershark/side-waffle/tree/master/TemplatePack/ProjectTemplates/Web/_Sample%20Multi%20Project.

Commanding answered 23/4, 2014 at 6:6 Comment(7)
I would really love it if you could expand on how to go about this 'more cumbersome' method?Gallivant
@Gallivant since I wrote that answer we have put together the wiki on multi-proj which explains it in detail github.com/ligershark/side-waffle/wiki/…Commanding
Still an issue. Followed the multi-project wiki and end up with an extra folder. Projects with nuget packages won't compile because of the extra folder.Puca
@klabranche you're right there are issues with how NuGet packages are handled. TB doesn't help much there yet. Here is an article docs.nuget.org/docs/reference/… with more info. Hopefully it will unblock you guys. I'd love to add more features to TB but I've been really busy lately and haven't had much time.Commanding
It's weird but $saferootprojectname$ was introduced long time ago by Tony Sneed hereLedeen
@SayedIbrahimHashimi apparently all pictures on your wiki have become obfuscated by the hosting platform.Scorecard
Photobucket has started blurring and watermarking all images if the host account went over a certain memory limit. Please replace all the pictures with new ones.Scorecard
L
3

Actually there is a workaround, it is ugly, but after diggin' the web I couldnt invent anything better. So, when creating a new instance of multiproject solution, you have to uncheck the 'create new folder' checkbox in the dialog. And before you start the directory structure should be like

 Projects
{no dedicated folder yet}

After you create a solution a structure would be the following:

Projects
    +--MyApplication1
         +-- Project1
         +-- Project2
    solution file

So the only minor difference from the desired structure is the place of the solution file. So the first thing you should do after the new solution is generated and shown - select the solution and select "Save as" in menu, then move the file into the MyApplication1 folder. Then delete the previous solution file and here you are, the file structure is like this:

Projects
    +--MyApplication1
         +-- Project1
         +-- Project2
         solution file
Ledeen answered 21/7, 2015 at 17:41 Comment(1)
This is not a solution to the question, you only modify the VS Solution structure after it is created.Scorecard
U
1

I made a project that keys off the YouTube tutorial of Joche Ojeda and the answer by EliSherer above that addresses the question at the top of this article, and also allows us to create a dialog box that shows check boxes to toggle which sub-projects get generated.

Please click here for my GitHub repo that does the dialog box and tries to fix the folder issue in this question.

The README.md at the Repository root goes into excruciating depth as to the solution.

EDIT 1: Relevant Code

I want to add to this post the relevant code that addresses the OP's question.

First, we have to deal with folder naming conventions for solutions. Note, that my code is only designed to deal with the case where we are NOT putting the .csproj and .sln in the same folder; i.e., the following checkbox should be left blank:

Leaving the Place Solution and Project in the Same Directory check box blank

NOTE: The construct /* ... */ is used to signify other code that is not relevant to this answer. Also, the try/catch block structure I utilize is pretty much identical to that of EliSherer, so I won't reproduce that here, either.

We need to put the following fields in the beginning of the WizardImpl class in the MyProjectWizard DLL (this is the Root DLL that is called during the generation of the Solution). Please note that all code snippets are taken from my GitHub Repo I am linking to, and I am only going to show the pieces that have to deal with answering the OP's question. I will, however, echo all using's where relevant:

using Core.Config;
using Core.Files;
using EnvDTE;
using Microsoft.VisualStudio.TemplateWizard;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;

namespace MyProjectWizard
{
    /// <summary>
    /// Implements a new project wizard in Visual Studio.
    /// </summary>
    public class WizardImpl : IWizard
    {
        /// <summary>
        /// String containing the fully-qualified pathname
        /// of the erroneously-generated sub-folder of the
        /// Solution that is going to contain the individual
        /// projects' folders.
        /// </summary>
        private string _erroneouslyCreatedProjectContainerFolder;

        /// <summary>
        /// String containing the name of the folder that
        /// contains the generated <c>.sln</c> file.
        /// </summary>
        private string _solutionFileContainerFolderName;

        /* ... */
    }
}

Here's how we initialize these fields (in the RunStarted method of the same class):

using Core.Config;
using Core.Files;
using EnvDTE;
using Microsoft.VisualStudio.TemplateWizard;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;

namespace MyProjectWizard
{
    /// <summary>
    /// Implements a new project wizard in Visual Studio.
    /// </summary>
    public class WizardImpl : IWizard
    {

        /* ... */

        public void RunStarted(object automationObject,
            Dictionary<string, string> replacementsDictionary,
            WizardRunKind runKind, object[] customParams)
        {
            /* ... */

           // Grab the path to the folder that 
           // is erroneously created to contain the sub-projects.
           _erroneouslyCreatedProjectContainerFolder =
               replacementsDictionary["$destinationdirectory$"];

            // Here, in the 'root' wizard, the $safeprojectname$ variable
            // contains the name of the containing folder of the .sln file
            // generated by the process.
            _solutionFileContainerFolderName = 
                replacementsDictionary["$safeprojectname$"];

            /* ... */
        }
    }
}

To be fair, I don't think that the value in the _solutionFileContainerFolderName field is ever used, but I wanted to put it there so you can see what value $safeprojectname$ takes on in the Root Wizard.

In the screen shots in this article and in the GitHub, I call the example dummy project BrianApplication1 and the solution is named the same. In this example, then, the _solutionFileContainerFolderName field will have the value of BrianApplication1.

If I tell Visual Studio I want to create the solution and project (really, the multi-project template) in the C:\temp folder, then $destinationdirectory$ gets filled with C:\temp\BrianApplication1\BrianApplication1.

The projects in the multi-project template all get initially generated underneath the C:\temp\BrianApplication1\BrianApplication1 folder, like so:

C:\
    |
    --- temp
         |
         --- BrianApplication1
              |
              --- BrianApplication1.sln
              |
              --- BrianApplication1  <-- extra folder that needs to go away
                   |
                   --- BrianApplication1.DAL
                   |    |
                   |    --- BrianApplication1.DAL.csproj
                   |    |
                   |    --- <other project files and folders>
                   --- BrianApplication1.WindowsApp
                   |    |
                   |    --- BrianApplication1.WindowsApp.csproj
                   |    |
                   |    --- <other project files and folders>

The whole point of the OP's post, and my solution, is to create a folder structure that hews to convention; i.e.:

C:\
    |
    --- temp
         |
         --- BrianApplication1
              |
              --- BrianApplication1.sln
              |
              --- BrianApplication1.DAL
              |    |
              |    --- BrianApplication1.DAL.csproj
              |    |
              |    --- <other project files and folders>
              --- BrianApplication1.WindowsApp
              |    |
              |    --- BrianApplication1.WindowsApp.csproj
              |    |
              |    --- <other project files and folders>

We are almost done with the Root implementation of IWizard's job. We still need to implement the RunFinished method (btw, the other IWizard methods are irrelevant to this solution).

The job of the RunFinished method is to simply remove the erroneously-created container folder for the sub-projects, now that they've all been moved up one level in the file system:

using Core.Config;
using Core.Files;
using EnvDTE;
using Microsoft.VisualStudio.TemplateWizard;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;

namespace MyProjectWizard
{
    /// <summary>
    /// Implements a new project wizard in Visual Studio.
    /// </summary>
    public class WizardImpl : IWizard
    {
        /* ... */

        /// <summary>Runs custom wizard logic when the wizard
        /// has completed all tasks.</summary>
        public void RunFinished()
        {
            // Here, _erroneouslyCreatedProjectContainerFolder holds the path to the
            // erroneously-created container folder for the
            // sub projects. When we get here, this folder should be
            // empty by now, so just remove it.

            if (!Directory.Exists(_erroneouslyCreatedProjectContainerFolder) ||
                !IsDirectoryEmpty(_erroneouslyCreatedProjectContainerFolder))
                return; // If the folder does not exist or is not empty, then do nothing

            if (Directory.Exists(_erroneouslyCreatedProjectContainerFolder))
                Directory.Delete(
                    _erroneouslyCreatedProjectContainerFolder, true
                );
        }
        
        /* ... */
        
        /// <summary>
        /// Checks whether the folder having the specified <paramref name="path" /> is
        /// empty.
        /// </summary>
        /// <param name="path">
        /// (Required.) String containing the fully-qualified pathname of the folder to be
        /// checked.
        /// </param>
        /// <returns>
        /// <see langword="true" /> if the folder contains no files nor
        /// subfolders; <see langword="false" /> otherwise.
        /// </returns>
        /// <exception cref="T:System.ArgumentException">
        /// Thrown if the required parameter,
        /// <paramref name="path" />, is passed a blank or <see langword="null" /> string
        /// for a value.
        /// </exception>
        /// <exception cref="T:System.IO.DirectoryNotFoundException">
        /// Thrown if the folder whose path is specified by the <paramref name="path" />
        /// parameter cannot be located.
        /// </exception>
        private static bool IsDirectoryEmpty(string path)
        {
            if (string.IsNullOrWhiteSpace(path))
                throw new ArgumentException(
                    "Value cannot be null or whitespace.", nameof(path)
                );
            if (!Directory.Exists(path))
                throw new DirectoryNotFoundException(
                    $"The folder having path '{path}' could not be located."
                );

            return !Directory.EnumerateFileSystemEntries(path)
                             .Any();
        }
        
        /* ... */
        
        }
    }
}

The implementation for the IsDirectoryEmpty method was inspired by a Stack Overflow answer and validated by my own knowledge; unfortunately, I lost the link to the appropriate article; if I can find it, I'll make an update.

OKAY, so now we've handled the job of the Root Wizard. Next is the Child Wizard. This where we add (a slight variation of) EliSherer's answer.

First, the fields we need to declare are:

using Core.Common;
using Core.Config;
using Core.Files;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.TemplateWizard;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Thread = System.Threading.Thread;

namespace ChildWizard
{
    /// <summary>
    /// Implements a wizard for the generation of an individual project in the
    /// solution.
    /// </summary>
    public class WizardImpl : IWizard
    {
        /* ... */
        
        /// <summary>
        /// Contains the name of the folder that was erroneously
        /// generated in order to contain the generated sub-projects,
        /// which we assume has the same name as the solution (without
        /// the <c>.sln</c> file extension, so we are giving it a
        /// descriptive name as such.
        /// </summary>
        private string _containingSolutionName;

        /// <summary>
        /// Reference to an instance of an object that implements the
        /// <see cref="T:EnvDTE.DTE" /> interface.
        /// </summary>
        private DTE _dte;

        /// <summary>
        /// String containing the fully-qualified pathname of the
        /// sub-folder in which this particular project (this Wizard
        /// is called once for each sub-project in a multi-project
        /// template) is going to live in.
        /// </summary>
        private string _generatedSubProjectFolder;

        /// <summary>
        /// String containing the name of the project that is safe to use.
        /// </summary>
        private string _subProjectName;
        
        /* ... */
    }
}

We initialize these fields in the RunStarted method thusly:

using Core.Common;
using Core.Config;
using Core.Files;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.TemplateWizard;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Thread = System.Threading.Thread;

namespace ChildWizard
{
    /// <summary>
    /// Implements a wizard for the generation of an individual project in the
    /// solution.
    /// </summary>
    public class WizardImpl : IWizard
    {
        /* ... */
        
         /// <summary>Runs custom wizard logic at the beginning of a template wizard run.</summary>
        /// <param name="automationObject">
        /// The automation object being used by the template
        /// wizard.
        /// </param>
        /// <param name="replacementsDictionary">
        /// The list of standard parameters to be
        /// replaced.
        /// </param>
        /// <param name="runKind">
        /// A
        /// <see cref="T:Microsoft.VisualStudio.TemplateWizard.WizardRunKind" /> indicating
        /// the type of wizard run.
        /// </param>
        /// <param name="customParams">
        /// The custom parameters with which to perform
        /// parameter replacement in the project.
        /// </param>
        public void RunStarted(object automationObject,
            Dictionary<string, string> replacementsDictionary,
            WizardRunKind runKind, object[] customParams)
        {

            /* ... */

            _dte = automationObject as DTE;

            _generatedSubProjectFolder =
                replacementsDictionary["$destinationdirectory$"];
                
            _subProjectName = replacementsDictionary["$safeprojectname$"];

            // Assume that the name of the solution is the same as that of the folder
            // one folder level up from this particular sub-project.
            _containingSolutionName = Path.GetFileName(
                Path.GetDirectoryName(_generatedSubProjectFolder)
            );

            /* ... */
        }
        
        /* ... */
    }
}

When this Child Wizard is called, e.g., to generate the BrianApplication1.DAL project, the fields get the following values:

  • _dte = Reference to the automation object exposed by the EnvDTE.DTE interface
  • _generatedSubProjectFolder = C:\temp\BrianApplication1\BrianApplication1\BrianApplication1.DAL
  • _subProjectName = BrianApplication1.DAL
  • _containingSolutionName = BrianApplcation1

Relevant to the OP's answer, initializing these fields is all the work that RunStarted needs to do. Now, let's see how I needed to adapt EliSherer's answer in the RunFinished method of the Child Wizard's code:

using Core.Common;
using Core.Config;
using Core.Files;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.TemplateWizard;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Thread = System.Threading.Thread;

namespace ChildWizard
{
    /// <summary>
    /// Implements a wizard for the generation of an individual project in the
    /// solution.
    /// </summary>
    public class WizardImpl : IWizard
    {
        /* ... */
        
        /// <summary>Runs custom wizard logic when the
        /// wizard has completed all tasks.</summary>
        public void RunFinished()
        {
            try
            {
                if (!_generatedSubProjectFolder.Contains(
                    _containingSolutionName + Path.DirectorySeparatorChar +
                    _containingSolutionName
                ))
                    return;

                //The projects were created under a separate folder -- lets fix 
                //it
                var projectsObjects = new List<Tuple<Project, Project>>();
                foreach (Project childProject in _dte.Solution.Projects)
                    if (string.IsNullOrEmpty(
                        childProject.FileName
                    )) //Solution Folder
                        projectsObjects.AddRange(
                            from dynamic projectItem in
                                childProject.ProjectItems
                            select new Tuple<Project, Project>(
                                childProject, projectItem.Object as Project
                            )
                        );
                    else
                        projectsObjects.Add(
                            new Tuple<Project, Project>(null, childProject)
                        );

                foreach (var projectObject in projectsObjects)
                {
                    var projectBadPath = projectObject.Item2.FileName;
                    if (!projectBadPath.Contains(_subProjectName))
                        continue; // wrong project

                    var projectGoodPath = projectBadPath.Replace(
                        _containingSolutionName + Path.DirectorySeparatorChar +
                        _containingSolutionName + Path.DirectorySeparatorChar,
                        _containingSolutionName + Path.DirectorySeparatorChar
                    );

                    _dte.Solution.Remove(projectObject.Item2);

                    var projectBadPathDirectory =
                        Path.GetDirectoryName(projectBadPath);
                    var projectGoodPathDirectory =
                        Path.GetDirectoryName(projectGoodPath);

                    if (Directory.Exists(projectBadPathDirectory) &&
                        !string.IsNullOrWhiteSpace(projectGoodPathDirectory))
                        Directory.Move(
                            projectBadPathDirectory, projectGoodPathDirectory
                        );

                    if (projectObject.Item1 != null) //Solution Folder
                    {
                        var solutionFolder =
                            (SolutionFolder)projectObject.Item1.Object;
                        solutionFolder.AddFromFile(projectGoodPath);
                    }
                    else
                    {
                        // TO BE COMPLETELY ROBUST, we should do
                        // File.Exists() on the projectGoodPath; since
                        // we are in a try/catch and Directory.Move would
                        // have otherwise thrown an exception if the
                        // folder move operation failed, it can be safely
                        // assumed here that projectGoodPath refers to a 
                        // file that actually exists on the disk.

                        _dte.Solution.AddFromFile(projectGoodPath);
                    }
                }

                ThreadPool.QueueUserWorkItem(
                    dir =>
                    {
                        Thread.Sleep(2000);
                        if (Directory.Exists(_generatedSubProjectFolder))
                            Directory.Delete(_generatedSubProjectFolder, true);
                    }, _generatedSubProjectFolder
                );
            }
            catch (Exception ex)
            {
                DumpToLog(ex);
            }
        }
    
        /* ... */
    }
}

More or less, this is the same answer as EliSherer, except, where he uses the expression _safeProjectName + Path.DirectorySeparatorChar + _safeProjectName, I substitute _safeProjectName with _containingSolutionName, which, if you look above the listing to the fields and their descriptive comments and example values, makes more sense in this context.

NOTE: I thought about explaining the RunFinished code in the Child Wizard line-by-line but I think I will leave that to the reader to figure out. Let me do some broad-brush:

  1. We check whether the path of the generated sub-project folder contains <solution-name>\<solution-name> such as is shown in the example value of the _generatedSubProjectFolder field and the OP's issue. If not, then stop as there is nothing to do.

NOTE: I use a Contains search and not an EndsWith as in EliSherer's original answer, due to the example value being what it is (and what I actually encountered during the crafting of this project).

  1. The next loop, through the solution's Projects, is basically copied straight from EliSherer. We sort out which Projects are merely Solution Folders and which are actual, well, bona-fide .csproj-based project entries. Like EliSherer, we just go one level down in Solution Folders. Recursion is left as an exercise for the reader.

  2. The loop that follows, which is over the List<Tuple<Project, Project>> that is built up in #2, is again, almost identical to EliSherer's answer, but with two important modifications:

  • We check the projectBadPath whether it contains the _subProjectName; if not, then we actually are iterating over one of the OTHER projects in the solution BESIDES the one that this particular call to the Child Wizard is dealing with; if so, we use a continue statement to skip it.
  • In the EliSherer answer, everywhere he used the contents of $safeprojectname$ in his pathname parsing expressions, I am using the "solution name" I derived from parsing the folder path in RunStarted, via the _containingSolutionName field.
  1. Then DTE is used to remove the project from the Solution being generated, temporarily. We then move the project's folder up on level in the file system. For robustness' sake, I test whether the projectBadPathDirectory (the "source" folder for the Directory.Move call) exists (pretty reasonable) and I also use string.IsNullOrWhiteSpace on the projectGoodPathDirectory just in case Path.GetDirectoryName does not return a valid value when called on the projectGoodPath for some reason.

  2. I then again, adapted the EliSherer code for dealing with a SolutionFolder or a project with a .csproj pathname to have DTE add the project BACK to the Solution being generated, this time, from the correct file system path.

I am fairly certain this code works because I did LOTS of logging (which then got removed, otherwise it would be like trying to see the trees through the forest). The logging infrastructure functions are still there in the body of the WizardImpl classes in both MyProjectWizard and ChildWizard, if you care to use them again.

As always, I make no promises regarding edge cases... =)

I tried many iterations of the EliSherer code before I could get all the test cases to work. By the way, which reminds me:

Test Cases

In each case, the desired outcome is the same: the folder structure of the generated .sln and .csproj should match convention, i.e., in the second folder-structure fence diagram above.

Each case simply says which project(s) to toggle on and off in the Wizard as shown in the GitHub repo.

  1. Generate DAL: True, Generate UI Layer: True
  2. Generate DAL: False, Generate UI Layer: True
  3. Generate DAL: True, Generate UI Layer: False

Since it's pointless to even run the generation process if both are set to False, then we simply do not include that as a fourth test case.

With the code I supply both above and in the repo linked, all test cases pass. With "passing" meaning, Visual Studio Solutions are generated with only the sub-project(s) selected, and the folder structure matches the conventional folder layout that solves the OP's original issue.

Uvea answered 16/9, 2021 at 3:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.