Adrianistán

El blog de Adrián Arroyo


Interfaces gráficas multiplataforma en C# con .NET Core y Avalonia

- Adrián Arroyo Calle

Microsoft sorprendió a todos con la publicación de .NET Core el 27 de junio de 2016. Por primera vez, la plaatforma .NET se volvía multiplataforma, con soporte a macOS y GNU/Linux. Y además se volvía software libre, con licencia MIT y con un desarrollo transparente donde cualquiera puede proponer mejoras, subir parches, etc...

Esto posibilita tener soporte de primer nivel para uno de los lenguajes mejor diseñados actualmente, C#, que hasta entonces tenía un soporte de segunda en Linux, a través de Mono.

Inicialmente el soporte de Microsoft a .NET Core abarca: aplicaciones de consola, aplicaciones web ASP.NET y UWP. Mucha gente se desanimó por no tener WinForms o WPF en .NET Core. Sin embargo eso no quiera decir que no se puedan hacer interfaces gráficas en .NET Core. Aquí os presento la librería Avalonia, que aunque está en beta todavía, funciona sorprendentemente bien y en un futuro dará mucho de que hablar.

Avalonia funciona en Windows, macOS, GNU/Linux, Android e iOS. Usa XAML para las interfaces y su diseño se asemeja a WPF, aunque se han hecho cambios aprovechando características más potentes de C#. Avalonia no es WPF multiplataforma, es mejor.

Creando un proyecto


En primer lugar, tenemos que instalar .NET Core 2.0. Se descarga en la página oficial. A continuación comprobamos que se ha instalado correctamente con:
dotnet --version

Creamos ahora un nuevo proyecto de tipo consola con new
dotnet new console -o GitHubRepos

Si todo va bien nos saldrá algo parecido a esto:

Se nos habrán generado una carpeta obj y dos archivos: Program.cs y GitHubRepos.csproj. El primero es el punto de entrada de nuestro programa, el otro es el fichero de proyecto de C#.

Vamos a probar que todo está en orden compilando el proyecto.
dotnet run

Añadiendo Avalonia


Vamos a añadir ahora Avalonia. Instalar dependencias antes en C# era algo complicado. Ahora es muy sencillo, gracias a la integración de dotnet con NuGet.
dotnet add package Avalonia
dotnet add package Avalonia.Win32
dotnet add package Avalonia.Skia.Desktop
dotnet add package Avalonia.Gtk3

¡Ya no tenemos que hacer nada más! Nuestra aplicación será compatible ya con Windows y GNU/Linux. Para el resto de plataformas, hay más paquetes disponibles en NuGet, sin embargo desconozco su grado de funcionamiento. Solo he probado la librería en Windows 7, Windows 10, Ubuntu y Debian.

Program.cs


Program.cs define el punto de entrada a la aplicación. Aquí en una aplicación Avalonia podemos dejarlo tan simple como esto:
using System;
using Avalonia;

namespace GitHubRepos
{
class Program
{
static void Main(string[] args)
{
AppBuilder
.Configure<App>()
.UsePlatformDetect()
.Start<MainWindow>();
}
}
}

Básicamente viene a decir que arranque una aplicación Avalonia definida en la clase App con la ventana MainWindow. A continuación vamos a definir esas clases.

App.cs y App.xaml


En Avalonia tenemos que definir una clase que represente a la aplicación, normalmente la llamaremos App. Crea un fichero llamado App.cs en la misma carpeta de Program.cs, con un contenido tan simple como este:
using Avalonia;
using Avalonia.Markup.Xaml;

namespace GitHubRepos
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
}
}

Lo que hacemos aquí es pedir al intérprete de XAML que lea App.xaml simplemente y por lo demás, es una mera clase hija de Application.

El fichero App.xaml contiene definiciones XAML que se aplican a todo el programa, como el estilo visual:
<Application xmlns="https://github.com/avaloniaui">
<Application.Styles>
<StyleInclude Source="resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"/>
</Application.Styles>
</Application>

MainWindow.cs y MainWindow.xaml


Ahora nos toca definir una clase para la ventana principal de la aplicación. El código para empezar es extremadamente simple también:
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace GitHubRepos
{
public class MainWindow : Window
{
public MainWindow()
{
Initialize();
}
private void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
}
}

Aquí hacemos lo mismo que con App.cs, mandamos cargar el fichero XAML. Este fichero XAML contiene los widgets que va a llevar la ventana. Parecido a HTML, QML de Qt, Glade de GTK o FXML de Java o XUL de Mozilla.
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="GitHub Repositories">
<StackPanel HorizontalAlignment="Center">
<Button Content="¡Hola mundo!"></Button>
</StackPanel>
</Window>

GitHubRepos.csproj


Antes de poder compilar es necesario modificar el fichero de proyecto para incluir los ficheros XAML en el binario. Se trata de añadir un nuevo ItemGroup y dentro de él un EmbeddedResource.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.5.1" />
<PackageReference Include="Avalonia.Skia.Desktop" Version="0.5.1" />
<PackageReference Include="Avalonia.Win32" Version="0.5.1" />
</ItemGroup>
<!-- HE AÑADIDO ESTO -->
<ItemGroup>
<EmbeddedResource Include="**\*.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<!-- HASTA AQUÍ -->
</Project>

Ahora ya podemos compilar con dotnet run.

Nos deberá salir algo como esto:

Añadiendo clicks


Vamos a darle vidilla a la aplicación añadiendo eventos. Para ello primero hay que darle un nombre al botón, un ID. Usamos Name en XAML. También usaremos un TextBlock para representar texto.
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="GitHub Repositories">
<StackPanel HorizontalAlignment="Center">
<Button Name="lanzar" Content="Lanzar dado"></Button>
<TextBlock Name="resultado">No has lanzado el dado todavía</TextBlock>
</StackPanel>
</Window>

Ahora en MainWindow.cs, en el constructor, podemos obtener referencia a los objetos XAML con Find.
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using System;

namespace GitHubRepos
{
public class MainWindow : Window
{
Button lanzar;
TextBlock resultado;
public MainWindow()
{
Initialize();

lanzar = this.Find<Button>("lanzar");
lanzar.Click += LanzarDado;

resultado = this.Find<TextBlock>("resultado");
}
private void Initialize()
{
AvaloniaXamlLoader.Load(this);
}

private void LanzarDado(object sender, RoutedEventArgs e)
{
var r = new Random().Next(0,6) + 1;
resultado.Text = $"Dado: {r}";
}
}
}

Y ya estaría. También comentar que la función LanzarDado puede ser async si nos conviene. A partir de ahora ya puedes sumergirte en el código de Avalonia (¡porque documentación todavía no hay!) y experimentar por tu cuenta.

Un ejemplo real, GitHubRepos


Ahora os voy a enseñar un ejemplo real de la comodidad que supone usar Avalononia con .NET Core. He aquí un pequeño programa que obtiene la lista de repositorios de un usuario y los muestra por pantalla.
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Collections.Generic;
using System.Runtime.Serialization.Json;
using System.Linq;

namespace GitHubRepos
{
public class MainWindow : Window
{
Button refresh;
TextBox username;
TextBlock status;
ListBox repos;
public MainWindow()
{
Initialize();

refresh = this.Find<Button>("refresh");
refresh.Click += RefreshList;

username = this.Find<TextBox>("username");
status = this.Find<TextBlock>("status");
repos = this.Find<ListBox>("repos");
}
private void Initialize()
{
AvaloniaXamlLoader.Load(this);
}

private async void RefreshList(object sender, RoutedEventArgs e)
{
var user = username.Text;
status.Text = $"Obteniendo repositorios de {user}";
var client = new HttpClient();
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
client.DefaultRequestHeaders.Add("User-Agent", "GitHubRepos - Avalonia");
try{
var downloadTask = client.GetStreamAsync($"https://api.github.com/users/{user}/repos");

var serializer = new DataContractJsonSerializer(typeof(List<GitHubRepo>));
var repoList = serializer.ReadObject(await downloadTask) as List<GitHubRepo>;

repos.Items = repoList.OrderByDescending(t => t.Stars).Select(repo => {
var item = new ListBoxItem();
item.Content=$"{repo.Name} - {repo.Language} - {repo.Stars}";
return item;
});
status.Text = $"Repositorios de {user} cargados";
}catch(HttpRequestException){
status.Text = "Hubo un error en la petición HTTP";
}catch(System.Runtime.Serialization.SerializationException){
status.Text = "El fichero JSON es incorrecto";
}
}
}
}

Y este es su correspondiente XAML:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="GitHub Repositories"
Width="300"
Height="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50"/>
<RowDefinition />
</Grid.RowDefinitions>

<StackPanel Grid.Row="0" HorizontalAlignment="Center">
<StackPanel Orientation="Horizontal">
<TextBox Name="username">aarroyoc</TextBox>
<Button Name="refresh" Content="Actualizar"></Button>
</StackPanel>
<TextBlock Name="status"></TextBlock>
</StackPanel>
<ListBox Grid.Row="1" ScrollViewer.VerticalScrollBarVisibility="Visible" SelectionMode="Single" Name="repos"></ListBox>
</Grid>
</Window>

Adicionalmente, he usado una clase extra para serializar el JSON.
using System.Runtime.Serialization;

namespace GitHubRepos{
[DataContract(Name="repo")]
public class GitHubRepo{

[DataMember(Name="name")]
public string Name;

[DataMember(Name="language")]
public string Language;
[DataMember(Name="stargazers_count")]
public int Stars;
}
}

El resultado es bastante satisfactorio:



 

Comentarios

Añadir comentario

Todos los comentarios están sujetos a moderación