65.9K
CodeProject 正在变化。 阅读更多。
Home

编写单文件生成器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (10投票s)

2013 年 11 月 22 日

LGPL3

9分钟阅读

viewsIcon

49563

downloadIcon

865

一种将“自定义工具”安装到所有 VS 版本(从 2008 到 2013)的技术。

简介  

单文件生成器 (SFG),也称为“自定义工具”,是一种简单的代码生成机制,自 Visual Studio 2003(我猜)以来就已存在于 Visual Studio 中。它接受单个文件作为输入,并生成单个文件作为输出。以下是我的自定义工具“LLLPG”运行时的屏幕截图:

SFG 通常在功能上非常有限。它们只能生成一个输出文件,Visual Studio 本身负责创建它,而你所能做的就是告诉它输出文件使用什么文件扩展名。为了让你的选择更加有限,你必须在获得输入文件名或输入文件内容之前选择文件扩展名。显然,可以编写一个行为像多文件生成器的 SFG,但我还没有弄清楚如何做到这一点。

背景   

我最近编写了一个名为 LLLPG 的解析器生成器,我希望用户能够轻松地在 Visual Studio 中使用它。每个文件的属性框中都有一个“自定义工具”选项,所以我寻找了一种使用该机制的方法。

最初,我发现了一篇关于 自定义工具的 CodeProject 文章。我确信它在 2008 年是一篇有用的文章,但今天它有些走入了死胡同。

单文件生成器的工作方式与大多数其他 Visual Studio 扩展不同。Visual Studio 的现代版本大多使用“MEF”(托管扩展框架)进行扩展,尽管 Visual Studio 的核心是非托管代码,但 MEF 扩展是 .NET 代码。在 MEF 扩展中,你需要使用大量的“魔法”属性:为了与 Visual Studio 通信,你需要在类和字段上放置大量属性,然后祈祷 Visual Studio 会注意到它们(因为如果它忽略它们,你又怎么可能找出原因呢?)MEF 扩展只适用于 Visual Studio 2010 及更高版本。

相比之下,SFG 是基于 COM 的,因此理论上你可以用非托管代码或 .NET 1.0 编写一个,而不是必须使用与 Visual Studio 相同的 .NET 版本。COM 是 1990 年代用于跨语言互操作性的旧机制。与 .NET 相比,它非常笨拙,也难以研究,因为当你搜索“COM”时,Google 会将“COM”识别为大多数网站名称的最后三个字母;你还不如搜索“THE”。幸运的是,.NET 使创建(更不用说使用)COM 组件相对容易。

我发现编写其他人实际会使用的 Visual Studio 扩展通常非常痛苦,因为:

  1. 你必须拥有付费(非 Express)版本的 Visual Studio 才能编写一个。
  2. 你必须安装与你的 VS 版本匹配的 Visual Studio SDK。例如,如果你有 Visual Studio 2013,你需要 Visual Studio 2013 SDK。如果没有项目所需的精确 SDK,你甚至无法在 Visual Studio 中打开该项目。
  3. 给定 Visual Studio SDK 版本 X,你似乎无法使用它来为 Visual Studio X-1 编写扩展。如果你只为 Visual Studio 2013 付费,你无法创建支持 Visual Studio 2012 或 2010 的扩展;在我的情况下,我发现 Visual Studio 2010 SDK 拒绝安装在装有 VS 2013 Professional 和 VS 2010 Express 的机器上。
  4. 你不能假设为 VS 2010 编写的扩展会在 VS 2012 或 VS 2013 中工作。需要单独测试。
  5. Visual Studio 可能会决定忽略你的扩展,不使用它,并且不给出任何理由。
而且需要注意的是,大多数类型的 VS 扩展在 Visual Studio Express 版本中不起作用。这不是你在购买并安装 Visual Studio 2010、2012 和 2013 并安装了三个匹配的 SDK 之后才想知道的消息!

微软的单文件生成器示例受这些限制。它需要 Visual Studio 2010 Pro(并且必须是 2010,2012 或 2013 的示例不存在)以及 VS2010 SDK,并且不清楚它是否可以在 Express 版本中工作(如果你能弄清楚,请告诉我)。当我尝试该示例时,它对我不起作用,由于注册表设置的一些问题:

 

不了解微软期望为 SFG 创建注册表设置的机制,我在 StackOverflowVisual Studio 可扩展性论坛 上等待答案超过三周,偶尔尝试对我的“vsix”项目进行新的调整,直到我最终放弃并尝试了完全不同的方法。

我发现关于 SFG 最好的事情是,有可能绕过所有这些废话。我找到了一种编写 SFG 项目的方法,它

  • 可以在 VS 2010、VS 2012 和 VS 2013 中打开和构建,包括 Express 版本
  • 不需要任何 Visual Studio SDK
  • 同时支持 2008 和 2013 的所有 Visual Studio(它可能也支持 VS 2005,我只是没有副本进行测试)。你的扩展可以在 VS 2008 中工作,即使它使用 VS 2008 不知道的 .NET 4。
  • 在 Visual Studio Express 版本中工作。
我的技术也有一些缺点:你将无法访问 Visual Studio 的所有功能,并且你无法轻松地将其他功能(例如语法高亮扩展)与 SFG 捆绑在一起。但是,嘿,你可能不是来这里思考安装问题的。让我们来谈谈如何实际编写自定义工具!

编写单文件生成器

在你可以基于我的技术编写 SFG 之前,你必须

  • 添加对 Microsoft.VisualStudio.Shell.Interop.dll 的引用,这是所有 Visual Studio 的标准组成部分。
  • 将源文件 CustomToolBase.csCodeGeneratorRegistrationAttribute.cs 从演示项目复制到你的项目。CustomToolBase 负责实现 IVsSingleFileGenerator 接口。
然后你所要做的就是编写一个派生类,它重写 DefaultExtension()Generate() 方法(以及一些属性,见下文)。就这样,你完成了!

作为参考,以下是 CustomToolBase 的代码

public abstract class CustomToolBase : IVsSingleFileGenerator
{
	protected abstract string DefaultExtension();
	public int DefaultExtension(out string defExt)
	{
		return (defExt = DefaultExtension()).Length;
	}
	
	protected abstract byte[] Generate(string inputFilePath, string inputFileContents, 
		string defaultNamespace, IVsGeneratorProgress progressCallback);

	public virtual int Generate(string inputFilePath, string inputFileContents, 
		string defaultNamespace, IntPtr[] outputFileContents, 
		out uint outputSize, IVsGeneratorProgress progressCallback)
	{
		try {
			byte[] outputBytes = Generate(inputFilePath, inputFileContents, 
				defaultNamespace, progressCallback);
			if (outputBytes != null) {
				outputSize = (uint)outputBytes.Length;
				outputFileContents[0] = Marshal.AllocCoTaskMem(outputBytes.Length);
				Marshal.Copy(outputBytes, 0, outputFileContents[0], 
					outputBytes.Length);
			} else {
				outputFileContents[0] = IntPtr.Zero;
				outputSize = 0;
			}
			return 0; // S_OK
		} catch(Exception e) {
			// Error msg in Visual Studio only gives the exception message, 
			// not the stack trace. Workaround:
			throw new COMException(string.Format("{0}: {1}\n{2}", 
				e.GetType().Name, e.Message, e.StackTrace));
		}
	}
} 

Generate 方法返回一个 byte[] 而不是 stringStringBuilder;这允许你选择用于输出文件的编码。以下是一个非常简单的 SFG 实现,它只是将文件转换为大写:

// Note: the class name is used as the name of the Custom Tool from the end-user's perspective.
[ComVisible(true)]
[Guid("91585B26-E0B4-4BEE-B4A5-12345678ABCD")]
[CodeGeneratorRegistration(typeof(ToUppercase), "Uppercasification!", vsContextGuids.vsContextGuidVCSProject, GeneratesDesignTimeSource = true)]
[CodeGeneratorRegistration(typeof(ToUppercase), "Uppercasification!", vsContextGuids.vsContextGuidVBProject, GeneratesDesignTimeSource = true)]
[CodeGeneratorRegistration(typeof(ToUppercase), "Uppercasification!", vsContextGuids.vsContextGuidVJSProject, GeneratesDesignTimeSource = true)]
//[ProvideObject(typeof(ToUppercase))]
public class ToUppercase : CustomToolBase
{
	protected override string DefaultExtension()
	{
		return ".txt";
	}
	protected override byte[] Generate(string inputFilePath, string inputFileContents,
		string defaultNamespace, IVsGeneratorProgress progressCallback)
	{
		return Encoding.UTF8.GetBytes(inputFileContents.ToUpperInvariant());
	}
}
  

Visual Studio 自定义工具是特定于语言的(也就是说,它们是否可用取决于希望使用自定义工具的项目的编程语言)。因此,你必须为每个你希望支持的语言指定一个 CodeGeneratorRegistration 属性;第三个参数是标识 Visual Studio 中项目类型的 GUID。C#、VB 和 J# 的 GUID 是

vsContextGuidVCSProject = "{FAE04EC1-301F-11D3-BF4B-00C04F79EFBC}";
vsContextGuidVBProject = "{164B10B9-B200-11D0-8C61-00A0C91E29D5}";
vsContextGuidVJSProject = "{E6FDF8B0-F3D1-11D4-8576-0002A516ECE8}"; 

在编写“正常”安装的 SFG 时,我感觉还需要一个 ProvideObjectAttribute(来自 Microsoft.VisualStudio.Shell.10.0.dll),但在这里不需要。但是,[ComVisible(true)] 是必需且重要的。

CodeGeneratorRegistrationAttribute 以源代码形式在 Microsoft 的单文件生成器示例中提供。但是,该版本的属性依赖于 Visual Studio SDK,因此我的示例包含一个不需要 SDK 的修改版本。我的属性版本在尝试以“正常”方式安装 SFG 时起作用,它只适用于以“我的”方式安装。那么我的方式是什么?继续阅读...

真正的问题:安装 SFG

正如我所说,使用 .vsix 文件(Visual Studio 的扩展安装机制)以“正常”方式安装 SFG 存在几个缺点。我的替代技术是拒绝 vsix 文件的束缚,编写自己的“安装程序”:

此安装程序旨在与任何 SFG 配合使用。你可以从示例项目中获取安装程序 Form 并将其添加到任何其他 SFG 项目中,它将简单地工作,无需更改任何内容(哦,等等,实际上我使用了来自 Loyc.Essentials.dll 的一个名为 IMessageSink 的抽象,但如果你不喜欢引用 Loyc.Essentials.dll,则可以简单地将 IMessageSink 替换为 MessageBox。)

此安装程序执行两件事:

  • 它在每个 Visual Studio 注册表“hive”(键)的 Generators 子键下创建一些注册表项。
  • 它注册关联 LllpgForVisualStudio.exe 中的 COM 类。创建 COM 对象的能力(不仅在 Visual Studio 中,而且在 Windows 中的任何地方)取决于某些注册表设置的存在。有一个名为 regasm.exe 的实用程序用于为 .NET 程序集创建这些注册表设置,还有一个名为 RegistrationServices 的 BCL 类用于执行相同的事情(顺便说一句,非 .NET COM DLL 的等效项称为 regsvr32)。

    我注意到的一个问题是,32 位和 64 位应用程序显然需要单独的 COM 注册。尽管用“AnyCPU”编写的 .NET 程序可以在 32 位和 64 位下工作,但我使用 RegistrationServices 类向 COM 注册程序集。不幸的是,如果安装程序构建为 AnyCPU,RegistrationServices 只会在 64 位系统上将模块注册为 64 位,而不是 32 位。由于 Visual Studio 是 32 位,它将无法找到 SFG。为了解决这个问题,将安装程序构建为“x86”(32 位)而不是 AnyCPU。

遗憾的是,我的解决方案还不完美。在某些情况下(对我来说不清楚),Visual Studio 可能在注册后不识别自定义工具(你将收到可怕的“在此系统上找不到自定义工具 'LLLPG'”消息)。问题是,每个 Visual Studio 副本不是一个而是两个注册表“hive”。一个位于

HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudioEditionName\NN.0 

(其中“Wow6432Node”仅存在于 64 位系统上),另一个将位于

HKEY_CURRENT_USER\Software\Microsoft\VisualStudioEditionName\NN.0_Config 

Aaron Marten 说:“永远不要编辑以 _Config 结尾的键”,因为第二个 hive 据说在 Visual Studio 启动时会被“清除”并重新创建。也就是说,“Config”区域用于正在运行的 Visual Studio 副本,当 Visual Studio 启动时,它应该将非 Config 区域中的内容复制到启动时的 Config 区域。由于某种原因,这并不总是发生。

如果 VS 开始拒绝将注册表设置复制到 _Config,最终用户将不得不关闭 VS 并以管理员权限运行 devenv.exe /setup。为此,请为 Visual Studio 创建一个快捷方式,并将快捷方式的“目标”更改为类似以下内容(路径因 Visual Studio 版本而异;此示例适用于 Visual Studio Express,而 Pro 版本使用 devenv.exe):

"C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\WDExpress.exe" /setup 

接下来,单击快捷方式编辑选项卡上的“高级”,并勾选“以管理员身份运行”。

显然,要求最终用户执行此操作是很麻烦的,而这种需要运行 devenv.exe /setup 正是我在从使用“标准”vsix 安装程序更改为这种新安装程序时试图避免的事情(很早的时候,我就让一个 vsix 安装程序在 VS2013 中工作了,只是需要运行 devenv /setup)。也许可以通过修改安装程序,使其在两个 hive 中都安装注册表项来避免此问题。但我还没有尝试过;目前,我将这个想法留给读者自行探索。

祝 SFG 作者们好运,晚安!

历史 

  • 2013 年 11 月 22 日:首次发布
© . All rights reserved.