[Xamarin.Forms] Addins y extensiones para Visual Studio macOS y Windows

Introducción

Visual Studio para macOS y Windows cuentan con modelos de extensión diferentes, modelos para crear interfaces de usuario diferentes y habitualmente trasladar un addin de macOS a una extension en Windows o viceversa, es costoso. ¿Podemos simplificar el proceso?, ¿podemos lograr compartir más elementos?.

En este artículo, vamos a ver como crear addins para Visual Studio macOS así como extensiones para Visual Studio Windows utilizando Xamarin.Forms para lograr compartir la UI.

Utilizaremos Measurer4000, herramienta diseñada para obtener métricas de código compartido en proyectos Xamarin. Nuestro objetivo será añadir la herramienta dentro de Visual Studio.

Add-ins para Visual Studio macOS

La arquitectura de Visual Studio para macOS es extensible. La forma de extender se basa en rutas de extensión que permiten que terceros extiendan el comportamiento. Por ejemplo: para extender la zona de edición de código, se expone /MonoDevelop/SourceEditor2/ContextMenu/Editor permitiendo añadir nuevos comandos en el menu contextual al hacer clic derecho.

AddIn Maker es un proyecto Open Source creado por Mikayla Hutchinson que permite el desarrollo y la depuración de add-ins para Visual Studio macOS desde Visual Studio macOS.

Para la gestión de la interfaz de usuario se utiliza GTK# o XWT.

Extensiones para Visual Studio Windows

En Windows tenemos la posibilidad de extender practicamente cualquier parte: menús, barras de herramientas, ventanas, etc.

Para crear extensiones se deben instalar las opciones de Desarrollo de extensiones de Visual Studio.

Extensiones en Windows

Para crear extensiones contamos con una plantilla de proyecto VSIX vacía que puede usar junto con las nuevas plantillas de elemento que crean extensiones de editor, ventanas de herramientas y comandos de menú.

Para la definición de la UI, utilizamos WPF.

Xamarin.Forms

Con la llegada de Xamarin.Forms 3.0 encontramos soporte a Linux gracias a un nuevo backend basado en GTK# además de un backend WPF.

Por lo que si usamos GTK# y WPF, ¿podríamos usar XAML y Xamarin.Forms para definir la UI de add-ins para Visual Studio tanto para macOS como para Windows?, ¿podría ser compartida?.

El proyecto en macOS

Comenzamos creando un proyecto de tipo IDE Extension:

IDE Extension

Este proyecto ya cuenta con la referencia a MonoDevelop.Addins. Continuamos añadiendo el paquete NuGet de Xamarin.Forms y Xamarin.Forms.Platform.GTK.

Para trabajar con Xamarin.Forms, debemos realizar la inicialización. La mejor opción para realizar esta tarea es realizarla nada más se abra el IDE. Para ello, vamos a crear un comando:

public class InitXamarinFormsCommand : CommandHandler
{
     protected override void Run()
     {
          Forms.Init();
     }
}

Y modificaremos el archivo Manifiest.addin.xml para añadir nuestro comando como punto de extensión en el arranque del IDE.

<Extension path="/MonoDevelop/Ide/StartupHandlers">
     <Class class="Measurer4000.Addin.Mac.Commands.InitXamarinFormsCommand"/>
</Extension>

Llega la hora de extender.

Añadimos otro punto de extensión:

<Extension path = "/MonoDevelop/Ide/MainMenu/Tools">
     <CommandItem id="Measurer4000.Addin.Mac.Commands.MeasurerCommand"/>
</Extension>

Fíjate que en esta ocasión, añadiremos un nuevo comando en el menu principal, opción herramientas.

Veamos la definición del comando:

public class MeasurerCommand : CommandHandler
{
     protected override void Update(CommandInfo info)
     {
          info.Visible = true;

          var projects = IdeApp.Workspace.GetAllProjects();

          if (projects.Any())
               info.Enabled = true;
          else
               info.Enabled = false;
     }

     protected override void Run()
     {
          new MeasurerWindow().Show();
     }
}

Comenzamos utilizando IdeApp. La clase estática MonoDevelop.Ide.IdeApp es su punto de entrada de extension de Visual Studio. Expone Workbench, Workspace, múltiples servicios  así como métodos relacionados con el ciclo de vida del IDE.

Habitualmente, la clase IdeApp se utiliza para acceder a Workbench o a Workspace. IdeApp.Wortkspace se usa para acceder al estado general del Ide durante una sesión de usuario. Podemos abrir nuevos proyectos y documentos, detectar archivos abiertos y mucho más.

En nuestro caso, lo utilizamos para detectar si el usuario tiene abierto al menos un proyecto.

En caso de abrir un proyecto, habilitamos la opción.

Al ejecutar el comando lanzamos una ventana llamada MeasurerWindow.

public class MeasurerWindow: Gtk.Window
{
     public MeasurerWindow()
     : base(Gtk.WindowType.Toplevel)
     {
          Title = "Measurer4000";
          WindowPosition = Gtk.WindowPosition.Center;

          var page = new MeasurerView();

          Add(page.CreateContainer());

          SetSizeRequest(800, 480);
     }
}

Es una ventana realizada con Gtk cuyo contenido utilizará una ContentPage de Xamarin.Forms. La clave para utilizar Xamarin.Forms es embeber el contenido. Tenemos disponible el método de extensión CreateContainer de Xamarin.Forms en el backend GTK para obtener la ContentPage como contenido nativo.

El proyecto en Windows

Comenzamos el proyecto en Windows añadiendo un comando.

Nuevo comando

Los menús y las barras de herramientas son la forma en que los usuarios acceden a los comandos en el VSPackage. Los comandos son funciones que realizan tareas, como mostrar una ventana o crear un nuevo archivo. Los menús y barras de herramientas son formas gráficas convenientes para presentar los comandos a los usuarios.

En este comando realizamos la inicializacion de Xamarin.Forms:

public static void Initialize(Package package)
{
     Instance = new MeasurerCommand(package);

     Forms.Init();
}

Al ejecutar el comando:

private void MenuItemCallback(object sender, EventArgs e)
{
     var measurerWindow = new MeasurerWindow();

     var measurerContent = new MeasurerView().ToFrameworkElement();
     measurerWindow.Content = measurerContent;

     measurerWindow.Show();
}

Lanzamos una ventana llamada MeasurerWindow (al igual que ya hacíamos en macOS), ventana WPF cuyo contenido es una ContentPage de Xamarin.Forms. La clave para utilizar Xamarin.Forms es de nuevo embeber el contenido. Tenemos disponible el método de extensión ToFrameworkElement de Xamarin.Forms en el backend WPF para obtener la ContentPage como contenido nativo.

La ventana:

<Window 
     x:Class="Measurer4000.Addin.Windows.Windows.MeasurerWindow"
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     mc:Ignorable="d"
     WindowStartupLocation="CenterScreen"
     Title="Measurer 4000">
     <Grid>

     </Grid>
</Window>

UI XAML compartida

Tanto en macOS como en Windows utilizamos una ContentPage para definir la interfaz de usuario. Hablamos de una interfaz definida en XAML que puede ser compartida en macOS y Windows, donde en cada plataforma, se utilizará un backend diferente para renderizar la UI con GTK# en el caso de macOS y con WPF en el caso de Windows.

<Grid>
     <Grid.RowDefinitions>
          <RowDefinition Height="Auto" />
          <RowDefinition Height="*" />
          <RowDefinition Height="Auto" />
     </Grid.RowDefinitions>
     <Grid Grid.Row="0"
          Margin="24">
          <Grid.ColumnDefinitions>
               <ColumnDefinition />
               <ColumnDefinition />
               <ColumnDefinition />
          </Grid.ColumnDefinitions>
          <StackLayout Grid.Column="0">
          <StackLayout Orientation="Vertical"
               HorizontalOptions="Center"
               Margin="0, 24, 0, 0">
               <Label Text="iOS Specific Code" />
               <Label Text="{Binding Stats.iOSSpecificCode}"
                    FontSize="30" />
          </StackLayout>
          <StackLayout Orientation="Vertical"
               HorizontalOptions="Center"
               Margin="0, 24, 0, 0">
               <Label Text="Share code in iOS" />
               <Label Text="{Binding Stats.ShareCodeIniOS}"
                    FontSize="30" />
          </StackLayout>
     </StackLayout>
     ...

En la interfaz utilizaremos algunos Labels donde msotrar información con cantidad de líneas así como porcentajes de código compartido y código específico. Por otro lado, utilizamos Oxyplot para mostrar gráficos de tipo tarta.

public MeasurerView()
{
     InitializeComponent();

     BindingContext = new CustomMainViewModel();
}

Toda la lógica del addin, modelos, ViewModels, etc. es la ya existente en la herramienta. Utilizaremos MVVM de exactamente la misma forma utilizada para crear Apps, en nuestro addin. En la ViewModel es necesario pasar la ruta de la solución para que pueda analizar y obtener toda la información.

En macOS tenemos en IdeApp.Workspace lo necesario para obtener la información necesaria de la solución con la que trabaja el usuario. Podemos obtener el listado de soluciones con el metodo GetAllSolutions.

var solutionPath = Path.Combine(Directory.GetCurrentDirectory(), solution.FileName);
_viewModel.MeasureSolutionByPath(solutionPath);

En el caso de Windows, obtenemos la instancia del objeto DTE (Development Tools Environment). DTE representa el entorno de desarrollo integrado (IDE) de Visual Studio y es el objeto de máximo nivel en la jerarquía. Nos otorga acceso a una gran variedad de propiedad y métodos para obtener información de la solución, propiedades, ventanas, etc.

var solution = ((DTE)ServiceProvider.GlobalProvider.GetService(typeof(DTE))).Solution;
var solutionPath = solution.FullName;
_viewModel.MeasureSolutionByPath(solutionPath);

Accedemos a la solución, y extraemos la ruta a la misma.

El resultado en macOS:

Addin en macOS

Y en Windows:

Addin en Windows

¿Qué te parece?. Quizás en un futuro sea una opción a la hora de extender Visual Studio. Ahora que tenemos el IDE en más de un sistema operativo, es hora de pensar en contar con una opción para crear extensiones que compartan la mayor cantidad de código posible.

Más información

Deja un comentario

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