为你选择正确的 Visual Studio 扩展性模型
可以使用三个主要扩展性模型、VSSDK、Community Toolkit 和 VisualStudio.Extensibility 扩展 Visual Studio。 本文介绍每个优点和缺点。 我们使用一个简单的示例来突出显示模型之间的体系结构和代码差异。
VSSDK
VSSDK(或 Visual Studio SDK)是 Visual Studio Marketplace 上大多数扩展所基于的模型。 此模型是 Visual Studio 本身构建的。 它是最完整、最强大的,但也是最复杂的,需要正确学习和使用。 使用 VSSDK 的扩展在与 Visual Studio 本身相同的进程中运行。 在与 Visual Studio 相同的进程中加载意味着具有访问冲突、无限循环或其他问题的扩展可能会崩溃或挂起 Visual Studio 并降低客户体验。 由于扩展在与 Visual Studio 相同的进程中运行,因此只能使用 .NET Framework生成扩展。 想要使用或合并使用 .NET 5 及更高版本的库的扩展程序无法使用 VSSDK 执行此操作。
随着 Visual Studio 本身的转换和演变,VSSDK 中的 API 已聚合多年。 在单个扩展中,你可能发现自己需要处理旧版遗留系统中基于 COM 的 API,轻松使用看似简单的 DTE,还要处理 MEF 导入和导出。 让我们以编写一个扩展的示例,该扩展读取文件系统中的文本并将其插入到编辑器中当前活动文档的开头。 以下代码片段显示了在基于 VSSDK 的扩展中调用命令时要编写的代码:
private void Execute(object sender, EventArgs e)
{
var textManager = package.GetService<SVsTextManager, IVsTextManager>();
textManager.GetActiveView(1, null, out IVsTextView activeTextView);
if (activeTextView != null && activeTextView is IVsTextViewEx nativeView)
{
ErrorHandler.ThrowOnFailure(nativeView.GetWindowFrame(out object frameValue));
IComponentModel2 compService = package.GetService<SComponentModel, IComponentModel2>();
IVsEditorAdaptersFactoryService editorAdapter = compService.GetService<IVsEditorAdaptersFactoryService>();
var wpfTextView = editorAdapter?.GetWpfTextView(activeTextView);
if (frameValue is IVsWindowFrame frame && wpfTextView != null)
{
var fileText = File.ReadAllText(Path.Combine(Path.GetTempPath(), "test.txt"));
wpfTextView.TextBuffer?.Insert(0, fileText);
}
}
}
此外,还需要提供一个 .vsct
文件,该文件定义命令配置,例如在 UI 中放置命令配置的位置、关联的文本等:
<Commands package="guidVSSDKPackage">
<Groups>
<Group guid="guidVSSDKPackageCmdSet" id="MyMenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS" />
</Group>
</Groups>
<Buttons>
<Button guid="guidVSSDKPackageCmdSet" id="InsertTextCommandId" priority="0x0100" type="Button">
<Parent guid="guidVSSDKPackageCmdSet" id="MyMenuGroup" />
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>Invoke InsertTextCommand (Unwrapped Community Toolkit)</ButtonText>
</Strings>
</Button>
<Button guid="guidVSSDKPackageCmdSet" id="cmdidVssdkInsertTextCommand" priority="0x0100" type="Button">
<Parent guid="guidVSSDKPackageCmdSet" id="MyMenuGroup" />
<Icon guid="guidImages1" id="bmpPic1" />
<Strings>
<ButtonText>Invoke InsertTextCommand (VSSDK)</ButtonText>
</Strings>
</Button>
</Buttons>
<Bitmaps>
<Bitmap guid="guidImages" href="Resources\InsertTextCommand.png" usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows, bmpPicStrikethrough" />
<Bitmap guid="guidImages1" href="Resources\VssdkInsertTextCommand.png" usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows, bmpPicStrikethrough" />
</Bitmaps>
</Commands>
如示例中所示,代码可能看起来不直观,并且不太可能让熟悉 .NET 的人轻松理解。 要了解许多概念,访问活动编辑器文本的 API 模式已过时。 对于大多数扩展程序,VSSDK 扩展是通过从在线资源复制和粘贴构建的,这可能会导致调试会话困难、反复试验以及挫败感。 在许多情况下,VSSDK 扩展可能不是实现扩展目标的最简单方法(尽管有时,它们是唯一的选择)。
社区工具包
Community Toolkit 是 Visual Studio 的开源基于社区的扩展性模型,用于包装 VSSDK,以简化开发体验。 由于它基于 VSSDK,因此受到与 VSSDK 相同的限制(即仅 .NET Framework,与 Visual Studio 的其余部分没有隔离,等等)。 继续以编写一个插入从文件系统中读取的文本的扩展为例,会使用社区工具包按如下所示为命令处理程序编写扩展:
protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
{
DocumentView docView = await VS.Documents.GetActiveDocumentViewAsync();
if (docView?.TextView == null) return;
var fileText = File.ReadAllText(Path.Combine(Path.GetTempPath(), "test.txt"));
docView.TextBuffer?.Insert(0, fileText);
}
在简单性和直观性方面,生成的代码得到了 VSSDK 的大幅改进! 我们不仅显著减少了行数,而且生成的代码看起来也合理。 无需了解 SVsTextManager
和 IVsTextManager
之间的区别。 API 设计更符合 .NET 平台的友好性,采用了常见的命名和异步模式,并优先考虑常见操作。 但是,社区工具包仍然基于现有的 VSSDK 模型构建,因此,基础结构的痕迹依然可见。 例如,仍需要 .vsct
文件。 虽然社区工具包在简化 API 方面做得非常出色,但它绑定到 VSSDK 的限制,并且没有办法简化扩展配置。
VisualStudio.Extensibility
VisualStudio.Extensibility 是新的扩展性模型,其中扩展在主 Visual Studio 进程之外运行。 由于这种基本的体系结构转变,新的模式和功能现在可用于 VSSDK 或社区工具包无法实现的扩展。 VisualStudio.Extensibility 提供了一组全新的 API,这些 API 一致且易于使用,允许扩展以 .NET 为目标,将扩展从 Visual Studio 的其余部分中引发的 bug 隔离开来,并使用户无需重启 Visual Studio 即可安装扩展。 但是,由于新模型是基于新的基础体系结构构建的,因此它还没有 VSSDK 和社区工具包具有的广度。 为了弥补这一差距,可以在进程 中运行 VisualStudio.Extensibility 扩展性扩展,这样就可以继续使用 VSSDK API。 但是,这样做意味着扩展只能面向 .NET Framework,因为它与基于 .NET Framework 的 Visual Studio 共享相同的进程。
继续以编写一个插入从文件中读取的文本的扩展为例,会使用 VisualStudio.Extensibility, 按如下所示编写扩展来进行命令处理:
public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
var activeTextView = await context.GetActiveTextViewAsync(cancellationToken);
if (activeTextView is not null)
{
var editResult = await Extensibility.Editor().EditAsync(batch =>
{
var fileText = File.ReadAllText(Path.Combine(Path.GetTempPath(), "test.txt"));
ITextDocumentEditor editor = activeTextView.Document.AsEditable(batch);
editor.Insert(0, fileText);
}, cancellationToken);
}
}
若要为放置、文本等配置命令,不再需要提供 .vsct
文件。 而是通过代码完成:
public override CommandConfiguration CommandConfiguration => new("%VisualStudio.Extensibility.Command1.DisplayName%")
{
Icon = new(ImageMoniker.KnownValues.Extension, IconSettings.IconAndText),
Placements = [CommandPlacement.KnownPlacements.ExtensionsMenu],
};
此代码更易于理解和遵循。 在大多数情况下,你可以通过 IntelliSense 的帮助通过编辑器编写此扩展,即使对于命令配置也是如此。
比较不同的 Visual Studio 扩展性模型
从示例中,你可能会注意到,使用 VisualStudio.Extensibility 时,命令处理程序中的代码行多于 Community Toolkit。 社区工具包是一款优秀的易用包装器,它基于使用 VSSDK 构建扩展;但是,也存在不那么显而易见的陷阱,这正是促使 VisualStudio.Extensibility 开发的原因。 为了理解转换和需求,特别是在社区工具包似乎也使得代码更易于编写和理解的情况下,让我们回顾一下示例,并比较代码更深层中发生的情况。
我们可以快速解包此示例中的代码,并查看 VSSDK 端实际调用的内容。 我们将仅专注于命令执行代码片段,因为 VSSDK 需要许多详细信息,而社区工具包将其隐藏得很好。 但是,一旦我们深入查看底层代码,你就会明白为什么这里的简洁性是一种权衡。 简单性隐藏了一些基础详细信息,这可能会导致意外行为、bug 甚至性能问题和崩溃。 以下代码片段展示了社区工具包代码的拆解,以便显示 VSSDK 调用:
private void Execute(object sender, EventArgs e)
{
package.JoinableTaskFactory.RunAsync(async delegate
{
var textManager = await package.GetServiceAsync<SVsTextManager, IVsTextManager>();
textManager.GetActiveView(1, null, out IVsTextView activeTextView);
if (activeTextView != null && activeTextView is IVsTextViewEx nativeView)
{
await package.JoinableTaskFactory.SwitchToMainThreadAsync();
ErrorHandler.ThrowOnFailure(nativeView.GetWindowFrame(out object frameValue));
IComponentModel2 compService = package.GetService<SComponentModel, IComponentModel2>();
IVsEditorAdaptersFactoryService editorAdapter = compService.GetService<IVsEditorAdaptersFactoryService>();
var wpfTextView = editorAdapter?.GetWpfTextView(activeTextView);
if (frameValue is IVsWindowFrame frame && wpfTextView != null)
{
var fileText = File.ReadAllText(Path.Combine(Path.GetTempPath(), "test.txt"));
wpfTextView.TextBuffer?.Insert(0, fileText);
}
}
});
}
此处存在几个问题,它们都围绕线程处理和异步代码展开。 我们将详细介绍每一个。
异步 API 与异步代码执行
首先需要注意的是,社区工具包中的 ExecuteAsync
方法在 VSSDK 中被包装成一个异步“触发并忘记”调用:
package.JoinableTaskFactory.RunAsync(async delegate
{
…
});
从核心 API 的角度来看,VSSDK 本身不支持异步命令执行。 也就是说,执行命令时,VSSDK 没有办法在后台线程上执行命令处理程序代码,等待它完成,并将用户返回到具有执行结果的原始调用上下文。 因此,即使 Community Toolkit 中的 ExecuteAsync API 在语法上是异步的,但它不是真正的异步执行。 由于它是一种“触发并忘记”的异步执行方式,你可以反复调用 ExecuteAsync,而无需等待上一次调用先完成。 虽然社区工具包在帮助扩展程序发现如何实现常见方案方面提供了更好的体验,但它最终无法解决 VSSDK 的基本问题。 在这种情况下,底层 VSSDK API 不是异步的,社区工具包提供的“触发并忘记”帮助程序方法无法正确解决异步让出,也无法正确处理客户端状态;它可能会隐藏一些潜在的难以调试的问题。
UI 线程与后台线程
社区工具包中这种包装的异步调用的另一个问题是,代码本身仍然从 UI 线程执行,如果不想冒冻结 UI 的风险,扩展开发人员需要自己弄明白如何正确地切换到后台线程。 与 Community Toolkit 可以隐藏 VSSDK 的干扰和额外代码一样,它仍需要了解 Visual Studio 中线程的复杂性。 在 VS 线程处理中学到的第一课之一是,并非所有内容都可以从后台线程运行。 换句话说,并非所有内容都是线程安全的,尤其是那些涉及到 COM 组件的调用。 因此,在上面的示例中,可以看到有一个调用切换到主 (UI) 线程:
await package.JoinableTaskFactory.SwitchToMainThreadAsync();
ErrorHandler.ThrowOnFailure(nativeView.GetWindowFrame(out object frameValue));
当然,你可以在此调用后切换回后台线程。 但是,作为使用 Community Toolkit 的扩展组件,需要密切关注代码所运行的线程,并确定它是否有冻结 UI 的风险。 在 Visual Studio 中,要做到无误的线程处理是困难的,需要正确使用 JoinableTaskFactory
来避免死锁。 编写正确处理线程的代码一直是一个棘手的问题,始终是 Bug 的根源,即便是我们内部的 Visual Studio 工程师也难以避免。 另一方面,VisualStudio.Extensibility 通过从进程外运行扩展并依赖于异步 API 端到端来完全避免此问题。
简单 API 与简单概念
由于 Community Toolkit 隐藏了 VSSDK 的许多复杂之处,因此它可能会给扩展程序一种虚假的简单感。 让我们继续学习相同的示例代码。 如果扩展程序不知道 Visual Studio 开发的线程要求,他们可能会假定其代码一直从后台线程运行。 他们不会介意从文本中读取文件的调用是同步的这一事实。 如果它在后台线程上,那么就是相关文件较大,它也不会冻结 UI。 但是,当代码解包到 VSSDK 时,它们会意识到情况并非如此。 因此,虽然社区工具包中的 API 当然更易于理解,并且更紧密地编写,因为它与 VSSDK 相关,但它受 VSSDK 限制的约束。 简单性可以掩盖重要的概念,如果扩展程序不理解,可能会造成更多的伤害。 VisualStudio.Extensibility 通过将进程外模型和异步 API 作为基础来避免主线程依赖项导致的许多问题。 虽然将操作移出进程可最大程度地简化线程,但但许多这些好处也适用于在进程中运行的扩展。 例如,VisualStudio.Extensibility 命令始终在后台线程上执行。 与 VSSDK API 交互仍需要深入了解线程处理的工作原理,但你至少不会像此例中那样付出意外挂起的代价。
比较图表
为了总结我们在上一节中详细介绍的内容,下表显示了一个快速比较:
VSSDK | 社区工具包 | VisualStudio.Extensibility | |
---|---|---|---|
运行时支持 | .NET Framework | .NET Framework | .NET |
从 Visual Studio 隔离 | ❌ | ❌ | ✅ |
简单 API | ❌ | ✅ | ✅ |
异步执行和 API | ❌ | ❌ | ✅ |
VS 方案广度 | ✅ | ✅ | ⏳ |
可安装且无需重启 | ❌ | ❌ | ✅ |
支持 VS 2019 及更低版本 | ✅ | ✅ | ❌ |
为了帮助你将比较应用到 Visual Studio 扩展性需求,下面是一些示例方案以及我们针对要使用的模型的建议:
- 我不熟悉 Visual Studio 扩展开发,我希望最简单的载入体验来创建高质量的扩展,我只需要支持 Visual Studio 2022 或更高版本。
- 在这种情况下,我们建议使用 VisualStudio.Extensibility。
- 我想编写面向 Visual Studio 2022 及更高版本的扩展。但是,VisualStudio.Extensibility 不支持我需要的所有功能。
- 我们建议在这种情况下,你应该采用一种混合方法来结合使用 VisualStudio.Extensibility 和 VSSDK。 可以创作在进程中运行的 VisualStudio.Extensibility 扩展插件,以便访问 VSSDK 或社区工具包 API。
- 我已有一个扩展,并想要更新它以支持较新版本。我希望我的扩展支持尽可能多的 Visual Studio 版本。
- 由于 VisualStudio.Extensibility 仅支持 Visual Studio 2022 及更高版本,因此 VSSDK 或 Community Toolkit 是本例的最佳选项。
- 我有一个现有扩展,我想迁移到VisualStudio.Extensibility,以利用 .NET 并在不重启的情况下进行安装。
- 此方案有点细微差别,因为 VisualStudio.Extensibility 不支持 Visual Studio 的下层版本。
- 如果现有扩展仅支持 Visual Studio 2022,并且具有所需的所有 API,则建议重写扩展以使用 VisualStudio.Extensibility。 但是,如果你的扩展需要 VisualStudio.Extensibility 尚不存在的 API,请继续创建一个 VisualStudio.Extensibility 扩展插件,该扩展 在进程 中运行,以便可以访问 VSSDK API。 随着时间的推移,您可以消除 VSSDK API 的用法,因为 VisualStudio.Extensibility 增加了支持,并让您的扩展在进程外运行。
- 如果你的扩展需要支持不支持 VisualStudio.Extensibility 的 Visual Studio 的下层版本,则我们建议你在代码库中进行一些重构。 将可跨 Visual Studio 版本共享的所有常见代码拉取到其自己的库,并创建面向不同扩展性模型的单独 VSIX 项目。 例如,如果扩展需要支持 Visual Studio 2019 和 Visual Studio 2022,则可以在解决方案中采用以下项目结构:
- MyExtension-VS2019(这是面向 Visual Studio 2019 的基于 VSSDK 的 VSIX 容器项目)
- MyExtension-VS2022(这是面向 Visual Studio 2022 的基于 VSSDK+VisualStudio.Extensibility 的 VSIX 容器项目)
- VSSDK-CommonCode(这是用于通过 VSSDK 调用 Visual Studio API 的通用库)。两个 VSIX 项目都可以引用此库来共享代码。
- MyExtension-BusinessLogic(这是一个通用库,其中包含与扩展的业务逻辑相关的所有代码。两个 VSIX 项目都可以引用此库来共享代码。
- 此方案有点细微差别,因为 VisualStudio.Extensibility 不支持 Visual Studio 的下层版本。
后续步骤
建议扩展程序在创建新扩展或增强现有扩展时从 VisualStudio.Extensibility 开始,如果遇到不受支持的方案,请使用 VSSDK 或社区工具包。 若要开始使用 VisualStudio.Extensibility,请浏览本节中介绍的文档。 您还可以参考 VSExtensibility GitHub 仓库 中的 示例,或提交 问题。