代码改变世界

Visual Studio 2008 可扩展性开发(四):添加新的命令

2009-03-09 01:12 Anders Cui 阅读(...) 评论(...) 编辑 收藏

前言

在之前的两篇随笔中,我介绍了Add-In的运行机制,这样对Add-In的事件、生命周期、与VS如何交互可以有个基本的了解了。现在是时候看看如何在VS中完成一些操作,这才是Add-In开发的目的所在。

一般的,Add-In应当提供一些界面元素,这样用户可以进行某些操作,比如在主菜单内添加一个菜单项,或者在编辑器的上下文菜单内添加一个菜单项,在本文中就来看看如何实现这些。

关于命令(Command)

考虑一个极为常见的场景:在编写代码的过程中,选中一段文本,点击Edit->Copy(或工具栏按钮)或者按下Ctrl+C,我们可以把选中文本拷贝到剪贴板,这个过程的背后发生了什么?

是什么完成的拷贝操作呢?答案是命令。可以认为命令就是某个特定的功能,如Copy、Paste、Cut等等。VS本身就内置大量的命令(有数千个之多),而上面说到的菜单项、工具栏或快捷键则执行了这些命令。通过Tools->Options菜单可以查看命令列表:

vs2008-commands

图1:VS的命令列表

值得注意的是,执行命令并非必须通过界面元素,比如在命令窗口中:

vs-commandwindow

图2:在命令窗口执行命令

这样也可以执行同样的命令。现在我们知道存在多种方式来执行命令,即菜单、工具栏、快捷键或命令窗口,这里统称为触发者。在VS中,触发者与命令是分离开来的:用户通过触发者来执行命令,而命令负责检查自身的状态(名称、是否可见、是否可用等等)并执行。这意味着,命令可以对应一个或多个菜单项,也可以不对应任何菜单项。

另一方面,对于同一个命令,比如Edit.Copy,仍然可能有不同的情况。在文本编辑器内和在解决方案管理器内的Edit.Copy命令执行内容并不相同。这里有一个命令目标(Command Target)的概念,VS将命令转向给了命令目标,而命令目标按自己的实现来执行该命令。总结下来就是:

对象 职责
触发者 提供一种方式供用户使用
命令 一个逻辑实体,检查自身的状态,可以执行命令,也可以转向一个命令目标
命令目标 根据传递过来的命令,按自己的实现来执行它


关于命令栏(CommandBar)

命令是个“乖孩子”,首先是我们让它做什么它就做什么,第二就是它不会单独出门,它会跟其它同伴待在一块儿,它们都被放在命令栏中。比如主菜单中File下的那些菜单项。

既然这样,如果希望向已经存在的命令栏内添加命令,就得首先找到命令栏才能添加。现在来看一个例子,向Tools菜单中添加一个菜单项。

添加一个新命令

使用Add-In向导新建一个Add-In,名字定为NEnhancer,意为对VS进行增强:),以后关于Add-In的例子都会放在这里面。注意要选中向Tools菜单添加一个菜单项。在Connect类的OnConnection方法中可以看到添加菜单项的代码如下:

C# Code - 向Tools菜单添加新项
    try
    {
       
// 如果需要向其它菜单栏添加命令,将Tools改为其它名称(要使用英文的)
       
// 关于这些菜单栏的名称,可以查看CommandBar.resx文件
        string resourceName;
        ResourceManager resourceManager
= new ResourceManager("NEnhancer.CommandBar", Assembly.GetExecutingAssembly());
        CultureInfo cultureInfo
= new CultureInfo(_applicationObject.LocaleID);
      
       
if(cultureInfo.TwoLetterISOLanguageName == "zh")
        {
            System.Globalization.CultureInfo parentCultureInfo
= cultureInfo.Parent;
            resourceName
= String.Concat(parentCultureInfo.Name, "Tools");
        }
       
else
        {
            resourceName
= String.Concat(cultureInfo.TwoLetterISOLanguageName, "Tools");
        }
        toolsMenuName
= resourceManager.GetString(resourceName);
    }
   
catch
    {
       
// 如果没能找到,就使用英文版本的名称
        toolsMenuName = "Tools";
    }

   
// 获取VS的主菜单
    CommandBar menuBarCommandBar =
        ((CommandBars)_applicationObject.CommandBars)[
"MenuBar"];

   
// 获取Tools菜单
    CommandBarControl toolsControl = menuBarCommandBar.Controls[toolsMenuName];
    CommandBarPopup toolsPopup
= (CommandBarPopup)toolsControl;

   
// 如果需要添加多个菜单项,这个try/catch代码块可以多次使用
   
// 但别忘了更新QueryStatus/Exec这两个方法
    try
    {
       
// 向Commands集合添加命令
        Command command = commands.AddNamedCommand2(_addInInstance, "NEnhancer", "NEnhancer",
           
"Executes the command for NEnhancer", true, 59, ref contextGUIDS,
            (
int)vsCommandStatus.vsCommandStatusSupported+(int)vsCommandStatus.vsCommandStatusEnabled,
            (
int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton);

       
// 向菜单中添加新项
        if((command != null) && (toolsPopup != null))
        {
            command.AddControl(toolsPopup.CommandBar,
1);
        }
    }
   
catch(System.ArgumentException)
    {
       
// 如果引发异常,可能是由于同名的命令已经存在了,可以忽略该异常
    }


代码的功能可以查看其中的注释。首先通过资源文件获取Tools菜单的名称(文化相关的),然后是获取主菜单,这里用的就是CommandBar,这里通过名称从DET2.CommandBars集合中进行查找。往下可以看到Tools菜单是主菜单栏下的一个CommandBarPopup控件,获取了Tools菜单后就可以给它添加命令了,其关键是AddNamedCommand2方法,它的参数信息为:

C# Code - Method Signature
Command AddNamedCommand2(AddIn AddInInstance, string Name, string ButtonText,
   
string Tooltip, bool MSOButton, object Bitmap, ref object[] ContextUIGUIDs,
   
int vsCommandStatusValue, int CommandStyleFlags, vsCommandControlType ControlType);
  • AddInInstance:用作命令对象的AddIn对象
  • Name:命令名称
  • ButtonText:当命令显示在菜单或工具栏时的文本
  • ToolTip:命令的提示信息
  • MSOButton:如果是true,表示使用预定义的图标,否则使用自定义的图标
  • Bitmap:如果MSOButton值为true,那么该参数的值将用于所用预定义图标的索引;否则用作自定义图标的Id
  • ContextUIGUIDs:VS定义了一些GUID值来标识其状态的改变。比如如果希望进入debug模式命令可用,可将该参数的值设置为vsContextGuidDebugging
  • sCommandStatusValue:命令的默认可用状态
  • CommandStyleFlags:该参数用于控制命令的外观,比如只显示图标、只显示文本或者都显示
  • ControlType:所添加命令对应的UI元素的类型,比如菜单项、组合框等

从本文开头举的例子可以看到,对于VS内置的菜单项,它们对应的命令名称是有一定规律的,即按照菜单的嵌套关系,如Edit.Copy,表示Edit下的Copy项(有空格的话要去掉)。那么对于我们的Add-In来说,也是有规律的,即Namespace.ClassName.CommandName,这里就是NEnhancer.Connect.NEnhancer。

命令的名称应当反应它的意图,所以这里把AddNamedCommand2的第二个参数改为CommandViewer。前面提到过,VS中有很多命令,它们按照命令栏来组织,这里就做一个Add-In来查看所有的命令和命令栏,方便以后的开发。

命令的执行

现在命令和菜单项是添加了,当用户点击菜单时,如何处理它呢?要实现IDTCommandTarget接口,一旦实现了它,我们的Connect类就成为一个合格的命令目标(Command Target)了。确切地说,当用户点击菜单时,VS要执行它的Exec方法,Exec方法有如下几个参数:

C# Code - Method Signature
void Exec(string CmdName, vsCommandExecOption ExecuteOption,
   
ref object VariantIn, ref object VariantOut, ref bool Handled);
  • CmdName:命令的全称,Add-In中命令的命名规则是Namespace.ClassName.CommandName
  • vsCommandExecOption:绝大多数情况下,该参数的值是vsCommandExecOptionDoDefault值,用于通知Add-In按部就班地行事
  • VariantIn:如果有数据要传给命令,就使用此参数
  • VariantOut:与VariantIn相反,此参数用于向调用者传递数据
  • Handler:如果设置为true,VS就知道命令已经执行完毕;如果为false,VS会寻找其它可以执行命令的方法,对Add-In来说,这意味着出错——不可能有其它的地方可以执行我们自定义的命令

命令的状态

有的命令并不总是可用,比如我们开发了一个Add-In,它专门针对于文本编辑器,如果没有任何文件打开,它就不应该是可用的。这里要使用IDTCommandTarget接口的另一个方法QueryStatus,它的参数有:

  • CmdName:命令的全称
  • NeededText:当前来说,该参数的值只会是vsCommandStatusTextWantedNone。但是在开发Add-In的时候,强烈建议要对此进行检查,因为VS还保留了其它的可能值作将来之用(可以参考向导生成的代码)
  • StatusOption:这是最重要的参数(ref),我们要给它赋值,告诉VS传入的这个命令是否支持、是否可用或者是否可见,这几种情况可以同时存在,此时对它们可以使用“或”操作。
  • CommandText:此参数当前VS还没有使用,不要给它赋值

好了,了解了命令的概念,也知道如何添加、执行命令了,剩下的就是实现命令的功能了。这里要新建一个窗体,添加一个TreeView来显示命令栏和命令:

commandbar-viewer-form

图3:CommandBarViewer窗体

要查看该窗体的具体代码,可以在文章末尾处下载代码。

现在只要在Connect类中稍作修改显示窗体:

C# Code - 执行命令


通过这个例子,我们可以了解如何添加新的命令并执行它,其中的关键方法包括OnConnection、AddNamedCommand2、Exec、QueryStatus。本文前面曾提到过命令的命名规则,我们可以由此获知某个菜单项对应的命令,这样就可以在Add-In中使用该命令而不需要再重新发明一次轮子。

执行已有命令

先看下图。

code-window-tab-context-menu

我们可以关闭当前文档,也可以关闭其它文档,就是不能关闭所有文档。有时候还是需要这个功能的,我想你应该用过Window->Close All Documents菜单项吧?它实现的正是我们所需要的,现在考虑如何使用这个已有的命令。首先得找到这个命令,按前面的规则,我们到Tools-Options里面去找(见图1),我们可以试着输入window.close,这时就可以看到了:Window.CloseAllDocuments。

接下来跟前面例子类似,首先要添加一个命令,前面是把命令添加到了主菜单栏(通过“MenuBar”获取命令栏),现在要添加到另外一个命令栏:“Easy MDI Document Window”,这名字有些奇怪(我是在所有的CommandBar中搜了好多次才找到的),在刚才添加Tools菜单项的代码下面添加如下代码:

C# Code - 添加CloseAllDocuments命令


要使用Window.CloseAllDocuments命令只要一行代码:

C# Code - Execute Command
_applicationObject.ExecuteCommand("Window.CloseAllDocuments", string.Empty);


现在菜单项看起来应该是这样的:

code-window-tab-context-menu2

赶紧试一试吧!这里再提一下,在测试、调试完成之后,如果要发布Add-In,最简单的方法是将dll和.AddIn放在[My Documents Path]\Visual Studio 2008\Addins,同时把那个用来测试的.AddIn文件(如NEnhancer - For Testing.AddIn)移掉。

代码下载请点击这里

我们身在何处?

本文首先介绍了命令和命令栏的概念,正是通过命令VS才可以与Add-In进行交互。然后通过两个例子解释了如何添加、执行命令,以及如何执行VS内置的命令,接下来我们就有办法操作VS的方方面面了:解决方案、项目、文档、代码等等,敬请期待:-)

参考

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