Condividi tramite


Scegliere il modello di estendibilità di Visual Studio appropriato

È possibile estendere Visual Studio usando tre modelli di estendibilità principali, VSSDK, Community Toolkit e VisualStudio.Extensibility. Questo articolo illustra i vantaggi e i svantaggi di ognuno di essi. Viene usato un semplice esempio per evidenziare le differenze di architettura e codice tra i modelli.

VSSDK

VSSDK (o Visual Studio SDK) è il modello basato sulla maggior parte delle estensioni in Visual Studio Marketplace . Questo modello è quello su cui si basa Visual Studio stesso. È il più completo e il più potente, ma anche il più complesso da imparare e usare correttamente. Le estensioni che usano VSSDK vengono eseguite nello stesso processo di Visual Studio stesso. Il caricamento nello stesso processo di Visual Studio significa che un'estensione con violazione di accesso, ciclo infinito o altri problemi può arrestare o arrestare Visual Studio e ridurre l'esperienza del cliente. Poiché le estensioni vengono eseguite nello stesso processo di Visual Studio, possono essere compilate solo con .NET Framework. Gli extender che vogliono usare o incorporare librerie che usano .NET 5 e versioni successive non possono farlo usando VSSDK.

Le API in VSSDK sono state aggregate nel corso degli anni man mano che Visual Studio stesso ha trasformato ed evoluto. In un'unica estensione, si può trovare in grappling con LE API basate su COM da impronta legacy, breezing attraverso la semplicità ingannevole di DTE, e tinkering con importazioni e esportazioni MEF . Si prenda un esempio di scrittura di un'estensione che legge il testo dal file system e lo inserisce all'inizio del documento attivo corrente all'interno dell'editor. Il frammento di codice seguente mostra il codice da scrivere per gestire quando viene richiamato un comando in un'estensione basata su 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);
        }
    }
}

Inoltre, è anche necessario fornire un .vsct file, che definisce la configurazione del comando, ad esempio dove inserirlo nell'interfaccia utente, il testo associato e così via:

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

Come si può notare nell'esempio, il codice può sembrare poco intuitivo ed è improbabile che qualcuno abbia familiarità con .NET per raccogliere facilmente. Esistono molti concetti da apprendere e i modelli API per accedere al testo dell'editor attivo sono antiquati. Per la maggior parte degli extender, le estensioni VSSDK vengono costruite dalla copia e incolla da origini online, che possono portare a sessioni di debug difficili, prove ed errori e frustrazione. In molti casi, le estensioni VSSDK potrebbero non essere il modo più semplice per raggiungere gli obiettivi di estensione (anche se a volte sono l'unica scelta).

Community Toolkit

Community Toolkit è il modello di estendibilità open source basato sulla community per Visual Studio che esegue il wrapping di VSSDK per un'esperienza di sviluppo più semplice. Poiché si basa su VSSDK, è soggetto alle stesse limitazioni di VSSDK ( ovvero solo .NET Framework, nessun isolamento dal resto di Visual Studio e così via). Continuando con lo stesso esempio di scrittura di un'estensione che inserisce il testo letto dal file system, usando Community Toolkit, l'estensione verrebbe scritta come segue per un gestore di comandi:

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

Il codice risultante è notevolmente migliorato da VSSDK in termini di semplicità e intuitività. Non solo è stato ridotto il numero di righe in modo significativo, ma anche il codice risultante sembra ragionevole. Non è necessario comprendere qual è la differenza tra SVsTextManager e IVsTextManager. Le API hanno un aspetto più simile a . NET-friendly, che adotta modelli di denominazione e asincroni comuni, insieme alla definizione delle priorità delle operazioni comuni. Tuttavia, Community Toolkit è ancora basato sul modello VSSDK esistente e quindi vestigia della struttura sottostante sanguinata. Ad esempio, un .vsct file è ancora necessario. Anche se Community Toolkit offre un ottimo lavoro per semplificare le API, è vincolato alle limitazioni di VSSDK e non ha un modo per semplificare la configurazione dell'estensione.

VisualStudio.Extensibility

VisualStudio.Extensibility è il nuovo modello di estendibilità in cui le estensioni vengono eseguite all'esterno del processo principale di Visual Studio. A causa di questo passaggio fondamentale dell'architettura, sono ora disponibili nuovi modelli e funzionalità per le estensioni che non sono possibili con VSSDK o Community Toolkit. VisualStudio.Extensibility offre un nuovo set di API coerenti e facili da usare, consente alle estensioni di usare .NET, isola i bug che derivano dalle estensioni del resto di Visual Studio e consente agli utenti di installare le estensioni senza riavviare Visual Studio. Tuttavia, poiché il nuovo modello è basato su una nuova architettura sottostante, non ha ancora l'ampiezza di VSSDK e Community Toolkit. Per colmare tale gap, è possibile eseguire le estensioni VisualStudio.Extensibility in fase di elaborazione, che consente di continuare a usare le API VSSDK. In questo modo, tuttavia, l'estensione può usare solo .NET Framework perché condivide lo stesso processo di Visual Studio, basato su .NET Framework.

Continuando con lo stesso esempio di scrittura di un'estensione che inserisce il testo da un file, usando VisualStudio.Extensibility, l'estensione verrà scritta come segue per la gestione dei comandi:

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

Per configurare il comando per posizionamento, testo e così via, non è più necessario fornire un .vsct file. Al contrario, viene eseguita tramite il codice:

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

Questo codice è più facile da comprendere e seguire. Nella maggior parte dei casi, è possibile scrivere questa estensione esclusivamente tramite l'editor con l'aiuto di IntelliSense, anche per la configurazione dei comandi.

Confronto tra i diversi modelli di estendibilità di Visual Studio

Nell'esempio è possibile notare che usando VisualStudio.Extensibility sono presenti più righe di codice rispetto a Community Toolkit nel gestore dei comandi. Community Toolkit è un wrapper di facilità d'uso eccellente sopra le estensioni di compilazione con VSSDK; Tuttavia, ci sono insidie che non sono immediatamente ovvie, ciò che ha portato allo sviluppo di VisualStudio.Extensibility. Per comprendere la transizione e la necessità, soprattutto quando sembra che Community Toolkit restituisca anche codice facile da scrivere e comprendere, esaminiamo l'esempio e confrontiamo ciò che accade nei livelli più profondi del codice.

È possibile annullare rapidamente il wrapping del codice in questo esempio e vedere cosa viene effettivamente chiamato sul lato VSSDK. Ci concentreremo esclusivamente sul frammento di esecuzione del comando, poiché sono disponibili numerosi dettagli necessari per VSSDK, che Community Toolkit nasconde in modo interessante. Tuttavia, una volta esaminato il codice sottostante, si comprenderà il motivo per cui la semplicità è un compromesso. La semplicità nasconde alcuni dei dettagli sottostanti, che possono causare comportamenti imprevisti, bug e persino problemi di prestazioni e arresti anomali. Il frammento di codice seguente mostra il codice Community Toolkit non sottoposto a wrapping per visualizzare le chiamate 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);    
            }
        }
    });
}

Ci sono alcuni problemi da risolvere e tutti ruotano intorno al threading e al codice asincrono. Verranno descritti in dettaglio ognuno di essi.

API asincrona e esecuzione di codice asincrono

La prima cosa da notare è che il ExecuteAsync metodo in Community Toolkit è una chiamata asincrona asincrona fire-and-forget in VSSDK:

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

VSSDK non supporta l'esecuzione asincrona dei comandi dal punto di vista dell'API principale. Ovvero, quando viene eseguito un comando, VSSDK non ha un modo per eseguire il codice del gestore dei comandi in un thread in background, attendere il completamento e restituire l'utente al contesto chiamante originale con i risultati dell'esecuzione. Pertanto, anche se l'API ExecuteAsync in Community Toolkit è sintatticamente asincrona, non è vera esecuzione asincrona. E poiché si tratta di un modo di fuoco e dimenticare l'esecuzione asincrona, è possibile chiamare ExecuteAsync over e più volte senza attendere il completamento della chiamata precedente per prima. Anche se Community Toolkit offre un'esperienza migliore in termini di aiutare gli extender a scoprire come implementare scenari comuni, in definitiva non può risolvere i problemi fondamentali con VSSDK. In questo caso, l'API VSSDK sottostante non è asincrona e i metodi helper fire-and-forget forniti da Community Toolkit non possono risolvere correttamente la resa asincrona e l'uso dello stato del client; potrebbe nascondere alcuni potenziali problemi di hard-to-debug.

Thread dell'interfaccia utente e thread in background

L'altro fallout con questa chiamata asincrona sottoposta a wrapping da Community Toolkit è che il codice stesso viene ancora eseguito dal thread dell'interfaccia utente ed è nello sviluppatore dell'estensione per capire come passare correttamente a un thread in background se non si vuole rischiare di bloccare l'interfaccia utente. Quanto Community Toolkit può nascondere il rumore e il codice aggiuntivo di VSSDK, è comunque necessario comprendere le complessità del threading in Visual Studio. Una delle prime lezioni apprese nel threading di Visual Studio è che non tutto può essere eseguito da un thread in background. In altre parole, non tutto è thread-safe, in particolare le chiamate che passano ai componenti COM. Nell'esempio precedente si noterà quindi che è presente una chiamata per passare al thread principale (UI):

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

È ovviamente possibile tornare a un thread in background dopo questa chiamata. Tuttavia, in qualità di extender che usa Community Toolkit, è necessario prestare particolare attenzione al thread su cui si trova il codice e determinare se ha il rischio di bloccare l'interfaccia utente. Il threading in Visual Studio è difficile da risolvere e richiede l'uso appropriato di JoinableTaskFactory per evitare deadlock. La difficoltà di scrivere codice che gestisce correttamente il threading è stata un'origine costante di bug, anche per i tecnici interni di Visual Studio. VisualStudio.Extensibility, d'altra parte, evita completamente questo problema eseguendo estensioni fuori processo e basandosi sulle API asincrone end-to-end.

API semplice e concetti semplici

Poiché Community Toolkit nasconde molte delle complessità di VSSDK, potrebbe dare agli extender un falso senso di semplicità. Continuare con lo stesso codice di esempio. Se un extender non conosce i requisiti di threading dello sviluppo di Visual Studio, potrebbe presupporre che il codice venga eseguito da un thread in background per tutto il tempo. Non si verifica alcun problema con il fatto che la chiamata per leggere un file dal testo è sincrona. Se si trova in un thread in background, l'interfaccia utente non verrà bloccata se il file in questione è di grandi dimensioni. Tuttavia, quando il codice viene decomcritto in VSSDK, si renderanno conto che non è questo il caso. Pertanto, mentre l'API di Community Toolkit sembra certamente più semplice da comprendere e più coesa da scrivere, perché è legata a VSSDK, è soggetta alle limitazioni di VSSDK. Le semplicità possono eseguire il glossario sui concetti importanti che, se gli extender non capiscono, possono causare più danni. VisualStudio.Extensibility evita i numerosi problemi causati dalle dipendenze main-thread concentrandosi sul modello out-of-process e sulle API asincrone come base. Anche se l'esaurimento del processo semplifica il threading, molti di questi vantaggi traggono vantaggio anche dalle estensioni eseguite in-process. Ad esempio, i comandi di VisualStudio.Extensibility vengono sempre eseguiti su un thread in background. L'interazione con le API VSSDK richiede comunque una conoscenza approfondita del funzionamento del threading, ma almeno non si pagherà il costo di blocchi accidentali, come in questo esempio.

Grafico di confronto

Per riepilogare quanto descritto in dettaglio nella sezione precedente, la tabella seguente illustra un confronto rapido:

VSSDK Community Toolkit VisualStudio.Extensibility
Supporto runtime .NET Framework .NET Framework .NET
Isolamento da Visual Studio
API semplice
Esecuzione asincrona e API
Ampiezza dello scenario di Visual Studio
Installabile senza riavvio
Supporta VS 2019 e versioni successive

Per applicare il confronto alle esigenze di estendibilità di Visual Studio, ecco alcuni scenari di esempio e le raccomandazioni su quale modello usare:

  • Non si ha familiarità con lo sviluppo di estensioni di Visual Studio e si vuole che l'esperienza di onboarding più semplice per creare un'estensione di alta qualità e sia sufficiente supportare Visual Studio 2022 o versione successiva.
    • In questo caso, è consigliabile usare VisualStudio.Extensibility.
  • Vorrei scrivere un'estensione destinata a Visual Studio 2022 e versioni successive. Tuttavia, VisualStudio.Extensibility non supporta tutte le funzionalità necessarie.
  • Ho un'estensione esistente e voglio aggiornarla per supportare le versioni più recenti. Si vuole che l'estensione supporti il maggior numero possibile di versioni di Visual Studio.
    • Poiché VisualStudio.Extensibility supporta solo Visual Studio 2022 e versioni successive, VSSDK o Community Toolkit è l'opzione migliore per questo caso.
  • Si dispone di un'estensione esistente di cui si vuole eseguire la migrazione a VisualStudio.Extensibility per sfruttare .NET e installare senza riavviare.
    • Questo scenario è leggermente più sfumato perché VisualStudio.Extensibility non supporta versioni di livello inferiore di Visual Studio.
      • Se l'estensione esistente supporta solo Visual Studio 2022 e include tutte le API necessarie, è consigliabile riscrivere l'estensione per usare VisualStudio.Extensibility. Tuttavia, se l'estensione necessita di API non ancora disponibili in VisualStudio.Extensibility, procedere e creare un'estensione VisualStudio.Extensibility in esecuzione in modo da poter accedere alle API VSSDK. Nel tempo è possibile eliminare l'utilizzo dell'API VSSDK perché VisualStudio.Extensibility aggiunge il supporto e sposta le estensioni per esaurire il processo.
      • Se l'estensione deve supportare versioni di livello inferiore di Visual Studio che non supportano VisualStudio.Extensibility, è consigliabile eseguire il refactoring nella codebase. Eseguire il pull di tutto il codice comune che può essere condiviso tra le versioni di Visual Studio nella propria libreria e creare progetti VSIX separati destinati a modelli di estendibilità diversi. Ad esempio, se l'estensione deve supportare Visual Studio 2019 e Visual Studio 2022, è possibile adottare la struttura di progetto seguente nella soluzione:
        • MyExtension-VS2019 (si tratta del progetto contenitore VSSDK basato su VSSDK destinato a Visual Studio 2019)
        • MyExtension-VS2022 (si tratta del progetto di contenitore VSSDK+VisualStudio.Extensibility basato su VISUAL Studio 2022)
        • VSSDK-CommonCode (si tratta della libreria comune usata per chiamare le API di Visual Studio tramite VSSDK. Entrambi i progetti VSIX possono fare riferimento a questa libreria per condividere il codice.
        • MyExtension-BusinessLogic (si tratta della libreria comune che contiene tutto il codice pertinente alla logica di business dell'estensione). Entrambi i progetti VSIX possono fare riferimento a questa libreria per condividere il codice.

Passaggi successivi

È consigliabile che gli extender inizino con VisualStudio.Extensibility durante la creazione di nuove estensioni o il miglioramento di quelli esistenti e usino VSSDK o Community Toolkit se si verificano scenari non supportati. Per iniziare, con VisualStudio.Extensibility, esaminare la documentazione presentata in questa sezione. È anche possibile fare riferimento al repository GitHub VSExtensibility per esempi o per segnalare problemi.