我做了一个项目,参考了
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
{
public class WizardImpl : IWizard
{
private string _erroneouslyCreatedProjectContainerFolder;
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
{
public class WizardImpl : IWizard
{
public void RunStarted(object automationObject,
Dictionary<string, string> replacementsDictionary,
WizardRunKind runKind, object[] customParams)
{
_erroneouslyCreatedProjectContainerFolder =
replacementsDictionary["$destinationdirectory$"];
_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
{
public class WizardImpl : IWizard
{
public void RunFinished()
{
if (!Directory.Exists(_erroneouslyCreatedProjectContainerFolder) ||
!IsDirectoryEmpty(_erroneouslyCreatedProjectContainerFolder))
return;
if (Directory.Exists(_erroneouslyCreatedProjectContainerFolder))
Directory.Delete(
_erroneouslyCreatedProjectContainerFolder, true
);
}
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
{
public class WizardImpl : IWizard
{
private string _containingSolutionName;
private DTE _dte;
private string _generatedSubProjectFolder;
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
{
public class WizardImpl : IWizard
{
public void RunStarted(object automationObject,
Dictionary<string, string> replacementsDictionary,
WizardRunKind runKind, object[] customParams)
{
_dte = automationObject as DTE;
_generatedSubProjectFolder =
replacementsDictionary["$destinationdirectory$"];
_subProjectName = replacementsDictionary["$safeprojectname$"];
_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
{
public class WizardImpl : IWizard
{
public void RunFinished()
{
try
{
if (!_generatedSubProjectFolder.Contains(
_containingSolutionName + Path.DirectorySeparatorChar +
_containingSolutionName
))
return;
var projectsObjects = new List<Tuple<Project, Project>>();
foreach (Project childProject in _dte.Solution.Projects)
if (string.IsNullOrEmpty(
childProject.FileName
))
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;
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)
{
var solutionFolder =
(SolutionFolder)projectObject.Item1.Object;
solutionFolder.AddFromFile(projectGoodPath);
}
else
{
_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
代码,但我认为我会留给读者自己去理解。让我来概括一下:
- 我们检查生成的子项目文件夹路径是否包含
<solution-name>\<solution-name>
,就像 _generatedSubProjectFolder
字段和 OP 的问题所示的示例值一样。如果没有,则停止,因为没有任何事情可做。
注意: 我使用的是Contains
搜索,而不是像EliSherer
原始答案中所使用的EndsWith
,这是由于示例值的特殊性(以及在制作此项目期间我遇到的实际情况)。
接下来循环遍历解决方案的Project
,基本上是直接从EliSherer
复制的。我们将哪些Project
只是解决方案文件夹和哪些是实际的.csproj
项目条目进行了排序。与EliSherer
一样,在解决方案文件夹中只进入了一级。递归留给读者自己练习。
随后的循环是对在#2中建立的List<Tuple<Project, Project>>
进行的,它与EliSherer
的答案几乎完全相同,但有两个重要的修改:
我们检查
projectBadPath
是否包含
_subProjectName
; 如果不是,则实际上我们正在迭代解决方案中除了这个特定调用
Child
向导所处理的项目之外的其他项目; 如果是,则使用
continue
语句跳过它。
在
EliSherer
的答案中,他在路径名解析表达式中使用了
$safeprojectname$
的内容,在我的代码中,我使用从
RunStarted
中解析文件夹路径得到的 "解决方案名称",通过
_containingSolutionName
字段。
然后使用 DTE
从正在生成的解决方案中暂时移除项目。接着将该项目的文件夹在文件系统中上移一级。为了保证鲁棒性,我测试了 projectBadPathDirectory
(Directory.Move
调用的“源”文件夹)是否存在(非常合理),并且对 projectGoodPathDirectory
使用了 string.IsNullOrWhiteSpace
,以防万一 Path.GetDirectoryName
在调用 projectGoodPath
的时候无法返回有效值。
然后,我再次修改了 EliSherer
的代码,以处理具有 .csproj
路径名的 SolutionFolder
或项目,使得 DTE
可以将项目从正确的文件系统路径添加回正在生成的解决方案中。
我相当确定这段代码能够工作,因为我做了很多日志记录(后来被删除了,否则就像在森林中寻找树木一样)。如果你愿意再次使用它们,
MyProjectWizard
和
ChildWizard
的
WizardImpl
类体中仍然保留着日志记录基础设施函数。
像往常一样,我对边缘情况不做承诺... =)
在我能够让所有测试用例都能正常工作之前,我尝试了许多
EliSherer
代码的迭代。顺便提一下:
测试用例
在每种情况下,所需的结果都是相同的:生成的
.sln
和
.csproj
的文件夹结构应该符合惯例,即在上面的第二个文件夹结构围栏图中。
每种情况只是简单地说明在向导中切换哪些项目,如GitHub存储库中所示。
- 生成 DAL:
是
,生成 UI 层: 是
- 生成 DAL:
否
,生成 UI 层: 是
- 生成 DAL:
是
,生成 UI 层: 否
如果两者都设置为 否
,则运行生成过程毫无意义,因此我们不将其包括在第四个测试案例中。
使用我提供的代码以及链接的存储库中的代码,所有测试案例都通过了。 "通过" 的意思是,只选择子项目生成 Visual Studio 解决方案,并且文件夹结构与解决 OP 最初问题的常规文件夹布局相匹配。