Skip to content

Files

Latest commit

dc284d2 · Jan 9, 2022

History

History
504 lines (387 loc) · 20.7 KB

4.md

File metadata and controls

504 lines (387 loc) · 20.7 KB

四、使用主机

技术经济评价概述

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

AddFromFile 方法允许您在磁盘上指定一个文件并将其添加到项目中。当我们创建一个文件并想将其添加到项目中时,这就派上了用场。

            public void AddFileToProject(Project project,
                        string fileName)
            {
               if (File.Exists(fileName))
                   project.ProjectItems.AddFromFile(fileName);
            }

代码清单 41:向项目添加一个现有文件

AddFromDirectory

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 为我们的模板打开了一些非常令人兴奋的机会。