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

.NET 插件 for Visual Studio 6.0

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (10投票s)

2005年12月20日

CPOL

10分钟阅读

viewsIcon

74034

downloadIcon

893

使用 .NET 插件扩展 VS6 IDE。

目录

引言

标题听起来可能有点过时(因为 Visual Studio 2005 刚刚发布),但我们在工作中已经有一些使用 Visual Studio 6.0 编写的大型项目。所以,我仍然需要使用它。另一方面,我被 .NET Framework 及其能力所吸引。在编写了几个涉及丑陋 COM 问题的纯 C++ 插件后,我真的很想切换到 .NET。因此,我编写了一个包装器插件,它将 Visual Studio IDE 的调用包装到一个 COM 注册的 .NET 库中。后来,我重新处理了这个包装器插件,以使用混合模式代码。这样就无需再注册 .NET 库了。该 .NET 库会收集同一目录中的所有 DLL 文件,并查找自定义属性来识别 .NET 插件,然后将其呈现给 IDE。所有插件的设置管理也由该库完成。该设计的主要目标是通过将新插件 DLL 复制到 _Addin_ 目录中,在下次启动时自动被 IDE 识别,从而简化扩展。

插件系统

这张图展示了插件系统的工作原理。_netwrapper.dll_ 像普通的 VS6 插件一样实现 _IDSAddIn_ 接口。它只是将 _IDSAddIn_ 调用重定向到完全用 C# 编写的 _VS6AddIn.dll_。该模块在启动时加载同一目录中的所有 DLL 文件,检查 _VS6AddInPlugInAttribute_,并通过 _IApplication_ 接口的 _AddCommand_ 将每个插件添加到 Visual Studio 6 IDE。(首次调用时,它还会调用 _AddCommandBarButton_ 成员来创建工具栏)。_netwrapper.dll_ 中 _ICommand_ 接口的实现有点棘手。安装插件的方法在编译时是未知的。我通过简单地按升序编号名称来重写 _IDispatch_ 接口的 _GetIDsOfNames_ 成员。我还重写了 _IDispatch_ 的 _Invoke_ 成员,它以命令名称作为参数调用 _PlugInManager_ 类中的 _InvokeCommand_。_PlugInManager_ 类使用命令名称来标识要调用的正确插件,并调用其 _InvokeCommand_ 成员。

如何安装插件系统

将 _netwrapper.dll_ 复制到 Visual Studio 的 _Addin_ 目录,例如“_c:\Program Files\ Microsoft Visual Studio\COMMON\MSDev98\AddIns_”。在 _addins_ 目录中创建一个名为 _netmultiaddin_ 的子目录,并将所有其他 DLL 文件复制到该子目录。启动 Visual Studio 6.0,应该会出现一个新的工具栏,其中包含所有已安装的 .NET 插件。如果您稍后添加新插件,在重启 IDE 时它不会自动出现,您必须从自定义对话框中手动将其添加到工具栏(或者禁用 Netwrapper 插件并在重启 IDE 后重新启用它)。

文件

插件框架

  • VS6AddInPlugInDefs.dll
    • 包含标记程序集为 .NET 插件所需的接口定义和自定义属性(_IConfigPlugIn_、_IVS6AddInPlugIn_、_SetModifiedEventHandler_、_VS6AddInPlugInAttribute_)。
  • Netwrapper.dll
    • 包含 IDE 作为插件所需的 COM 接口。它将对该插件的调用重定向到 .NET 世界。
  • Vs6addin.dll
    • 包含 _PlugInManager_ 类和 _Options_ 插件。
  • Vs6addinUtils.dll
    • 包含供某些插件使用的实用工具类。

附带的示例插件

AutoComment.dll 通过行注释或块注释注释掉当前选择的内容。

CStringEvaluator.dll 在调试中断模式下通过选择 _CString_ 变量来检查 _CString_ 变量的内容。如果 _CString_ 包含 SQL _SELECT_ 语句,则可以在数据网格中查看结果集。

GetFont.dll 创建 _LOGFONT_ 声明,其中包含通过常见对话框选择的字体数据。

 

IDManager.dll 包含两个插件:**资源 ID 排序器**,它重新编号 _resource.h_ 文件中的 ID,以及**资源 ID 数组生成器**,它创建一个数组声明,包含资源 ID 及其名称。

MenuBinder.dll 查找并打开选定菜单项的菜单处理程序,如果合适的话还有对话框类及其对话框资源。

RegexSearcher.dll 提供用于在目录、项目或打开文件上进行搜索的完整 .NET 正则表达式。

 

SimpleTools.dll 包含两个插件:_ShowDesign_ 打开已打开对话框实现的对话框资源或头文件。_ShowHeader_ 打开 C++ 实现文件的头文件,反之亦然。

SnippetManager.dll 提供一个用于将代码片段拖放和管理的管理器。

TCHandler.dll 将当前打开文件中的选区包装到预定义的 _try_/_catch_ 处理程序中。

WinDiffer.dll 对两个文本选择调用 windiff 或类似程序。

如何创建自己的插件

  1. 启动 Visual Studio 2003
  2. 创建一个新的类库项目。
  3. 添加对 _VS6AddInPlugInDefs.dll_ 的引用。
  4. 添加对 Visual C++ 共享对象(6.0)(DSSharedObjects COM 组件)的引用。
  5. (可选)添加对 Visual C++ 调试器(6.0)、Visual C++ 项目系统(6.0)和 Visual C++ 文本编辑器(6.0)的引用。
  6. (可选)添加对 _VS6AddInUtils.dll_ 的引用。
  7. 创建一个实现 _VS6AddIn.IVS6AddInPlugIn_ 接口的新类。
  8. 如果您的插件需要用户配置,请添加一个或多个实现 _VS6AddIn.IConfigPlugIn_ 接口的用户控件。
  9. 将 _VS6AddIn.VS6AddInPlugInAttribute_ 添加到您的程序集文件,例如 _[assembly: VS6AddIn.VS6AddInPlugInAttribute( typeof( <YourNameSpace>.<YourClassName>),"<YourCommandName>")]_,如果您的 DLL 包含多个插件,可以多次添加。
  10. 为插件的 _InvokeCommand_ 成员添加功能。
  11. 生成项目。

如何实现 IVS6AddInPlugIn 接口

属性

  • string Commanddefs {get;}

    为您的插件返回 "YourCommandName\nButtontext\nStatusbartext\nTooltiptext"。

  • string CommandName {get;}

    在此返回 "YourCommandName",与程序集文件中的相同。

  • int ButtonNr {get;}

    返回包装器工具栏位图中的按钮编号(0 到 15 是标准位图供您使用)。

  • int ButtonType {get;}

    返回按钮类型(_DSSharedObjects.DsButtonType_ 类型)。

  • Bitmap LargeBitmap {get;}

    为大型工具栏返回一个 32x32 像素、16 色的位图(供将来使用),RGB(192,192,192) 为透明。

  • Bitmap SmallBitmap {get;}

    为小型工具栏返回一个 16x16 像素、16 色的位图(供将来使用),RGB(192,192,192) 为透明。

方法

  • bool InvokeCommand(DSSharedObjects.Application app);

    启动命令,_app_ 是 Visual Studio 实例的应用程序对象,成功时返回 _true_,否则返回 _false_。

  • System.Windows.Forms.Control[] GetConfigObjects();

    返回用于配置您的插件的 GUI 对象,这些对象是派生自 _System.Windows.Forms.Control_ 并实现 _IConfigPlugIn_ 接口的对象数组。

  • void ShutDown(bool bLastTime);

    如果您的插件需要进行清理,请在此进行。_bLastTime_ 如果用户正在卸载(卸载)插件,则为 _true_。如果 Visual Studio 正在退出,则 _bLastTime_ 为 _false_。

如何实现 IConfigPlugIn 接口

属性

  • string Caption {get};

    为选项页面提供一个标题。

方法

  • void LoadDefault();

    将默认数据加载到子控件中。

  • void LoadData();

    将数据(可能来自注册表)加载到子控件中。

  • void SaveData();

    将数据(可能保存到注册表)从子控件中保存。

  • void About();

    显示作者信息。

事件

  • SetModifyEventHandler SetModifyEvent {get;set;}

    调用此方法以向选项窗口报告用户更改。

注意事项

不要忘记为 IDE 使用的每个 COM 对象调用 _System.Runtime.InteropServices.Marshal.ReleaseComObject()_,否则 IDE 在退出时会卡住,等待这些 COM 对象被释放。我在 .NET COM 对象包装器递增引用计数时发现了一些奇怪的行为。因此,如果您将包装过的 COM 指针传递给函数调用,请始终在函数退出前调用 _ReleaseComObject_。但是,如果您将指针传递给类的构造函数并将其存储在成员变量中,则不要在构造函数结束时调用 _ReleaseComObject_。也许存在一些我不知道的明确定义的逻辑。您可以通过检查 IDE 关闭后任务管理器中是否仍然存在 _msdev.exe_ 进程来轻松检查您的插件是否缺少 _ReleaseComObject_ 调用。如果该进程仍然存在但没有活动,它正在等待 COM 指针被释放,但框架会在 AppDomain 关闭时释放它,所以这是一种死锁情况。

以下代码片段展示了我是如何处理 COM 指针的

Using DSTextEditor;

public bool InvokeCommand(DSSharedObjects.Application App)
{
    ITextDocument txt = null;
    ITextSelection sel = null;

    try
    {
        txt = App.ActiveDocument as ITextDocument;
        
        if (txt!=null)
        {
            sel = txt.Selection as ITextSelection;
            if (sel!=null && sel.Text!="")
            {
                // Do some interessting stuff here
                return true;
            }
        }
    }
    catch(Exception ex)
    {
        return false;
    }
    finally
    {
        if (txt!=null)
            Marshal.ReleaseComObject(txt);
        if (sel!=null)
            Marshal.ReleaseComObject(sel);
        if (App!=null)                    
            Marshal.ReleaseComObject(App);
    }
}

重新构建插件系统

在我的第一个尝试中,我通过一个包装器类将 _VS6AddIn.PlugInManager_ 暴露给 COM。在 _netwrapper.dll_ 中,我实例化了这个 COM 对象并通过 COM 调用其成员。这种方法需要使用 _regasm.exe_ 工具和 /codebase 开关注册 _VS6AddIn.DLL_,因为它不能与 _netwrapper.dll_ 放在同一个目录中(这会在启动时使 IDE 感到困惑,试图注册这些插件 DLL)。但是,如果您使用 /codebase 开关,Microsoft 强烈建议您对程序集进行签名。这就是为什么我所有的 DLL 都带有强名称签名。对此类插件使用 Debug 或 Release 生成配置。使用 _regasm.exe_ /codebase _vs6addin.dll_ /_tlb:vs6addin.tlb_ 来注册 Pluginmanager,并使用 _regasm.exe_ /u /codebase _vs6addin.dll_ /_tlb:vs6addin.tlb_ 来取消注册它。

后来,我决定使用混合模式代码从 _netwrapper.dll_ 中移除丑陋的 COM 代码。因此,我添加了一个名为 CLR(使用编译器的 /clr 开关)的新生成配置。_MANAGED_ 仅在此配置使用时定义。这种安装方法非常简单,只需将 _netwrapper.dll_ 复制到 IDE 的 _addin_ 目录,并将所有 .NET 插件 DLL 和依赖 DLL 复制到一个名为 _netmultiaddin_ 的子目录中。CLR 配置与所有项目的 Release 配置相同,但 Netwrapper 项目除外。

在整个解决方案的首次生成之前,您必须提供自己的强名称,该名称由 .NET Framework SDK 的 _sn.exe_ 工具生成。生成一个密钥对文件,将其重命名为 _vs6addin.snk_,并用您的文件替换项目中的所有 _vs6addin.snk_ 文件。如果您使用的是 _netwrapper.dll_ 的混合模式版本,也可以删除所有密钥文件,但不要忘记在生成配置的“_Wrapper Assembly Key File_”以及每个插件的程序集文件中(_[assembly: AssemblyKeyFile()]_)删除对它的引用。

基本上,项目应该可以在每个 Visual Studio .NET(2002、2003 和 2005)上生成,但我是在 Visual Studio 2003 上开发的,因此使用此版本的 Visual Studio 生成它应该不需要进行任何更改。

未解决的问题

尽管这是一个计算机程序,但仍有一些未完成的工作要做。在阅读了 Nick Hodapp 的文章(见链接部分)后,我尝试将 _netwrapper.dll_ 实现为 MFC 扩展 DLL,但这在 CLR 开关下不起作用。因此,如果有人知道如何解决这个问题,请告知我。

另一个行为不当的是工具栏位图的处理。MSDEV 通过包含位图的 DLL 的资源标识符和实例句柄接收完整的工具栏位图。因此,这一次,_netwrapper.dll_ 包含固定的工具栏,而插件的位图被忽略,因为我不知道如何向 IDE 提供动态位图。_vs6addin.dll_ 中的 _PlugInManager_ 类中已经有一个函数,可以将所有插件的位图编译成一个完整的工具栏位图。但问题是如何将其呈现给 IDE。另外,我不知道如何将创建的位图转换为每像素 4 位,但我认为这很容易解决。在首次启动时更改新创建的工具栏标题时,还会出现另一个问题。我使用了 Nick Hodapp(见链接部分)提到的技巧,但这在 Unicode 生成时不起作用。但是这些未解决的问题并不影响功能,只是“外观”问题。

链接

© . All rights reserved.