共用方式為


為您選擇正確的 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 大幅改善! 我們不僅大幅減少行數,而且產生的程式代碼看起來也合理。 不需要瞭解 SVsTextManagerIVsTextManager之間的差異。 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 與異步程式碼執行

首先要注意的是,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 線程與背景線程

社群工具包的這個包裝異步呼叫的另一個問題是程式碼本身仍然是在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 不支援我需要的所有功能。
    • 在此情況下,建議您採用混合式方法,結合VisualStudio.Extensibility和 VSSDK。 您可以撰寫 VisualStudio.Extensibility 延伸模組,在進程中執行,這可讓您存取 VSSDK 或 Community Toolkit 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 開始,當您建立新的擴充功能或增強現有擴充功能時,如果您遇到不支援的案例,請使用 VSSDK 或 Community Toolkit。 若要開始使用 VisualStudio.Extensibility,請瀏覽本節中提供的檔。 您也可以參考 VSExtensibility GitHub 存放庫範例,或 檔案問題。