Visual Studio 扩展 - 从 Add-in 到 VSPackage






4.97/5 (8投票s)
将您的(已弃用的)插件迁移到 VSPackage...
引言
随着 Visual Studio 2013 的发布——已经有一段时间了——微软决定将插件标记为已弃用...
Visual Studio 插件在 Visual Studio 2013 中已被弃用。您应该将您的插件升级到 VSPackage 扩展。
这句话现在出现在 MSDN 上讨论插件的每个帮助页面上。段落的最后一部分——未加引号——提供了一个如何进行迁移的示例链接,但 IMHO 它是无价值的。
所以,这是我关于如何从插件迁移到 VSPackage 的版本...
背景
为了了解我们目前的状况,简要总结一下可以扩展 Visual Studio IDE 的方法...
宏
宏是一系列命令和指令,您可以将它们组合成一个命令来自动完成一项任务。
微软的这种说法很清楚,我只补充一点,宏引擎与 Office 产品线中的引擎相同,并且自 Visual Studio 6 以来没有改变。事实是我认识的没有人使用它...
插件
插件功能更强大,因为暴露的 COM 接口使您能够访问 Visual Studio 的对象模型在不同层——用于不同目的。然而,归根结底,插件适合添加和处理来自 IDE 的命令,仅此而已...
VSPackage
通过这种方法,您可以实现(几乎)任何功能。添加新的工具箱、编辑器、菜单和工具栏。事实上,IDE 的大部分功能都来自 Visual Studio 开发团队制作的集成包。这意味着从 IDE 的角度来看,您的包与微软制作的包没有区别。
托管可扩展性框架 (MEF)
MEF(^) 是 .NET 框架的一部分,最初独立开发,然后合并到 4.0 版本中。随着 Visual Studio 2010 的发布,整个编辑器使用 WPF 重写。这成为了 IDE 的第一个不是基于 COM 思想的部分,并立即利用了 MEF 的能力。第二个巨大的优势(在不使用 COM 之后)是最简单的部署——只需将生成的 dll 复制到特定文件夹即可。自 2010 年以来,由于 Visual Studio 至今仍有 99% 是 COM,因此这种思想没有在其他领域使用。
即使 MEF 扩展的数量不断增长,其使用也受限于扩展的类型,因此今天大多数扩展仍在使用 VSPackage 方法。在接下来的几行中,我将展示一个插件和一个 VSPackage 的并行示例,它们都做同样的事情,以解释如何将您的知识提升到一个新的水平...
使用代码
在文章的正文中,我将展示一些代码(很多?),但这些代码片段无法编译成一个完整、可运行的解决方案,请参阅附加的源代码...
代码是用 Visual Studio 2013 编写的——但它向后兼容到 2010(代码!不是项目)。
有史以来最无用的扩展
我将使用的示例确实无用,因为它什么都不做,但它允许您为项目中的任何 .cs 文件选择一些属性——从下拉列表中...
该值将保存在项目文件中并保持不变。
<ItemGroup>
<Compile Include="Default.aspx.cs">
<DependentUpon>Default.aspx</DependentUpon>
<SubType>ASPXCodeBehind</SubType>
<ItemColor>Blue</ItemColor>
</Compile>
</ItemGroup>
尽管此扩展无用,但它已经拥有了插件通常拥有的所有主要元素
- 创建一个新的菜单项(在本例中为下拉列表)并将其添加到所需的菜单中
- 处理来自项目项和 IDE 的事件,即
- 检查菜单项的状态(启用/禁用、隐藏/显示)
- 在本例的下拉列表中,需要用数据填充以供选择
- 根据已保存的值设置当前选定的值
- 处理用户的新选择
现在我们知道扩展需要做什么,我将并排展示代码(好的——一个在另一个下面),一个用于旧的插件方式,一个用于新的 VSPackage 方式...
创建菜单项
插件
Command oCommand = oCommands.AddNamedCommand2(
_AddIn,
"ItemColorAddIn", // CommandName
"Item Color", // ButtonText
"Select color for Item", // TooltipText
false,
null,
null,
( int )vsCommandStatus.vsCommandStatusSupported + ( int )vsCommandStatus.vsCommandStatusEnabled,
0,
vsCommandControlType.vsCommandControlTypeDropDownCombo
);
这很简单,您只需传入一些参数——命令名称、显示名称、默认状态、项目类型——即可获得一个漂亮的下拉对象。
VSPackage
<Commands package="ItemColorPackagePkg">
<Combos>
<Combo guid="ItemColorPackage" id="Command" idCommandList="CommandList" type="DropDownCombo" defaultWidth="30" priority="0">
<Parent guid="ItemColorPackage" id="Group" />
<CommandFlag>DynamicVisibility</CommandFlag>
<Strings>
<ButtonText>Item Color</ButtonText>
<CommandName>ItemColorPackage</CommandName>
<ToolTipText>Select color for Item</ToolTipText>
</Strings>
</Combo>
</Combos>
</Commands>
哇!这真是一个巨大的改变。是的,在 VSPackage 中,您在 XML 文件中定义命令。Visual Studio Command Table (^) 是声明项目及其层次结构的新方法(请参阅下一节)。在创建第一个严肃的 VSPackage 之前,您需要从头到尾了解它。现在,了解两者之间的转换就足够了。
- 在插件中,您直接在代码中声明下拉列表,因此无需额外的标识,而在 VSPackage 中,您为下拉列表分配
guid
和id
以便稍后从代码中引用它。 - VSPackage 没有下拉列表的默认状态——不需要这个,因为 IDE 默认会分配可见性...
<Parent>
元素也是声明的一部分,而在插件中则在代码中。
将菜单项分配给 IDE
插件
CommandBar oCommandBar = ( ( CommandBars )_Application.CommandBars )[ "Item" ];
oCommand.AddControl( oCommandBar );
这很简单——选择您的菜单——在本例中是 Item
——然后将该项添加到其中...
VSPackage
<Groups>
<Group guid="ItemColorPackage" id="Group" priority="0">
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_ITEMNODE"/>
</Group>
</Groups>
这一边也很容易。主要区别在于您不是直接将新项添加到现有的 IDE 菜单中,而是创建一个容器——Group
——用于此。如果您查看前面的示例,Parent
元素在此处精确地标识了组(guid
-id
对)。
注意: 如果要扩展的菜单项不是众所周知的菜单项之一,您会有点麻烦。MSDN 上有关此的文档非常糟糕,因此您可能需要花费大量时间来弄清楚使用哪个
guid
。这里有两个我用来开始的链接——Visual Studio 菜单的 GUID 和 ID 和 VsMenus 类。
为新项添加事件处理程序
插件
您不需要添加任何事件处理程序。当您创建新项(AddNamedCommand2
)时,第一个参数将您标识为所有者。每次您的项发生事件时,IDE 都会通过 IDTCommandTarget
接口调用您(稍后会看到它的实现)。该接口有两个必须实现的方法:Exec
和 QueryStatus
。所有功能都通过这些方法实现,有时方式很笨拙...
VSPackage
OleMenuCommandService oOleMenuCommandService = ( OleMenuCommandService )GetService( typeof( IMenuCommandService ) ); if ( oOleMenuCommandService != null ) { CommandID oCommandID = new CommandID( Consts.ItemColorPackage, ( int )Consts.Command ); OleMenuCommand oCommand = new OleMenuCommand( CommandInvoke, oCommandID ); oOleMenuCommandService.AddCommand( oCommand ); }
IDE 为扩展开发人员提供了一些服务(^)。这些服务实际上是扩展可以使用的接口(它也应该是其他扩展而不是核心 IDE 提供的服务)。我们这里使用的服务是 OleMenuCommandService
,它实现了在 IDE 中处理菜单项(命令)所需的所有功能。挂接到该服务的步骤是:
- 查找服务
- 使用我们在
VSCT
文件中也使用的guid
-id
信息创建一个命令对象... - 将命令对象添加到服务
...
如前所述,我们感兴趣的是三个事件。一个用于用值列表填充下拉列表,一个用于提供 IDE 当前状态下的下拉列表状态(主要是可见性),一个用于设置或获取下拉列表的当前值。现在我们将一一查看它们...
填充下拉列表
插件
public void Exec ( string CommandName, vsCommandExecOption ExecuteOption, ref object In, ref object Out, ref bool Handled )
{
if ( CommandName == "ItemColorAddIn.Connect.ItemColorAddIn_1" )
{
Out = new string[ ] { "Red", "Green", "Blue" };
Handled = true;
}
}
正如您所见,我们所做的就是使用 ItemColorAddIn.Connect.ItemColorAddIn_1
命令名称处理 Exec 方法,同时返回一个字符串列表作为值。
这可能是查看插件命令名称如何生成的最好(也是唯一)的地方。您可能还记得,在创建下拉列表时,我们分配了名称 ItemColorAddIn
。那么 ItemColorAddIn.Connect.ItemColorAddIn_1
从何而来?IDE 使用 namespace.class.command
格式来唯一标识您的命令。这很简单,但那个 _1
是做什么的?!还记得吗?IDTCommandTarget
只有两个方法!为了克服这一点,IDE 为下拉列表创建了第二个命令名称——添加了 _1
——用于下拉列表需要值时...
VSPackage
CommandID oCommandListID = new CommandID( Consts.ItemColorPackage, ( int )Consts.CommandList );
OleMenuCommand oCommandList = new OleMenuCommand( CommandListInvoke, oCommandListID );
oOleMenuCommandService.AddCommand( oCommandList );
<Combo guid="ItemColorPackage" id="Command" idCommandList="CommandList" type="DropDownCombo" defaultWidth="90" priority="0">
private void CommandListInvoke ( object sender, EventArgs e )
{
OleMenuCmdEventArgs oOleMenuCmdEventArgs = ( OleMenuCmdEventArgs )e;
if ( oOleMenuCmdEventArgs.OutValue != IntPtr.Zero )
{
Marshal.GetNativeVariantForObject( new string[ ] { "Red", "Green", "Blue" }, oOleMenuCmdEventArgs.OutValue );
}
}
IMHO VSPackage 有一个好得多的方法——您想要一个命令?添加它。这正是我在代码中所做的。在 VSCT
文件中,我将相同的命令附加到 idCommandList
属性,该属性正是为此而存在的...现在,每次下拉列表需要值列表时,IDE 都会调用为此目的附加的命令。此代码样本的第三部分显示了这里的处理程序,它也返回一个字符串列表作为下拉列表的值。
查询状态
插件
public void QueryStatus ( string CommandName, vsCommandStatusTextWanted NeededText, ref vsCommandStatus Status, ref object CommandText )
{
if ( ItemToHandle( ) )
{
Status = ( vsCommandStatus )vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusEnabled;
}
else
{
Status = ( vsCommandStatus )vsCommandStatus.vsCommandStatusInvisible;
}
}
这很简单——如果我们要处理的项目是我们应该处理的,则显示下拉列表,否则隐藏它。
VSPackage
oCommand.BeforeQueryStatus += CommandQueryStatus;
private void CommandQueryStatus ( object sender, EventArgs e )
{
OleMenuCommand oCommand = ( OleMenuCommand )sender;
oCommand.Visible = ItemToHandle( );
}
<CommandFlag>DynamicVisibility</CommandFlag>
首先,我们必须将某个方法绑定到 BeforeQueryStatus 事件。从那里,这里唯一的变化是我们在下拉列表上直接设置可见性。请注意,可见性上的更改仅有效,因为我们在 VSCT
文件中的下拉列表上设置了 CommandFlag
...
注意:
ItemToHandle
是如何检查选定的项目是否是我们想要处理的项目的简化。我在这里不详细介绍它的功能,因为这会将文章引向一个完全不同的方向。您可以在附加的示例中看到确切的代码...
值设置器/获取器
插件
public void Exec ( string CommandName, vsCommandExecOption ExecuteOption, ref object In, ref object Out, ref bool Handled )
{
if ( CommandName == "ItemColorAddIn.Connect.ItemColorAddIn" )
{
uint nItemId = GetSelectedItem();
if ( string.IsNullOrEmpty( Convert.ToString( In ) ) )
{
string szOut;
GetItemAttribute( nItemId, "ItemColor", out szOut );
Out = szOut;
}
else
{
SetItemAttribute( nItemId, "ItemColor", Convert.ToString( In ) );
}
Handled = true;
}
}
IDE 调用 IDTCommandTarget
的 Exec
方法——这次使用原始命令名称——如果 In
参数有值,您就设置,否则就是获取,您应该使用 Out
参数返回当前值...
VSPackage
private void CommandInvoke ( object sender, EventArgs e )
{
OleMenuCmdEventArgs oOleMenuCmdEventArgs = ( OleMenuCmdEventArgs )e;
uint nItemId = GetSelectedItem();
if ( oOleMenuCmdEventArgs.OutValue != IntPtr.Zero )
{
string szOut;
GetItemAttribute( nItemId, "ItemColor", out szOut );
Marshal.GetNativeVariantForObject( szOut, oOleMenuCmdEventArgs.OutValue );
}
else if ( oOleMenuCmdEventArgs.InValue != null )
{
SetItemAttribute( nItemId, "ItemColor", Convert.ToString( oOleMenuCmdEventArgs.InValue ) );
}
}
这一部分几乎相同——CommandInvoke
是分配给新菜单项的事件处理程序,并取代了插件中的 Exec
方法。在我们如何决定它是 getter 还是 setter 事件方面有一个细微的差别,但是...
注意:
GetSelectedItem
、GetItemAttribute
和SetItemAttribute
与上一节中的ItemToHandle
相同,请参阅附加的演示...
摘要
将您的扩展开发迁移到 VSPackage 是件好事(无论是否已弃用)。您可以获得更大的灵活性和对代码的控制。然而,VSPackage 是 Visual Studio 基于 COM 的核心的一个不太紧密的包装器,所以当您使用 VSPackage 时,您将进入 COM 世界,并且您需要了解封送处理以及 COM 如何使用 guid。您还将代码和声明式方法放在同一个包中,并且在代码和 VSCT 中使用相同的 guid 时,您需要处理相同的问题(事实上,同一个 guid 将在两个地方定义,因此在更改和添加新 guid 时,您需要格外小心)。
就我个人而言,我喜欢在一个环境中工作,所以我经常编写许多简单的小扩展来满足我的需求,而无需离开 Visual Studio IDE。如果您也一样,并且有很多插件,那么这篇文章可以帮助您轻松地将它们迁移到 VSPackage...
关注点
当您深入升级时,您可能会发现 Visual Studio 扩展很容易变得相当复杂。如果您是新手,请先阅读一些内容
这些链接将教您如何从扩展内部访问 IDE,并可能帮助您以更好的方式完成工作...
历史
2014 年 2 月 5 日 - 原始帖子