DTE 是 Visual Studio 提供的对象,用于在 Visual Studio 中自动执行许多任务。你可能会听到各种关于数字电视代表什么的故事。一些例子包括:
- 设计时可扩展性
- 开发人员工具扩展性
- 开发工具环境
无论使用什么名称,此对象都允许您以编程方式控制 Visual Studio 的大多数方面。对我们这里的讨论更重要的是,这也是从 Visual Studio 中运行的模板的宿主。
要访问这个对象,我们从 template
指令开始。
<#@ template debug="true" hostSpecific="true" #>
将 hostSpecific
设置为 true 将确保模板的基类包含一个名为 host
的属性。现在我们有了 host
属性,我们还需要程序集和引用来使用 DTE。
<#@ assembly name="EnvDTE" #>
<#@ assembly name="EnvDTE100" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="EnvDTE100" #>
代码清单 29:使用 DTE 所需的引用
有了这个,我们通过这个 host
属性访问这个 DTE。常见的模式是:
IServiceProvider serviceProvider = (IServiceProvider)host ;
VisualStudio = serviceProvider.GetService(typeof(EnvDTE.DTE)) as DTE;
代码清单 30:通过主机访问 DTE
现在我们有了 DTE 对象,让我们看看我们可以用它做什么。
在这一章中,我们将在一个我们称之为 DTEHelper
的新类中构建方法。
在我们的解决方案中,创建一个新的类库项目,并将其命名为T4 实用程序。我们将在这个项目中创建新的 DTEHelper
类。首先,我们需要安装 Visual Studio 软件开发工具包。可以从微软下载中心获取最新版本。
我们需要在这个 T4Utilities 项目中添加一些参考资料,这些资料现在可以使用:
- 羡慕嫉妒恨
- 微软。visual studio . texttemplating . interfaces . 10.0
因为我们依赖主机来访问 DTE,所以让我们提供一个构造函数,要求任何用户都给我们主机。
private DTE VisualStudio { get; set; }
public DTEHelper(ITextTemplatingEngineHost host)
{
var serviceProvider = (IServiceProvider)host ;
VisualStudio = serviceProvider
.GetService(typeof(EnvDTE.DTE)) as DTE;
}
代码清单 DTEHelper 的构造函数
DTE 提供了对解决方案中所有项目的访问。这有点复杂,因为 DTE 实际上是一个 C++ COM 对象。因此,浏览一个集合并不像我们习惯的那样简单。让我们创建一个方法,让我们以一种更容易处理的格式访问项目列表。
public IList<Project> GetAllProjects()
{
var returnValue = new List<Project>();
foreach (Project project in VisualStudio.Solution.Projects)
{
returnValue.Add(project);
}
return returnValue;
}
代码清单 32:获取解决方案中所有项目的列表
现在我们在 IList
中有了这个,我们可以通过 Project
暴露的任何属性轻松过滤。通过 Kind
属性过滤,我们可以得到一个只有 C#项目的列表。
public IList<Project> GetCSharpProjects()
{
return GetAllProjects().Where
(p => p.Kind ==
"{66A26720-8FB5-11D2-AA7E-00C04F688DDE}").ToList();
}
代码清单 33:获取 CSharp 项目列表
"{66A26720-8FB5-11D2-AA7E-00C04F688DDE}"
是 C#项目的 Guid 。这是因为 Kind 属性实际上是从项目文件中的项目类型 Guid元素返回最后一个项目类型 Guid 。正如您可能知道的,一个项目可能有多个项目类型,因为它们变得相当精细,但是语言通常被认为是“真正的项目”,任何其他项目都有味道。考虑到“添加新项目”对话框的结构,这是有意义的。首先选择语言,然后选择语言中的项目类型。
图 10:添加新项目对话框。选择语言优先。
完整的项目类型列表很长,可能会有变化,但可以在这里找到。
在浏览这个列表时,很明显,您可能想要过滤的不仅仅是语言。事实证明这有点复杂。为了发挥这种魔力,我们必须更深入地研究 COM Interop。
public IList<string> GetProjectTypeGuids(EnvDTE.Project proj)
{
string projectTypeGuids = "";
int result = 0;
var solution = GetService
<Microsoft.VisualStudio.Shell.Interop.IVsSolution>();
Microsoft.VisualStudio.Shell.Interop.IVsHierarchy hierarchy =
null;
result = solution.GetProjectOfUniqueName(proj.UniqueName,
out hierarchy);
if (result == 0)
{
Microsoft.VisualStudio.Shell.Interop.IVsAggregatableProject
aggregatableProject =
(Microsoft.VisualStudio.Shell.Interop.IVsAggregatableProject)
hierarchy;
result = aggregatableProject.GetAggregateProjectTypeGuids
(out projectTypeGuids);
}
return projectTypeGuids.Split(';');
}
private T GetService<T>() where T : class
{
T service = null;
IntPtr serviceIntPtr;
int hr = 0;
var guid = typeof(T).GUID;
Microsoft.VisualStudio.OLE.Interop.IServiceProvider
serviceProvider =
(Microsoft.VisualStudio.OLE.Interop.IServiceProvider)
VisualStudio;
hr = serviceProvider.QueryService(ref guid, ref guid,
out serviceIntPtr);
if (hr != 0)
{
System.Runtime.InteropServices.Marshal
.ThrowExceptionForHR(hr);
}
else if (!serviceIntPtr.Equals(IntPtr.Zero))
{
service = System.Runtime.InteropServices.Marshal
.GetObjectForIUnknown(serviceIntPtr) as T;
System.Runtime.InteropServices.Marshal
.Release(serviceIntPtr);
}
if (service == null)
throw new Exception("Error retrieving service");
return service;
}
代码清单 34:深入 COM 互操作获取所有项目类型 GUIDS
现在我们可以使用 ProjectTypeGuids
方法来使用一些更有趣的过滤器,比如检索所有测试项目。测试项目的 Guid
是{3AC096D0-A1C2-E12C-1390-A8335801FDAB}
。
public IEnumerable<Project> GetTestProjects()
{
var projects = GetAllProjects()
.Where(project => GetProjectTypeGuids(project)
.Any(subProject => subProject ==
"{3AC096D0-A1C2-E12C-1390-A8335801FDAB}"));
return projects;
}
代码清单 35:获取所有测试项目
现在,我们可以确定我们可能感兴趣的项目,让我们将注意力转向我们可能想对其中一个项目做什么。
我们可能想做的第一件事是获取项目中的项目列表。因为项目可以包含也会有项目项的文件夹(事实上,任何项目项都可以有嵌套的项目项),所以检索所有项目的列表需要递归。
public IEnumerable<ProjectItem> GetAllProjectItems
(ProjectItems projectItems)
{
foreach (ProjectItem projectItem in projectItems)
{
foreach (ProjectItem subItem in
GetAllProjectItems(projectItem.ProjectItems))
{
yield return subItem;
}
yield return projectItem;
}
}
代码清单 36:获取项目中的所有项目项
使用这种方法,我们可以轻松地找到项目中的所有模板。
public IEnumerable<ProjectItem >GetAllTemplates
(ProjectItems projectItems)
{
return GetAllProjectItems(projectItems)
.Where(p => p.Name.EndsWith(".tt"));
}
代码清单 37:查找项目中的所有模板
现在我们可以找到一个模板列表,能够保存每个模板就很好了。这也将运行自定义工具,该工具将评估模板。
给定一个 ProjectItem
,我们可以轻松做到这一点。我们要做的就是保存物品。
public void SaveItem(ProjectItem item)
{
var needToClose = !item.IsOpen;
item.Open();
item.Save();
if (item.Document != null)
if (needToClose)
item.Document.Close();
}
代码清单 38:保存项目项
Visual Studio 要求在保存项目之前将其打开。我们不想让一堆原本没有打开的模板保持打开状态,也不想关闭任何已经打开的模板。这就对了。
这为编排一些复杂的工作流提供了可能,在这些工作流中,您可以让模板控制模板集合的执行顺序。例如,如果第一个模板创建了第二个模板需要的工件,您可能希望确保一个特定的模板先于另一个模板运行。
您还可以设计一个模板来创建新项目。为此,您需要在 T4Utilities 项目中添加对 EnvDTE80
的引用,并将其包含在 using
语句中,用于 DTEHelper
类。
现在我们可以添加 CreateProject
方法:
public Project CreateProject(string projectName)
{
if (GetAllProjects().Any(p => p.Name == projectName))
return GetAllProjects()
.FirstOrDefault(t=>t.Name == projectName);
var solution = (Solution2)VisualStudio.Solution;
var templatePath =
solution.GetProjectTemplate("ClassLibrary.zip",
"CSharp");
var solutionPath = Path.GetDirectoryName
(VisualStudio.Solution.FullName);
var path = Path.Combine(solutionPath, projectName);
return solution.AddFromTemplate(templatePath, path,
projectName, false);
}
代码清单 39:创建一个项目
这个方法将创建一个 C#类库。如果需要创建不同类型的项目,只需将参数更改为 GetProjectTemplate
方法调用即可。
请注意,该函数首先检查以确保项目尚未创建,因此可以安全地多次调用该方法,而不会造成任何损害。
现在您有了一个新项目,让我们来探索一下如何向其中添加一个项目。
ProjectItems
课暴露了四种有趣的方法。
AddFolder
方法在项目中创建新文件夹。这样,您就可以很容易地为 MVC 项目初始化目录结构。
public void MimicMVCFolders(Project project)
{
var existingProjectItems =
GetAllProjectItems(project.ProjectItems)
.Select(s => s.Name);
if (!existingProjectItems.Contains("Models"))
project.ProjectItems.AddFolder("Models");
if (!existingProjectItems.Contains("Views"))
project.ProjectItems.AddFolder("Views");
if (!existingProjectItems.Contains("Controllers"))
project.ProjectItems.AddFolder("Controllers");
}
代码清单 40:创建 MVC 应用程序使用的文件夹
AddFromFile
方法允许您在磁盘上指定一个文件并将其添加到项目中。当我们创建一个文件并想将其添加到项目中时,这就派上了用场。
public void AddFileToProject(Project project,
string fileName)
{
if (File.Exists(fileName))
project.ProjectItems.AddFromFile(fileName);
}
代码清单 41:向项目添加一个现有文件
AddFromDirectory
方法遍历给定的目录及其子目录,自动将其所有项目添加到项目中。实际上,这递归地调用 AddFromFile
,将使您不必重复调用 AddFromFile
函数。
从代码生成的角度来看, AddFromTemplate
方法并不相关,但是它允许您从现有模板创建新的项目项。我们通常会完整地创建我们的项目项,然后将它们添加到解决方案中,但是这对于显示生成项目的文件夹结构中的预期内容也很有用。
public void AddFileFromTemplateToProject
(ProjectItems project, string fileName)
{
string parentPath = string.Empty;
var parent = project.Parent as Project;
if (parent != null)
parentPath = parent.FullName;
else
{
var parentItem = project.Parent as ProjectItem;
if (parentItem == null)
throw new Exception
("Could not retrieve parent path");
parentPath = parentItem.FileNames[0];
}
var projectPath = Path.GetDirectoryName(parentPath);
if (File.Exists(Path.Combine(projectPath, fileName)))
return;
Solution2 solution =
this.VisualStudio.Solution as Solution2;
var itemPath = solution.GetProjectItemTemplate
("Class.zip", "csharp");
project.AddFromTemplate(itemPath, fileName);
}
代码清单 42:从模板添加文件
此函数可用于向项目或添加到项目的任何文件夹中添加新类。我们还注意了这个函数,以确保它可以被多次调用而不会引发任何异常,因为它在添加之前会检查以确保该项尚未被添加。
var proj = vs.CreateProject("book.help");
vs.MimicMVCFolders (proj);
vs.AddFileToProject(proj, path);
vs.AddFileFromTemplateToProject(proj.ProjectItems,
"NewClass.cs");
var models = vs.GetAllProjectItems
(proj.ProjectItems).FirstOrDefault
(p=>p.Name == "Models");
vs.AddFileFromTemplateToProject(models.ProjectItems,
"SampleModel.cs");
var controller = vs.GetAllProjectItems
(proj.ProjectItems).FirstOrDefault
(p=>p.Name == "Controllers");
vs.AddFileFromTemplateToProject(controller.ProjectItems,
"SampleController.cs");
代码清单 43:将各个部分组合在一起
运行这段代码后,我们将有一个名为 book.help 的新项目,它有三个文件夹,分别叫做模型、视图和控制器。我们将在根文件夹中有一个名为 NewClass
的类,在模型和控制器文件夹中有样本类。
关于 T4 的一个恼人的事情是,默认情况下,生成的内容将存储在模板下的一个嵌套文件中,与模板相同,加上您指定的任何扩展名。这其实很少是我们想要的。
在本章中,我们看到了向项目添加新内容的各种方法,包括创建一个新项目。在上一章中,我们看到了如何使用运行时模板,它允许我们创建可以在运行时运行的模板,并在字符串变量中获得生成的输出。我们可以将这两条信息结合起来,以便能够将生成的内容放在解决方案中我们想要的任何位置,并根据我们的需要命名它们。
让我们回到我们一直在工作的 T4Utilities 项目,创建一个新的运行时模板,并将其称为class creator . TT。
ClassCreater
会相当简单。我们将定义一个模板,它有几个用于生成类的属性。 ClassCreator
增加以下代码:
<#@ template language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
public class <#=ClassName#>
{
<#
foreach (var property in Properties)
{
#>
public <#=property.DataType #> <#= property.Name#> {get;set;}
<# }
#>
}
<#+
public string ClassName{get ; set;}
public IList<PropertyDescription> Properties{get;set;}
public class PropertyDescription
{
public string Name {get; set;}
public string DataType {get;set;}
}
#>
代码清单 44:类生成器模板
现在,回到图书项目,创建一个新的文本模板,并将其命名为 SetupProject.tt 。
在这个模板中,我们需要声明一个由 ClassCreator
模板创建的类的实例。我们将初始化这个类的属性并调用 TransformText
方法,将输出存储到一个局部变量中。我们将把局部变量写到一个文件中,然后添加到一个新创建的项目中。
<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="EnvDTE100" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="EnvDTE100" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ assembly name="$(SolutionDir)T4Utilities\bin\Debug\T4Utilities.dll" #>
<#@ import namespace = "T4Utilities" #>
<#
var template = new T4Utilities.ClassCreator();
template.ClassName = "SampleModel";
template.Properties =
new List<ClassCreator.PropertyDescription>();
template.Properties.Add (
new ClassCreator.PropertyDescription
{Name = "FirstName", DataType = "string"});
template.Properties.Add (
new ClassCreator.PropertyDescription
{Name = "LastName", DataType = "string"});
template.Properties.Add (
new ClassCreator.PropertyDescription
{Name = "StreetAddress", DataType = "string"});
template.Properties.Add (
new ClassCreator.PropertyDescription
{Name = "City", DataType = "string"});
template.Properties.Add (
new ClassCreator.PropertyDescription
{Name = "State", DataType = "string"});
template.Properties.Add (
new ClassCreator.PropertyDescription
{Name = "Zip", DataType = "string"});
template.Properties.Add (
new ClassCreator.PropertyDescription
{Name = "HireDate", DataType = "DateTime"});
template.Properties.Add (
new ClassCreator.PropertyDescription
{Name = "LastStatusChangeDate",
DataType = "DateTime"});
var vs = new DTEHelper(this.Host);
var proj = vs.CreateProject("book.help.MVC");
vs.MimicMVCFolders (proj);
var models = vs.GetAllProjectItems(proj.ProjectItems)
.FirstOrDefault (p=>p.Name == "Models");
var path = Path.Combine(models.FileNames[0],
"SampleModel.cs");
using (StreamWriter writer = new StreamWriter(path))
{
var code = template.TransformText();
writer.Write (code);
vs.AddFileToProject(proj, path);
}
#>
代码清单 45: SetupProject.tt
如您所见,能够操作 DTE 为我们的模板打开了一些非常令人兴奋的机会。