Extendiendo la interfaz gráfica (UI) de Visual Studio 2010 utilizando C# .NET

noviembre 10, 2010
By

En este tutorial se verá cómo extender parte de la interfaz gráfica (UI) de Visual Studio 2010 utilizando C#. El ejemplo consistirá en agregar un nuevo elemento al menú contextual de un proyecto en el Explorador de Soluciones que como acción accederá y modificará un valor de la configuración del proyecto seleccionado (Figura 1).

vs-context-menu 
Figura 1 – Elemento personalizado en el menú contextual del proyecto

Para poder extender Visual Studio 2010, lo primero que hay que hacer es descargarse el Visual Studio 2010 SDK. Este SDK instala las plantillas y las herramientas necesarias para extender Visual Studio.

El primer artículo que recomiendo leer (en inglés) es el siguiente: Inside the Visual Studio SDK (http://msdn.microsoft.com/en-us/library/cc138569.aspx). Este artículo es un buen punto de partida para conocer qué se puede extender en Visual Studio 2010. En principio existen varias vías para extender Visual Studio 2010: creando un Visual Studio Package (VSPackage), utilizando el Visual Studio General Automation Model o utilizando las extensiones Managed Extensibility Framework (MEF). Cada una de estas variantes extiende un determinado aspecto de Visual Studio, por lo que una solución integral pudiera necesitar utilizar estas tres variantes al mismo tiempo.

Como el objetivo de este tutorial es extender un menú de Visual Studio, crearemos un nuevo Visual Studio Package, que es una de estas plantillas que viene con el SDK (Aparece en Visual C# > Extensibility cuando lanzamos la ventana de creación de un nuevo proyecto). Esta plantilla trae un asistente que nos desplegará un ejemplo, listo para su uso, de cómo crear: un menú, una ventana de herramientas (toolbox window) o un editor personalizado (esto no significa que esas sean las únicas características extensibles en Visual Studio 2010). Para probar y depurar nuestro desarrollo, esta plantilla automáticamente cargará una instancia aislada de Visual Studio, registrará el paquete que estamos desarrollando y depurará automáticamente el proyecto. Es importante resaltar el término de aislada: el Visual Studio que se carga utiliza otra configuración y otra personalización, por lo que si esta instancia “se rompe” producto del propio desarrollo, la instancia de desarrollo de Visual Studio quedará intacta. Resulta recursivamente gracioso pero a su vez poderoso y genial la idea de “Utilizar un Visual Studio para Depurar un Visual Studio que a su vez puede estar Depurando otro proyecto”. Para conocer más sobre cómo crear un menú, una ventana de herramientas así como aprender cómo integrarse a las ventanas predeterminadas del Visual Studio, recomiendo leer el artículo (en inglés) del MSDN: VSPackage Walkthroughs (http://msdn.microsoft.com/en-us/library/cc138565.aspx)

En cuanto a la extensión de la interfaz de usuario (UI) de Visual Studio, que es el tema que nos ocupa en este tutorial, recomiendo leer (en inglés) el siguiente artículo: How VSPackages Add User Interface Elements to the IDE (http://msdn.microsoft.com/en-us/library/bb166229.aspx). Este artículo brinda algunas recomendaciones sobre cómo extender la interfaz gráfica de usuario (UI) así como una guía conceptual sobre la organización de dicha interfaz. En el caso de la extensión de los menús, hay que tener claro las definiciones de Grupo de Menú, Menú y Comando.

Un Grupo de Menú es un conjunto de menús que se visualiza de manera gráfica entre separadores de menús. Por ejemplo, en la figura 1, los menús Build, Rebuild, Clean, Publish… y Run Code Análysis pertenecen a un mismo grupo de menú. Un Menú sería entonces un elemento, como por ejemplo el menú “Build”, con forma de Botón o ComboBox. Por último, un Comando  definirá la acción a realizar por el menú al hacer clic. Adicionalmente está el concepto de Bitmap que es la figura que aparece en el lateral izquierdo del menú.

Cada uno de estos elementos (Grupo de Menú, Menú y Comando) se identifica por un guid que define a que conjunto de elementos pertenece el elemento y un identificador id que lo identifica de manera única dentro del grupo de elementos. Las posiciones de dónde ubicar los menús en la interfaz de Visual Studio también se identifican por un ID. Por ejemplo, el ID de la ubicación del menú contextual de un proyecto es IDM_VS_CTXT_PROJNODE. El artículo del MSDN que muestra cómo conocer los identificadores de estos elementos y las posiciones es: GUIDs and IDs of Visual Studio Commands (http://msdn.microsoft.com/en-us/library/cc826040.aspx). Básicamente, estas definiciones se pueden encontrar en los ficheros del SDK de Visual Studio: SharedCmdDef.vsct, ShellCmdDef.vsct, VsDbgCmdUsed.vsct, Venusmenu.vsct, entre otros que se encuentran en la carpeta: Visual Studio SDK installation path\VisualStudioIntegration\Common\Inc.

Teniendo en cuenta estos conceptos, comencemos a desarrollar nuestro ejemplo. Al crear un proyecto de tipo VSPackage, digamos con nombre MenuExamplePackage y con la opción de crear un menú marcada en el asistente de esta plantilla, el Visual Studio nos creará un fichero con extensión .vsct (MenuExamplePackage.vsct en este ejemplo) que es un XML en donde se define el diseño de la interfaz que queremos adicionarle al Visual Studio 2010. A la primera región de dicho XML que le prestaremos atención es a la que define los Grupos de Menús.

<Group guid="guidMenuExamplePackageCmdSet" id="MyMenuGroup" priority="0x0200">
  <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_PROJNODE"/>
</Group>

De manera predeterminada el asistente nos crea un grupo de menú con un menú y un comando de ejemplo. Como puede el lector observar, a dicho grupo se le define un Padre (Parent) que de manera predeterminada contiene un ID igual a IDM_VS_MENU_TOOLS que referencia al menú Herramientas (Tools) del Visual Studio. Si dicho ID lo reemplazamos por IDM_VS_CTXT_PROJNODE entonces el menú de ejemplo que aparecería en el menú Herramientas, ahora aparecerá en el menú contextual de un proyecto. El lector podrá experimentar con diferentes identificadores para obtener resultados diversos. Adicionalmente hay un atributo priority que define la posición dentro del menú: mientras mayor sea el número más para el final (para abajo) aparecerá el elemento.

Luego aparece la región <Buttons></Buttons> que es donde se definen los menús y a qué grupo pertenecen.

<Button guid="guidMenuExamplePackageCmdSet" id="cmdidMyCommand" priority="0x0100" type="Button">
  <Parent guid="guidMenuExamplePackageCmdSet" id="MyMenuGroup" />
  <Icon guid="guidImages" id="bmpPic1" />
  <Strings>
    <CommandName>cmdidMyCommand</CommandName>
    <ButtonText>Acción de ejemplo</ButtonText>
  </Strings>
</Button>

En este ejemplo se ha definido un menú de tipo botón con texto “Acción de ejemplo” que pertenece al grupo MyMenuGroup definido previamente (vea el nodo xml Parent anidado dentro de Button). Al menú se le define qué icono tendrá asociado (tiene que estar definido el bitmap con id bmpPic1 en este caso) y el nombre del comando asociado, en este caso cmdidMyCommand.

Hasta el momento de manera declarativa en el xml hemos definido la forma/diseño (el qué) de lo que queremos, pero faltaría asociar el funcionamiento (el cómo). Para ello, en la inicialización del VSPackage se vincula el id del menú declarado en el xml (en este caso cmdidMyCommand que representa el valor 0×0100) con un método (escrito en C#) que contendrá el funcionamiento.

protected override void Initialize()
{
    Trace.WriteLine (string.Format(CultureInfo.CurrentCulture, "Entering Initialize() of: {0}", this.ToString()));
    base.Initialize();

    // Add our command handlers for menu (commands must exist in the .vsct file)
    OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
    if ( null != mcs )
    {
        // Create the command for the menu item.
        CommandID menuCommandID = new CommandID(GuidList.guidMenuExamplePackageCmdSet, (int)PkgCmdIDList.cmdidMyCommand);
        MenuCommand menuItem = new MenuCommand(MenuItemCallback, menuCommandID);
        mcs.AddCommand(menuItem);
    }
}

Como se muestra en el código anterior, para registrar la acción o comando de elementos de menús en el IDE de Visual Studio se utiliza la clase OleMenuCommandService que es quien brinda la API para el registro de las acciones. Note que primeramente se crea un objeto de tipo CommandID que contiene el id del conjunto de comandos y el id específico del comando definido por el menú y luego se crea propiamente un comando (tipo MenuCommand) que asocia el id del menú a un método que contendrá el comportamiento del mismo. Finalmente se registra el menú en el Visual Studio utilizando el servicio de menús y comandos (OleMenuCommandService).

Esto de los identificadores puede traer confusión así que retomemos el asunto con algunas aclaraciones:

  • Todos los identificadores de menús son numéricos (entero sin signo)
  • En el xml (.vsct) aparecen identificadores textuales como cmdidMyCommand o IDM_VS_CTXT_PROJNODE. Estos identificadores se mapean a un número en la región <Symbols> del .vsct por comodidad. Tenga cuidado que no se repitan los valores. En este ejemplo el id cmdidMyCommand sería el 0×0100. En el caso de IDM_VS_CTXT_PROJNODE no aparecerá en nuestro .vsct porque es un ID de sistema que ya viene mapeado en el fichero SharedCmdPlace.vsct
  • Por un problema de organización la plantilla VSPackage nos crea una clase PkgCmdID (PkgCmdID.cs) en donde debemos nosotros manualmente crear campos que tengan el mismo texto y valor asociado que aquellas asociaciones que realizamos en la sección <Symbols> del .vsct. Note que esto es puramente organizativo, ya que no pasa nada si en el inicializador del VSPackage cuando creamos un CommandID escribimos literalmente el número. No obstante es más elegante y organizado tener los valores ubicados en variables.

Para los programadores de C++ esta filosofía les parecerá familiar debido a que así es como se definen los elementos de menús en las aplicaciones nativas del sistema operativo (algo como #define ID_MYMENU 0×0100).

Ahora solo queda probar que el menú aparezca donde lo concebimos y realice la acción expresada en el método MenuItemCallback. Uno de los objetivos de este tutorial es mostrar cómo acceder a la configuración del proyecto seleccionado y modificar algún valor al realizar un clic en el menú creado anteriormente. Para ello, centrémonos en el desarrollo del cuerpo del método MenuItemCallback.

private void MenuItemCallback(object sender, EventArgs e)
{
    EnvDTE80.DTE2 dte = (EnvDTE80.DTE2)this.GetService(typeof(SDTE));

    var project = ((Array)dte.ActiveSolutionProjects).GetValue(0) as Project;
    var config = project.ConfigurationManager.ActiveConfiguration;
    var configProps = config.Properties;

    Property startArg = configProps.Item("StartArguments");

    // Show a Message Box to prove we were here
    IVsUIShell uiShell = (IVsUIShell)GetService(typeof(SVsUIShell));
    Guid clsid = Guid.Empty;
    int result;
    Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(uiShell.ShowMessageBox(
                0,
                ref clsid,
                "MenuExamplePackage",
                string.Format(CultureInfo.CurrentCulture, "Startup Args: {0}", startArg.Value),
                string.Empty,
                0,
                OLEMSGBUTTON.OLEMSGBUTTON_OK,
                OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST,
                OLEMSGICON.OLEMSGICON_INFO,
                0,        // false
                out result));

    startArg.Value = "/this /is /a /test";
}

Desde versiones anteriores de Visual Studio, existe un objeto que representa al IDE. Este objeto se conoce como DTE y aparece en el ensamblado EnvDTE. Este objeto ha sufrido modificaciones y extensiones cada vez que ha aparecido una nueva versión de Visual Studio. Actualmente se utiliza una versión mejorada DTE2 que reemplaza al viejo DTE. Cada versión de Visual Studio viene con un nuevo ensamblado: EnvDTE, EnvDTE80, EnvDTE90 y EnvDTE100 hasta la fecha. Por suerte para nosotros cada ensamblado no reemplaza al anterior sino que lo complementa (salvo el traspaso de DTE hacia DTE2) y todos estos ensamblados vienen con Visual Studio 2010 manteniendo la compatibilidad y funcionalidad. Eso significa que no es necesario modificar códigos que utilicen una versión anterior del IDE para poder utilizarlos en Visual Studio 2010 (a menos por supuesto que se quieran utilizar características que no se encontraban en versiones anteriores).

Para acceder al DTE2 desde nuestro VSPackage utilizamos el método GetService pasando como parámetro el tipo de SDTE. El objeto devuelto será de tipo EnvDTE80.DTE2. Como se muestra en el código de ejemplo, utilizando este objeto podemos acceder al proyecto activo, pedirle la configuración actual del proyecto (Debug, Release o alguna personalizada) y acceder a sus propiedades. Como podrá observar se está solicitando los parámetros de inicio de la aplicación StartArguments (suponiendo que el proyecto sea una aplicación de consola) y mediante la instrucción StartArguments.Value según sea utilizada se puede obtener o modificar su valor. El tipo Project y de manera general los tipos que representan características de proyectos se encuentran en los ensamblados: VSLangProj, VSLangProj2 y sus versiones con sufijo 90 y 100.

Adicionalmente en el código de ejemplo se está obteniendo acceso al Shell de Visual Studio (IVsUIShell) con el objetivo de lanzar un cuadro de mensaje que muestre el valor de la propiedad StartArguments del proyecto. Note que pudiéramos haber utilizado el clásico MessageBox.Show de Windows.Forms, pero es más elegante integrarse a la interfaz (dígase de paso hecha en WPF) de Visual Studio 2010.

Para finalizar recomiendo la lectura (en inglés) del artículo: CodeBlog: Writing a Blogging Extension for Visual Studio 2010 que brinda un ejemplo más práctico de una extensión para Visual Studio 2010.

Tags: ,

3 Responses to Extendiendo la interfaz gráfica (UI) de Visual Studio 2010 utilizando C# .NET

  1. Leo on abril 17, 2012 at 2:54 am

    En este(excelente) articulo te centras en VSPackage.
    Quisiera saber que me da VSPackage que no me da MEF(no conozco ninguno de las 2 opciones), ya que tengo que agregar un add-in que digamos q’ se llamara LMZ:
    1)ToolWindowPane, que por supuesto la ventana “LMZ” se abrira desde Tools
    2)En Tools|Options aparezca una antrada de “LMZ” para poder hazer definiciones
    3)Poder crear un proyecto del tipo “LMZ” a traves de File|New|Project|LMZ
    gracias
    Leo Zylber
    lzylber@nds.com
    leomoshe@gmail.com

  2. kiquenet on junio 11, 2012 at 3:53 am

    Hola,

    tenía un Addin en VS2008 y VS2010. Ahora ha surgido otro proyecto de extensibilidad para VS2010, en el que se tiene que manejar un tipo de proyecto custom.

    Nos planteamos utilizar VSPackage, pero queremos que la curva de aprendizaje sea mínima.

    Algún proyecto de open source (con código fuente completo) que pueda servir de ejemplo avanzado y de buenas prácticas para crear un VSPackage con VS2010.

    La idea es tener un “addin” con un menú específico y también posibilidad de manejar tipos de proyectos custom, que necesitamos crear.

    Saludos y gracias.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*

Acerca del autor...

Alejandro Tamayo

Web: http://www.linkedin.com/in/atamayocastillo
Alejandro Tamayo
Professor, Researcher, Developer, Consultant and technology enthusiast. Master of Science (MSc) in Computer Science and member of Weboo Research Group.Leer completo