適切な Visual Studio 拡張機能モデルを選択する
VSSDK、Community Toolkit、VisualStudio.Extensibility の 3 つの主要な機能拡張モデルを使用して、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 を使用して使用することはできません。
VSSDK の API は、Visual Studio 自体が変換および進化するにつれて、長年にわたって集計されてきました。 1 つの拡張機能で、従来のインプリントの 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 は、開発エクスペリエンスを容易にするために VSSDK をラップする、Visual Studio 用のオープン ソースのコミュニティベースの拡張モデルです。 VSSDK に基づいているため、VSSDK と同じ制限が適用されます (つまり、.NET Framework のみ、Visual Studio の残りの部分からの分離はありません)。 Community Toolkit を使用して、ファイル システムから読み取られたテキストを挿入する拡張機能を記述する同じ例を引き続き使用すると、拡張機能はコマンド ハンドラー用に次のように記述されます。
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 に優しい外観と感触になり、一般的な名前付けと非同期パターンを採用し、一般的な操作を優先しています。 ただし、Community Toolkit は引き続き既存の VSSDK モデルに基づいて構築されているため、基になる構造の痕跡が読み取られます。 たとえば、.vsct
ファイルは引き続き必要です。 Community Toolkit は API を簡素化する優れた仕事をしますが、VSSDK の制限に拘束され、拡張機能の構成を簡単にする方法はありません。
VisualStudio.Extensibility
VisualStudio.Extensibility は、拡張機能が Visual Studio のメイン プロセスの外部で実行される新しい拡張モデルです。 この基本的なアーキテクチャの変化により、VSSDK または Community Toolkit では不可能な拡張機能で新しいパターンと機能を使用できるようになりました。 VisualStudio.Extensibility には、一貫性があり使いやすい API の完全に新しいセットが用意されています。これにより、拡張機能は .NET をターゲットにし、Visual Studio の他の部分から拡張機能から発生するバグを分離し、Visual Studio を再起動せずに拡張機能をインストールできます。 ただし、新しいモデルは新しい基になるアーキテクチャに基づいて構築されているため、VSSDK と Community Toolkit の幅はまだありません。 そのギャップを埋めるために、VisualStudio.Extensibility 拡張機能をプロセス で実行すると、VSSDK API を引き続き使用できます。 ただし、これを行うと、.NET Framework に基づく Visual Studio と同じプロセスが共有されるため、拡張機能は .NET Framework のみを対象にすることができます。
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 よりも多くのコード行があることに気付く場合があります。 Community Toolkit は、VSSDK を使用した拡張機能の構築に優れた使いやすさラッパーです。ただし、VisualStudio.Extensibility の開発の原因となったのは、すぐには明らかではない落とし穴があります。 移行とニーズを理解するために、特に Community Toolkit が記述しやすく理解しやすいコードを生み出しているように見える場合は、例を確認し、コードの深層で何が起こっているのかを比較してみましょう。
このサンプルのコードをすばやくラップ解除し、VSSDK 側で実際に何が呼び出されているかを確認できます。 VSSDK に必要な詳細が多数あるため、コマンド実行スニペットのみに焦点を当てます。これは、Community Toolkit がうまく隠れているからです。 しかし、基になるコードを見ると、ここでのシンプルさがトレードオフである理由を理解できます。 このシンプルさにより、基になる詳細の一部が非表示になり、予期しない動作、バグ、さらにはパフォーマンスの問題やクラッシュが発生する可能性があります。 次のコード スニペットは、VSSDK 呼び出しを示すためにラップ解除された Community Toolkit コードを示しています。
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 と非同期コードの実行
最初に注意すべき点は、Community Toolkit の ExecuteAsync
メソッドは、VSSDK でラップされた非同期のファイア アンド フォーゲット呼び出しであるということです。
package.JoinableTaskFactory.RunAsync(async delegate
{
…
});
VSSDK 自体は、コア API の観点からの非同期コマンド実行をサポートしていません。 つまり、コマンドが実行されると、VSSDK には、バックグラウンド スレッドでコマンド ハンドラー コードを実行し、完了するまで待機して、実行結果を含む元の呼び出し元のコンテキストにユーザーを返す方法がありません。 したがって、Community Toolkit の ExecuteAsync API は構文的に非同期ですが、実際の非同期実行ではありません。 非同期実行を開始して後の処理を気にしない方法であるため、前の呼び出しが完了するのを待たずに、何度も ExecuteAsync を呼び出すことができます。 Community Toolkit は、エクステンダーが一般的なシナリオを実装する方法を見つけ出すという点で優れたエクスペリエンスを提供しますが、最終的には VSSDK の基本的な問題を解決することはできません。 この場合、基になる VSSDK API は非同期ではなく、Community Toolkit によって提供される fire-and-forget ヘルパー メソッドでは、非同期の生成とクライアント状態の操作に適切に対処できません。それにより、潜在的にデバッグが困難な問題を隠す可能性があります。
UI スレッドとバックグラウンド スレッド
Community Toolkit からのこのラップされた非同期呼び出しのもう 1 つのフォールアウトは、コード自体が UI スレッドから引き続き実行され、UI をフリーズさせるリスクがない場合にバックグラウンド スレッドに正しく切り替える方法を見つけ出す拡張機能開発者にあるということです。 コミュニティ ツールキットが VSSDK のノイズと余分なコードを隠すことができる限り、Visual Studio でのスレッド処理の複雑さを理解する必要があります。 VS スレッドで学習する最初のレッスンの 1 つは、すべてがバックグラウンド スレッドから実行できるわけではないということです。 言い換えると、すべてがスレッド セーフであるわけではありません。特に COM コンポーネントに入る呼び出しです。 そのため、上記の例では、メイン (UI) スレッドに切り替える呼び出しがあることがわかります。
await package.JoinableTaskFactory.SwitchToMainThreadAsync();
ErrorHandler.ThrowOnFailure(nativeView.GetWindowFrame(out object frameValue));
もちろん、この呼び出しの後にバックグラウンド スレッドに切り替えることができます。 ただし、Community Toolkit を使用するエクステンダーとして、コードが存在するスレッドに細心の注意を払い、UI をフリーズさせるリスクがあるかどうかを判断する必要があります。 Visual Studio でのスレッド処理は適切に行われにくく、デッドロックを回避するために JoinableTaskFactory
を適切に使用する必要があります。 スレッド処理を正しく処理するコードを記述する際の苦労は、Visual Studio の内部エンジニアであっても、バグの絶え間ない原因となっています。 一方、VisualStudio.Extensibility では、プロセス外で拡張機能を実行し、非同期 API にエンドツーエンドで依存することで、この問題を完全に回避できます。
単純な API と単純な概念
Community Toolkit は VSSDK の複雑さの多くを隠しているため、エクステンダーに誤ったシンプル感を与える可能性があります。 同じサンプル コードを続けてみましょう。 エクステンダーが Visual Studio 開発のスレッド要件を知らなかった場合、コードはバックグラウンド スレッドからずっと実行されていると見なされる可能性があります。 テキストからファイルを読み取る呼び出しが同期的であるという事実に問題はありません。 バックグラウンド スレッド上にある場合、問題のファイルが大きい場合、UI はフリーズしません。 ただし、コードが VSSDK にラップ解除されると、そうではないことに気付くでしょう。 そのため、Community Toolkit の API は確かに理解が簡単で、書き込みがより一貫性のあるように見えますが、VSSDK に関連付けられているため、VSSDK の制限の対象となります。 シンプルさは、エクステンダーが理解していない場合に、より多くの害を引き起こす可能性がある重要な概念を覆す可能性があります。 VisualStudio.Extensibility は、プロセス外モデルと非同期 API を基盤として重点的に取り組むことで、メイン スレッドの依存関係によって引き起こされる多くの問題を回避します。 プロセスを使い果たすとスレッド処理が最も簡単になりますが、これらの利点の多くは、インプロセスで実行される拡張機能にも引き継がわれます。 たとえば、VisualStudio.Extensibility コマンドは常にバックグラウンド スレッドで実行されます。 VSSDK API を使用するためには、スレッド処理の動作に関する詳細な知識が必要ですが、この例のように、誤ってハングすることを避けられるので余計なコストは発生しません。
比較グラフ
前のセクションで詳しく説明した内容を要約するために、次の表に簡単な比較を示します。
VSSDK | Community Toolkit | 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 または Community Toolkit API にアクセスできます。
- 既存の拡張機能があり、新しいバージョンをサポートするように更新したいと考えています。拡張機能で可能な限り多くのバージョンの Visual Studio をサポートする必要があります。
- VisualStudio.Extensibility では Visual Studio 2022 以降のみがサポートされるため、この場合は VSSDK または Community Toolkit が最適なオプションです。
- 私は.NETを利用するために、再起動せずにインストールできるVisualStudio.Extensibilityへの既存の拡張機能の移行を希望しています。
- VisualStudio.Extensibility では下位バージョンの Visual Studio がサポートされていないため、このシナリオは少し微妙です。
- 既存の拡張機能が Visual Studio 2022 のみをサポートし、必要なすべての API がある場合は、VisualStudio.Extensibility を使用するように拡張機能を書き直することをお勧めします。 ただし、VisualStudio.Extensibility にまだない API が拡張機能で必要な場合は、VSSDK API にアクセスできるように、プロセスで実行する VisualStudio.Extensibility 拡張機能を作成します。 VisualStudio.Extensibility によるサポートが追加されるに従って、VSSDK API の使用を徐々に無くし、拡張機能を別プロセスで実行できるように移行することができます。
- 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 または Community Toolkit を使用することをお勧めします。 VisualStudio.Extensibility を使い始めるには、このセクションに記載されているドキュメントを参照してください。 VSExtensibility GitHub リポジトリ を参照して、サンプル したり、問題を報告したりすることもできます。