使用Visual Studio模板和创建目录时出现问题

31

我正在尝试创建一个Visual Studio(2010)模板(多项目)。一切看起来都很好,除了项目被创建在解决方案的子目录中。这不是我想要的行为。

zip文件包含:

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

这是我的根模板:

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

使用这个模板创建解决方案时,我最终会得到像这样的目录:

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

有人能提供帮助吗?

编辑:

修改<CreateNewFolder>false</CreateNewFolder>的值,无论是true还是false,似乎都没有任何变化。


Fabian,我也遇到了同样的问题。你能找到不使用WizardExtension的解决方案吗? - Daniil Veriga
说实话,我不记得了。这是一个非常古老的问题,我不再使用这个模板了。 - Fabian Vilers
1
谢谢你的回答!我会考虑不再使用那个模板。:) - Daniil Veriga
这个问题有新的信息吗? - Dominic Jonas
6个回答

9
要在根目录下创建解决方案(而不是将其嵌套在子文件夹中),您必须创建两个模板: 1)ProjectGroup stub模板,其中包含您的向导,最终将从您的内容中创建新项目 2)项目模板
采用以下方法:
1. 添加类似于以下内容的模板:
  <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. 添加代码到向导中

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

但是AddXProject将项目添加到某个子文件夹而不是解决方案根文件夹本身... - Yaron

7
这是基于@drweb86的回答并加以改进和解释。请注意以下几点:
  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.

例子:

   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
这个答案太棒了!一定要非常注意步骤1和2。需要一个根解决方案(即假的),以及另一个在子目录中的根解决方案,该解决方案用于将解决方案创建到您选择的目录中。我还不得不在RunFinished中设置陷阱,以确保仅运行一次。只需在第一次设置布尔值即可。我无法使其他答案正确运行。 - Orn Kristjansson
2
"@drweb86" == "Siarhei Kuchuk" --> "@drweb86" 等于 "Siarhei Kuchuk" - Daniel James Bryars
2
很抱歉,我不理解你的代码块中关于文件夹结构的语法。RootTemplateFix.vstemplateTemplate Folder 是否在同一个目录中?还是说 Template FolderRootTemplateFix.vstemplate 深一层? - inejwstine
@Yaron,如果您能进一步澄清您所遵循的步骤,那将是很好的。从这个答案中,工作流程还不够清晰。 - alelom
1
你有没有办法创建一个拥有这个设置的 Github 仓库?当我尝试运行时,它无法复制“Template”文件夹中的任何项,因为它在 roottemplate.vstemplate 文件中没有链接。最终我只得到了根模板和向导中的代码失败,因为没有一个文件位于扩展目录中。 - Mike G
显示剩余2条评论

5

使用向导的另一种解决方案:

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

这支持一个解决方案文件夹的层级(如果需要,您可以将我的解决方案递归以支持每个层级)。
请确保按照最常引用到最少引用的顺序将项目放在< ProjectCollection >标记中,因为会涉及到项目的添加和删除。

4
多项目模板非常棘手。我发现处理$safeprojectname$使得创建多项目模板并正确替换命名空间值几乎是不可能的。我不得不创建一个自定义向导,它点亮了一个新变量$saferootprojectname$,它始终是用户输入的新项目名称的值。
SideWaffle(这是一个有许多模板的模板包)中,我们有一些多项目模板。SideWaffle使用TemplateBuilder NuGet包。TemplateBuilder具有您需要的用于多项目模板的向导。
我有一个使用TemplateBuilder创建项目模板的6分钟视频。对于多项目模板,该过程有点繁琐(但仍比没有TemplateBuilder好得多)。在SideWaffle源代码中有一个示例多项目模板,位于https://github.com/ligershark/side-waffle/tree/master/TemplatePack/ProjectTemplates/Web/_Sample%20Multi%20Project

1
如果您能详细介绍一下如何使用这种“更繁琐”的方法,我会非常感激。 - JDandChips
2
仍然存在问题。按照多项目维基的步骤,最终出现了一个额外的文件夹。由于这个额外的文件夹,带有NuGet包的项目无法编译。 - Kevin LaBranche
@klabranche,你说得对,NuGet包的处理存在问题。TB在这方面帮助不大。这里有一篇文章http://docs.nuget.org/docs/reference/packages-in-visual-studio-templates提供更多信息。希望它能解决你们的问题。我很想为TB添加更多功能,但最近非常忙,没有太多时间。 - Sayed Ibrahim Hashimi
很奇怪,但$saferootprojectname$早在Tony Sneed 这里介绍过。 - Konstantin Chernov
@SayedIbrahimHashimi,你的维基上所有图片都被托管平台模糊化了。 - alelom
显示剩余2条评论

3

实际上,有一个解决方法,虽然不太美观,但在搜索网络后,我没有想出更好的办法。因此,在创建多项目解决方案的新实例时,您必须在对话框中取消“创建新文件夹”复选框。在开始之前,目录结构应该像这样:

 Projects
{no dedicated folder yet}

创建解决方案后,其结构如下所示:
Projects
    +--MyApplication1
         +-- Project1
         +-- Project2
    solution file

所需结构唯一的小差异就是解决方案文件的位置。因此,在生成并显示新解决方案后,您应该首先选择解决方案并在菜单中选择“另存为”,然后将文件移动到MyApplication1文件夹中。然后删除以前的解决方案文件,这样您就可以得到以下文件结构:

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

这不是问题的解决方案,你只是在创建后修改了VS解决方案结构。 - alelom

1
我做了一个项目,参考了Joche Ojeda的YouTube教程以及上面由EliSherer提供的回答,解决了本文开头提出的问题,并且允许我们创建一个对话框,显示复选框以切换生成哪些子项目。
点击这里查看我的GitHub仓库,其中包含对话框并尝试修复本问题中的文件夹问题。
仓库根目录下的README.md详细介绍了解决方案。 编辑1:相关代码 我想在这篇文章中添加与OP问题相关的代码。
首先,我们必须处理解决方案的文件夹命名约定。请注意,我的代码仅设计用于处理不将.csproj和.sln放在同一文件夹中的情况;即应留空以下复选框:

将“在同一目录中留下Place Solution和Project”复选框空白

注意: 构造/* ... */用于表示与本答案无关的其他代码。我使用的try/catch块结构几乎与EliSherer相同,因此我不会在这里再次重复。

我们需要将以下字段放置在MyProjectWizard DLL中WizardImpl类的开头(这是在生成解决方案期间调用的Root DLL)。请注意,所有代码片段都来自我链接到的GitHub Repo,并且我只会展示与回答OP问题有关的部分。但是,我将在相关的位置提供所有using

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;

        /* ... */
    }
}

这里是如何初始化这些字段(在同一类的 RunStarted 方法中):
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$"];

            /* ... */
        }
    }
}

公正地说,我认为_solutionFileContainerFolderName字段中的值从未被使用,但我想把它放在那里,这样您就可以看到$safeprojectname$Root向导中的取值。
在本文和GitHub的屏幕截图中,我将示例虚拟项目称为BrianApplication1,解决方案名称相同。在此示例中,_solutionFileContainerFolderName字段的值将为BrianApplication1
如果我告诉Visual Studio我要在C:\temp文件夹中创建解决方案和项目(实际上是多项目模板),那么$destinationdirectory$将填充为C:\temp\BrianApplication1\BrianApplication1
多项目模板中的项目最初都生成在C:\temp\BrianApplication1\BrianApplication1文件夹下,如下所示:
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>

OP的帖子和我的解决方案的整个重点是创建符合惯例的文件夹结构,即:

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>

我们几乎完成了`IWizard`的`Root`实现。我们仍需要实现`RunFinished`方法(顺便提一下,其他`IWizard`方法与此解决方案无关)。
`RunFinished`方法的作用是删除错误创建的子项目容器文件夹,因为它们已经在文件系统中向上移动了一个级别。
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();
        }
        
        /* ... */
        
        }
    }
}

实现IsDirectoryEmpty方法的灵感来自于Stack Overflow上的一个答案,并得到了我的验证;不幸的是,我丢失了适当文章的链接;如果我找到了,我会进行更新。
好的,现在我们已经处理了Root向导的工作。接下来是Child向导。这里我们添加(略有变化的)EliSherer的答案。
首先,我们需要声明的字段是:
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;
        
        /* ... */
    }
}

我们在RunStarted方法中这样初始化这些字段:
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)
            );

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

当调用此Child向导时,例如生成BrianApplication1.DAL项目时,字段将获得以下值:
  • _dte = 引用由EnvDTE.DTE接口公开的自动化对象
  • _generatedSubProjectFolder = C:\temp\BrianApplication1\BrianApplication1\BrianApplication1.DAL
  • _subProjectName = BrianApplication1.DAL
  • _containingSolutionName = BrianApplcation1
与OP的答案相关的是,初始化这些字段就是RunStarted需要做的全部工作。现在,让我们看看我需要如何在Child向导代码的RunFinished方法中改进EliSherer的答案:
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);
            }
        }
    
        /* ... */
    }
}

大体上来说,这与 EliSherer 的答案相同,只是他在使用表达式 _safeProjectName + Path.DirectorySeparatorChar + _safeProjectName 时,我用 _containingSolutionName 替换了 _safeProjectName。如果您查看上面的字段及其描述性注释和示例值,这在这个上下文中更有意义。

注意:我考虑逐行解释 Child 向导中的 RunFinished 代码,但我认为我会留给读者自己去理解。让我来概括一下:

  1. 我们检查生成的子项目文件夹路径是否包含 <solution-name>\<solution-name>,就像 _generatedSubProjectFolder 字段和 OP 的问题所示的示例值一样。如果没有,则停止,因为没有任何事情可做。

注意: 我使用的是Contains搜索,而不是像EliSherer原始答案中所使用的EndsWith,这是由于示例值的特殊性(以及在制作此项目期间我遇到的实际情况)。

  1. 接下来循环遍历解决方案的Project,基本上是直接从EliSherer复制的。我们将哪些Project只是解决方案文件夹和哪些是实际的.csproj项目条目进行了排序。与EliSherer一样,在解决方案文件夹中只进入了一级。递归留给读者自己练习。

  2. 随后的循环是对在#2中建立的List<Tuple<Project, Project>>进行的,它与EliSherer的答案几乎完全相同,但有两个重要的修改:

我们检查 projectBadPath 是否包含 _subProjectName; 如果不是,则实际上我们正在迭代解决方案中除了这个特定调用 Child 向导所处理的项目之外的其他项目; 如果是,则使用 continue 语句跳过它。
EliSherer 的答案中,他在路径名解析表达式中使用了 $safeprojectname$ 的内容,在我的代码中,我使用从 RunStarted 中解析文件夹路径得到的 "解决方案名称",通过 _containingSolutionName 字段。
  1. 然后使用 DTE 从正在生成的解决方案中暂时移除项目。接着将该项目的文件夹在文件系统中上移一级。为了保证鲁棒性,我测试了 projectBadPathDirectoryDirectory.Move 调用的“源”文件夹)是否存在(非常合理),并且对 projectGoodPathDirectory 使用了 string.IsNullOrWhiteSpace,以防万一 Path.GetDirectoryName 在调用 projectGoodPath 的时候无法返回有效值。

  2. 然后,我再次修改了 EliSherer 的代码,以处理具有 .csproj 路径名的 SolutionFolder 或项目,使得 DTE 可以将项目从正确的文件系统路径添加回正在生成的解决方案中。

我相当确定这段代码能够工作,因为我做了很多日志记录(后来被删除了,否则就像在森林中寻找树木一样)。如果你愿意再次使用它们,MyProjectWizardChildWizardWizardImpl类体中仍然保留着日志记录基础设施函数。
像往常一样,我对边缘情况不做承诺... =)
在我能够让所有测试用例都能正常工作之前,我尝试了许多EliSherer代码的迭代。顺便提一下:
测试用例
在每种情况下,所需的结果都是相同的:生成的.sln.csproj的文件夹结构应该符合惯例,即在上面的第二个文件夹结构围栏图中。
每种情况只是简单地说明在向导中切换哪些项目,如GitHub存储库中所示。
  1. 生成 DAL: ,生成 UI 层:
  2. 生成 DAL: ,生成 UI 层:
  3. 生成 DAL: ,生成 UI 层:

如果两者都设置为 ,则运行生成过程毫无意义,因此我们不将其包括在第四个测试案例中。

使用我提供的代码以及链接的存储库中的代码,所有测试案例都通过了。 "通过" 的意思是,只选择子项目生成 Visual Studio 解决方案,并且文件夹结构与解决 OP 最初问题的常规文件夹布局相匹配。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接