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

NUnit 入门

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (6投票s)

2009年10月13日

Zlib

34分钟阅读

viewsIcon

44275

downloadIcon

493

如何创建与 Visual Studio 2003 兼容的 Visual Studio 2008 插件。

目录

引言

动机

几年前,我为 Visual Studio 2002 编写了 版本控制构建插件。自初始发布以来,该插件已更新至新版本的 Visual Studio,包括 VS 2008。然而,最近有几位用户抱怨无法在 Visual Studio 2008 上启动它,收到了未处理的 COM 异常错误。由于当前版本的插件仍然使用 COM 注册,而 COM 注册已从 Visual Studio 2005“禁用”,我认为使用新的基于 XML 的插件注册可能会避免这个问题。由于我对基于 XML 的注册没有经验,我决定创建一个插件并在 Visual Studio 2008 中进行测试,然后将其调整为与 VS 2003 兼容。在开发此插件的过程中,我从互联网上找到了许多有用的信息和提示,我将它们用于此工具,并希望正确地归功于所有这些资源。毫无疑问,插件开发者的头号资源是 Carlos Quintero 的 MZ-Tools 页面,尤其是 Visual Studio .NET 可扩展性资源部分。我经常觉得这些页面比 MSDN 页面更有价值。

NUnit Starter 插件的原理

由于我使用 NUnit 进行单元测试,因此该项目的想法是创建一个可以简化启动 NUnit 测试的插件。Nunit 具有图形界面,可以非常清晰地显示已处理的测试,并区分通过的测试和失败的测试。您可以将 NUnit 作为一个独立应用程序启动,选择包含测试的程序集并运行它们,但我认为最好直接在 Visual Studio 中启动测试,而不离开 IDE。单个测试夹通常放在单独的文件中,如果用户只想运行该测试夹,只需右键单击解决方案资源管理器中相应的项,然后从上下文菜单中启动测试。如果用户想运行程序集中的所有测试,他/她将以同样的方式进行,单击相应的项目项。

如果测试因某种神秘原因失败,用户需要对其进行调试,在测试中的相应命令处设置断点。从 Visual Studio 调试 NUnit 测试相当麻烦:用户必须启动 NUnit GUI,然后将 Visual Studio 调试器附加到相应的进程,最后启动测试。我的插件的另一个很棒的功能将是尽可能自动化此过程。

在开发过程中,我发现已经存在其他(也许更好)的解决方案,仅举两个免费工具为例:

这些工具都未能完全满足我的需求:第一个工具对我来说似乎有点“大材小用”,因为它有很多附加功能;第二个工具缺少运行单个测试的功能。

文章结构

本文包含两大部分:

  • 关于创建和测试插件的通用描述,以及
  • NUnit Starter 插件的描述

第一部分面向不熟悉插件的读者。熟悉插件开发的读者很可能会跳过它,但我希望关于向后兼容性和安装问题的最后两部分对他们来说也可能很有趣。第二部分讨论 NUnit Starter 的实现细节。

创建插件

插件是一个类库,可以从宿主应用程序(例如 Visual Studio 或 Office 应用程序)运行,提供附加或修改的功能。如果从插件向导开始,创建插件的骨架相当直接。然而,插件向导中有几个选项可能会让不熟悉插件的开发者感到困惑。在本节中,我将尝试解释这些选项以及如何之后进行纠正。熟悉此主题的读者可以跳过本节。

除了初始代码外,插件向导还会创建一个.Addin配置文件,在下一小节中进行描述。此文件中的设置根据插件向导中选择的选项进行设置,如下面的描述所示。

要启动插件向导,在“新建项目”对话框的左侧“项目类型”窗格中,打开“其他项目类型”组,然后选择“可扩展性”子组。在右侧“模板”窗格中,选择“Visual Studio Add-in”,然后键入插件项目的名称。请注意,为了向后兼容性,已选择 .NET Framework 2.0 作为目标平台。您以后随时可以在项目设置中更改目标平台。

Add New Project dialog

在“欢迎”屏幕之后,您需要选择将用于编写插件代码的编程语言,即生成代码骨架的编程语言。我为这个项目选择了 C#。

下一个对话框提供了一个插件可用宿主的列表。这些选项会在 .Addin 文件中生成一组 HostApplication 标签。对于插件,我通常会取消选中“宏”选项。请注意,此页和后续页面中的任何选项都可以在向导完成后轻松更改,因为它们直接与生成的 .Addin 文件或代码骨架的内容相关。

输入您的插件名称(.Addin 文件中的 FriendlyName 标签)和简短描述(Description 标签)后,下一页会提供插件选项。“是否要创建命令栏...”选项决定了向导是否还会生成插件与 Visual Studio GUI 交互所需的代码。如果未选中此选项,插件向导生成的 Connect 类将仅包含实现 IDTExtensibility2 接口的方法。

  • OnAddInsUpdate
  • OnBeginShutdown
  • OnConnection
  • OnDisconnection
  • OnStartupComplete

从名称上可以看出,这些方法仅代表在插件状态更改的各个阶段处理的事件处理程序。这些方法都不允许用户直接与插件代码交互——用户需要手动添加控件才能将它们绑定到插件中的方法。

因此,如果您想创建一个带有 GUI 控件的插件,允许调用插件代码,最简单的方法是勾选上述选项。在这种情况下,Connect 类还将实现 IDTCommandTarget 接口,其中包含 ExecQueryStatus 方法。Exec 方法用于启动命令,而 QueryStatus 方法控制命令的状态,即相应控件的状态。最后但同样重要的是,勾选此选项后,生成的 Connect 类将包含创建命令和“工具”菜单顶部相应项的代码。此菜单项用于启动我们插件中的命令。

当然,如果您忘记勾选此选项,稍后可以手动添加 ExecQueryStatus 方法。但是,从头开始编写创建命令和 GUI 元素的代码对于初学者来说相当复杂,所以如果您希望您的插件提供用户交互,请不要忘记勾选此选项。

使用此页面上的第二个选项(“我希望我的插件加载...”)(CommandLineSafe),我们可以控制插件是否在 Visual Studio 启动时自动加载(LoadBehavior)。如果未选中此选项,则必须通过 Visual Studio 的“工具”菜单启动的插件管理器加载插件。为了调试目的,尤其是当插件注册出现问题时,这种选项是更好的解决方案,所以我通常会将其关闭。您随时可以在插件管理器中更改此选项。

第三个选项(“我的插件永远不会弹出模态 UI...”)(CommandLineSafe)应该被选中,如果您打算创建应在无人值守操作中运行的插件,例如从命令行构建。

最后一个选项页面允许输入将包含在“帮助关于 Visual Studio”对话框中的可选信息(AboutBoxDetailsAboutIconData 标签)。

插件向导完成后,项目结构将如上图所示。

Solution Explorer after Add-In Wizard

.Addin 文件

除了前面提到的 Connect 类之外,您可能还会注意到两个 .Addin 文件:一个存储在项目文件夹中,另一个(其名称带有“For Testing”后缀)作为链接附加到项目,并存储在当前用户的 Addin 文件夹中。该文件夹位于当前用户文档文件夹内的“Visual Studio 20xx”文件夹中(例如,“C:\Users\UserName\Documents\Visual Studio 2008\Addins”)。如果您打开这两个文件,您会发现它们的内容相同,除了 Assembly 标签——链接的 .Addin 文件(带有“For Testing”后缀的那个)包含插件 DLL 的实际名称及其完整路径,如生成的项目初始设置所示。

.Addin 文件用于插件测试和调试:如果您从 Visual Studio 运行插件项目,它将打开另一个 Studio 实例。在启动过程中,Studio 会检查几个预定义文件夹的内容,并从这些文件夹中找到的所有 .Addin 文件中收集信息。如果其中任何插件被设置为在宿主(即 Studio)启动期间加载,它将被自动激活——否则用户必须从插件管理器激活它。

让我们看看 .Add-in 文件的内容。

<?xml version="1.0" encoding="UTF-16" standalone="no"?>
   <Extensibility xmlns="http://...">
     <HostApplication>
       <Name>Microsoft  Visual Studio</Name>
       <Version>9.0</Version>
     </HostApplication>
     <HostApplication>
       <Name>Microsoft  Visual Studio</Name>
       <Version>8.0</Version>
     </HostApplication>
     <Addin>
       <FriendlyName>NUnit  Starter</FriendlyName>
       <Description>Launch  NUnit tests from Visual Studio IDE.</Description>
       <AboutBoxDetails>For more  information ...</AboutBoxDetails>
       <AboutIconData>000001000600202010...</AboutIconData>
       <Assembly>C:\Users\  ... \NUnitStarter.dll</Assembly>
       <FullClassName>NUnitStarter.Connect</FullClassName>
       <LoadBehavior>0</LoadBehavior>
       <CommandPreload>1</CommandPreload>
       <CommandLineSafe>0</CommandLineSafe>
     </Addin>
   </Extensibility>

首先,您会注意到一个插件的目标宿主应用程序列表。目前,此列表可能包括 Visual Studio 2005(即 8.0 版本)、2008(9.0 版本)和 2010(10.0 版本)。Addin 标签内是我们在插件向导中实际输入的插件设置。因此,如果您在插件向导中遗漏了某些设置,可以手动在 .Add-in 文件中进行更正。由于这些设置中的大多数都在 MSDN 中进行了文档记录,我将跳过它们。

AboutBoxDetailsAboutIconData 在您打开 Visual Studio 中的“帮助关于”对话框时显示。AboutIconData 是十六进制格式的二进制图标数据。因此,如果您使用某种十六进制编辑器将这些数据复制到一个文件中,您将得到一个 ICO 文件。插件向导生成的默认数组包含小(16x16)和大(32x32)的图标,颜色分别为 16、256 和 1670 万色。要更改图标,您需要使用某种图标编辑器(或 Visual Studio),创建一个 ICO 文件,然后使用十六进制编辑器打开该文件,复制其十六进制表示并粘贴到 .Addin 文件中。

如果勾选了“是否要创建命令栏...”选项,插件向导会将 CommandPreload 标签设置为 1。这将强制 Visual Studio “预加载”插件,即在启动后立即加载它,以执行首次初始化。初始化后,插件将被卸载,然后自动或通过插件管理器重新加载,并且不再执行此“预加载”。此标志的含义将在我们讨论创建命令和控件时得到澄清。

需要注意的是,如果您更改了插件程序集名称或其位置(例如,如果您更改了项目设置中的输出文件夹),您必须不要忘记同步用于调试的 .Addin 文件中的 Assembly 标签。否则,Studio 将无法找到相应的程序集并加载它。对于 Visual Studio 2002/2003 中创建的插件也存在类似的问题:当开发人员最初创建插件项目时,插件向导会注册相应的 DLL 和 Connect 类。但是,一旦其中任何一个被修改,插件就会停止工作,并且需要手动重新注册或运行安装过程。

插件方法的执行顺序

在插件向导创建了 Connect 类骨架之后,您可以启动新实例的 Visual Studio 或为项目启动调试会话来运行该插件。虽然自动生成的代码不实现任何功能,但这对初学者来说非常有用,可以帮助他们了解宿主应用程序调用方法的顺序。如果您将从插件项目启动插件,只需在每个方法中插入断点。否则,如果您打算从外部启动的 Visual Studio 实例运行它,您可以简单地在每个方法中插入消息框调用。

我们可以解析三种不同的插件初始化序列(每种前面都有对 Connect 构造函数的调用!)

  1. 所谓的“预加载”序列调用以下方法:
    • OnConnection,参数为 connectMode = ext_cm_UISetup
    • OnDisconnection,参数为 disconnectMode = ext_dm_UISetupComplete

    请注意,此序列仅在插件首次运行时执行一次。正如您从 OnDisconnection 方法调用中可能注意到的,插件会立即卸载,然后由以下两个序列之一重新初始化。此序列仅用于首次初始化,如下文所述。

  2. 如果插件配置为随 Visual Studio 自动加载(即 LoadBehavior 设置为 1),则会调用以下方法:
    • OnConnection,参数为 connectMode = ext_cm_Startup
    • OnAddInsUpdate
    • OnStartupComplete
  3. 如果插件是从插件管理器初始化的,则会调用以下方法:
    • OnConnection,参数为 connectMode = ext_cm_AfterStartup
    • OnAddInsUpdate

有两种可能的卸载场景:

  1. 如果插件从插件管理器卸载,则仅调用以下方法:
    • OnDisconnection,参数为 disconnectMode = ext_dm_UserClosed
  2. 如果在插件仍处于加载状态时退出 Visual Studio,则会调用以下方法:
    • OnBeginShutdown,然后是
    • OnDisconnection,参数为 disconnectMode = ext_dm_HostShutdown

正如您所见,OnConnectionOnDisconnection 方法总是会被调用,而不管插件是如何加载或卸载的,它们在 connectModedisconnectMode 参数上有所不同。

只要我们不调用插件添加的命令(如下节所述),宿主应用程序就不会调用 ExecQueryStatus 方法。

创建命令和 UI 控件

如果您在插件向导中选择了创建命令栏,则生成的 OnConnection 方法将包含代码,用于在“工具”菜单中创建命令和相应的项。命令通过其名称进行标识,该名称作为 AddNamedCommand2 方法的第二个参数提供。此名称(前缀为 Connect 类的完整名称)在 ExecQueryStatus 方法中进行验证,以查找已启动的命令。由于此代码注释得很清楚,我相信没有必要详细分析它。让我们关注一些不那么明显的事实。

从生成的代码可以看出,插件可以创建用于启动插件命令的自己的控件。这些控件可以绑定到现有控件(菜单或工具栏),也可以创建在自己的工具栏上。在这两种情况下,控件都可以是临时的或永久的。临时控件在每次激活插件时都会重新创建,并且应在 OnDisconnection 方法中卸载插件时删除。带有临时控件的插件更容易卸载,因为不需要额外的清理。但是,如果您想为某个命令/控件分配快捷方式,或者需要在会话之间保留工具栏布局,则必须使用永久控件。有关如何创建临时或永久控件的详细描述,可以在 此处找到。请注意,插件向导生成的代码在“工具”菜单中创建了一个永久项。

回到 OnConnection 方法,重要的是要记住,此方法是插件的实际“入口点”。每当插件被激活时,宿主应用程序都会调用它。这可能发生在用户从插件管理器激活它时,或者(如果配置了)在 Visual Studio 启动期间。但正如我们在上一节中所见,在这些调用之前,如果 CommandPreload 标签设置为 1OnConnection 方法将首次被调用以检测插件。此调用应用于创建永久控件。

我们如何确定 OnConnection 方法是在哪个阶段调用的?答案由 connectMode 参数提供:

  • 当插件首次运行时,此枚举参数的值为 ext_cm_UISetup
  • 如果插件随 Visual Studio 启动自动启动,则其值为 ext_cm_Startup
  • 如果插件已从插件管理器激活,则其值为 ext_cm_AfterStartup

因此,如果我们想创建永久控件,最好的方法是检查 connectMode 参数是否为 ext_cm_UISetup 值,然后初始化控件。

if (connectMode ==  ext_ConnectMode.ext_cm_UISetup)
{
  // initialize permanent controls
}   

由于 OnConnection 仅会以该值调用一次 connectMode,控件初始化代码将不再执行。另一方面,要创建临时控件,用户应该检查 connectMode 参数是否为 ext_cm_AfterStartup 值并初始化临时控件。

if (connectMode ==  ext_ConnectMode.ext_cm_AfterStartup)
{
  // initialize temporary controls
}  

读者可能会注意到,我们遗漏了插件设置为随 Visual Studio 启动自动加载的情况,即当 connectMode 等于 ext_cm_Startup 时。理论上,我们也可以在上面的代码中添加这个条件,但更好的方法是从 OnStartupComplete 方法中支持这种情况。因此,我们确保在创建控件之前 Visual Studio GUI 已完全初始化。

OnConnection 方法的第一个参数(object application)是对 Visual Studio 宿主的引用。此引用提供了指向 Visual Studio 自动化(automation)的链接,通过它我们可以访问 Visual Studio IDE 中的控件和当前解决方案等对象。通常插件需要与 Visual Studio 通信,因此将此引用存储到类成员中至关重要,正如在 OnConnection 方法中自动生成的代码所做的那样。

您必须牢记,每次调用 OnConnection 方法时,都会在 Connect 类的单独实例上进行,因此所有必要的数据必须(重新)初始化。但是,我们如何才能访问在预加载阶段创建的永久命令和控件呢?它们已附加到 Visual Studio 并缓存到用户配置文件中的 CmdUI.prf 文件中,并且只要插件未被卸载,它们就可以使用。

执行插件命令

插件向导生成的 OnConnection 方法代码会将命令添加到 EnvDTE 对象(即 Visual Studio)的 Commands 集合中,并将控件(“工具”菜单中的项)附加到它。当用户单击该控件时,Visual Studio 将调用 Exec 方法。用户负责在此方法中实现适当的操作(例如,您可以只放置一个显示消息框的语句)。由于每个插件可能有一个以上的命令,因此启动的命令通过提供给方法的第一个参数(命令的全名)进行标识。在这种情况下,代码必须区分哪个命令已被启动,首先通过比较其名称,然后执行相应的代码。

QueryStatus 方法在每次需要更新命令状态时被调用。因此,在此方法中,用户可以启用/禁用或隐藏/显示插件控件。

调试插件

插件可以像任何其他应用程序一样进行调试。作为简单测试,在 Connect 类的构造函数中设置一个断点,然后在调试模式下启动插件应用程序。首先,将加载一个新实例的Visual Studio,然后执行将停止在设置的断点处。退出第二个Visual Studio 实例将结束调试会话。

要了解第二个Visual Studio 实例如何启动,您应该检查插件项目的调试设置。

Debug settings for an add-in project

您会注意到在“启动操作”选项下,设置为启动Visual Studio 作为外部应用程序,并且其目录设置为工作目录。注意到“命令行参数”值 /resetaddin NUnitStarter.Connect 很重要。简而言之,此命令行参数会将插件重置到初始状态,就像它从未运行过一样。因此,每次运行调试会话时,OnConnection 方法将首先以 ext_cm_UISetup 值用于 connectMode 参数被调用,就像插件第一次安装后一样。这可能会让缺乏经验的开发人员感到困惑,他们可能会认为在插件安装到目标计算机之后,每次启动Visual Studio 都会发生这种情况。在实际生活中,此调用仅在Visual Studio 首次安装插件后运行。

如果您想在另一个版本的Visual Studio 中测试插件,只需更正“启动外部程序”和“工作目录”框的内容,并将 .Addin 文件复制到对应于该版本Visual Studio 的位置。请注意,Visual Studio 2002/2003 在调试它之前需要 COM 注册插件并在注册表中进行更改。

插件注册

有两种方法可以将插件注册到 Visual Studio:

  1. COM 注册
  2. 基于 XML 的注册

COM 注册是注册插件到旧版本 Visual Studio(包括 2002 和 2003 版本)的唯一方法。在安装插件时必须注册条目(即 Connect)类,并必须在 Visual Studio 注册表分支中添加新项。启动时,Visual Studio 首先读取 Software\Microsoft\VisualStudio\x.x\Addins 分支下的注册表项(其中 x.x 代表 Visual Studio 的实际版本,例如 VS 2002 或 VS 2003 的“7.0”或“7.1”)。此分支可以位于 HKEY_LOCAL_MACHINEHKEY_CURRENT_USER 中,具体取决于插件是为所有用户还是仅为当前用户安装的。然后它搜索已注册的 COM 对象列表以查找插件 DLL 的实际位置。

Registry content for Add-in

虽然 COM 注册仍可用于新版本的 Visual Studio,但 2005 版本引入了更简单的基于 XML 的注册,无需注册 COM 对象或更改注册表。要注册插件,只需将包含正确信息的相应 .Addin 文件放置在预配置文件夹之一中即可。如前所述,启动时,Visual Studio 会扫描所有这些文件夹并记录所有相应的插件。可以在“选项”对话框的“环境-插件/宏安全”页面中找到(预)配置文件夹的列表,如下图所示。

Add-in file paths

保持向后兼容性

我的意图之一是使插件与早期版本的 Visual Studio 兼容,特别是与 2003 版本。此限制会引发几个应澄清的问题。

第一个问题与运行代码的 CLR 版本有关。正如读者可能知道的,独立应用程序将运行在编译时为其目标设置的 CLR 上(除非在 .config 文件中另有指定)。如果目标计算机上没有该 CLR,则会使用稍后兼容的运行时。然而,对于插件,CLR 版本由宿主应用程序决定:Visual Studio 2003 实例化 CLR 1.1,而 VS 2005 和 VS 2008 使用 CLR 2.0。您应该注意 .NET Framework 和 CLR 版本之间的区别——.NET Framework 3.0 和 3.5 使用 CLR 2.0;有关详细信息,建议您查看 这篇文章

要编写可以在不同 CLR 上运行的通用代码,它必须与最低版本兼容。当针对 CLR 1.1 和 CLR 2.0 编写代码时,这意味着我们必须避免使用像部分类或泛型这样的构造,因为它们不受 CLR 1.1 支持。这可能会在 VS 2005 或 2008 的设计器中生成窗体时带来麻烦——生成的代码必须手动合并到单个文件中,以规避部分类定义。

比 CLR 版本兼容性更严重的问题是 .NET Framework(这里我指的是类库)版本不一致。如果 .NET Framework 完全向后兼容,那么我们只需要关心使用最低版本中可用的类。不幸的是,情况并非如此。例如,CommandBar 和相关控件定义在 .NET Framework 1.1 的 Microsoft.Office.Core 程序集中。另一方面,.NET Framework 2.0(及更高版本)在新的程序集(当然也在不同的命名空间中)中定义了这些控件:Microsoft.VisualStudio.CommandBars

我想到的唯一解决方案是为同一个源代码创建每个 .NET Framework 版本的插件程序集的不同版本。有必要创建两个不同的项目,每个项目都以不同的 .NET Framework 为目标,但都包含相同的源代码文件集并引用兼容的 .NET Framework 程序集。使用预处理器语句,我们可以有条件地包含/排除每个目标不同的代码部分。条件代码包含提供了优化代码部分或为较新版本的 .NET Framework 提供旧版本不支持的功能的机会,如下一节所述。

这种方法的主要缺点是需要手动同步两个项目。每次在一个项目中添加或删除文件时,都必须在另一个项目中重复该操作。如果同一个项目可以为每个目标版本拥有不同的构建配置(这实际上是可能的,如下所述),并且为每个配置引用不同的 .NET Framework 的程序集,就可以避免这种情况。由于我不知道一种为每个配置有条件地引用程序集的简单方法,所以我使用不同的项目。另一方面,不同的项目可以同时为不同的目标构建输出,而无需更改配置。

假设 .NET Framework 1.1 目标项目定义了 TARGETTING_FX_1_1 符号。在这种情况下,对于处理命令栏的代码,以下条件解决了命名空间不一致的问题:

#if  TARGETTING_FX_1_1
  using Microsoft.Office.Core;
#else
  using Microsoft.VisualStudio.CommandBars;
#endif

同样,预处理器指令...

#if !TARGETTING_FX_1_1
  using EnvDTE80;
  using _DTE = EnvDTE80.DTE2;
#endif

...使 _DTE 成为更丰富的 DTE2 接口的别名。

MSBee 用于在 Visual Studio 2005/2008 中构建 .NET 1.1 项目

Visual Studio 2005 和 2008 原生只支持 .NET Framework 2.0(及更高版本)项目,所以如果我们选择为 .NET 1.1 创建插件,我们将需要使用 Visual Studio 2003 构建相应的项目。幸运的是,您可以使用 MSBee 来绕过此限制,并从 VS 2005 或 2008 构建 .NET 1.1 项目。有关如何在 Visual Studio 2008 中设置 .NET 1.1 项目的详细说明,请参阅 此处

总结:如何创建跨平台项目

本小节提供了有关如何创建和处理针对 .NET Framework 1.1 和 2.0 的插件解决方案的简要摘要。前提是您已安装 MSBee(和 .NET Framework 1.1)。

  • 使用插件向导创建一个插件骨架。为了使后续的兼容性更改更容易,请选择 .NET Framework 2.0 作为目标平台。
  • 编写插件的代码,同时考虑到代码必须尽可能与 .NET 1.1(及相应的 CLR)兼容,以便相同的代码可以用于两个目标。
  • 在您完成代码编写并且插件按预期工作后,创建项目(.csproj.vbproj)文件的副本,对文件进行必要的修改,如 此处所述,并将其添加到插件解决方案中。在新增项目中,将“活动解决方案平台”更改为 .NET 1.1,如链接所述。这将向项目定义添加 TARGETTING_FX_1_1 符号;此符号可用于条件编译。
  • 在代码中进行必要的修改以使其与 .NET 1.1 兼容。如果存在由设计器生成的表单代码,您将需要将部分类定义合并到单个文件类定义中,并删除多余的文件(文件名带有 Designer 后缀的文件)。一些仅在 .NET 2.0 中支持的控件属性必须通过条件编译排除——否则编译器将报告错误。例如,.NET Framework 中的 Button 控件具有 UseVisualStyleBackColor 属性,该属性在 .NET 1.1 中不存在。因此,我们必须在为其赋值的语句周围添加条件编译,以允许编译 .NET 1.1:
  // .NET  2.0 specific property
#if !TARGETTING_FX_1_1
  this.m_button.UseVisualStyleBackColor = true;
#endif

虽然可以毫无问题地在设计器中打开这种合并的代码,但您必须注意,如果您对表单进行任何更改,设计器将覆盖自动生成的代码,您所有的手动修改(包括条件编译语句)都将丢失。因此,我更倾向于将这种排除的代码移出设计器自动生成的部分,放到表单构造函数中,紧跟在调用 InitializeComponents 方法之后。

  • 在针对 .NET 1.1 的项目中,将对 .NET Framework 2.0 程序集的引用替换为对 Windows\Microsoft.NET\Framework\v1.1.4322 文件夹中定义的相应程序集的引用。如前所述,对 Microsoft.VisualStudio.CommandBars 的引用必须替换为对 Microsoft.Office.Core 的引用,并且您将不得不放弃 .NET 2.0 特定的程序集,如 EnvDTE80
  • 如果存在您不想放弃的 .NET 2.0 特定代码,您将不得不编写 .NET 1.1 兼容的代理程序,并使用条件编译语句来包含或排除相应的代码段。
  • 由于 .NET 1.1 插件使用基于 COM 的注册,因此您不需要相应项目中的 .Addin 文件,所以可以从项目中排除(不要删除)它们。

插件安装

使用 XML 基于的注册安装插件非常简单:

  1. 插件程序集必须复制到目标文件夹。
  2. .Addin 文件中的 <Assembly> 标签内的路径必须调整,以指向此目标文件夹。
  3. .Addin 文件必须放置在预定义文件夹之一中,如 此处所述。

这些步骤可以手动完成,但如果您想分发您的软件,安装包是客户期望您交付的。Carlos Quintero 提供了 详细描述,说明如何使用 Inno Setup 工具创建安装包,但我将坚持使用我习惯的 Microsoft Installer。

要创建安装程序,您需要为您的Visual Studio 项目添加一个安装项目:在“其他项目类型”中,选择“设置和部署”。然后,您需要从插件项目中添加输出文件,这基本上完成了上面提到的第一点。

接下来,我们必须确保 .Addin 文件会被复制到正确的文件夹。它应该是选项对话框中“环境 – 插件/宏安全”部分已列出的文件夹之一。请注意,有几个选项可用,但它们因 Visual Studio 版本而异。如果我们打算支持所有版本,选择一个不同 Visual Studio 版本通用的文件夹是合理的。

当安装插件时,用户可以选择是为所有用户安装还是仅为自己(即当前用户)安装。对于所有用户,.Addin 文件的最佳目标位置是...

%ALLUSERSPROFILE%\Application Data\Microsoft\MSEnvShared\AddIns

...因为 VS 2005 和 2008 都列出了此文件夹。但是,“应用程序数据”即使对于本地化的 Visual Studio 版本也是硬编码的,但这绝不对应于本地化 Windows 版本中应用程序数据的本地化名称!有关详细说明,您可以查看 Carlos Quinteros 的文章。第二个选择是...

%VSCOMMONAPPDATA%\AddIns

...带有每个要支持的版本子文件夹的文件夹,即...

%VSCOMMONAPPDATA%\AddIns\8.0 
%VSCOMMONAPPDATA%\AddIns\9.0

...分别为 Visual Studio 2005 和 2008。

对于当前用户,选择更简单,因为没有“本地化错误”。

%APPDATA%\Microsoft\MSEnvShared\AddIns

但是,如何强制安装程序将 .Addin 文件复制到适当的文件夹(记住在卸载时将其删除)并将其中的正确路径写入?安装程序不支持这些操作,因此我们需要自己实现它们。为此,我们将创建一个派生自 Installer 类(定义在 System.Configuration.Install.dll 程序集中)的类,只需从 Visual Studio 环境中添加一个新的 Installer 类即可。Installer 类有几个虚拟方法,可能在安装或卸载的不同阶段被安装程序实用程序调用。对于我们的安装,我们必须重写 InstallUninstall 方法。我们派生类中的 Install 方法将首先调用基类中的实现,更改文件内容以包含插件程序集的正确路径,将 .Addin 文件复制到相应的目标文件夹,最后保存到目标文件夹的路径,以便卸载可以删除它们。Uninstall 方法将删除所有 .Addin 文件。

要选择 .Addin 文件的目标文件夹,我们必须知道用户是选择为所有用户安装插件还是仅为当前用户安装。这将由安装程序实用程序在“allusers”参数中提供,如本节末尾所述。我们的 Installer 类将从 Context 属性中获取此参数。

bool allUsers =  (string)Context.Parameters["allusers"] == "1"; 

现在我们知道要将 .Addin 文件复制到哪里,最后一个需要解决的障碍是找出文件是从哪里复制的?一个简单的解决方案是将安装程序设置为将此文件复制到安装其他程序集文件的输出文件夹,然后修改它,将修改后的版本复制到 .Addin 文件的目标文件夹,最后删除原始文件。然而,这种方法可能存在风险,因为在我们尝试修改时,.Addin 文件可能被安装程序锁定。因此,我选择了 此处提出的方法:.Addin 文件被包含在程序集中作为资源,我们的 Installer 类将其提取,调整内容并将其保存到适当的位置。

要激活 installer 类,必须将其包含在“自定义操作”中:右键单击设置项目,选择“查看”–“自定义操作”。在“自定义操作”视图中,将包含 installer 类的项目输出添加到 InstallUninstall 方法。

Custom Actions

我们可以添加一个条件来仅在相应的 Visual Studio 版本安装时才安装特定插件版本(.NET 2.0 或 .NET 1.1)。要实现这一点,安装程序必须首先通过在 HKEY_CLASSES_ROOT 注册表配置单元中查找 VisualStudio.DTE.x.y 注册表项来检查 Visual Studio 是否已安装,其中 x.y 代表版本:VS 2003 为 7.1,VS2005 为 8.0,依此类推。例如,要验证 Visual Studio 2008 是否已安装,请右键单击设置项目,选择“查看”–“启动条件”,然后“添加注册表搜索条件”。将条件重命名,例如“搜索 VS2008”,定义一个保存搜索结果的属性(例如 VS2008_INSTALLED),输入要搜索的注册表项(“VisualStudio.DTE.9.0”)和根(对于 HKEY_CLASSES_ROOT,根为“vsdrrHKCR”)。如果未找到注册表项,属性将为空字符串。

Launch Conditions

现在,要在 Visual Studio 2008 已安装的情况下仅安装 .NET 2.0 版本的插件,请为设置项目打开“文件系统”视图并添加条件...

VS2008_INSTALLED <> ""

...对于相应插件项目的主要输出。

如果以同样的方式为其他 Visual Studio 版本添加了验证,您可以使用逻辑 Or 运算符组合条件,如下所示:

Conditional setup

最后,如上所述,Installer 类必须知道 ALLUSERS 属性,才能知道 .AddIn 文件应该复制到哪里。我们将此值作为 CustomActionData 传递给我们的 Installer 类——只需在设置项目的“自定义操作”视图中输入...

/allusers=[ALLUSERS]

...(参见上面的“自定义操作”截图)。

Visual Studio 2003 的安装

创建 Visual Studio 2003(或更早版本)的插件时,需要注意两个问题:

  1. 插件程序集必须注册为 COM 对象。
  2. Visual Studio 从注册表中检查可用插件,因此必须设置相应的注册表项。

插件 COM 注册应由安装程序应用程序完成。为方便起见,在添加 .NET 1.1 项目的输出后,打开该输出的属性窗口,找到“注册”属性并将其更改为“vsdrpCOM”。值得注意的是,这将注册程序集中的所有 public 类。因此,您应该尽可能将类声明为 internal

将所需的注册表项和值添加到安装过程中非常简单:右键单击设置项目并选择“查看”–“注册表”。在“注册表视图”中,在 Visual Studio AddIns 键下添加一个带有 Connect 类完整名称(即包含命名空间)的键。您需要在“用户/计算机注册表”下创建键的整个路径,安装程序将自动选择该键是在 HKEY_CURRENT_USER 还是 HKEY_LOCAL_MACHINE 分支中创建,具体取决于用户在安装期间的选择。注册表项包含几乎与 .Addin 文件中的条目相同的内容,因此您可以直接复制它们。

Registry settings in setup

关于 NUnit Starter

实现

NUnitStarter 插件启动 NUnit,提供命令行参数来启动特定测试。由于 NUnit 不支持多个参数,因此只能启动单个测试框架或仅启动一个程序集中的测试。有两种受支持的场景:

  1. 当用户右键单击解决方案资源管理器中的项时,插件会检查相应项是否是带有 .NET 兼容(C#、VB.NET 或 J#)代码的文件,以及该文件中的任何类是否应用了 TextFixture 属性。如果满足前一个条件,命令将显示在上下文菜单中,但仅在满足后者条件时启用。
  2. 当用户单击一个项目时,会验证它是否为 .NET 项目,并在上下文菜单中显示命令。没有进行其他检查,因为这些检查可能需要很长时间。

在这两种情况下,用户选择命令后,插件会根据当前选择创建相应的命令行参数并启动 NUnit 测试。对于单个项,将只运行文件中找到的第一个测试夹。对于程序集,将运行程序集中所有可用的测试。

下图显示了简化的类图。Connect 类包含一个 NUnitExecutor 实例,该实例负责在用户选择命令时启动带有 NUnit 测试框架的进程。NUnitExecutor 包含两个 public 方法,用于加载和运行 GUI 测试以及调试测试。方法从 Connect 类的 Exec 方法调用。为了确保 NUnit GUI 在 Visual Studio 关闭时关闭,并防止多个 NUnit 进程同时运行,NUnitExecutor 实现 IDisposable 接口,其中 Dispose 方法负责关闭活动的 NUnit 实例。

Class Diagram

SelectedItemInfoNUnitExecutor 提供关于解决方案资源管理器中当前选定项的必要信息。CommandStatus 类包含两个 public 方法:IsAvailableIsEnabled。它们用于显示/隐藏和启用/禁用启动测试的控件,并从 Connect 类的 QueryStatus 方法调用。UiElements 类负责创建和销毁命令及相应的控件。SelectedItemType 是一个实用类,用于确定选定项的类型:它是 .NET 项目还是其他类型的项目,它是 .NET 源文件还是其他类型的文件。

除了启动测试之外,插件还允许调试测试单元。由于 EnvDTE.Process 属性只能附加正在运行的进程(相当于从 Visual Studio IDE 附加进程),我最初的想法是按照 NUnit 文档中的建议来实现调试:

  1. 启动 NUnit GUI 测试框架,但与测试命令相反,不自动运行测试。
  2. 将 NUnit 进程附加到 Visual Studio——该工具会自动搜索进程 ID。
  3. 用户必须单击 NUnit GUI 中的“运行”按钮开始调试。

实现此方法的代码如下:

System.Diagnostics.Process proc = new  System.Diagnostics.Process();
string cla = ...;  // command line  arguments
proc.StartInfo = CreateProcessStartInfo(nUnitExecutable, cla);
proc.Start();
proc.WaitForInputIdle();  // wait until  process is fully initialized
EnvDTE.Process proc2Attach =  GetProcessById(applicationObject, proc.Id);
proc2Attach.Attach();

private EnvDTE.Process  GetProcessById(_DTE applicationObject, int procId)
{
  EnvDTE.Processes  processes = applicationObject.Debugger.LocalProcesses;
  foreach  (EnvDTE.Process proc in processes)
  {
    if  (proc.ProcessID == procId)
      return  proc;
  }
}

与此同时,我遇到了 Unitit 插件提供的出色解决方案,该插件允许完全自动调试,无需后续用户交互。简而言之,想法如下:

  1. 使用 WinApi CreateProcess 函数创建挂起进程。
  2. 创建一个用于默认调试传输的公共语言运行时调试引擎。
  3. 将调试器附加到挂起进程,最后
  4. 恢复挂起进程。

实现上述过程的简化代码如下:

Win32Api.StartupInfo si =  new Win32Api.StartupInfo();
Win32Api.ProcessInformation pi;
bool success = Win32Api.CreateProcess(null,  cmdLine, IntPtr.Zero, IntPtr.Zero, true,  
    Win32Api.CreationFlags.CREATE_SUSPENDED |  Win32Api.CreationFlags.CREATE_NO_WINDOW, 
    IntPtr.Zero, null, ref si, out pi);
Process2 p2 = (Process2)GetProcessById(applicationObject, pi.ProcessId);
Engine[] engines = new  Engine[1];
Debugger2 debugger = (Debugger2)applicationObject.Debugger;
Transport transport = debugger.Transports.Item(1);
engines[0] = transport.Engines.Item("Managed");
p2.Attach2(engines);
Win32Api.ResumeThread(pi.ThreadHandle);

为了调试目的,我们不需要花哨的 NUnit GUI 窗口,因此启动控制台代替,并且窗口被隐藏。

不幸的是,这种方法仅由 Visual Studio 2005(及更高版本)自动化模型支持,因此对于 Visual Studio 2003,我们只能采用前一种方法。

使用 NUnit Starter

为了使用 NUnit Starter 插件,除了支持的 Visual Studio 版本之一(2003/2005/2008)和 .NET Framework 2.0 之外,您还必须安装 NUnit。对于运行测试,可以使用任何近期版本的 NUnit(2.4.x 或 2.5.x)。然而,要调试测试,NUnit 运行时版本必须与Visual Studio 使用的 CLR 版本匹配。这意味着,如果您想在 Visual Studio 2003 上调试测试,必须使用 NUnit 2.4.x,而要在 Visual Studio 2005 或 2008 上调试测试,则必须安装 NUnit 2.5.x。请注意,不同版本的 NUnit 可以在同一台机器上共存。

如果您安装了上述 NUnit 版本,插件应该会在首次加载时自动配置自身,搜索已安装的最新兼容的 NUnit 版本。应该指出的是,该插件不会自动加载,但用户必须通过插件管理器对话框加载并配置启动行为。

您可以通过运行 Configurator 应用程序重新配置 NUnit 设置(例如,如果您安装了新版本的 NUnit)。配置中的命令行参数可能包含占位符 {0} 和 {1},分别代表程序集名称和(可选的)包含要运行的测试的文件的名称。

更多详细信息可以在插件安装的 Readme.txt 文件中找到。

该插件已在所有版本的 NUnit 2.4.x 和 2.5.x(最高到 2.5.2)下进行了测试。

插件不适用于 Visual Studio 2010。

历史

  • 1.0(2009 年 10 月 12 日)- 初始版本,运行在 Visual Studio 2003/2005/2008 上,支持 NUnit 2.4.x 和 2.5.x。
© . All rights reserved.