自定义工具说明






4.84/5 (33投票s)
描述了自定义工具是什么以及如何编程它们
引言
自定义工具并非一项广为人知的技术——事实上,它们是Visual Studio基础设施中“几乎看不见”的参与者。本文将介绍它们是什么,如何使用它们,并提供一个编程示例。请注意,这是一个针对初学者的教程,因此我不会展示任何高级内容。
要编译/运行示例,您需要安装Visual Studio 2008 Service Pack 1。请注意,Service Pack 非常重要,因为在此之前,希望编写自定义工具的VS2008开发者会因为一些冲突的UUID而陷入停滞。顺便说一句,VS2005对于自定义工具的开发来说是没问题的,尽管API存在细微差别。
您还需要安装Visual Studio 2008 SDK。
什么是自定义工具?
以下是一些描述自定义工具的陈述:
- 它是一个文件生成器。
- 它会生成代码隐藏文件。
- 它扩展了Visual Studio。
- 它存储在
ComVisible
DLL文件中。 - 它使用注册表。
自定义工具是一个文件生成器,因为它的目的是从现有文件中生成文件。该工具的最初意图是仅生成一个文件,但通过编写一些自定义代码,您可以生成多个文件。这有什么用处呢?比如,从XSD生成数据集?自定义工具正是实现这一目标的机制。人们可以想到自定义工具的更多用途,例如:
- 使用外部XSLT转换转换XML源文件
- 获取HTML文件引用的所有图像
- 预览类的XML序列化形式
- … 以及更多
我们如何告诉一个文件使用自定义工具?很简单:在解决方案树中选择文件,然后打开属性窗口(按F4)。然后,输入您想要使用的自定义工具的名称。下面是它的样子:
指定自定义工具后,它将使用选定的文件作为输入运行。如果您拼写错误,或者自定义工具损坏,Visual Studio都会通知您。如果一切顺利,您将得到一些新生成的文件!这些文件去哪里了?它们进入了代码隐藏。换句话说,它们在解决方案树中位于选定项的下一级。下面是一个图示:
如上截图所示,代码隐藏文件出现在指定了自定义工具的项(例如Neurovisual.xml)的正下方。它们都有相同的带有蓝色箭头的图标——我不知道为什么,据我所知,没有办法改变它。
现在我们知道了生成的文件去向,一个好问题是何时生成。文件会在每次保存源文件(使用自定义工具的文件)时(重新)生成。您也可以通过右键单击文件并选择运行自定义工具来强制重新生成。
Voilà!您已经获得了生成的代码。这种魔力的实现得益于Visual Studio提供的可扩展性API。具体来说,它提供了一个接口——IVsSingleFileGenerator
——自定义工具必须实现。然而,不幸的是,在公共类型上实现此接口并编译DLL并不能使自定义工具在Visual Studio中可用——还需要采取一些额外的步骤。
Visual Studio需要知道您的工具。由于VS使用COM(Component Object Model)进行扩展,它需要知道每个自定义工具的全局唯一标识符(GUID)。这意味着四件事:
- 您必须为每个自定义工具指定一个
Guid
。 - 您必须将您的DLL注册为COM组件(使用regasm.exe)。
- 您必须添加注册表项,以便Visual Studio能够找到您的工具。
- 您必须将自定义工具放置在GAC中。
在下一节中,我们将逐步介绍创建自定义工具的过程。
基本示例
让我们创建一个基本的自定义工具——一个计算文件行数并生成包含该数字的文本文件的工具。以下是让该工具工作的步骤:
- 创建一个普通的类库项目。这里不需要设置特殊的东西。我建议坚持使用.NET 2.0,但如果您愿意,也可以随意使用.NET 3.5。只要您不将UI从自定义工具中移除,3.5应该没问题。
注意:有些人建议创建集成包,因为它们可以调试。我没有测试过。
- 添加对
Microsoft.VisualStudio.Shell.Interop
程序集的引用。这个程序集将帮助我们与Visual Studio shell进行交互。 - 创建一个新类——我们称之为
LineCountGenerator
。让该类实现IVsSingleFileGenerator
接口。生成方法存根。该接口非常简单——您只有两个方法:DefaultExtension()
和Generate()
。 DefaultExtension()
方法需要知道生成的文件应该有什么扩展名。在我们的例子中,我们想创建一个文本文件,所以我们返回“.txt”。是的,点是必需的。public int DefaultExtension(out string pbstrDefaultExtension) { pbstrDefaultExtension = ".txt"; return pbstrDefaultExtension.Length; }
Generate()
函数有点棘手。这是它的轮廓:public int Generate(string wszInputFilePath, string bstrInputFileContents, string wszDefaultNamespace, IntPtr[] rgbOutputFileContents, out uint pcbOutput, IVsGeneratorProgress pGenerateProgress)
让我们逐个讨论这些参数:
wszInputFilePath
包含正在生成内容的文件路径。bstrInputFileContents
包含输入文件的内容,作为一个单独的字符串;这可能看起来是多余的,因为我们已经知道了路径,但我能说什么呢——它很方便,可以节省一行代码。wszDefaultNamespace
包含当前解决方案或文件夹的默认命名空间的名称;这是一个有用的信息,可以用于生成代码。rgbOutputFileContenst
有点棘手。基本上,您需要将生成文件的字节写入此变量。但是,您不能直接写入(因此是IntPtr[]
类型)——相反,您必须使用System.Runtime.InteropServices.AllocCoTaskMem
分配器来创建内存并将类型字节写入其中。这听起来可能很难,但实际上并不难——我们稍后会看到它是如何完成的。pcbOutout
必须设置为我们写入rgbOutputFileContents
的字节数。pGenerateProgress
是一个接口,我们可以用它来告诉Visual Studio操作需要多长时间。只有当您的自定义工具执行耗时操作时,这才有意义。在我们的简单示例中,我们将忽略此参数。
如果一切顺利,您需要从函数返回
VSConstants.S_OK
——要获取此枚举值,您需要在项目中添加对Microsoft.VisualStudio.Shell
的引用。或者,您也可以直接返回0(零)。- 我们准备填充
Generate()
函数。让我们先获取行数:int lineCount = bstrInputFileContents.Split('\n').Length;
现在,我们使用
Encoding
类获取要写入的字节以及它们的数量:byte[] bytes = Encoding.UTF8.GetBytes(lineCount.ToString()); int length = bytes.Length;
获取字节后,我们需要使用COM任务分配器将它们写入:
rgbOutputFileContents[0] = Marshal.AllocCoTaskMem(length); Marshal.Copy(bytes, 0, rgbOutputFileContents[0], length);
没有内存释放——Visual Studio会为我们处理。现在剩下的是设置写入的字节数,并返回
S_OK
。pcbOutput = (uint)length; return VSConstants.S_OK;
我们还没有完成!到目前为止,我们只有工具的功能,还没有添加COM支持。我们现在就来做。
- 我们的自定义工具需要一个GUID——一个唯一的标识符。这个标识符基本上是一串字符。最简单的方法是在Visual Studio命令提示符下运行guidgen.exe。如果您有ReSharper,可以使用
nguid
Live Template来生成一个。将GUID添加到类文件中,使其看起来像这样:[Guid("A4F30983-CAD7-454C-BB27-00BCEECF2A67")] public class LineCountGenerator : IVsSingleFileGenerator { ⋮ }
- 为了与COM一起工作,您需要将程序集标记为
ComVisible
。打开AssemblyInfo.cs,并将相应的属性设置为true
。 - 为了在COM中使用,我们的类型需要被注册。.NET类型通过在命令行使用regasm.exe工具进行COM互操作注册。很简单——只需输入regasm后跟您的程序集名称进行注册;使用/u标志进行注销。
Visual Studio也可以为您处理注册。只需打开项目属性,然后选择“生成”选项卡。在底部,您会看到“注册COM互操作”的复选框。选中它,您在开发自定义工具时就不需要regasm(如果您计划部署自定义工具,仍然需要regasm)。
- 我们快完成了——倒数第二步是将有关自定义工具的信息添加到注册表。您需要在以下注册表路径下添加一个带有工具名称的子项:
SOFTWARE\Microsoft\VisualStudio\visual_studio_version\Generators\{language_guid}
这里有两个变量:
- visual_studio_version是您希望插件工作的VS版本。8.0对应VS2005,9.0对应VS2008。
- language_guid决定自定义工具可用于哪种语言。Interop程序集中不存在GUID常量,所以我只是在文件中记录了常量。例如,C#的GUID是fae04ec1-301f-11d3-bf4b-00c04f79efbc。不要忘记花括号!
现在我们知道了在哪里放置子项,让我们讨论一下子项应该包含什么。总体而言,子项应该包含以下值:
- 默认值应包含自定义工具的用户友好描述。
CLSID
应指向我们创建的GUID。GeneratesDesignTimeSource
用于指示源文件是否可用于可视化设计器。这个的确切含义尚不确定。我只是将其值设置为1
(一)。
将上述数据与自定义工具关联的最简单方法是使用属性,因此我们的行计数器自定义工具现在看起来如下:
[Guid("A4F30983-CAD7-454C-BB27-00BCEECF2A67")] [CustomTool("LineCountGenerator", "Counts the number of lines in a file.")] public class LineCountGenerator : IVsSingleFileGenerator
CustomTool
类(由Chris Stephano提供,请参见[1])是一个简单的属性类——我不会在此展示它(它在示例代码中)。唯一需要注意的是,它不幸地不能继承自GuidAttribute
,否则一切都会看起来更加优雅。但现在,我们遇到了一个问题:如何将所有这些精彩的元数据集成并从中创建注册表条目。 - 在注册.NET程序集以进行COM互操作时,我们可以选择指定静态函数,在类型注册时执行自定义步骤。有什么比这更好的地方来将自定义工具添加到注册表中呢?我们所要做的就是获取类型的元数据并进行注册。言归正传,以下是这两个函数的完整代码:
[ComRegisterFunction] public static void RegisterClass(Type t) { GuidAttribute guidAttribute = getGuidAttribute(t); CustomToolAttribute customToolAttribute = getCustomToolAttribute(t); using (RegistryKey key = Registry.LocalMachine.CreateSubKey( GetKeyName(CSharpCategoryGuid, customToolAttribute.Name))) { key.SetValue("", customToolAttribute.Description); key.SetValue("CLSID", "{" + guidAttribute.Value + "}"); key.SetValue("GeneratesDesignTimeSource", 1); } } [ComUnregisterFunction] public static void UnregisterClass(Type t) { CustomToolAttribute customToolAttribute = getCustomToolAttribute(t); Registry.LocalMachine.DeleteSubKey(GetKeyName( CSharpCategoryGuid, customToolAttribute.Name), false); }
我不会深入讨论所有细节——这些函数使用了一些额外的辅助方法来构建注册表键。重要的是,通过创建这两个函数,我们使自定义工具能够自行注册到Visual Studio。
编译后,还剩下最后两个步骤。您必须注册程序集以进行COM互操作,并将其放置在全局程序集缓存(GAC)中。这两个操作的顺序无关紧要。
- 正如我之前提到的,工具的COM注册发生在编译时(如果指定了相应的项目选项),或者在运行regasm.exe时发生。要注册该工具,您将调用:
regasm YourCustomTool.dll
要注销,请使用/u开关调用它:
regasm /u YourCustomTool.dll
- 为了让VS识别您的工具,它需要在VS启动时位于GAC中。因此,在启动VS之前,请使用以下命令将您的自定义工具添加到GAC:
gacutil /i YourCustomTool.dll
程序集删除使用/u开关完成,您必须记住删除.dll后缀,因为该工具需要程序集的显示名称,而不是文件名。
gacutil /u YourCustomTool
如果您已将项目设置为自动COM注册自定义工具,请随时将gacutil的调用添加到后期生成步骤中。但请注意,您可能需要指定gacutil.exe程序的完整路径。
- 重要提示:VS在运行时将您的自定义工具加载到内存中。这意味着即使您取消注册它,重新编译并重新注册新版本,VS也不会立即识别它。您需要重新启动VS才能看到更改。
好了,这基本上涵盖了让您自己的自定义工具正常工作的必要步骤。让我们回顾一下创建自定义工具所需的步骤。
- 创建一个普通的库项目。
- 添加一个继承自
IVsSingleFileGenerator
的类。 - 按照前面所述实现函数。
- 为类添加
Guid
和CustomTool
属性。 - 复制ComRegister/Unregister函数。
- 将程序集标记为
ComVisible
。 - 注册程序集以进行COM互操作。
- 将程序集放入GAC。
多个文件
IVsSingleFileGenerator
的一个问题是它只能生成一个代码隐藏文件。有时,我们可能希望有多个。幸运的是,Adam Langley([2])创建了一个解决方案来解决这个问题,该方案允许我们的自定义工具创建多个文件。他的例子尤其有趣——他展示了一个生成器,它获取HTML文件并将其中引用的所有图像作为代码隐藏文件添加。为了完整起见,我将简要描述该解决方案——如果您有兴趣,可以随时阅读原始文章。
这里简要回顾一下您需要从中派生自定义工具以获得多文件生成功能的类。该类名为VsMultiFileGenerator<T>
。此类是一个枚举器,T
泛型参数由您的子类定义。类型可以是任何东西:此泛型参数主要是供您自行处理。最合理的选择是将其定义为string
。
您需要覆盖的abstract
方法如下:
IEnumerator<T> GetEnumerator()
在这里您返回将成为文件的元素序列。string GetFileName(string element)
此方法负责根据您从上一个方法生成的元素来确定目标文件名。byte[] GenerateContent(string element)
此方法根据我们之前提供的元素生成字节流。byte[] GenerateSummaryContent()
为默认元素生成内容——即IVsSingleFileGenerator
期望的那个。我稍后会更详细地介绍它。string GetDefaultExtension()
返回摘要内容的默认扩展名。此方法调用和前一个方法基本上是IVsSingleFileGenerator
的Generate()
和GetDefaultExtension()
方法的传播。
我承诺要解释“摘要内容”,所以这里是。基本上,多文件生成器是一个单文件生成器,它还执行额外的操作(例如,您知道的,生成其他文件)。然而,在执行完这些之后,它被迫以老方式生成至少一个文件,以满足IVsSingleFileGenerator
接口的合同。这并不总是很好——例如,如果您正在编写一个自适应生成器,它在实际执行之前不知道它将创建的文件类型,那么您就遇到了麻烦——您将最终得到一个额外的文件添加到代码隐藏中(因为您必须有一个具有定义扩展名的文件)。不过,这只是一个表面问题,并不会影响功能。而且,如果您决定聪明点,向VS提供null
数据和0(零)长度,您将得到一个错误对话框。别说我没警告过您!
要查看多文件生成器的示例用法,您可以看看我编写的多文件XSL转换器[3]。
零碎
关于自定义工具,有几件事需要提及。
首先,与源代码的集成并不总是如您所愿。生成的文件似乎会像往常一样添加到源代码中,但有时您可能会遇到这种情况:某些人可以看到它们,而另一些人看不到。我不知道这其中的机制——我只在SourceSafe中遇到过,所以我有点希望TFS能更好地处理它们。无论如何,一个用于XML到XSL转换的自定义工具确实在一个商业项目上成功使用过。让您知道。
如果您想知道自定义工具和普通的VS插件有什么区别,嗯,差别不大!事实上,插件更好,因为它们不会生成多余的“摘要”文件。自定义工具机制主要用于1对1的文件转换,这些转换几乎是自动发生的(主要是保存时)。您也可以在插件中编程实现相同的保存触发功能。我的建议是,对于基本转换(例如,获取类的序列化预览),请使用自定义工具。对于任何更严肃的事情,最好编写一个插件。
不一定需要为文件显式指定自定义工具。相反,您可以将其与文件扩展名关联。例如,Visual Studio会为.tt文件扩展名这样做。这允许任何以.tt扩展名保存的文件由文本模板处理器(也称为T4)执行。创建您自己的关联很容易——在向注册表写入信息时,不要创建带有自定义工具名称的子项(例如MyGenerator
),而是指定自定义工具将始终适用的文件的扩展名(例如.myfile)。别忘了扩展名本身前面的点!
结论
我希望本文证明了自定义工具并不难编程。当然,需要采取一些步骤,但我已经全部描述了,所以希望没有什么能阻止您在需要时编写自己的自定义工具。
如果您喜欢这篇文章,请投票。如果您不喜欢,也请投票,并告诉我怎样可以做得更好。谢谢!
参考文献
- 用于Visual Studio .NET的XSL转换代码生成器,Chris Stephano
- 在Visual Studio 2005中创建自定义工具以生成多个文件,Adam Langley
- 用于Visual Studio的多文件XSL转换自定义工具,Dmitri Nesteruk
历史
- 2008年11月26日:初始版本