LearnVSXNow! #13- VS IDE中的菜单和命令
几乎所有的VSPackage都有用户交互,用户可以通过点击Visual Studio中的菜单或工具栏来激活VSPackage的功能或显示相关的界面。
在这一篇文章里,我们来看一下Visual Studio的菜单和工具栏是如何被定义、创建、显示和使用的。不过这篇文章我只是说一下一些基本的知识,到下一篇文章我们再来看一些示例代码。
一些概念
我们创建的VSPackage的功能可以被别的package调用,也可以被最终用户用,可以被最终用户用的功能被称作“命令(command)”,例如打印、添加文件,等等。
但是用户如果想用我们的命令的话,我们必须提供某种方式给他们用才行。最常见的方式是创建一个菜单项,用户可以点击菜单来使用这些命令。另外,我们也可以让用户在类似控制台的地方敲入文本来调用我们的命令,例如VS的命令窗口(视图|其他窗口|命令窗口)。
Visual Studio的菜单和工具条的功能是一样的:当用户点了它们,VS就会调用和它们绑定起来的命令。对于命令来说,它并不知道自己是由菜单调用的还是由工具条调用的。
- 菜单通常显示在IDE的最顶部,并且会分组显示菜单项;IDE的一些元素(例如tool window、document window、window frame)也会有它们的上下文菜单,当用户在它们上面点击右键的时候会显示出来。
- 工具条通常是一堆控件的集合,这些控件和菜单项的功能是一样的:都是为了执行命令。这些控件可以是按钮、下拉框、列表框、文本框或者分隔按钮。
所以,在这篇文章里,不管是菜单项,还是工具条上的控件,我一概用“菜单项”这个名字来表示它们。
静态和动态的菜单项
菜单项可以是静态的,也可以是动态的。静态的意思是这些菜单项只会被实例化和初始化一次(通常在package初始化的时候),并由始自终地保留它们的状态;动态的意思是这些菜单项在初始化之后,可以改变它们的状态或者外观,或者根据上下文的信息动态的创建这些菜单项。对于静态菜单项,一个很好的例子是用于显示一个工具窗的菜单项;动态菜单项的例子则是“最近的文件”这个菜单项。
区分菜单和命令的概念
在传统的Windows Forms开发中,开发人员经常把同一个事件处理方法附加到多个菜单项或工具条项上面,并分别处理这些菜单项或工具条项的状态。例如,如果一个菜单项和一个工具条项有相同的功能,他们会把同一个事件处理方法附加到这个菜单项和工具条项上面,并且分别处理它们的enabled/disabled状态。
但是在Visual Studio中,菜单项和命令的概念有更为清晰的区分。
命令负责判断它自己的状态(显示名称、可见性、可用性等等),并执行命令处理方法;菜单项负责显示一个命令的外观,并且提供一种方式供用户触发命令。
这意味着一个命令可以绑定到零个、一个或者多个菜单项上面。命令本身知道自己的状态,并且会把这个状态报告给相关的菜单项:开发人员只需要设置命令的状态就行了,不用管到底有多少个菜单项和它有关联。菜单项会根据命令的状态自动调整它们的外观。
区分命令和命令目标的概念
现在我们已经弄清楚了菜单项和命令的区别了,让我们来看一下另外一个要搞清楚的东西:当调用一个命令的时候,命令本身也许并不知道要执行什么代码逻辑。
命令只是一个逻辑上存在的实体,命令的目标(Command Target)才知道命令该如何执行。在VS IDE里,有一个命令路由模型,可以把一个对命令的请求转到命令目标上。命令目标可以执行和这个命令相关的逻辑,也可以什么都不做,表明自己不支持这个命令(例如这个目标不知道该如何处理这个命令)。命令目标甚至可以把命令转给别的命令目标来处理。
现在让我们来看一个例子。在“编辑”菜单和Visual Studio的标准工具条上,有剪切、复制和粘帖这几个菜单项,这些菜单项甚至也可以添加到一些右键菜单中。这些菜单项绑定到了“剪切”、“复制”和“粘帖”这几个命令上。其实在Visual Studio中并没有一个单独的对象知道如何执行这几个命令,IDE根据当前的上下文信息把请求转发给相应的命令目标。例如,如果当前活动窗口是文本编辑器的话,IDE就会把命令转发给文本编辑器;在用属性窗的时候,命令就转给了属性窗;用ASPX设计器的时候,命令就转给了ASPX设计器。所以,文本编辑器、属性窗、ASPX设计器都是命令目标。这些命令目标自己决定是否支持转过来的命令。
总结一下这几个概念
现在让我们总结一下和命令相关的概念:
概念 | 职责 |
菜单项(Menu Item和Toolbar item) | 为命令提供界面,并根据命令的状态来显示界面 |
命令(Command) | 是一个逻辑实体,它本身不一定包含命令的执行逻辑。 |
命令目标(Command Target) | 命令目标知道如何执行一个命令。它可以转发、执行或甚至拒绝一个命令。它也可以控制命令的状态。 |
Visual Studio里的菜单项和命令处理
这一节我们来看一下VS是如何处理菜单和命令的。
命令的可见性
VS中的某些菜单和工具条会根据上下文的不同显示或者隐藏。例如,“项目”和“调试”菜单在没有打开项目的时候是不可见的;没有连上团队服务器之前,你也看不到团队(Team)这个菜单。
命令可以定义在如下不同的地方(或者说是逻辑上属于这些地方):
- VS IDE。所有定义在VS IDE里的命令都是可见的。
- Package。Package可以决定是否显示它定义的命令。另外,别忘了VS的绝大部分是由各种各样的package组成的。
- 活动的项目(active project)。在同一时刻,VS里只会有一个活动的项目,只有属于这个活动项目的命令才是可见的。
- 活动的编辑器(active editor)。如果同时打开了多个文件的话,同一时刻只会有一个活动的编辑器,只有属于这个活动的编辑器的命令才是可见的,属于其他编辑器的命令是不可见的。另外,有的设计器是支持不同的文件类型的(例如Image Editor),有可能命令对其中一种文件类型可用,但是对其他的文件类型不可用。例如我们可以为一个ico文件设置透明度,但是不可以为bmp文件设置。所以,根据文件类型来显示不同的命令,也属于编辑器的责任。
- 工具窗(tool window)。工具窗也有自己的命令。
可见性的上下文
你也许感觉到了,我们漏掉了一个重要的东西没有讲。我之前举了一个例子:项目和调试菜单在没有打开项目之前是不可见的。但是,Visual Studio是怎么做到在项目没有打开的情况下隐藏命令,在打开项目后又显示命令的呢?
Visual Studio允许我们对命令的可见性做进一步的控制。IDE定义了一些上下文,命令的可见性可以和这些上下文绑定起来。这些上下文如下:
上下文名称 | 描述 |
NoSolution | 在VS IDE中没有打开任何解决方案(此时解决方案浏览器是空的) |
SolutionExists | VS IDE中打开了解决方案。可以是一个空的解决方案,或者是通过打开一个文件而自动创建的解决方案,又或者是含有一个或多个项目的解决方案。 |
EmptySolution | VS IDE中打开了一个空的解决方案(该解决方案下不包含任何项) |
SolutionHasSingleProject | VS IDE中打开了一个解决方案,并且这个解决方案只包含一个项目。 |
SolutionHasMultipleProjects | VS IDE中打开了一个解决方案,并且这个解决方案包含多个项目。 |
SolutionBuilding | 当前解决方案或其中的任何一个项目正在生成的过程中。生成结束后,这个上下文就无效了。 |
Debugging | VS IDE正处于调试模式:调试器被附加到一个进程。 |
DesignMode | VS IDE处于设计模式(即不是调试模式) |
FullScreenMode | VS IDE以全屏的方式运行(可以通过点击“视图|全屏”菜单来进入全屏模式) |
Dragging | 在VS IDE里正发生一个拖动的操作。 |
如果一个命令绑定到了多个上下文,那么当VS IDE处于其中一个上下文的时候,这个命令就是可见的。
命令路由和上下文嵌套
VS IDE、package和package里的对象(例如编辑器和工具窗)定义了很多命令。根据当前上下文的不同,一个命令可以被不同的命令目标执行。
Visual Studio有一个良好的路由结构,规定了在一定的上下文之内的命令执行的规则。这个路由从最里面的上下文开始,依次向最外部的上下文转发请求,直到它转到了全局的上下文。每一个上下文都有一个所谓的命令目标,用于执行命令。
那么,什么是“最里面的”上下文,什么又是“全局的”上下文呢?
上下文是可以嵌套的,例如我们创建了一个带有工具窗的package,并注册到了VS IDE中的话,我们就有了如下结构的上下文:
- 最外层的(即全局的)上下文就是VS IDE本身。
- 我们的package加载到IDE之后,package自己的上下文就是一个嵌套在VS IDE里的上下文。
- 当工具窗被创建后,工具窗的上下文又变成了嵌套在package里的上下文。
路由算法从上下文嵌套树的叶子节点开始,一直冒泡到树的根节点,即全局上下文。
路由算法
命令冒泡到的节点被称作“活动命令上下文”。如果代表活动命令上下文的对象并不是一个命令目标,命令会继续冒泡到上一级节点。如果活动命令上下文是一个命令目标的话,就可以处理这个命令,或者告诉IDE“我不知道如何处理这个命令”,命令就会继续向上一层冒泡。
路由算法定义了如下几个级别(从叶子节点到根节点):
- 外接程序(Present Add-in)。命令首先会传递给已经注册和加载的外接程序(Add-ins)。
- 上下文菜单(快捷菜单)。如果命令位于上下文菜单里,那么属于这个上下文菜单的命令目标对象可以处理这个命令。
- 有焦点的窗口。当前有焦点的窗口是下一个可以处理命令的对象。窗口有很多类型,可以是工具窗,也可以是文档窗口。每种类型的窗口处理命令的方式是不同的。
- 文档窗口(Document Window)。我们到现在还没有讲到文档窗口是什么,在以后的文章里我们会用一个主题来讲解它。文档窗口逻辑上由两部分组成:用于显示文档的document view,和用于处理文档信息的document data。命令首先传递给document view,如果document view不支持这个命令的话,就会传递给document data。
- 工具窗(Tool Window)。某些工具窗会在自己内部传递命令,例如解决方案浏览器,它会在自己内部把命令从叶子节点依次传递到解决方案节点。
- 当前项目。如果当前项目不能处理命令,命令会转给上一级节点,直到解决方案节点。(VS SDK允许创建子项目类型(即flavor项目),所以一个项目的上级节点不一定是解决方案节点)。
- package。每个package都应该处理它们自己定义的命令,虽然从理论上说这不是必须的,但既然package不能处理这些命令,那还定义它们干嘛?
- 全局级别。如果命令在前面几个级别里没有被处理,那么就会转到全局级别这里。
package的按需加载
在第五篇里,我提到过package是按需加载的,也就是说当package里的对象(例如工具条、编辑器等等)要被创建了,或者package的service要被别的地方调用了,package才会加载到内存里。但是package会包含菜单,如果为了显示菜单而加载package,那么这个按需加载的模型看起来就不是那么回事了。那么,如果不加载package,怎样才能显示相应的菜单呢?
关键在于package的注册(参见第8篇里关于regpkg.exe的说明)。通过注册package,对应的菜单就会保存到注册表中。Visual Studio通过读取注册表里的信息来显示菜单。当用户点了某个菜单之后,VS就会找到对应的package,如果该package还没加载进来,那么就会执行下面几步:
- 加载相关的程序集到内存里。
- 创建这个package类的实例(通过调用package类的默认构造函数)。这个时候我们的package还不知道关于VS的任何上下文,所以我们不能够在package的的构造函数里放一些和上下文有关的初始化代码(例如试图访问一个VS Service)。
- 装载(Site)package。此时我们的package装载到VS中。
- 调用package的Initialize方法。我们可以override这个方法,放一些初始化代码。在这里就可以放一些和VS上下文有关的代码了。另外,如果我们的package定义了菜单,也应该在这里把菜单和对应的命令绑定起来。
用户点了某个菜单之后,VS就找到相关的命令,并执行它。如果我们忘了把菜单和命令绑定起来,点击菜单就会没有任何反应——当然,虽然没有反应,但我们的package会因此而加载进来。
另外,我提到过命令目标将负责更新命令的状态。如果路由算法路由到一个还没被加载到内存的package的时候,VS并不会去加载这个package,而只是用这个命令的初始状态代替。
总结
在这篇文章里我给了大家一个关于菜单、菜单项、工具条、命令和命令目标的简要的概括。
Visual Studio把UI和它们相应的功能给分开了。在不同的上下文里,同一个命令(例如剪切、复制、粘帖)有可能执行不同的动作。
Visual Studio里定义了命令目标的概念。一个命令目标知道如何更新命令的状态,如何执行命令。VS IDE定义了一个路由算法,可以把命令转发给不同级别的命令对象。
在下一篇文章里,我们来看一看用于实现今天我们提到的这些概念的VSX的类型。
原文链接:http://dotneteers.net/blogs/divedeeper/archive/2008/02/22/LearnVSXNowPart13.aspx
出处: http://www.cnblogs.com/default
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。