我做了一个项目,关闭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 代码,但我想我会留给读者自己弄清楚。让我做一些粗略的描述:
- 我们检查生成的子项目文件夹的路径是否包含
<solution-name>\<solution-name>,如_generatedSubProjectFolder字段的示例值和OP的问题所示。如果没有,那就停下来,因为无事可做。
注意:我使用 Contains 搜索,而不是 EliSherer 的原始答案中的 EndsWith,因为示例值就是它的样子(以及我实际遇到的)在这个项目的制作过程中)。
-
下一个循环,通过解决方案的Projects,基本上直接从EliSherer 复制而来。我们整理出哪些Projects 仅仅是解决方案文件夹,哪些是实际的,嗯,真正的基于.csproj 的项目条目。像EliSherer 一样,我们只是在解决方案文件夹中向下一层。递归留给读者作为练习。
-
接下来的循环是在 #2 中构建的 List<Tuple<Project, Project>> 之上,再次与 EliSherer 的答案几乎相同,但有两个重要的修改:
- 我们检查
projectBadPath是否包含_subProjectName;如果不是,那么我们实际上正在迭代解决方案中的其他项目之一,除了这个对Child 向导的特定调用正在处理的项目;如果是这样,我们使用continue 语句跳过它。
- 在
EliSherer 答案中,无论他在哪里使用$safeprojectname$ 的内容在他的路径名解析表达式中,我都使用我通过_containingSolutionName 解析RunStarted 中的文件夹路径得出的“解决方案名称”字段。
-
然后DTE 用于暂时从正在生成的解决方案中删除项目。然后,我们在文件系统中将项目的文件夹向上移动。为了稳健起见,我测试了projectBadPathDirectory(Directory.Move 调用的“源”文件夹)是否存在(非常合理),我还在projectGoodPathDirectory 上使用string.IsNullOrWhiteSpace,以防万一Path.GetDirectoryName 不存在出于某种原因在 projectGoodPath 上调用时返回一个有效值。
-
然后,我再次调整EliSherer 代码以处理SolutionFolder 或具有.csproj 路径名的项目,以使DTE 将项目添加回正在生成的解决方案,这一次,从正确的文件系统路径。
我相当肯定这段代码有效,因为我做了很多日志记录(然后被删除,否则就像试图通过森林看到树木一样)。如果您想再次使用它们,日志基础设施功能仍然存在于MyProjectWizard 和ChildWizard 中的WizardImpl 类的主体中。
与往常一样,我对边缘情况不做任何承诺... =)
我尝试了多次 EliSherer 代码迭代,然后才能让所有测试用例工作。顺便说一句,这让我想起了:
测试用例
在每种情况下,期望的结果都是相同的:生成的 .sln 和 .csproj 的文件夹结构应该符合约定,即在上面的第二个文件夹结构围栏图中。
每个案例都只是说明在向导中打开和关闭哪些项目,如 GitHub 存储库中所示。
- 生成 DAL:
True,生成 UI 层:True
- 生成 DAL:
False,生成 UI 层:True
- 生成 DAL:
True,生成 UI 层:False
由于如果两者都设置为False,即使运行生成过程也毫无意义,所以我们根本不将其作为第四个测试用例。
使用我在上面和链接的 repo 中提供的代码,所有测试用例都通过了。具有“通过”的含义,Visual Studio Solutions 仅在选择子项目的情况下生成,并且文件夹结构与解决 OP 原始问题的常规文件夹布局相匹配。