Issue with visual studio template & directory creation

11,584

Solution 1

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);
}

Solution 2

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.

Solution 3

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.

Solution 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.

Solution 5

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
Share:
11,584
Fabian Vilers
Author by

Fabian Vilers

Entrepreneur, Software Craftsman, Amateur Photographer, Father, and Gamer.

Updated on June 26, 2022

Comments

  • Fabian Vilers
    Fabian Vilers about 2 years

    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.

    • Daniil Veriga
      Daniil Veriga about 10 years
      Fabian, I've faced the same problem. Were you able to find a solution for that without using WizardExtension?
    • Fabian Vilers
      Fabian Vilers about 10 years
      TBH, I don't remember. This is a very old question and I don't use this template stuff anymore.
    • Daniil Veriga
      Daniil Veriga about 10 years
      Thank you for the answer! I'll think about not using that template stuff as well:)
    • Dominic Jonas
      Dominic Jonas over 7 years
      Are there some new informations about this problem?
  • JDandChips
    JDandChips over 9 years
    I would really love it if you could expand on how to go about this 'more cumbersome' method?
  • Sayed Ibrahim Hashimi
    Sayed Ibrahim Hashimi over 9 years
    @JDandChips 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/…
  • Kevin LaBranche
    Kevin LaBranche over 9 years
    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.
  • Sayed Ibrahim Hashimi
    Sayed Ibrahim Hashimi over 9 years
    @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.
  • Yaron
    Yaron almost 9 years
    But the AddXProject adds the projects to some sub folder and not the solution root folder itself...
  • Konstantin Chernov
    Konstantin Chernov almost 9 years
    It's weird but $saferootprojectname$ was introduced long time ago by Tony Sneed here
  • Orn Kristjansson
    Orn Kristjansson over 8 years
    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.
  • Daniel James Bryars
    Daniel James Bryars almost 8 years
    "@drweb86" == "Siarhei Kuchuk"
  • inejwstine
    inejwstine about 5 years
    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?
  • alelom
    alelom almost 5 years
    This is not a solution to the question, you only modify the VS Solution structure after it is created.
  • alelom
    alelom almost 5 years
    @SayedIbrahimHashimi apparently all pictures on your wiki have become obfuscated by the hosting platform.
  • alelom
    alelom almost 5 years
    @Yaron it would be good if you could further clarify the steps you followed. The workflow is not clear enough from this answer.
  • alelom
    alelom almost 5 years
    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.
  • Yaron
    Yaron over 4 years
    @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.
  • Yaron
    Yaron over 4 years
    @inejwstine 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.
  • Mike G
    Mike G about 3 years
    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.