Partager via


Choisissez le bon modèle d’extensibilité de Visual Studio pour vous

Vous pouvez étendre Visual Studio à l’aide de trois principaux modèles d’extensibilité, VSSDK, Community Toolkit et VisualStudio.Extensibility. Cet article couvre les avantages et inconvénients de chacun. Nous utilisons un exemple simple pour mettre en évidence les différences architecturales et de code entre les modèles.

VSSDK

VSSDK (ou Visual Studio SDK) est le modèle sur lequel la plupart des extensions du Visual Studio Marketplace sont basées. Ce modèle est celui sur lequel Visual Studio lui-même est construit. C’est le plus complet et le plus puissant, mais aussi le plus complexe à apprendre et à utiliser correctement. Les extensions qui utilisent VSSDK s’exécutent dans le même processus que Visual Studio lui-même. S’exécuter dans le même processus que Visual Studio signifie qu’une extension ayant une violation d’accès, une boucle infinie ou d’autres problèmes peut planter ou bloquer Visual Studio et dégrader l’expérience utilisateur. Et parce que les extensions s’exécutent dans le même processus que Visual Studio, elles ne peuvent être construites qu’en utilisant .NET Framework. Les développeurs qui souhaitent utiliser ou incorporer des bibliothèques utilisant .NET 5 et versions ultérieures ne peuvent pas le faire avec VSSDK.

Les API de VSSDK ont été agrégées au fil des ans à mesure que Visual Studio lui-même se transformait et évoluait. Dans une seule extension, vous pouvez vous retrouver à jongler avec des API basées sur COM héritées, à naviguer dans la simplicité trompeuse de DTE et à jouer avec les importations et exportations de MEF. Prenons l’exemple de l’écriture d’une extension qui lit le texte à partir du système de fichiers et l’insère au début du document actif dans l’éditeur. Le fragment de code suivant montre le code que vous écririez pour gérer l’invocation d’une commande dans une extension basée sur 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);
        }
    }
}

De plus, vous devrez également fournir un fichier .vsct, qui définit la configuration de la commande, comme son emplacement dans l’interface utilisateur, le texte associé, etc.

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

Comme vous pouvez le voir dans l’exemple, le code peut sembler contre-intuitif et peu accessible pour quelqu’un familiarisé avec .NET. Il y a de nombreux concepts à apprendre et les modèles d’API pour accéder au texte de l’éditeur actif sont archaïques. Pour la plupart des développeurs d’extensions, les extensions VSSDK sont construites à partir de copier-coller à partir de sources en ligne, ce qui peut entraîner des sessions de débogage difficiles, des essais et des erreurs, et de la frustration. Dans de nombreux cas, les extensions VSSDK ne sont peut-être pas le moyen le plus simple d’atteindre les objectifs d’extension (bien que parfois, elles soient le seul choix).

Kit de ressources de la communauté

Community Toolkit est le modèle d’extensibilité open source basé sur la communauté pour Visual Studio qui encapsule VSSDK pour offrir une expérience de développement plus simple. Parce qu’il est basé sur VSSDK, il est soumis aux mêmes limitations que VSSDK (c’est-à-dire, .NET Framework uniquement, pas d’isolation du reste de Visual Studio, etc.). En poursuivant avec le même exemple d’écriture d’une extension qui insère le texte lu à partir du système de fichiers, en utilisant Community Toolkit, l’extension serait écrite comme suit pour un gestionnaire de commandes :

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

Le code résultant est considérablement amélioré par rapport à VSSDK en termes de simplicité et d’intuitivité ! Non seulement nous avons réduit le nombre de lignes de manière significative, mais le code résultant semble également raisonnable. Il n’est pas nécessaire de comprendre la différence entre SVsTextManager et IVsTextManager. Les API semblent et se comportent davantage comme celles de .NET, en adoptant des schémas de dénomination et des modèles async courants, ainsi qu’en priorisant les opérations courantes. Cependant, Community Toolkit est toujours construit sur le modèle VSSDK existant et donc, des vestiges de la structure sous-jacente transparaissent. Par exemple, un fichier .vsct est toujours nécessaire. Bien que Community Toolkit simplifie grandement les API, il est lié aux limitations de VSSDK et ne propose pas de moyen de simplifier la configuration des extensions.

VisualStudio.Extensibility

VisualStudio.Extensibility est le nouveau modèle d’extensibilité où les extensions s’exécutent en dehors du processus principal de Visual Studio. Grâce à ce changement architectural fondamental, de nouveaux modèles et capacités sont désormais disponibles pour les extensions, ce qui n’était pas possible avec VSSDK ou Community Toolkit. VisualStudio.Extensibility offre un ensemble totalement nouveau d’API, cohérentes et faciles à utiliser, permet aux extensions de cibler .NET, isole les bugs qui surviennent dans les extensions du reste de Visual Studio, et permet aux utilisateurs d’installer des extensions sans redémarrer Visual Studio. Cependant, comme le nouveau modèle est construit sur une nouvelle architecture sous-jacente, il n’a pas encore la même étendue que VSSDK et le Community Toolkit. Pour combler cette lacune, vous pouvez exécuter vos extensions VisualStudio.Extensibility in process, ce qui vous permet de continuer à utiliser les API de VSSDK. Cependant, cela signifie que votre extension ne peut cibler que .NET Framework puisqu’elle partage le même processus que Visual Studio, qui est basé sur .NET Framework.

En poursuivant avec le même exemple d’écriture d’une extension qui insère le texte d’un fichier, en utilisant VisualStudio.Extensibility, l’extension serait écrite comme suit pour la gestion de commandes :

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

Pour configurer la commande pour son placement, son texte, etc., vous n’avez plus besoin de fournir un fichier .vsct. C’est désormais fait par du code :

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

Ce code est plus facile à comprendre et à suivre. Pour la plupart, vous pouvez écrire cette extension purement via l’éditeur avec l’aide d’IntelliSense, même pour la configuration des commandes.

Comparaison des différents modèles d’extensibilité de Visual Studio

D’après l’exemple, vous remarquerez peut-être qu’en utilisant VisualStudio.Extensibility, il y a plus de lignes de code que dans Community Toolkit dans le gestionnaire de commandes. Community Toolkit est un excellent wrapper qui simplifie la création d’extensions avec VSSDK ; cependant, il y a des pièges qui ne sont pas immédiatement évidents, ce qui a conduit au développement de VisualStudio.Extensibility. Pour comprendre la transition et le besoin, surtout lorsqu’il semble que Community Toolkit permet également de créer un code facile à écrire et à comprendre, examinons l’exemple et comparons ce qui se passe dans les couches plus profondes du code.

Nous allons rapidement déballer le code de cet exemple et voir ce qui est réellement appelé du côté de VSSDK. Nous allons nous concentrer uniquement sur l’extrait d’exécution de la commande, car il y a de nombreux détails que VSSDK nécessite, que Community Toolkit masque de manière agréable. Mais une fois que nous regardons le code sous-jacent, vous comprendrez pourquoi la simplicité ici est un compromis. La simplicité masque certains des détails sous-jacents, ce qui peut entraîner des comportements inattendus, des bugs, voire des problèmes de performance et des plantages. L’extrait de code suivant montre le code de Community Toolkit déballé pour montrer les appels 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);    
            }
        }
    });
}

Il y a quelques problèmes à aborder ici, et ils tournent tous autour du threading et du code async. Nous allons passer en revue chacun d’eux en détail.

API async contre exécution de code async

La première chose à noter est que la méthode ExecuteAsync dans Community Toolkit est un appel async enveloppé en fire-and-forget dans VSSDK :

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

VSSDK lui-même ne prend pas en charge l’exécution asynchrone des commandes d’un point de vue API central. C’est-à-dire que lorsqu’une commande est exécutée, VSSDK n’a pas de moyen d’exécuter le code du gestionnaire de commandes sur un thread en arrière-plan, d’attendre qu’il se termine, et de retourner l’utilisateur au contexte d’appel d’origine avec les résultats d’exécution. Donc, même si l’API ExecuteAsync dans Community Toolkit est syntaxiquement async, il ne s’agit pas d’une véritable exécution async. Et parce qu’il s’agit d’une exécution async en fire-and-forget, vous pourriez appeler ExecuteAsync à plusieurs reprises sans jamais attendre que l’appel précédent soit terminé. Bien que Community Toolkit offre une meilleure expérience en aidant les développeurs d’extensions à découvrir comment implémenter des scénarios courants, il ne peut en fin de compte pas résoudre les problèmes fondamentaux de VSSDK. Dans ce cas, l’API sous-jacente de VSSDK n’est pas asynchrone, et les méthodes helper fire-and-forget fournies par Community Toolkit ne peuvent pas correctement gérer le yield async et travailler avec l’état du client ; elles peuvent masquer certains problèmes potentiels difficiles à déboguer.

Thread UI contre thread en arrière-plan

L’autre conséquence de cet appel async enveloppé de Community Toolkit est que le code lui-même est toujours exécuté à partir du thread UI, et c’est au développeur d’extension de trouver comment correctement passer à un thread en arrière-plan s’il ne veut pas risquer de bloquer l’UI. Autant Community Toolkit peut masquer le bruit et le code supplémentaire de VSSDK, il exige toujours que vous compreniez les complexités du threading dans Visual Studio. Et l’une des premières leçons que vous apprenez dans le threading de VS est que tout ne peut pas être exécuté à partir d’un thread en arrière-plan. En d’autres termes, tout n’est pas thread-safe, en particulier les appels qui vont dans les composants COM. Donc, dans l’exemple ci-dessus, vous voyez qu’il y a un appel pour basculer vers le thread principal (UI) :

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

Bien sûr, vous pouvez revenir à un thread en arrière-plan après cet appel. Cependant, en tant que développeur d’extension utilisant Community Toolkit, vous devrez faire très attention au thread sur lequel votre code s’exécute et déterminer s’il y a un risque de geler l’UI. Le threading dans Visual Studio est difficile à bien faire et nécessite l’utilisation correcte de JoinableTaskFactory pour éviter les deadlocks. La lutte pour écrire du code qui gère correctement le threading a été une source constante de bugs, même pour nos ingénieurs internes de Visual Studio. VisualStudio.Extensibility, en revanche, évite complètement ce problème en exécutant les extensions hors du processus et en s’appuyant sur des API async de bout en bout.

API simple contre concepts simples

Parce que Community Toolkit masque de nombreuses complexités de VSSDK, cela pourrait donner aux développeurs d’extensions un faux sentiment de simplicité. Continuons avec le même exemple de code. Si un développeur d’extension ne connaissait pas les exigences de threading du développement Visual Studio, il pourrait supposer que son code est exécuté à partir d’un thread en arrière-plan tout le temps. Ils ne verraient aucun problème avec le fait que l’appel pour lire un fichier texte est synchrone. S’il est sur un thread en arrière-plan, il ne gèlera pas l’UI si le fichier en question est volumineux. Cependant, lorsque le code est déballé en VSSDK, ils se rendront compte que ce n’est pas le cas. Ainsi, bien que l’API de Community Toolkit semble certainement plus simple à comprendre et plus cohérente à écrire, parce qu’elle est liée à VSSDK, elle est soumise aux limitations de VSSDK. Les simplifications peuvent masquer des concepts importants qui, si les développeurs d’extensions ne les comprennent pas, peuvent causer plus de tort que de bien. VisualStudio.Extensibility évite les nombreux problèmes causés par les dépendances au thread principal en se concentrant sur le modèle hors processus et les API async comme base. Bien que l’exécution hors processus simplifierait le threading au maximum, bon nombre de ces avantages se répercutent également sur les extensions exécutées en processus. Par exemple, les commandes VisualStudio.Extensibility sont toujours exécutées sur un thread en arrière-plan. Interagir avec les API de VSSDK nécessite toujours une connaissance approfondie du fonctionnement du threading, mais au moins vous n’aurez pas à subir les coûts des gels accidentels, comme dans cet exemple.

Graphique comparatif

Pour résumer ce que nous avons couvert en détail dans la section précédente, le tableau suivant montre une comparaison rapide :

VSSDK Kit de ressources de la communauté VisualStudio.Extensibility
Prise en charge d’exécution .NET Framework .NET Framework .NET
Isolation de Visual Studio
API simple
Exécution et API async
Étendue des scénarios VS
Installable sans redémarrage
Prend en charge VS 2019 et versions antérieures

Pour vous aider à appliquer la comparaison à vos besoins d’extensibilité de Visual Studio, voici quelques scénarios types et nos recommandations sur le modèle à utiliser :

  • Je suis nouveau dans le développement d’extensions Visual Studio et je veux la meilleure expérience d’intégration pour créer une extension de haute qualité, et je n’ai besoin de supporter que Visual Studio 2022 ou une version ultérieure.
    • Dans ce cas, nous vous recommandons d’utiliser VisualStudio.Extensibility.
  • Je souhaite écrire une extension qui cible Visual Studio 2022 et les versions ultérieures. Cependant, VisualStudio.Extensibility ne prend pas en charge toutes les fonctionnalités dont j’ai besoin.
    • Nous vous recommandons dans ce cas d’adopter une méthode hybride en combinant VisualStudio.Extensibility et VSSDK. Vous pouvez créer une extension VisualStudio.Extensibility qui s’exécute dans le processus, ce qui vous permet d’accéder aux API de VSSDK ou de Community Toolkit.
  • J’ai une extension existante et je souhaite la mettre à jour pour prendre en charge les versions plus récentes. Je veux que mon extension prenne en charge autant de versions de Visual Studio que possible.
    • Comme VisualStudio.Extensibility ne prend en charge que Visual Studio 2022 et versions ultérieures, VSSDK ou Community Toolkit est la meilleure option pour ce cas.
  • J’ai une extension existante que je souhaite migrer vers VisualStudio.Extensibility pour tirer parti de .NET et de l’installation sans redémarrage.
    • Ce scénario est un peu plus nuancé puisque VisualStudio.Extensibility ne prend pas en charge les versions antérieures de Visual Studio.
      • Si votre extension existante ne prend en charge que Visual Studio 2022 et dispose de toutes les API dont vous avez besoin, nous vous recommandons de réécrire votre extension pour utiliser VisualStudio.Extensibility. Mais si votre extension nécessite des API que VisualStudio.Extensibility n’a pas encore, alors allez-y et créez une extension VisualStudio.Extensibility qui s’exécute dans le processus afin que vous puissiez accéder aux API de VSSDK. Avec le temps, vous pouvez éliminer l’utilisation des API de VSSDK à mesure que VisualStudio.Extensibility ajoute du support et déplacer vos extensions pour qu’elles s’exécutent hors processus.
      • Si votre extension doit prendre en charge des versions antérieures de Visual Studio qui ne prennent pas en charge VisualStudio.Extensibility, nous vous recommandons de faire un peu de refactorisation dans votre base de code. Extrayez tout le code commun pouvant être partagé entre les versions de Visual Studio dans sa propre bibliothèque, et créez des projets VSIX séparés qui ciblent différents modèles d’extensibilité. Par exemple, si votre extension doit prendre en charge Visual Studio 2019 et Visual Studio 2022, vous pouvez adopter la structure de projet suivante dans votre solution :
        • MyExtension-VS2019 (il s’agit de votre projet conteneur VSIX basé sur VSSDK qui cible Visual Studio 2019)
        • MyExtension-VS2022 (il s’agit de votre projet conteneur VSIX basé sur VSSDK + VisualStudio.Extensibility qui cible Visual Studio 2022)
        • VSSDK-CommonCode (il s’agit de la bibliothèque commune utilisée pour appeler les API de Visual Studio via VSSDK. Vos deux projets VSIX peuvent référencer cette bibliothèque pour partager du code.)
        • MyExtension-BusinessLogic (il s’agit de la bibliothèque commune qui contient tout le code pertinent pour la logique métier de votre extension. Vos deux projets VSIX peuvent référencer cette bibliothèque pour partager du code.)

Étapes suivantes

Notre recommandation est que les développeurs d’extensions commencent avec VisualStudio.Extensibility lors de la création de nouvelles extensions ou de l’amélioration de celles existantes, et utilisent VSSDK ou le Community Toolkit s’ils rencontrent des scénarios non pris en charge. Pour commencer avec VisualStudio.Extensibility, parcourez la documentation présentée dans cette section. Vous pouvez également consulter le référentiel VSExtensibility GitHub pour des exemples ou pour signaler des problèmes.