0
点赞
收藏
分享

微信扫一扫

VS插件编写


上一篇随笔Macro和Add-In初探介绍了如何开发两者的HelloWorld程序。没错,宏确实简单易行。不过在某些情况下,比如在商业软件中,宏在性能和知识产权方面可能会带来麻烦,此时那把更好的锤子是Add-In。

在初探一文中,我介绍了如何使用Add-In向导来开发第一个Add-In。VS是一款很棒的开发工具,它的各种向导(以及其它模板、可视化工具等)做得非常好,不过我发现这一强大之处到头来反而给人诟病。其中一种说法是,这些方便的工具让初学者入门容易,并惯坏了他们,以致于想登堂入室就难得多了。客观地说,这不是VS的错,VS没有阻止你去了解这些工具的背后所在。这些工具会生成大量代码,我们需要主动去了解它们,《程序员修炼之道》中曾提到:

Don't Use Wizard Code You Don't Understand

很明显,作者不是说不能使用向导,而是说要在了解向导的前提下使用它。尤其是你写的代码要跟向导生成的代码混在一起的时候,这些代码终究要变成你的代码,而Add-In的开发正是如此!所以我们必须得先了解Add-In向导做了些什么。

Add-In向导在收集信息

Add-In向导共有六步,每一步我们都可以输入一定的信息,告诉VS如何设置。这可以看作是向导收集信息的过程,这些信息包括:

编程语言:可以选择C#、VB.NET、VC,如果是手工编写Add-In,就没这个限制了
宿主环境:Add-In可同时运行在不同版本的VS IDE和/或Macro IDE内
名称和描述
菜单命令:VS据此生成一些代码,在Tools菜单中添加一个新的菜单项
命令行运行支持:这样的Add-In说明它不会呈现需要用户介入的UI,如模式对话框
启动时加载:VS可以在启动时自动加载Add-In
About对话框:可以将Add-In的信息显示在About对话框中
信息收集完毕后,VS会生成一个新的Add-In项目。

Add-In项目

Add-In项目是一个类库项目(可以参考初探一文中做的例子),仅此而已。该项目包含了“Connect.cs”文件,它定义了Connect类,还有一个配置文件FirstAddin.AddIn。

打开Connect.cs,我们仔细分析一下。Connect类实现了两个接口,一是IDTExtensibility2,该接口用于在Add-In和IDE之间进行通信;二是IDTCommandTarget,如果选择了向导中的UI选项,就需要实现它。

IDTExtensibility2包含5个方法:

OnConnection:在加载Add-In时调用
OnStartupComplete:在Add-In随着VS的启动完成加载后调用
OnAddInsUpdate:在VS加载或卸载Add-In时调用
OnBeginShutdown:在VS关闭时调用
OnDisconnection:在卸载Add-In时调用
在文件顶部可以看到引用了若干个命名空间,对于Add-In开发来说最重要的是其中三个:Extensibility、EnvDTE和EnvDTE80。Extensibility定义了IDTExtensibility2使用的类型;后面两个命名空间则定义了自动化对象模型(Automation Object Model,以下简称AOM)中的类型。

回到前面的5个方法,最重要的一个是OnConnection,VS在加载Add-In时调用它,通过第一个参数application将AOM的根对象传入,向导产生的代码将该对象的引用保存_applicationObject中;同时通过第三个参数addInInst将当前Add-In所对应的AddIn对象传入,保存在_addInInstance中。再往下看,这些代码将向Tools菜单添加一个菜单命令(如果你在向导中选中该选项的话),其中包括如下代码:

C# Code
if(connectMode == ext_ConnectMode.ext_cm_UISetup)
{
}

connectMode参数的值表示Add-In是如何加载的。如果Add-In通过菜单命令加载,那么该参数的值为ext_ConnectMode.ext_cm_UISetup。

对于另外4个方法,向导没有产生任何代码。而对于IDTCommandTarget接口的两个方法QueryStatus和Exec,则添加必要的代码来管理菜单命令以及命令点击事件的处理。Connect类中就这些内容了,那我们在向导中选择的宿主环境、名称描述等信息放在哪里呢?

.Addin文件

在我们的例子中可以看到,有个文件FirstAddin.AddIn,Add-In通过这个文件向VS进行注册。来看看它的结构如何。

它本质上是XML文件(就像模板和Code Snippet的配置文件一样):

XML Code - Add-In配置信息
<?xml version="1.0" encoding="UTF-16" standalone="no"?>
<Extensibility xmlns="http://schemas.microsoft.com/AutomationExtensibility">
    <HostApplication>
        <Name>Microsoft Visual Studio</Name>
        <Version>9.0</Version>
    </HostApplication>
    <Addin>
        <FriendlyName>MyFirstAddin</FriendlyName>
        <Description>MyFirstAddin, it's so exciting!</Description>
        <Assembly>FirstAddin.dll</Assembly>
        <FullClassName>FirstAddin.Connect</FullClassName>
        <LoadBehavior>0</LoadBehavior>
        <CommandPreload>1</CommandPreload>
        <CommandLineSafe>0</CommandLineSafe>
    </Addin>
</Extensibility>

这些信息主要分为3类:

1)宿主环境

通过<HostApplication>节点来配置该Add-In适用于哪些宿主环境,该节点数目、顺序不限。在<Name>节点中说明宿主环境的名称,除了Microsoft Visual Studio还可以是Microsoft Visual Studio Macros,也就是Macros IDE;在<Version>节点中说明支持的版本,还可以是7.1、8.0,也可以用*表示支持所有版本。

2)Add-In信息

<Addin>节点指定了Add-In本身的信息。它可以包含如下子节点:

<FriendlyName>:可选的,为Add-In指定一个有意义的名称;
<Description>:可选的,为Add-In指定有意义的描述信息;
<AboutBoxDetails>和<AboutIconData>:都是可选的,如果要在About对话框中显示Add-In的话,该节点用于指定其详细信息和图标;
<Assembly>:必填的,Add-In所在的程序集;
<FullClassName>:必填的,指定程序集内实现了IDTExtensibility2接口的类,要使用完全限定名称;
<LoadBehavior>:可选的,指定VS加载Add-In的方式,0表示VS不会自动加载,必须手工加载;1表示Add-in在VS启动的时候加载;4表示通过命令行方式加载;
<CommandPreload>:可选的,指定Add-In应当预先加载;
<CommandLineSafe>:可选的,指定Add-In是否是命令行安全的以及是否显示用户界面。
3)选项页(Tools Options Page)信息

我们可以很容易地在VS的Tools -> Options对话框中添加自己的选项页,从而对Add-In进行配置,不过这里先行略过,在后续的随笔中将会介绍。

CommandBar.resx

除了Connect.cs和.AddIn文件,还有一个文件是CommandBar.resx,这里面存放了一个命令条(CommandBar)的文本值的列表。它针对的是不同的自然语言,实际上在Connect.cs中,在获取Tools菜单时就用到了它。

想一想,现在有了一个编译好的程序集还有.Addin配置文件,那VS就有足够的信息来启动、管理Add-In了。问题是,把这两个文件放在哪里呢?在项目当中有一个FirstAddin - For Testing.AddIn文件,这个文件存放的位置是[My Documents Path]\Visual Studio 2008\Addins,在我们按下F5测试Add-In的时候VS就是使用这个文件来加载的,查看它里面的配置可以看到它指向的程序集正是当前项目编译后的程序集。所以我们的Add-In编译完毕后,FirstAddin - For Testing.Addin删掉,把程序集和FirstAddin.Addin文件拷贝到[My Documents Path]\Visual Studio 2008\Addins下,就算是一种最简单的部署了。

加载和管理Add-In

在生成Add-In后,需要把它加载进VS。如果你在向导中选择在VS启动时加载,那么VS会在每次启动时自动加载Add-In。如果选择通过菜单命令加载,你也可以打开VS后,通过Add-In Manager(菜单Tools -> Add-In Manager)修改相关的设定。

  

我们身在何处?

本文主要关注的是Add-In向导所产生的代码,其中的重点是Connect.cs和.Addin文件。Connect类是Add-In的实现类,有了它一个程序集才得以成为一个Add-In;.Addin文件中包含了Add-In的配置信息,VS以此来管理Add-In。有了这些,我们对Add-In的运行机制就有了更清楚的认识,在下一篇随笔中,我将介绍Add-In中的生命周期和事件。

参考

《Professional Visual Studio® 2008 Extensibility》
《Working with Microsoft Visual Studio® 2005》

作者:Anders Cui
在上篇Add-In运行机制解析(上)中,我分析了Add-In向导生成的代码,从中我们知道只要创建一个类库,它包含实现了IDTExtensibility2接口的类,然后为其建立.addin配置文件,就可以实现一个Add-In了。本文将更进一步,介绍Add-In的事件和生命周期,为今后的开发打下基础。

Add-In的事件

Add-In是事件驱动的,可以猜到的事件有加载、卸载、状态改变等等。事实上,这些事件都与IDTExtensibility2接口有关,也就是该接口的5个方法:



如果要了解这些方法如何执行,一个办法是在这些方法中加一个MessageBox,然后通过Add-In Manager进行一些操作,来观察事件的执行。现在使用Add-In向导建立一个简单的Add-In,名字为LifeCycleAddin,不要选择在Tools菜单显示命令,也不要选择在VS启动时加载。然后把Connect类的代码简化一下:

C# Code - Add-In事件演示
/// <summary>The object for implementing an Add-in.</summary>
public class Connect : IDTExtensibility2
{
    public Connect()
    {
    }

    /// <summary>
    /// Receives notification that the Add-in is being loaded.
    /// </summary>
    public void OnConnection(object application, ext_ConnectMode connectMode, 
        object addInInst, ref Array custom)
    {
        _applicationObject = (DTE2)application;
        _addInInstance = (AddIn)addInInst;

        MessageBox.Show(string.Format("Event: OnConnection, connectMode: {0}", connectMode));
    }

    /// <summary>
    /// Receives notification that the Add-in is being unloaded.
    /// </summary>
    public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
    {
        MessageBox.Show(string.Format("Event: OnDisconnection, connectMode: {0}", disconnectMode));
    }

    /// <summary>
    /// Receives notification when the collection of Add-ins has changed.
    /// </summary>
    public void OnAddInsUpdate(ref Array custom)
    {
        MessageBox.Show("OnAddInsUpdate");
    }

    /// <summary>
    /// Receives notification that the host application has completed loading.
    /// </summary>
    public void OnStartupComplete(ref Array custom)
    {
        MessageBox.Show("OnStartupComplete");
    }

    /// <summary>
    /// Receives notification that the host application is being unloaded.
    /// </summary>
    public void OnBeginShutdown(ref Array custom)
    {
        MessageBox.Show("OnBeginShutdown");
    }
    private DTE2 _applicationObject;
    private AddIn _addInInstance;
}

每个方法的注释说明了相应的事件何时触发。OnConnection是在Add-In加载的时候;OnDisconnection是在Add-In卸载的时候;OnAddInsUpdate是在所有Add-In的集合状态发生改变的时候;OnStartupComplete是在宿主环境加载完成的时候;OnBeginShutdown则是在宿主环境将被关闭的时候。现在编译项目,然后关闭VS。

打开VS,开始第一回合的观察。由于没有选择在VS启动时加载,所以现在什么也不会发生。打开Add-In Manager,对于LifeCycleAddin,将其设置为可用,确定。这时触发了OnConnection,connectMode为ext_cm_AfterStartup,也就是说在VS启动之后才加载的;然后还触发了OnAddinsUpdate,因为LifeCycleAddin的状态改变了。再次打开Add-In Manager,对于LifeCycleAddin,将其设置为启动时加载,确定,再次触发OnAddinsUpdate。现在关闭VS,由于Add-In已经加载,所以会触发OnBeginShutdown,然后是OnDisconnection,说明Add-In已经被卸载。

打开VS,开始第二回合的观察。由于选择了在VS启动时加载,所以此时触发了OnConnection,connectMode为ext_cm_Startup,也就是说是在VS启动时加载的;之后连续触发了OnAddinsUpdate和OnStartupComplete,只有设置为在VS启动时加载才可能触发该事件。至此,5个事件都已经触发过了。

比较有意思的是OnAddinsUpdate。现在打开Add-In Manager,改变另一个Add-In的设置,点击确定,该事件也会触发,这就是前面所说的“所有Add-In的集合状态发生改变的时候”。

由前面的介绍可以了解到,实现IDTExtensibility2接口是每个Add-In的核心所在。但是仅仅这些显然还不够。我们不仅需要知道VS合适启动、卸载或改变了Add-In,我们还要能够在这些时候访问VS,否则开发VS的Add-In也就没意义了。这就用到了VS的自动化对象模型(Automation Object Model)。

VS自动化对象模型简介

在Connect.cs文件的顶部using部分,可以看到两个命名空间:EnvDTE和EnvDTE80。EnvDTE表示开发环境工具扩展(Environment Development Tools Extensibility,常简称为DTE),就是在这里定义了VS的自动化对象模型(以下简称AOM)。而EnvDTE80的80表示8.0版本的AOM,其实还有一个表示9.0版本的EnvDTE90,但没有引用进来。

简单说一下它们的关系。EnvDTE表示VS2005之前的DTE版本,在每个版本中微软都会修复一些bug或添加新的功能,到了VS2005,微软使用了EnvDTE80表示新版本的变化(包括修复和增强),同时对于那些旧版本中已经存在的类,在后面加了一个数字2表示该类的新版本,如CodeFunction2表示是EnvDTE80中的新类型,而CodeFunction则表示EnvDTE中对应的那个类。EnvDTE90与此类似,比如Solution3、Solution2和Solution分别表示三个版本中表示解决方案的类。对于这些不同版本的类,微软的做法是用新版本的类继承旧的版本,然后进行扩展。

但是相对于EnvDTE80与EnvDTE之间的变化,EnvDTE90的变化要小很多。大部分时候EnvDTE80就够用了,所以默认情况下,Connect.cs文件没有引用EnvDTE90。以后当你看到带着2或3后缀的类型,就能明白它的来历了。下面是AOM的结构图(点击查看大图):



不出意外的是,结构很复杂。原因有二:首先VS本身很复杂,DTE用来表示VS中的元素,不能不复杂;其次,AOM和DTE源自COM,在.NET和COM间的互操作增强一些额外的工作。不过不用担心,这些类封装得非常之好,用起来还是比较容易的。

这个类型结构的顶端是DTE/DTE2,它是所有其它类型的容器。DTE主要包含5部分内容:解决方案和项目、命令(Command)、事件、文档、调试器,通过这些,我们就能够操作VS的方方面面(可以先看一下图中类的名字)。在后续的随笔中,你将看到这些内容的详细用法。

再议IDTExtensibility2接口

现在回到IDTExtensibility2接口,仔细了解一下它的各个方法。

1)OnConnection

在实现这个接口的时候,我们需要获得DTE对象,这样才能操作VS,这件事要在OnConnection中去做。

C# Code - Method Signature
public void OnConnection(object application, ext_ConnectMode connectMode, 
            object addInInst, ref Array custom)

application参数持有AOM根对象的引用,它同时实现了EnvDTE.DTE和EnvDTE80.DTE2接口,所以在我们的例子中,它被转换为DTE2。connectMode参数告诉Add-In是以何种方式加载的,它的值来自Extensibility.ext_ConnectMode枚举:

ext_cm_AfterStartup:在VS启动之后加载
ext_cm_Startup:在VS启动之时加载
ext_cm_External:在VS外部加载(VS已经不再使用该值)
ext_cm_CommandLine:从命令行加载
ext_cm_Solution:在解决方案内加载
ext_cm_UISetup:在建立用户界面时加载
我们可以根据该参数值的不同进行相应的操作,比如如果是ext_cm_UISetup,可以在菜单上添加一条命令(就像Add-In向导所做的那样)。

Add-In本身是AddIn接口的一个实例,addInInst参数持有该实例的引用,我们可以将该值保存下来备用。最后,IDTExtensibility2接口的每个方法都有一个custom参数,Add-In的宿主环境可以通过它来传递宿主相关的信息,不过VS总是传递一个空的数组(汗。。。)。

2)OnStartupComplete

OnStartupComplete事件仅仅在Add-In随VS启动加载的时候才会触发。

C# Code - Method Signature
void OnStartupComplete(ref Array custom)

如果一个Add-In随VS启动而加载,OnConnection并非总是进行初始化的好地方——比如,Add-In加载的较早,而Add-In需要访问的VS组件尚未加载完毕。

3)OnAddInsUpdate

C# Code - Method Signature
void OnAddInsUpdate(ref Array custom)

在某个Add-In被加载或卸载的时候,OnAddInsUpdate事件会触发。OnAddInsUpdate事件没有提供被加载或卸载Add-In的信息,不过我们有办法获取到。大体原理是:通过DTE.AddIns/DTE2.AddIns集合我们能够获取到所有的Add-In,里面的元素类型为AddIn,AddIn有个Connected属性,用以表示该Add-In是否处于加载状态,我们在首次触发OnAddInsUpdate事件的时候记录所有Add-In的状态,在下次触发的时候就知道那些Add-In状态改变了,这里就不再给出代码了。

4)OnBeginShutDown

C# Code - Method Signature
void OnBeginShutdown(ref Array custom)

如果在一个Add-In运行的时候关闭VS,OnBeginShutDown事件会触发。我们在这个时候可以做一些必要的清理工作。

5)OnDisconnection

C# Code - Method Signature
void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)

在Add-In的生命周期结束的时候,OnDisconnection事件会触发。它跟OnBeginShutDown事件的不同之处在于,这里结束的是Add-In而不是VS。disconnectMode参数的值来自Extensibility.ext_DisconnectMode枚举:

ext_dm_HostShutdown:因为VS关闭而卸载
ext_dm_UserClosed:在VS运行时卸载
ext_dm_UISetupComplete:在用户界面创建完毕后卸载
ext_dm_SolutionClosed:在解决方案关闭时卸载
它的作用类似于ext_ConnectMode,我们可以根据Add-In卸载方式的不同采取不同的动作。

唔,至此Add-In的事件和生命周期介绍完毕。

我们身在何处?

本文主要介绍了VS Add-In的事件和生命周期,通过这些知识,我们能够知道在何时获取需要的信息;同时还简单介绍了VS自动化对象模型。加上Add-In运行机制解析(上),我们应当对Add-In的运行机制有个基本的了解,为接下来的开发打下基础。到现在我们还不足以写出真正有用的Add-In,从下一篇开始我将介绍如何开发真正有用的Add-In。

举报

相关推荐

0 条评论