前言
在之前的两篇随笔中,我介绍了Add-In的运行机制,这样对Add-In的事件、生命周期、与VS如何交互可以有个基本的了解了。现在是时候看看如何在VS中完成一些操作,这才是Add-In开发的目的所在。
一般的,Add-In应当提供一些界面元素,这样用户可以进行某些操作,比如在主菜单内添加一个菜单项,或者在编辑器的上下文菜单内添加一个菜单项,在本文中就来看看如何实现这些。
关于命令(Command)
考虑一个极为常见的场景:在编写代码的过程中,选中一段文本,点击Edit->Copy(或工具栏按钮)或者按下Ctrl+C,我们可以把选中文本拷贝到剪贴板,这个过程的背后发生了什么?
是什么完成的拷贝操作呢?答案是命令。可以认为命令就是某个特定的功能,如Copy、Paste、Cut等等。VS本身就内置大量的命令(有数千个之多),而上面说到的菜单项、工具栏或快捷键则执行了这些命令。通过Tools->Options菜单可以查看命令列表:
图1:VS的命令列表
值得注意的是,执行命令并非必须通过界面元素,比如在命令窗口中:
图2:在命令窗口执行命令
这样也可以执行同样的命令。现在我们知道存在多种方式来执行命令,即菜单、工具栏、快捷键或命令窗口,这里统称为触发者。在VS中,触发者与命令是分离开来的:用户通过触发者来执行命令,而命令负责检查自身的状态(名称、是否可见、是否可用等等)并执行。这意味着,命令可以对应一个或多个菜单项,也可以不对应任何菜单项。
另一方面,对于同一个命令,比如Edit.Copy,仍然可能有不同的情况。在文本编辑器内和在解决方案管理器内的Edit.Copy命令执行内容并不相同。这里有一个命令目标(Command Target)的概念,VS将命令转向给了命令目标,而命令目标按自己的实现来执行该命令。总结下来就是:
对象 | 职责 |
触发者 | 提供一种方式供用户使用 |
命令 | 一个逻辑实体,检查自身的状态,可以执行命令,也可以转向一个命令目标 |
命令目标 | 根据传递过来的命令,按自己的实现来执行它 |
关于命令栏(CommandBar)
命令是个“乖孩子”,首先是我们让它做什么它就做什么,第二就是它不会单独出门,它会跟其它同伴待在一块儿,它们都被放在命令栏中。比如主菜单中File下的那些菜单项。
既然这样,如果希望向已经存在的命令栏内添加命令,就得首先找到命令栏才能添加。现在来看一个例子,向Tools菜单中添加一个菜单项。
添加一个新命令
使用Add-In向导新建一个Add-In,名字定为NEnhancer,意为对VS进行增强:),以后关于Add-In的例子都会放在这里面。注意要选中向Tools菜单添加一个菜单项。在Connect类的OnConnection方法中可以看到添加菜单项的代码如下:
![](http://img.e-com-net.com/image/info8/33f39c8bb37e46c7a58b67c845bbc57f.gif)
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方法,它的参数信息为:
![](http://img.e-com-net.com/image/info8/33f39c8bb37e46c7a58b67c845bbc57f.gif)
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方法有如下几个参数:
![](http://img.e-com-net.com/image/info8/33f39c8bb37e46c7a58b67c845bbc57f.gif)
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来显示命令栏和命令:
图3:CommandBarViewer窗体
要查看该窗体的具体代码,可以在文章末尾处下载代码。
现在只要在Connect类中稍作修改显示窗体:
![](http://img.e-com-net.com/image/info8/ffdcf21b5f0a4c8eac90d35bbbcd3545.gif)
通过这个例子,我们可以了解如何添加新的命令并执行它,其中的关键方法包括OnConnection、AddNamedCommand2、Exec、QueryStatus。本文前面曾提到过命令的命名规则,我们可以由此获知某个菜单项对应的命令,这样就可以在Add-In中使用该命令而不需要再重新发明一次轮子。
执行已有命令
先看下图。
我们可以关闭当前文档,也可以关闭其它文档,就是不能关闭所有文档。有时候还是需要这个功能的,我想你应该用过Window->Close All Documents菜单项吧?它实现的正是我们所需要的,现在考虑如何使用这个已有的命令。首先得找到这个命令,按前面的规则,我们到Tools-Options里面去找(见图1),我们可以试着输入window.close,这时就可以看到了:Window.CloseAllDocuments。
接下来跟前面例子类似,首先要添加一个命令,前面是把命令添加到了主菜单栏(通过“MenuBar”获取命令栏),现在要添加到另外一个命令栏:“Easy MDI Document Window”,这名字有些奇怪(我是在所有的CommandBar中搜了好多次才找到的),在刚才添加Tools菜单项的代码下面添加如下代码:
![](http://img.e-com-net.com/image/info8/ffdcf21b5f0a4c8eac90d35bbbcd3545.gif)
要使用Window.CloseAllDocuments命令只要一行代码:
![](http://img.e-com-net.com/image/info8/33f39c8bb37e46c7a58b67c845bbc57f.gif)
_applicationObject.ExecuteCommand("Window.CloseAllDocuments", string.Empty);
现在菜单项看起来应该是这样的:
赶紧试一试吧!这里再提一下,在测试、调试完成之后,如果要发布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》