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 nel di 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 implica che un'estensione con una violazione di accesso, un ciclo infinito o altri problemi può bloccare o interrompere Visual Studio e compromettere l'esperienza utente. Poiché le estensioni vengono eseguite nello stesso processo di Visual Studio, possono essere compilate solo usando .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, è possibile che ci si trovi con API basate su COMdall'impronta legacy, con la semplicità ingannevole di DTE, e smanettare con importazioni ed esportazioni di 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 file .vsct
, 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, sebbene familiare con .NET, lo comprenda facilmente. Esistono molti concetti da apprendere e i modelli API per accedere al testo dell'editor attivo sono antiquati. Per la maggior parte degli sviluppatori di extender, le estensioni VSSDK vengono costruite copiando e incollando da fonti online, il che può portare a sessioni di debug difficili, tentativi 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).
Strumenti per la Comunità
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 sono più amichevoli per .NET, adottando modelli di denominazione e asincroni comuni, insieme alla priorità data alle operazioni comuni. Tuttavia, Community Toolkit è ancora basato sul modello VSSDK esistente e quindi le vestigia della struttura sottostante sono visibili. Ad esempio, è ancora necessario un file .vsct
. 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 di VisualStudio.Extensibility nel processo, 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 file .vsct
. 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 scompattare rapidamente il codice in questo esempio e vedere cosa viene effettivamente chiamato dal 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 metodo ExecuteAsync
in Community Toolkit è una chiamata asincrona di tipo "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 "eseguire e dimenticare" per l'esecuzione asincrona, è possibile chiamare ExecuteAsync più volte di seguito senza attendere che la chiamata precedente venga completata 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 di supporto per l'esecuzione senza attesa di risultato forniti da Community Toolkit non possono risolvere correttamente la gestione dell'asincronia e la gestione dello stato del client; potrebbero nascondere alcuni potenziali problemi difficili da individuare e risolvere.
Thread dell'interfaccia utente contro thread in background
L'altra conseguenza di questa chiamata asincrona sottoposta a wrapping da Community Toolkit è che il codice stesso viene ancora eseguito dal thread dell'interfaccia utente e spetta allo sviluppatore dell'estensione 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 verso i 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 corretto 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 prenderanno alcun problema con il fatto che la chiamata per leggere un file da un 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 semplificazioni possono trascurare concetti importanti che, se non compresi dagli estensori, 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. Sebbene l'uscita dal processo semplifichi maggiormente il threading, molti di questi benefici si estendono anche alle estensioni eseguite all'interno del processo. 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 | |
---|---|---|---|
di supporto del runtime di | .NET Framework | .NET Framework | .NET |
isolamento da Visual Studio | ❌ | ❌ | ✅ |
API semplice | ❌ | ✅ | ✅ |
esecuzione asincrona e API | ❌ | ❌ | ✅ |
Ampiezza dello scenario di Visual Studio | ✅ | ✅ | ⏳ |
installabile senza riavviare | ❌ | ❌ | ✅ |
supportaVS 2019 edi seguito | ✅ | ✅ | ❌ |
Per applicare il confronto alle esigenze di estendibilità di Visual Studio, ecco alcuni scenari di esempio e le raccomandazioni su quale modello usare:
-
non ho familiarità con lo sviluppo di estensioni di Visual Studio e voglio l'esperienza di onboarding più semplice per creare un'estensione di alta qualità e ho solobisogno disupportare 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à dinecessarie.
- In questo caso è consigliabile adottare un metodo ibrido per combinare VisualStudio.Extensibility e VSSDK. È possibile creare un'estensione VisualStudio.Extensibility che viene eseguita nel processo, che consente di accedere alle API VSSDK o Community Toolkit.
-
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 eseguire la migrazione aVisualStudio.Extensibility per sfruttare .NET e installarla 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 richiede API non ancora disponibili in VisualStudio.Extensibility, procedere e creare un'estensione VisualStudio.Extensibility che viene eseguita nel processo in modo da poter accedere alle API VSSDK. Col tempo è possibile eliminare l'utilizzo dell'API VSSDK man mano che VisualStudio.Extensibility aggiunge il supporto e spostare le estensioni per eseguirle fuori dal processo.
- Se l'estensione deve supportare versioni di livello inferiore di Visual Studio che non supportano VisualStudio.Extensibility, è consigliabile eseguire il refactoring nella codebase. Raccogli tutto il codice comune che può essere condiviso tra le versioni di Visual Studio in una libreria separata e crea progetti VSIX separati per diversi modelli di estendibilità. 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 basato su VSSDK destinato a Visual Studio 2019)
- MyExtension-VS2022 (si tratta del progetto contenitore VSSDK+VisualStudio.Extensibility basato su VSIX destinato a 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.
- Questo scenario è leggermente più sfumato perché VisualStudio.Extensibility non supporta versioni di livello inferiore di Visual Studio.
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 di VSExtensibility per gli esempi o per segnalare problemi .