【问题标题】:Issue with visual studio template & directory creationVisual Studio 模板和目录创建问题
【发布时间】:2010-10-07 14:37:08
【问题描述】:

我正在尝试制作 Visual Studio (2010) 模板(多项目)。一切似乎都很好,除了项目是在解决方案的子目录中创建的。这不是我要寻找的行为。

压缩文件包含:

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

有什么帮助吗?

编辑:

看来修改&lt;CreateNewFolder&gt;false&lt;/CreateNewFolder&gt;,无论是真还是假,都不会改变任何东西。

【问题讨论】:

  • Fabian,我也遇到了同样的问题。您是否能够在不使用 WizardExtension 的情况下找到解决方案?
  • TBH,我不记得了。这是一个非常古老的问题,我不再使用这个模板的东西了。
  • 感谢您的回答!我也会考虑不使用那个模板的东西:)
  • 有关于这个问题的一些新信息吗?

标签: visual-studio templates project


【解决方案1】:

要在根级别创建解决方案(而不是将它们嵌套在子文件夹中),您必须创建两个模板: 1) 带有向导的 ProjectGroup 存根模板,该模板将在最后从您的向导创建新项目 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 将项目添加到某个子文件夹而不是解决方案根文件夹本身...
【解决方案2】:

这是基于@drweb86 的回答,并进行了一些改进和解释。
请注意以下几点:

  1. 带有项目链接的真实模板位于某个虚拟文件夹下,因为您不能拥有多个根 vstemplate。 (Visual Studio 在这种情况下根本不会显示您的模板)。
  2. 所有子项目\模板必须位于真正的模板文件夹下。
    Zip模板内部结构示例:

    RootTemplateFix.vstemplate
    -> Template Folder
       YourMultiTemplate.vstemplate
            -->Sub Project Folder 1
               SubProjectTemplate1.vstemplate
            -->Sub Project Folder 2
               SubProjectTemplate2.vstemplate
            ...
    
  3. 在根模板向导中,您可以运行用户选择表单并将它们添加到静态变量中。子向导可以将这些全局参数复制到他们的私有字典中。

例子:

   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 中设置一个陷阱,以确保只运行一次。只需在第一次设置一个布尔值。我无法让其他答案正确运行。
  • "@drweb86" == "Siarhei Kuchuk"
  • 恐怕我不明白您解释文件夹结构的代码块的语法。 RootTemplateFix.vstemplateTemplate Folder 在同一个目录中吗?还是Template Folder 更深一层?
  • @Yaron 如果您能进一步阐明您遵循的步骤,那就太好了。这个答案的工作流程不够清楚。
  • 有什么方法可以创建一个有这个设置的 github 存储库吗?当我这样做时,它无法复制“模板”文件夹中的任何项目,因为它没有在 roottemplate.vstemplate 文件中链接。我最终得到的只是根模板,向导中的代码失败了,因为没有文件位于扩展目录中。
【解决方案3】:

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

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

这支持一级解决方案文件夹(如果您愿意,可以使我的解决方案递归以支持每一级)

确保将项目放在&lt;ProjectCollection&gt; 标记中,按照引用次数最多到最少的顺序。因为项目的删除和添加。

【讨论】:

    【解决方案4】:

    多项目模板非常棘手。我发现处理$safeprojectname$ 几乎不可能创建多项目模板并正确替换命名空间值。我必须创建一个自定义向导来点亮一个新变量 $saferootprojectname$,它始终是用户在新项目名称中输入的值。

    SideWaffle(这是一个包含许多模板的模板包)中,我们有几个多项目模板。 SideWaffle 使用 TemplateBuilder NuGet 包。 TemplateBuilder 具有多项目模板所需的向导。

    我有一个6 minute video on creating project templates with TemplateBuilder。对于多项目模板,该过程有点麻烦(但仍然比没有 TemplateBuilder 好得多。我在 SideWaffle 源代码中有一个示例多项目模板,地址为https://github.com/ligershark/side-waffle/tree/master/TemplatePack/ProjectTemplates/Web/_Sample%20Multi%20Project

    【讨论】:

    • 如果您能详细说明如何使用这种“更麻烦”的方法,我会非常喜欢它?
    • @JDandChips 自从我写了这个答案后,我们将 wiki 放在了 multi-proj 上,详细解释了它github.com/ligershark/side-waffle/wiki/…
    • 还是个问题。遵循多项目 wiki 并最终得到一个额外的文件夹。由于额外的文件夹,带有 nuget 包的项目将无法编译。
    • @klabranche 你说得对,NuGet 包的处理方式存在问题。结核病在那里还没有多大帮助。这是一篇文章docs.nuget.org/docs/reference/…,其中包含更多信息。希望它能解开你们的障碍。我很想为 TB 添加更多功能,但我最近真的很忙,没有太多时间。
    • 这很奇怪,但是 $saferootprojectname$ 是很久以前由 Tony Sneed here介绍的
    【解决方案5】:

    实际上有一种解决方法,它很丑陋,但是在挖掘网络之后,我无法发明更好的方法。因此,在创建多项目解决方案的新实例时,您必须取消选中对话框中的“创建新文件夹”复选框。并且在你开始之前的目录结构应该是这样的

     Projects
    {no dedicated folder yet}
    

    创建解决方案后,结构将如下所示:

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

    因此,与所需结构的唯一细微差别是解决方案文件的位置。因此,在生成并显示新解决方案后您应该做的第一件事 - 选择解决方案并在菜单中选择“另存为”,然后将文件移动到 MyApplication1 文件夹中。然后删除之前的解决方案文件,到这里,文件结构是这样的:

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

    【讨论】:

    • 这不是问题的解决方案,您只是在创建后修改VS解决方案结构。
    【解决方案6】:

    我做了一个项目,关闭YouTube tutorial of Joche Ojeda 和上面EliSherer 的答案,解决了本文顶部的问题,还允许我们创建一个对话框,显示复选框以切换哪个子- 生成项目。

    click here 获取我的 GitHub 存储库,该存储库执行对话框并尝试修复此问题中的文件夹问题。

    Repository 根目录下的README.md 对解决方案有着极其深刻的影响。

    编辑 1:相关代码

    我想在这篇文章中添加解决 OP 问题的相关代码。

    首先,我们必须处理解决方案的文件夹命名约定。请注意,我的代码仅用于处理我们没有将.csproj.sln 放在同一个文件夹中的情况;即,以下复选框应留空:

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

    注意: 构造 /* ... */ 用于表示与此答案无关的其他代码。另外,我使用的try/catch 块结构与EliSherer 的块结构几乎相同,所以我也不会在这里重现。

    我们需要将以下字段放在MyProjectWizard DLL 中的WizardImpl 类的开头(这是在生成解决方案期间调用的Root DLL)。请注意,所有代码 sn-ps 均取自我要链接到的 GitHub 存储库,我只会展示必须处理回答 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>
    

    我们几乎完成了Root 实现IWizard 的工作。我们仍然需要实现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 的情况下,我将_safeProjectName 替换为_containingSolutionName,如果您查看列表上方的字段和它们的描述性 cmets 和示例值在这种情况下更有意义。

    注意:我曾想过在Child 向导中逐行解释RunFinished 代码,但我想我会留给读者自己弄清楚。让我做一些粗略的描述:

    1. 我们检查生成的子项目文件夹的路径是否包含&lt;solution-name&gt;\&lt;solution-name&gt;,如_generatedSubProjectFolder字段的示例值和OP的问题所示。如果没有,那就停下来,因为无事可做。

    注意:我使用 Contains 搜索,而不是 EliSherer 的原始答案中的 EndsWith,因为示例值就是它的样子(以及我实际遇到的)在这个项目的制作过程中)。

    1. 下一个循环,通过解决方案的Projects,基本上直接从EliSherer 复制而来。我们整理出哪些Projects 仅仅是解决方案文件夹,哪些是实际的,嗯,真正的基于.csproj 的项目条目。像EliSherer 一样,我们只是在解决方案文件夹中向下一层。递归留给读者作为练习。

    2. 接下来的循环是在 #2 中构建的 List&lt;Tuple&lt;Project, Project&gt;&gt; 之上,再次与 EliSherer 的答案几乎相同,但有两个重要的修改:

    • 我们检查projectBadPath是否包含_subProjectName;如果不是,那么我们实际上正在迭代解决方案中的其他项目之一,除了这个对Child 向导的特定调用正在处理的项目;如果是这样,我们使用continue 语句跳过它。
    • EliSherer 答案中,无论他在哪里使用$safeprojectname$ 的内容在他的路径名解析表达式中,我都使用我通过_containingSolutionName 解析RunStarted 中的文件夹路径得出的“解决方案名称”字段。
    1. 然后DTE 用于暂时从正在生成的解决方案中删除项目。然后,我们在文件系统中将项目的文件夹向上移动。为了稳健起见,我测试了projectBadPathDirectoryDirectory.Move 调用的“源”文件夹)是否存在(非常合理),我还在projectGoodPathDirectory 上使用string.IsNullOrWhiteSpace,以防万一Path.GetDirectoryName 不存在出于某种原因在 projectGoodPath 上调用时返回一个有效值。

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

    我相当肯定这段代码有效,因为我做了很多日志记录(然后被删除,否则就像试图通过森林看到树木一样)。如果您想再次使用它们,日志基础设施功能仍然存在于MyProjectWizardChildWizard 中的WizardImpl 类的主体中。

    与往常一样,我对边缘情况不做任何承诺... =)

    我尝试了多次 EliSherer 代码迭代,然后才能让所有测试用例工作。顺便说一句,这让我想起了:

    测试用例

    在每种情况下,期望的结果都是相同的:生成的 .sln.csproj 的文件夹结构应该符合约定,即在上面的第二个文件夹结构围栏图中。

    每个案例都只是说明在向导中打开和关闭哪些项目,如 GitHub 存储库中所示。

    1. 生成 DAL:True,生成 UI 层:True
    2. 生成 DAL:False,生成 UI 层:True
    3. 生成 DAL:True,生成 UI 层:False

    由于如果两者都设置为False,即使运行生成过程也毫无意义,所以我们根本不将其作为第四个测试用例。

    使用我在上面和链接的 repo 中提供的代码,所有测试用例都通过了。具有“通过”的含义,Visual Studio Solutions 仅在选择子项目的情况下生成,并且文件夹结构与解决 OP 原始问题的常规文件夹布局相匹配。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-02-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-08-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多