共用方式為


為您選擇正確的 Visual Studio 擴充性模型

您可以使用三個主要擴充性模型來擴充Visual Studio:VSSDK、Community Toolkit和VisualStudio.Extensibility。 本文涵蓋每個項目的優缺點。 我們使用簡單的範例來反白顯示模型之間的架構和程式代碼差異。

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 的其餘部分沒有隔離,依此限制等等)。 使用 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 的簡單性和直覺性而言,產生的程式代碼大幅改善! 我們不僅大幅減少行數,而且產生的程式代碼看起來也合理。 不需要瞭解和IVsTextManager之間的差異SVsTextManager。 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 其餘部分產生的 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 多一行。 Community Toolkit 是搭配 VSSDK 建置延伸模組的絕佳方便使用包裝函式;不過,有些陷阱並不明顯,導致 VisualStudio.Extensibility 的開發。 若要了解轉換和需求,特別是當社群工具組似乎也會產生容易撰寫和瞭解的程式代碼時,讓我們來檢閱範例,並比較程式代碼更深層層中發生的情況。

我們可以快速解除包裝此範例中的程序代碼,並查看 VSSDK 端實際呼叫的內容。 我們將只專注於命令執行代碼段,因為 VSSDK 需要許多詳細數據,社群工具組確實隱藏得很好。 但是,一旦我們查看基礎程序代碼,您將了解為什麼這裏的簡單性是取捨。 簡單性會隱藏一些基礎詳細數據,這可能會導致非預期的行為、Bug,甚至是效能問題和當機。 下列代碼段顯示未包裝的 Community Toolkit 程式代碼,以顯示 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 ,Community Toolkit 中的 方法是 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 線程與背景線程

社群工具組這個包裝異步呼叫的另一個影響是程式代碼本身仍從UI線程執行,而延伸模塊開發人員會瞭解如何在不想凍結UI時正確地切換至背景線程。 只要 Community Toolkit 可以隱藏 VSSDK 的雜訊和額外程式代碼,它仍然需要您瞭解 Visual Studio 中線程的複雜性。 您在 VS 線程中學到的第一課之一,就是並非所有專案都可以從背景線程執行。 換句話說,並非所有專案都是安全線程,尤其是進入 COM 元件的呼叫。 因此,在上述範例中,您會看到有切換至 main (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 不支援我需要的所有功能。
  • 我有現有的擴充功能,而且想要更新它以支援較新版本。我希望我的擴充功能盡可能支援盡可能多的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 開始,當您建立新的擴充功能或增強現有擴充功能時,如果您遇到不支援的案例,請使用 VSSDK 或 Community Toolkit。 若要開始使用 VisualStudio.Extensibility,請瀏覽本節中提供的檔。 您也可以參考 VSExtensibility GitHub 存放庫 ,以取得 範例 或檔案 問題