Výběr správného modelu rozšiřitelnosti sady Visual Studio za vás

Visual Studio můžete rozšířit pomocí tří hlavních modelů rozšiřitelnosti, VSSDK, Community Toolkit a VisualStudio.Extensibility. Tento článek popisuje výhody a nevýhody každého z nich. Jednoduchý příklad používáme ke zvýraznění rozdílů v architektuře a kódu mezi modely.

VSSDK

Sada VSSDK (nebo Visual Studio SDK) je model, na který je založená většina rozšíření na Webu Visual Studio Marketplace . Tento model je založený na samotné sadě Visual Studio. Je to nejúplnější a nejvýkonnější, ale také nejsložitější naučit se a používat správně. Rozšíření, která používají sadu VSSDK, běží ve stejném procesu jako samotná sada Visual Studio. Načítání ve stejném procesu jako Sada Visual Studio znamená, že rozšíření, které má porušení přístupu, nekonečné smyčky nebo jiné problémy, může dojít k chybovému ukončení nebo zablokování sady Visual Studio a snížení uživatelského prostředí. A protože rozšíření běží ve stejném procesu jako Visual Studio, je možné je sestavit pouze pomocí rozhraní .NET Framework. Extendery, které chtějí použít nebo začlenit knihovny, které používají .NET 5 a novější, nemůžou k tomu použít VSSDK.

Rozhraní API v sadě VSSDK se v průběhu let agregovala, jak se Sada Visual Studio transformovala a vyvíjela. V jednom rozšíření se můžete setkat s rozhraními API založenými na modelu COM ze starší verze otisku, vysunutí prostřednictvím deceptivní jednoduchosti DTE a tinkeringu s importy a exporty MEF . Podívejme se na příklad zápisu přípony, která čte text ze systému souborů a vloží ho na začátek aktuálního aktivního dokumentu v editoru. Následující fragment kódu ukazuje kód, který byste napsali pro zpracování při vyvolání příkazu v rozšíření založeném na 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);
        }
    }
}

Kromě toho byste také museli zadat .vsct soubor, který definuje konfiguraci příkazu, například kam ho umístit do uživatelského rozhraní, přidružený text atd.:

<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>

Jak vidíte v ukázce, může se kód zdát neintuitivní a je nepravděpodobné, že by někdo obeznámený s .NET snadno vyzvedl. Existuje mnoho konceptů, které se dají naučit a vzory rozhraní API pro přístup k aktivnímu textu editoru jsou antiquatované. U většiny rozšiřujících rozšíření se rozšíření VSSDK vytvářejí z kopírování a vkládání z online zdrojů, což může vést k obtížnému ladění relací, zkušebních a chyb a frustraci. V mnoha případech nemusí být rozšíření VSSDK nejjednodušším způsobem, jak dosáhnout cílů rozšíření (i když někdy jsou jedinou volbou).

Community Toolkit

Community Toolkit je opensourcový model rozšiřitelnosti založený na komunitě pro Visual Studio, který zabalí sadu VSSDK pro snadnější vývojové prostředí. Vzhledem k tomu, že je založená na sadě VSSDK, podléhá stejným omezením jako VSSDK (tj. pouze rozhraní .NET Framework, žádná izolace od zbytku sady Visual Studio atd.). Pokračování ve stejném příkladu zápisu přípony, která vloží text přečtený ze systému souborů pomocí sady Community Toolkit, bude toto rozšíření zapsáno následujícím způsobem pro obslužnou rutinu příkazu:

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);
}

Výsledný kód je z VSSDK mnohem vylepšen z hlediska jednoduchosti a intuitivnosti. Nejen že jsme výrazně snížili počet řádků, ale výsledný kód vypadá rozumně i. Není třeba pochopit, jaký je rozdíl mezi SVsTextManager a IVsTextManager. Rozhraní API vypadají a cítí více . ROZHRANÍ NET, které zahrnuje běžné vzory pojmenování a asynchronních vzorů, spolu s stanovením priorit běžných operací. Komunitní sada nástrojů je ale stále postavená na existujícím modelu VSSDK, a proto se přesahují podkladové struktury. Například .vsct soubor je stále nutný. Komunitní sada nástrojů sice skvěle zjednodušuje rozhraní API, ale je vázána na omezení sady VSSDK a nemá způsob, jak zjednodušit konfiguraci rozšíření.

VisualStudio.Extensibility

VisualStudio.Extensibility je nový model rozšiřitelnosti, kde rozšíření běží mimo hlavní proces sady Visual Studio. Vzhledem k tomuto zásadnímu architektonickému posunu jsou teď nové vzory a možnosti dostupné pro rozšíření, která nejsou možná v sadě VSSDK nebo Community Toolkit. VisualStudio.Extensibility nabízí zcela novou sadu rozhraní API, která jsou konzistentní a snadno použitelná, umožňuje rozšíření cílit na .NET, izoluje chyby, které vznikají z rozšíření ze zbytku sady Visual Studio, a umožňuje uživatelům instalovat rozšíření bez restartování sady Visual Studio. Vzhledem k tomu, že nový model je založený na nové základní architektuře, ještě nemá šířku, kterou má sada VSSDK a Community Toolkit. K přemostění této mezery můžete spustit rozšíření VisualStudio.Extensibility v procesu, což vám umožní pokračovat v používání rozhraní API sady VSSDK. To však znamená, že vaše rozšíření může cílit pouze na .NET Framework, protože sdílí stejný proces jako Visual Studio, který je založený na rozhraní .NET Framework.

Pokračování ve stejném příkladu zápisu přípony, která vloží text ze souboru pomocí VisualStudio.Extensibility, bude přípona zapsána následujícím způsobem pro zpracování příkazů:

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);
                
    }
}

Pokud chcete nakonfigurovat příkaz pro umístění, text atd., nemusíte už zadávat .vsct soubor. Místo toho se provádí prostřednictvím kódu:

public override CommandConfiguration CommandConfiguration => new("%VisualStudio.Extensibility.Command1.DisplayName%")
{
    Icon = new(ImageMoniker.KnownValues.Extension, IconSettings.IconAndText),
    Placements = [CommandPlacement.KnownPlacements.ExtensionsMenu],
};

Tento kód je srozumitelnější a srozumitelnější. Ve většině případů můžete toto rozšíření napsat čistě prostřednictvím editoru pomocí Technologie IntelliSense, a to i pro konfiguraci příkazů.

Porovnání různých modelů rozšiřitelnosti sady Visual Studio

Z ukázky si můžete všimnout, že pomocí visualstudio.Extensibility existuje více řádků kódu než Community Toolkit v obslužné rutině příkazů. Community Toolkit je vynikající obálka snadného použití nad rozšířeními budov pomocí VSSDK; Existují však nástrahy, které nejsou okamžitě zřejmé, což je to, co vedlo k vývoji VisualStudio.Extensibility. Pokud chcete porozumět přechodu a potřebě, zejména pokud se zdá, že komunitní sada nástrojů také vede k tomu, že kód, který se dá snadno napsat a pochopit, podívejme se na příklad a porovnejme, co se děje v hlubších vrstvách kódu.

Kód v této ukázce můžeme rychle rozbalit a zjistit, co se skutečně volá na straně VSSDK. Zaměříme se výhradně na fragment kódu provádění příkazů, protože existuje mnoho podrobností, které VSSDK potřebuje, což Community Toolkit pěkně skryje. Jakmile se ale podíváme na základní kód, pochopíte, proč je tady jednoduchost kompromisem. Jednoduchost skryje některé základní podrobnosti, což může vést k neočekávanému chování, chybám a dokonce problémům s výkonem a chybovým ukončením. Následující fragment kódu ukazuje kód sady Community Toolkit, který se rozbalil a zobrazil volání 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);    
            }
        }
    });
}

Tady je několik problémů, které se mají dostat, a všechny se točí kolem threadingu a asynchronního kódu. Projdeme si jednotlivé podrobnosti.

Asynchronní rozhraní API versus asynchronní spouštění kódu

První věcí, kterou je třeba poznamenat, je, že ExecuteAsync metoda v komunitní sadě Nástrojů je zabalená asynchronní volání fire-and-forget v VSSDK:

package.JoinableTaskFactory.RunAsync(async delegate
{
  …
});

Samotný VSSDK nepodporuje asynchronní spouštění příkazů z pohledu základního rozhraní API. To znamená, že když se spustí příkaz, VSSDK nemá způsob, jak spustit kód obslužné rutiny příkazu ve vlákně na pozadí, počkat na dokončení a vrátit uživatele do původního volajícího kontextu s výsledky spuštění. I když je rozhraní API ExecuteAsync v komunitní sadě Nástrojů syntakticky asynchronní, není to skutečné asynchronní spuštění. A protože se jedná o fire a zapomenutý způsob asynchronního spuštění, můžete volat ExecuteAsync znovu a znovu, aniž byste museli čekat na dokončení předchozího volání jako první. Komunitní sada nástrojů sice poskytuje lepší prostředí z hlediska pomoci rozšiřujícím uživatelům zjistit, jak implementovat běžné scénáře, ale nakonec nedokáže vyřešit základní problémy s VSSDK. V tomto případě základní rozhraní API sady VSSDK není asynchronní a pomocné metody fire-and-forget poskytované Community Toolkit nemůžou správně řešit asynchronní výnosy a pracovat se stavem klienta; může skrýt některé potenciální problémy s pevným laděním.

Vlákno uživatelského rozhraní versus vlákno na pozadí

Druhý pád s tímto zabaleným asynchronním voláním ze sady Community Toolkit je, že samotný kód se stále spouští z vlákna uživatelského rozhraní a je na vývojáři rozšíření, abyste zjistili, jak správně přepnout na vlákno na pozadí, pokud nechcete riskovat zmrazení uživatelského rozhraní. Stejně jako community toolkit může skrýt šum a další kód VSSDK, stále vyžaduje, abyste porozuměli složitosti vláken v sadě Visual Studio. A jednou z prvních lekcí, které se naučíte ve vláknech VS, je, že ne všechno se dá spustit z vlákna na pozadí. Jinými slovy, ne vše je bezpečné pro vlákno, zejména volání, která přecházejí do komponent modelu COM. V předchozím příkladu tedy vidíte, že existuje volání pro přepnutí do hlavního vlákna (UI):

await package.JoinableTaskFactory.SwitchToMainThreadAsync();
ErrorHandler.ThrowOnFailure(nativeView.GetWindowFrame(out object frameValue));

Po tomto volání můžete samozřejmě přepnout zpět na vlákno na pozadí. Jako rozšiřující nástroj pomocí sady Community Toolkit ale budete muset věnovat úzkou pozornost tomu, na jakém vlákně je kód zapnutý, a určit, jestli má riziko zmrazení uživatelského rozhraní. Nitění v sadě Visual Studio je obtížné a vyžaduje správné použití JoinableTaskFactory , aby nedocházelo k zablokování. Cílem je napsat kód, který se správně zabývá vlákny, konstantním zdrojem chyb, a to i pro naše interní techniky sady Visual Studio. VisualStudio.Extensibility na druhé straně se tomuto problému úplně vyhne tím, že z procesu dochází rozšíření a spoléhá na asynchronní rozhraní API na konci.

Jednoduché rozhraní API a jednoduché koncepty

Vzhledem k tomu, že Komunitní sada nástrojů skrývá mnoho složitých funkcí sady VSSDK, mohla by rozšiřujícím objektům poskytnout falešný smysl pro jednoduchost. Pojďme pokračovat se stejným vzorovým kódem. Pokud extender nevěděl o požadavcích na vlákno vývoje sady Visual Studio, může předpokládat, že se kód spouští z vlákna na pozadí celou dobu. Nebudou mít žádný problém se skutečností, že volání pro čtení souboru z textu je synchronní. Pokud je na vlákně na pozadí, uživatelské rozhraní se nezablokuje, pokud je příslušný soubor velký. Když se ale kód rozbalí do sady VSSDK, zjistí, že tomu tak není. I když rozhraní API ze sady Community Toolkit určitě vypadá jednodušší pochopit a lépe pochopit, protože je svázané s VSSDK, podléhá omezením sady VSSDK. Simplicity můžou glosovat důležité koncepty, které v případě, že rozšíření nerozumí, může způsobit větší škodu. VisualStudio.Extensibility zabraňuje mnoha problémům způsobeným závislostmi main-thread tím, že se zaměřuje na model mimo proces a asynchronní rozhraní API jako základ. I když tento proces dochází, zjednodušilo by to nejvíce vlákno, mnohé z těchto výhod se přenesou také do rozšíření, která běží v procesu. Příkazy VisualStudio.Extensibility se například vždy spouštějí na vlákně na pozadí. Interakce s rozhraními API VSSDK stále vyžaduje podrobné znalosti o tom, jak funguje dělení na vlákna, ale aspoň nezaplatíte náklady na náhodné zablokování, jako v tomto příkladu.

Srovnávací graf

Pokud chcete shrnout, co jsme podrobně probrali v předchozí části, ukazuje následující tabulka rychlé porovnání:

VSSDK Community Toolkit VisualStudio.Extensibility
Podpora modulu runtime .NET Framework .NET Framework .NET
Izolace ze sady Visual Studio
Jednoduché rozhraní API
Asynchronní spouštění a rozhraní API
Scénář VS – šířka
Instalovatelné bez restartování
Podporuje VS 2019 a níže.

Abychom vám pomohli s porovnáním potřeb rozšiřitelnosti sady Visual Studio, tady jsou některé ukázkové scénáře a naše doporučení k použití modelu:

  • S vývojem rozšíření sady Visual Studio teprve začínám a chci nejsnadnější prostředí pro onboarding vytvořit vysoce kvalitní rozšíření a potřebuji jenom podporu sady Visual Studio 2022 nebo novější.
    • V tomto případě doporučujeme použít VisualStudio.Extensibility.
  • Chci napsat rozšíření, které cílí na Visual Studio 2022 a novější. VisualStudio.Extensibility ale nepodporuje všechny funkce, které potřebuji.
    • V tomto případě doporučujeme použít hybridní metodu kombinování sady VisualStudio.Extensibility a VSSDK. Můžete vytvořit rozšíření VisualStudio.Extensibility, které se spouští v procesu, což umožňuje přístup k rozhraním API sady VSSDK nebo Community Toolkit.
  • Mám existující rozšíření a chci ho aktualizovat tak, aby podporoval novější verze. Chci, aby moje rozšíření podporovalo co nejvíce verzí sady Visual Studio.
    • Vzhledem k tomu, že VisualStudio.Extensibility podporuje pouze Visual Studio 2022 a novější, je v tomto případě nejlepší volbou sada VSSDK nebo Community Toolkit.
  • Mám existující rozšíření, které chci migrovat na VisualStudio.Extensibility, aby bylo možné využít výhod .NET a nainstalovat bez restartování.
    • Tento scénář je trochu nuančí, protože VisualStudio.Extensibility nepodporuje verze sady Visual Studio nižší úrovně.
      • Pokud vaše stávající rozšíření podporuje jenom Visual Studio 2022 a má všechna potřebná rozhraní API, doporučujeme přepsat rozšíření tak, aby používalo VisualStudio.Extensibility. Pokud ale vaše rozšíření potřebuje rozhraní API, která visualStudio.Extensibility ještě nemá, pokračujte vytvořením rozšíření VisualStudio.Extensibility, které se spouští v procesu , abyste měli přístup k rozhraním API sady VSSDK. Přesčas můžete eliminovat využití rozhraní API VSSDK, protože VisualStudio.Extensibility přidává podporu a přesouvá rozšíření, aby se proces neskončí.
      • Pokud vaše rozšíření potřebuje podporovat verze sady Visual Studio nižší úrovně, které nepodporují VisualStudio.Extensibility, doporučujeme provést refaktoring v základu kódu. Stáhněte si veškerý společný kód, který lze sdílet napříč verzemi sady Visual Studio, do vlastní knihovny a vytvořte samostatné projekty VSIX, které cílí na různé modely rozšiřitelnosti. Pokud například vaše rozšíření potřebuje podporovat Visual Studio 2019 a Visual Studio 2022, můžete ve svém řešení přijmout následující strukturu projektu:
        • MyExtension-VS2019 (to je projekt kontejneru VSSDK založený na VSSIX, který cílí na Visual Studio 2019)
        • MyExtension-VS2022 (to je váš kontejner VSSDK+VisualStudio.Extensibility založený na VSIX, který cílí na Visual Studio 2022)
        • VSSDK-CommonCode (jedná se o běžnou knihovnu, která se používá k volání rozhraní API sady Visual Studio prostřednictvím sady VSSDK. Oba projekty VSIX můžou odkazovat na tuto knihovnu a sdílet kód.)
        • MyExtension-BusinessLogic (to je společná knihovna, která obsahuje veškerý kód, který je relevantní pro obchodní logiku vašeho rozšíření. Oba projekty VSIX můžou odkazovat na tuto knihovnu a sdílet kód.)

Další kroky

Naším doporučením je, že rozšiřující moduly začínají visualStudio.Extensibility při vytváření nových rozšíření nebo vylepšení existujících rozšíření a pokud narazíte na nepodporované scénáře, použijte sadu VSSDK nebo Community Toolkit. Začněte tím, že s visualStudio.Extensibility přejdete do dokumentace uvedené v této části. Můžete také odkazovat na úložiště GitHub pro VSExtensibility pro ukázky nebo na problémy se soubory.