Elija el modelo de extensibilidad de Visual Studio adecuado.
Puede ampliar Visual Studio con tres modelos de extensibilidad principales, VSSDK, Community Toolkit y VisualStudio.Extensibility. En este artículo se tratan las ventajas y desventajas de cada uno. Usamos un ejemplo sencillo para resaltar las diferencias de arquitectura y código entre los modelos.
VSSDK
VSSDK (o SDK de Visual Studio) es el modelo en el que se basan la mayoría de las extensiones de la de Visual Studio Marketplace. Este modelo es lo que se basa en Visual Studio. Es el más completo y el más potente, pero también el más complejo para aprender y usar correctamente. Las extensiones que usan VSSDK se ejecutan en el mismo proceso que Visual Studio. Cargar en el mismo proceso que Visual Studio significa que una extensión que tiene una violación de acceso, un bucle infinito u otros problemas puede bloquearse o colgar Visual Studio y empeorar la experiencia del cliente. Además, dado que las extensiones se ejecutan en el mismo proceso que Visual Studio, solo se pueden compilar con .NET Framework. Los extensores que desean usar o incorporar bibliotecas que usan .NET 5 y versiones posteriores no pueden hacerlo mediante VSSDK.
Las API de VSSDK se han agregado a lo largo de los años a medida que Visual Studio se transforma y evoluciona. En una sola extensión, puede encontrarse lidiando con las APIs basadas en COM de un legado de impronta, navegando con facilidad a través de la engañosa simplicidad de DTE, y experimentando con las importaciones y exportaciones de MEF. Veamos un ejemplo de escritura de una extensión que lee el texto del sistema de archivos e lo inserta al principio del documento activo actual dentro del editor. El fragmento de código siguiente muestra el código que escribiría para controlar 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 tendría que proporcionar un archivo .vsct
, que define 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 puede ver en el ejemplo, el código puede parecer poco intuitivo y es poco probable que alguien que esté familiarizado con .NET pueda entenderlo fácilmente. Hay muchos conceptos para aprender y los patrones de API para acceder al texto del editor activo están anticuados. Para la mayoría de los extensores, las extensiones de VSSDK se crean copiando y pegando desde fuentes en línea, lo que puede provocar sesiones de depuración complicadas, ensayos y errores, y frustración. En muchos casos, es posible que las extensiones de VSSDK no sean la manera más fácil de lograr los objetivos de extensión (aunque a veces son la única opción).
Kit de herramientas de la comunidad
Community Toolkit es el modelo de extensibilidad basado en la comunidad y de código abierto para Visual Studio que ajusta VSSDK para una experiencia de desarrollo más sencilla. Dado que se basa en VSSDK, está sujeto a las mismas limitaciones que VSSDK (es decir, solo .NET Framework, sin aislamiento del resto de Visual Studio, etc.). Siguiendo con el mismo ejemplo de escritura de una extensión que inserta el texto leído desde el sistema de archivos, mediante Community Toolkit, la extensión se escribiría de la siguiente manera para un controlador 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 ha mejorado mucho más que VSSDK en términos de simplicidad e intuitividad. No solo hemos disminuido significativamente el número de líneas, sino que el código resultante también parece razonable. No es necesario comprender cuál es la diferencia entre SVsTextManager
y IVsTextManager
. Las APIs se ven y se sienten más afines a .NET, adoptando patrones asincrónicos y de nomenclatura comunes, junto con la priorización de operaciones comunes. Sin embargo, Community Toolkit sigue basándose en el modelo VSSDK existente y, por tanto, los vestigios de la estructura subyacente son aparentes. Por ejemplo, un archivo .vsct
sigue siendo necesario. Aunque Community Toolkit realiza un gran trabajo para simplificar las API, está enlazado a las limitaciones de VSSDK y no tiene una manera de simplificar la configuración de la extensión.
VisualStudio.Extensibility
VisualStudio.Extensibility es el nuevo modelo de extensibilidad donde las extensiones se ejecutan fuera del proceso principal de Visual Studio. Debido a este cambio arquitectónico fundamental, los nuevos patrones y funcionalidades ahora están disponibles para las extensiones 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 tengan como destino .NET, aísle 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, dado que el nuevo modelo se basa en una nueva arquitectura subyacente, aún no tiene la amplitud que VSSDK y Community Toolkit tienen. Para salvar esa brecha, puede ejecutar las extensiones de extensibilidad de VisualStudio.Extensibility en proceso, lo que le permite seguir usando las API de VSSDK. Sin embargo, si lo hace, significa que la extensión solo puede tener como destino .NET Framework, ya que comparte el mismo proceso que Visual Studio, que se basa en .NET Framework.
Siguiendo con el mismo ejemplo de escritura de una extensión que inserta el texto de un archivo, con VisualStudio.Extensibility, la extensión se escribiría de la siguiente manera para el control de comandos:
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 colocar, texto, etc., ya no es necesario proporcionar un archivo .vsct
. En su lugar, se realiza a través del 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 exclusivamente 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, puede observar que con VisualStudio.Extensibility hay más líneas de código que Community Toolkit en el controlador de comandos. Community Toolkit es un excelente contenedor fácil de usar además de la creación de extensiones con el VSSDK; sin embargo, hay problemas que no son inmediatamente obvios, lo que llevó al desarrollo de VisualStudio.Extensibility. Para comprender la transición y las necesidades, especialmente cuando parece que Community Toolkit también da como resultado código fácil de escribir y comprender, vamos a revisar el ejemplo y comparar lo que sucede en las capas más profundas del código.
Podemos desempaquetar rápidamente el código de este ejemplo y ver qué se está invocando realmente en el VSSDK. Nos centraremos únicamente en el fragmento de código de ejecución de comandos, ya que hay numerosos detalles que VSSDK necesita y que Community Toolkit oculta eficazmente. Pero una vez que veamos el código subyacente, comprenderá por qué la simplicidad aquí es un compromiso. La simplicidad oculta algunos de los detalles subyacentes, lo que puede provocar un comportamiento inesperado, errores e incluso problemas de rendimiento y bloqueos. En el fragmento de código siguiente se muestra el código del Kit de herramientas de la comunidad desencapsulado para mostrar las llamadas de 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 algunos problemas que debemos abordar aquí, y todos giran en torno a subprocesos y código asincrónico. Veremos cada uno con 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
en Community Toolkit es una llamada asincrónica envuelta y de "disparar y olvidar" en VSSDK:
package.JoinableTaskFactory.RunAsync(async delegate
{
…
});
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 manera 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 resultados de ejecución. Por lo tanto, aunque la API ExecuteAsync en Community Toolkit sea sintácticamente asincrónica, no es verdadera ejecución asincrónica. Y dado que es una forma de desencadenar y olvidarse de la ejecución asincrónica, podría 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 cuanto a 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 auxiliares de tipo "dispara y olvida" proporcionados por Community Toolkit no pueden abordar adecuadamente el rendimiento asincrónico ni trabajar con el estado del cliente, lo que puede ocultar algunos posibles problemas difíciles de depurar.
Subproceso de interfaz de usuario frente a subproceso en segundo plano
Otro problema con esta llamada asincrónica empaquetada del Community Toolkit es que el propio código se sigue ejecutando desde el subproceso de la interfaz de usuario, y corresponde al 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. Aunque Community Toolkit puede ocultar el ruido visual y el código adicional de VSSDK, todavía requiere que comprenda las complejidades del manejo de subprocesos en Visual Studio. Y una de las primeras lecciones que aprende al trabajar con subprocesos en Visual Studio es que no todo se puede ejecutar desde segundo plano. En otras palabras, no todo puede ser un subproceso, especialmente las llamadas a los componentes COM. Así, en el ejemplo anterior, ve que hay una llamada para cambiar al subproceso principal (UI):
await package.JoinableTaskFactory.SwitchToMainThreadAsync();
ErrorHandler.ThrowOnFailure(nativeView.GetWindowFrame(out object frameValue));
Por supuesto, puede volver a un subproceso de ejecución en segundo plano tras esta llamada. Sin embargo, como extensor mediante Community Toolkit, deberá prestar mucha atención a en qué subproceso se está ejecutando su código y determinar si existe el riesgo de congelar la interfaz de usuario. La programación con subprocesos en Visual Studio es difícil de lograr correctamente y requiere el uso adecuado de JoinableTaskFactory
para evitar bloqueos. La lucha por escribir código que se ocupa de subprocesos correctamente 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 al ejecutar extensiones fuera del proceso y confiar en las API asincrónicas de un extremo a otro.
Api simple frente a conceptos simples
Dado que Community Toolkit oculta muchas de las complejidades de VSSDK, podría dar a los extensores una falsa sensación de simplicidad. Vamos a continuar con el mismo código de ejemplo. Si un extensor no conocía los requisitos de subprocesos del desarrollo de Visual Studio, podría suponer que su código se ejecuta desde un subproceso en segundo plano todo el tiempo. No tendrán problemas con el hecho de que la llamada a leer un archivo de texto sea sincrónica. Si se encuentra en un subproceso en segundo plano, no bloqueará la interfaz de usuario si el archivo en cuestión es grande. Sin embargo, cuando el código se desencapsula en VSSDK, se dará cuenta de que no es el caso. Por lo tanto, aunque la API del Kit de herramientas de la comunidad es ciertamente más sencilla de entender y más fácil de escribir, ya que está vinculada a VSSDK, está sujeta a limitaciones de VSSDK. Las simplicidades pueden pasar por alto conceptos importantes que, si quienes amplían no los entienden, pueden causar más daño. VisualStudio.Extensibility evita los muchos problemas causados por las dependencias del subproceso principal centrándose en el modelo fuera de proceso y las API asincrónicas como base. Aunque la ejecución fuera de proceso simplificaría la mayoría de los subprocesos, muchas de estas ventajas también se transfieren a las extensiones que se ejecutan en el proceso. Por ejemplo, los comandos de extensibilidad de VisualStudio.Extensibility siempre se ejecutan en un subproceso en segundo plano. La interacción con las API de VSSDK todavía requiere un conocimiento profundo de cómo funcionan los subprocesos, pero al menos no incurrirá en bloqueos inesperados, como en este ejemplo.
Gráfico de comparación
Para resumir lo que se trata en detalle en la sección anterior, en la tabla siguiente se muestra una comparación rápida:
VSSDK | Community Toolkit | 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 de VS | ✅ | ✅ | ⏳ |
Instalación sin reinicio | ❌ | ❌ | ✅ |
AdmiteVS 2019 yversiones anteriores | ✅ | ✅ | ❌ |
Para ayudarle a aplicar la comparación con las necesidades de extensibilidad de Visual Studio, estos son algunos escenarios de ejemplo y nuestras recomendaciones sobre qué modelo usar:
- Soy nuevo en el desarrollo de extensiones de Visual Studio y quiero el proceso más sencillo para crear una extensión de alta calidad y solonecesitocompatibilidad con Visual Studio 2022 o superior.
- En este caso, se recomienda usar VisualStudio.Extensibility.
- me gustaría escribir una extensión destinada a Visual Studio 2022 y versiones posteriores. Sin embargo,VisualStudio.Extensibility no admite todas las funciones deque necesito.
- En este caso, se recomienda adoptar un método híbrido para combinar VisualStudio.Extensibility y VSSDK. Puede crear una extensión para VisualStudio.Extensibility que se ejecuta dentro del proceso, lo que le permite acceder a las API de VSSDK o del Community Toolkit.
- tengo una extensión existente y quiero actualizarla para admitir versiones más recientes. Quiero que mi extensión admita tantas versiones de Visual Studio como sea posible.
- Dado que VisualStudio.Extensibility solo admite Visual Studio 2022 y versiones posteriores, VSSDK o Community Toolkit es la mejor opción para este caso.
- tengo una extensión existente que me gustaría migrar aVisualStudio.Extensibility para aprovechar .NET e instalar sin reiniciar.
- Este escenario es un poco más matiz, ya que VisualStudio.Extensibility no admite versiones de nivel descendente de Visual Studio.
- Si la extensión existente solo admite Visual Studio 2022 y tiene todas las API que necesita, se recomienda volver a escribir la extensión para usar VisualStudio.Extensibility. Pero si la extensión necesita API que VisualStudio.Extensibility aún no tiene, proceda y cree una extensión de VisualStudio.Extensibility que se ejecute en el proceso para que pueda acceder a las API de VSSDK. Con el tiempo, puede eliminar el uso de la API de VSSDK a medida que VisualStudio.Extensibility agrega compatibilidad y mover sus extensiones para que se ejecuten fuera del proceso.
- Si la extensión necesita admitir versiones de nivel inferior de Visual Studio que no tienen compatibilidad con VisualStudio.Extensibility, se recomienda realizar algunas refactorizaciones en el código base. Extraiga todo el código común que se puede compartir entre las versiones de Visual Studio en su propia biblioteca y cree proyectos VSIX independientes que tienen como destino diferentes modelos de extensibilidad. Por ejemplo, si la extensión necesita admitir Visual Studio 2019 y Visual Studio 2022, puede adoptar la siguiente estructura de proyecto en la solución:
- MyExtension-VS2019 (este es el proyecto de contenedor basado en VSSDK para Visual Studio 2019)
- MyExtension-VS2022 (este es el proyecto de contenedor VSSDK+VisualStudio.Extensibility basado en VSIX que tiene como destino Visual Studio 2022)
- VSSDK-CommonCode (esta es la biblioteca común que se usa 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 para la lógica de negocios de la extensión. Ambos proyectos VSIX pueden hacer referencia a esta biblioteca para compartir código).
- Este escenario es un poco más matiz, ya que VisualStudio.Extensibility no admite versiones de nivel descendente de Visual Studio.
Pasos siguientes
Nuestra recomendación es que los extensores comiencen con *VisualStudio.Extensibility* al crear nuevas extensiones o mejorar las existentes, y utilicen *VSSDK* o *Community Toolkit* si se encuentran en escenarios no admitidos. Para empezar, con VisualStudio.Extensibility, examine la documentación presentada en esta sección. También puede hacer referencia al repositorio de GitHub de VSExtensibility para ejemplos o para enviar problemas.