Compartir a través de


Elija el modelo de extensibilidad de Visual Studio adecuado para usted.

Puede ampliar Visual Studio usando tres modelos de extensibilidad principales: VSSDK, Community Toolkit y VisualStudio.Extensibility. En este artículo se describen los pros y los contras de cada uno de ellos. Usamos un ejemplo sencillo para resaltar las diferencias arquitectónicas y de código entre los modelos.

VSSDK

VSSDK (o Visual Studio SDK) es el modelo en el que se basan la mayoría de las extensiones de Visual Studio Marketplace. Este modelo es en el que se basa el propio Visual Studio. Es el más completo y potente, pero también el más complejo de aprender y usar correctamente. Las extensiones que utilizan VSSDK se ejecutan en el mismo proceso que el propio Visual Studio. Cargarse en el mismo proceso que Visual Studio significa que una extensión que tenga una violación de acceso, un bucle infinito u otros problemas puede bloquear o colgar Visual Studio y degradar la experiencia del cliente. Y como las extensiones se ejecutan en el mismo proceso que Visual Studio, solo pueden crearse utilizando .NET Framework. Los extensores que deseen utilizar o incorporar bibliotecas que utilicen .NET 5 y versiones posteriores no pueden hacerlo utilizando VSSDK.

Las API de VSSDK se han ido agregando a lo largo de los años a medida que el propio Visual Studio se transformaba y evolucionaba. En una sola extensión, puedes encontrarte lidiando con APIs basadas en COM de impronta heredada, deslizándote a través de la engañosa simplicidad de DTE, y jugueteando con importaciones y exportaciones MEF. Tomemos como ejemplo la escritura de una extensión que lea el texto del sistema de archivos y lo inserte al principio del documento activo actual dentro del editor. El siguiente fragmento muestra el código que se escribiría para manejar cuando se invoca un comando en una extensión basada en 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);
        }
    }
}

Además, también necesitaría proporcionar un archivo .vsct que defina la configuración del comando, como dónde colocarlo en la interfaz de usuario, el texto asociado, 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>

Como se puede ver en el ejemplo, el código puede parecer poco intuitivo y es poco probable que alguien familiarizado con .NET lo aprenda fácilmente. Hay muchos conceptos que aprender y los patrones de la API para acceder al texto del editor activo son anticuados. Para la mayoría de los extensores, las extensiones de VSSDK se construyen copiando y pegando de fuentes en línea, lo que puede dar lugar a difíciles sesiones de depuración, ensayo y error, y frustración. En muchos casos, las extensiones del VSSDK pueden no ser la forma más sencilla de alcanzar los objetivos de la extensión (aunque a veces, son la única opción).

Kit de herramientas de la comunidad

Community Toolkit es el modelo de extensibilidad de código abierto basado en la comunidad para Visual Studio que envuelve el VSSDK para facilitar la experiencia de desarrollo. Al estar basado en el VSSDK, está sujeto a las mismas limitaciones que este (es decir, solo .NET Framework, sin aislamiento del resto de Visual Studio, etc.). Siguiendo con el mismo ejemplo de escribir una extensión que inserte el texto leído del sistema de ficheros, utilizando Community Toolkit, la extensión se escribiría como sigue para un manejador de comandos:

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

El código resultante es mucho mejor que el de VSSDK en términos de simplicidad e intuitividad. No solo hemos reducido significativamente el número de líneas, sino que el código resultante también tiene un aspecto razonable. No hay necesidad de entender cuál es la diferencia entre SVsTextManager y IVsTextManager. Las API se ven y se sienten más amigables con .NET, adoptando patrones comunes de nomenclatura y async, junto con la priorización de operaciones comunes. Sin embargo, Community Toolkit sigue basándose en el modelo VSSDK existente, por lo que se perciben vestigios de la estructura subyacente. Por ejemplo, un archivo .vsct sigue siendo necesario. Aunque Community Toolkit hace un gran trabajo simplificando las API, está atado a las limitaciones de VSSDK y no tiene una forma de simplificar la configuración de las extensiones.

VisualStudio.Extensibility

VisualStudio.Extensibility es el nuevo modelo de extensibilidad en el que las extensiones se ejecutan fuera del proceso principal de Visual Studio. Debido a este cambio arquitectónico fundamental, las extensiones disponen ahora de nuevos patrones y capacidades que no son posibles con VSSDK o Community Toolkit. VisualStudio.Extensibility ofrece un conjunto completamente nuevo de API coherentes y fáciles de usar, permite que las extensiones se dirijan a .NET, aísla los errores que surgen de las extensiones del resto de Visual Studio y permite a los usuarios instalar extensiones sin reiniciar Visual Studio. Sin embargo, debido a que el nuevo modelo se basa en una nueva arquitectura subyacente, aún no tiene la amplitud que tienen VSSDK y el Community Toolkit. Para salvar esa distancia, puede ejecutar sus extensiones VisualStudio.Extensibility en proceso, lo que le permite seguir utilizando las API de VSSDK. Sin embargo, al hacerlo, su extensión solo puede dirigirse a .NET Framework, ya que comparte el mismo proceso que Visual Studio, que se basa en .NET Framework.

Continuando con el mismo ejemplo de escribir una extensión que inserte el texto de un archivo, utilizando VisualStudio.Extensibility, la extensión se escribiría de la siguiente manera para el manejo del comando:

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

Para configurar el comando para la colocación, el texto, etc., ya no es necesario proporcionar un archivo .vsct. En su lugar, se hace a través de código:

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

Este código es más fácil de entender y seguir. En su mayor parte, puede escribir esta extensión únicamente a través del editor con la ayuda de IntelliSense, incluso para la configuración de comandos.

Comparación de los diferentes modelos de extensibilidad de Visual Studio

En el ejemplo, puedes notar que usando VisualStudio.Extensibility, hay más líneas de código que Community Toolkit en el manejador de comandos. Community Toolkit es una envoltura excelente y fácil de usar para crear extensiones con el VSSDK; sin embargo, existen problemas que no son obvios a primera vista y que llevaron al desarrollo de VisualStudio.Extensibility. Para entender la transición y la necesidad, especialmente cuando parece que el Community Toolkit también da como resultado un código fácil de escribir y entender, revisemos el ejemplo y comparemos lo que ocurre en las capas más profundas del código.

Podemos desenvolver rápidamente el código en este ejemplo y ver lo que realmente se está llamando en el lado VSSDK. Vamos a centrarnos únicamente en el fragmento de ejecución del comando, ya que hay numerosos detalles que VSSDK necesita y que Community Toolkit oculta muy bien. Pero una vez que veamos el código subyacente, entenderá por qué la simplicidad aquí es una compensación. La simplicidad oculta algunos de los detalles subyacentes, que pueden dar lugar a comportamientos inesperados, errores e incluso problemas de rendimiento y bloqueos. El siguiente fragmento de código muestra el código del Community Toolkit desenvuelto para mostrar las llamadas al 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);    
            }
        }
    });
}

Hay pocas cuestiones en las que entrar aquí, y todas giran en torno a los hilos y el código asincrónico. Repasaremos cada uno de ellos en detalle.

API asincrónica frente a ejecución de código asincrónico

Lo primero que hay que tener en cuenta es que el método ExecuteAsync de Community Toolkit es una llamada asincrónica "fire-and-forget" envuelta en VSSDK:

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

El propio VSSDK no admite la ejecución asincrónica de comandos desde la perspectiva de la API principal. Es decir, cuando se ejecuta un comando, VSSDK no tiene una forma de ejecutar el código del controlador de comandos en un subproceso en segundo plano, esperar a que finalice y devolver al usuario al contexto de llamada original con los resultados de la ejecución. Por lo tanto, aunque la API ExecuteAsync de Community Toolkit es sintácticamente asincrónica, no es una verdadera ejecución asincrónica. Y debido a que es una forma de ejecución asincrónica de "disparar y olvidar", podrías llamar a ExecuteAsync una y otra vez sin esperar a que la llamada anterior se complete primero. Aunque Community Toolkit proporciona una mejor experiencia en términos de ayudar a los extensores a descubrir cómo implementar escenarios comunes, en última instancia no puede resolver los problemas fundamentales con VSSDK. En este caso, la API subyacente de VSSDK no es asincrónica, y los métodos de ayuda fire-and-forget proporcionados por Community Toolkit no pueden abordar adecuadamente el rendimiento asincrónico y el trabajo con el estado del cliente; puede ocultar algunos problemas potenciales difíciles de depurar.

Plano de interfaz de usuario frente a segundo plano

La otra desventaja de esta llamada asincrónica envuelta de Community Toolkit es que el propio código se sigue ejecutando desde el subproceso de interfaz de usuario, y es responsabilidad del desarrollador de la extensión averiguar cómo cambiar correctamente a un subproceso en segundo plano si no quiere arriesgarse a congelar la interfaz de usuario. Por mucho que Community Toolkit pueda ocultar el ruido y el código adicional de VSSDK, sigue siendo necesario comprender las complejidades del subproceso en Visual Studio. Y una de las primeras lecciones que se aprenden en VS threading es que no todo se puede ejecutar desde un segundo plano. En otras palabras, no todo es thread safe, particularmente las llamadas que van a componentes COM. Así, en el ejemplo anterior, ves que hay una llamada para cambiar al hilo principal (UI):

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

Por supuesto, puede volver a un subproceso en segundo plano después de esta llamada. Sin embargo, como extensor que utiliza Community Toolkit, tendrá que prestar mucha atención al subproceso en el que se encuentra su código y determinar si existe el riesgo de congelar la interfaz de usuario. El subproceso en Visual Studio es difícil de obtener y requiere el uso adecuado de JoinableTaskFactory para evitar interbloqueos. La lucha por escribir código que se ocupe correctamente de los subprocesos ha sido una fuente constante de errores, incluso para nuestros ingenieros internos de Visual Studio. VisualStudio.Extensibility, por otro lado, evita este problema por completo mediante la ejecución de extensiones fuera de proceso y confiando en API asincrónicas de extremo a extremo.

API sencilla frente a conceptos sencillos

Dado que Community Toolkit oculta muchas de las complejidades de VSSDK, podría dar a los extensores una falsa sensación de simplicidad. Sigamos con el mismo código de ejemplo. Si un extensor no conociera los requisitos de subprocesamiento del desarrollo de Visual Studio, podría asumir que su código se ejecuta desde un subproceso en segundo plano todo el tiempo. No tendrán ningún problema con el hecho de que la llamada a leer un archivo de texto sea síncrona. Si está en un subproceso en segundo plano, no congelará la interfaz de usuario si el archivo en cuestión es grande. Sin embargo, cuando el código es desenvuelto a VSSDK, se darán cuenta de que ese no es el caso. Así pues, aunque la API de Community Toolkit parece sin duda más sencilla de entender y más coherente de escribir, al estar vinculada a VSSDK, está sujeta a las limitaciones de VSSDK. Las simplicidades pueden pasar por alto conceptos importantes que, si los extensores no entienden, pueden causar más daño. VisualStudio.Extensibility evita los muchos problemas causados por las dependencias del hilo principal centrándose en el modelo fuera de proceso y en las API asincrónicas como base. Si bien la ejecución fuera del proceso es lo que más simplificaría la ejecución de subprocesos, muchos de estos beneficios se trasladan también a las extensiones que se ejecutan dentro del proceso. Por ejemplo, los comandos de VisualStudio.Extensibility siempre se ejecutan en un subproceso en segundo plano. Interactuar con las APIs de VSSDK todavía requiere un conocimiento profundo de cómo funciona el threading, pero al menos no pagará el coste de cuelgues accidentales, como en este ejemplo.

Gráfico de comparación

Para resumir lo que hemos tratado en detalle en la sección anterior, la siguiente tabla muestra una rápida comparación:

VSSDK Kit de herramientas de la comunidad VisualStudio.Extensibility
Soporte en tiempo de ejecución .NET Framework .NET Framework .NET
Aislamiento de Visual Studio
API simple.
Ejecución asincrónica y API
Amplitud del escenario VS
Instalable sin reiniciar
Compatible con VS 2019 y versiones inferiores

Para ayudarle a aplicar la comparación a sus necesidades de extensibilidad de Visual Studio, aquí hay algunos escenarios de ejemplo y nuestras recomendaciones sobre qué modelo utilizar:

  • Soy nuevo en el desarrollo de extensiones de Visual Studio y quiero la experiencia de incorporación más sencilla para crear una extensión de alta calidad, y solo necesito compatibilidad con Visual Studio 2022 o superior.
    • En este caso, le recomendamos que utilice VisualStudio.Extensibility.
  • Me gustaría escribir una extensión dirigida a Visual Studio 2022 y superiores. Sin embargo, VisualStudio.Extensibility no admite todas las funciones que necesito.
    • En este caso, le recomendamos que adopte un método híbrido para combinar VisualStudio.Extensibility y VSSDK. Puede crear una extensión de VisualStudio.Extensibility que se ejecute en proceso y que le permita acceder a las API de VSSDK o del kit de herramientas de la comunidad.
  • Tengo una extensión y quiero actualizarla para que admita versiones más recientes. Quiero que mi extensión sea compatible con tantas versiones de Visual Studio como sea posible.
    • Dado que VisualStudio.Extensibility solo es compatible con Visual Studio 2022 y versiones superiores, VSSDK o Community Toolkit es la mejor opción para este caso.
  • Tengo una extensión existente que me gustaría migrar a VisualStudio.Extensibility para aprovechar las ventajas de .NET e instalarla sin reiniciar.
    • Este escenario es un poco más matizado, ya que VisualStudio.Extensibility no es compatible con versiones inferiores de Visual Studio.
      • Si su extensión existente solo es compatible con Visual Studio 2022 y tiene todas las API que necesita, le recomendamos que reescriba su extensión para utilizar VisualStudio.Extensibility. Pero si su extensión necesita API que VisualStudio.Extensibility aún no tiene, entonces siga adelante y cree una extensión VisualStudio.Extensibility que se ejecute en proceso para que pueda acceder a las API de VSSDK. Con el tiempo, podrá eliminar el uso de las API de VSSDK a medida que VisualStudio.Extensibility añada compatibilidad y mueva sus extensiones para que se ejecuten fuera de proceso.
      • Si su extensión necesita ser compatible con versiones inferiores de Visual Studio que no son compatibles con VisualStudio.Extensibility, le recomendamos que refactorice su código base. Extrae todo el código común que pueda compartirse entre versiones de Visual Studio a su propia biblioteca, y crea proyectos VSIX separados que se dirijan a diferentes modelos de extensibilidad. Por ejemplo, si su extensión necesita ser compatible con Visual Studio 2019 y Visual Studio 2022, puede adoptar la siguiente estructura de proyecto en su solución:
        • MyExtension-VS2019 (este es su proyecto contenedor VSIX basado en VSSDK que apunta a Visual Studio 2019)
        • MyExtension-VS2022 (este es su proyecto contenedor VSIX basado en VSSDK+VisualStudio.Extensibility orientado a Visual Studio 2022)
        • VSSDK-CommonCode (esta es la biblioteca común que se utiliza para llamar a las API de Visual Studio a través de VSSDK. Ambos proyectos VSIX pueden hacer referencia a esta biblioteca para compartir código).
        • MyExtension-BusinessLogic (esta es la biblioteca común que contiene todo el código pertinente a la lógica de negocio de su extensión. Ambos proyectos VSIX pueden hacer referencia a esta biblioteca para compartir código).

Pasos siguientes

Nuestra recomendación es que los extensores comiencen con VisualStudio.Extensibility cuando creen nuevas extensiones o mejoren las existentes, y utilicen VSSDK o Community Toolkit si se encuentran con escenarios no compatibles. Para empezar con VisualStudio.Extensibility, consulte la documentación que se presenta en esta sección. También puede consultar el repositorio de GitHub de VSExtensibility para ver ejemplos o plantear problemas.