AvaloniaUI - GUIs plattformübergreifend

Was ist AvaloniaUI?

Es handelt sich dabei um ein fortschrittliches Framework für GUI-Anwendungen und es ist plattformübergreifend für Linux, Windows und MacOS verfpgbar. An Android wird gerade gearbeitet. Geschrieben ist es für und mit C#. Der Hauptentwickler Gorky wollte ursprünglich WPF nach Linux portieren, ist aber an unzulänglichkeiten und zu tiefen Windows-Spezifika gescheitertn. Damals unter dem Arbeitstitel Avalonia. Dieser war inspiriert von WPFs Arbeitstitel "Avalon". Als er erkannte, dass eine bloße Portierung zwecklos ist, machte sich Gorky an einen Re-Write von Grund auf und baute dort wesentliche Verbesserungen ein. Das lief damals unter dem Projekttitel "Perspex". Als es markenrechtliche Probleme gab, nanne er es in "AvaloniaUI" um. Heute ist das Framework ziemlich gereift, fühlt sich manchmal aber noch buggy an. Technisch aber wirkt es wie ein WPF++.

Voraussetzungen / Distribution

Da das GUI-Framework auf .Net core oder Mono aufbaut, muss selbiges vorher installiert sein bzw. mitgeliefert werden. Für Windows empfiehlt sich die Distribution weiterhin als MSI-Paket, derweil für Linux dieser Tage Snap in die engere Wahl kommt. Bei Snap kann man ein Snap für .Net core als Basis festlegen, welches dann für alle weiteren Programme als Basis verwendet werden kann. Somit reduziert sich die Größe des Programm-Snaps.

Aussehen

Es lassen sich ansprechende und funktionale GUIs komfortabel damit erstellen. Da AvaloniaUI komplett Theme-basiert ist, kann das aussehen vollkommen angepasst werden. Themes sind Sache der Anwendungen. Ein Satz Standardthemes in dunkel und hell ist im Framework enthalten.

Programmierung

Wie schon WPF, kann man AvaloniaUI auf mehrere Weisen programmieren. Zunächst ist da die unterliegende Sprache. Da es auf der CLR-Aufbaut, können alle CLR-Sprachen dafür verwendet werden. Allen voran natürlich C#, aber auch VB.Net oder F#.

Die Definition der GUI-Komponenten erfolgt üblicherweise in XAML. XAML ist eine standardisierte XML-basierte Sprache. Fachlich ist es eine Notation, um Objektbäume zu definieren. Hinter jeder XAML-Datei gibt es auch ein sogenanntes Code-Behind. Eine z.B. C#-Datei, die in ihrem Konstruktor die XAML-Definition durch den XAML-Interpreter jagt. Tatsächlich wird beim Compile-Vorgang eine binäre, komprimierte Version von XAML in die Ressourcen geschrieben, genannt, BAML. Indes lässt sich alles, was mit XAML geht, auch mit einer CLR-Sprache programmatisch erreichen.

Beispiel-XAML eines Fensters:

 

Einfaches Fenster mit Events (xaml)

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Fenstertitel"
        x:Class="Views.MainWindow">
        <Button Click="OnButtonClick" IsDefault="True">OK-Button</Button>
</Window>

Einfaches Fenster mit Events (cs)

public class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
#if DEBUG
            this.AttachDevTools();
#endif
        }

        private void InitializeComponent()
        {
            AvaloniaXamlLoader.Load(this);
        }
        
        private void OnButtonClick(object sender, PointerPressedEventArgs e)
        {
        	Console.WriteLine("OK.");
        }
    }

Hier wird ein Fenster mit einem Button in XAML definiert und das Click-Event mit einem Handler im Code-Behind verknüpft. Ähnlichekiten zu WPF sind vorhanden.

Es handelt sich dabei aber eher um die simple Art AvaloniaUI zu programmieren und sie neigt zu Spaghetticode.

View mit internem Binding

<Window xmlns="https://github.com/avaloniaui"
        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"
        xmlns:local="clr-namespace:AvaloniaControls;assembly=AvaloniaControls"
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        x:Class="AvaloniaControls.MainWindow"
        Title="AvaloniaControls">
  <StackPanel>
    <Button>bla</Button>
    <local:Star Width="53" Height="53" Fill="Yellow" NumberOfSpikes="{Binding  ElementName=Sl, Path=Value, Mode=OneWay}" 
                InnerRadiusRatio="0.4"
                Stroke="Black" StrokeThickness=".5"/>
    <Slider Minimum="3" Maximum="30" SmallChange="1" Width="300" Value="6"
            TickFrequency="1" IsSnapToTickEnabled="True"
            Name="Sl"/>
    <TextBox Text="{Binding ElementName=Sl, Path=Value, Mode=TwoWay}" Width="300" />
  </StackPanel>
</Window>

View mit internem Binding

So sieht die Anwendung dann aus. Bewegt man den Slider, so ändert sich auch das Textfeld und der Stern malt sich korrekt neu. Man kann auch in das Textfeld die gewünschte Zahl eingeben und der Slider passt sich an, wie auch in der Folge der Stern. Dies zeigt die Mächtigkeit dieser Datenbindung. Wenn jetzt noch die Bindung gegenüber einem Datenmodell hinzukommt, wird es richtig spannend.

MVVM und ReactiveUI

MVVM

Das bevorzugte Entwurfsmuster sowohl für WPF, als auch für AvaloniaUI ist MVVM. Das steht für Model, View, Viewmodel. Es handelt sich dabei um eine entartete Variante von MVC (Model, View, Controller) und hat sich in den letzen Jahren als sehr wartungsfreundlich erwiesen. Also ein empfohlenes Muster.

Wesentlich ist dabei, dass das Viewmodel als Datenkontext hinter die View (das Fenster oder Control) gelegt wird und man mit sogenannten Bindings View-Inhalte mit Viewmodel-Properties bidirektional verknüpfen kann. Das heißt, alle Updates werden schon eimal vom Framework gehandhabt und man muss sich darum nicht kümmern. Darüber hinaus entsteht im Musterfall keinerlei verquickung ziwschen View und Viewmodel. Somit ist die Wiederverwendbarkeit von Model und Viewmodel viel besser gegeben als bei bisheriger Programmierung.

MVVM & ReactiveUI

Die nächste Stufe der Evolution ist ReactiveUI. Bei ReactiveUI handelt es sich um ein ergänzendes Framework rund um das Konzept des Observable (Beobachtermuster). Es ist sowohl für WPF als auch für AvaloniaUI verfügbar. Prinzipiell geht es um Zeitliche Abfolgen von Ereignissen. Das Framework erlaubt die Kombination und Zeitliche Einebnung (z.B. Throttle) von Observable-Ereignissen. Das Ganze ist im Prinzip überall verwendbar. Hier wird es verwendet, um die oft sehr komplexen Zusammenhänge von Properties innerhalb von Viewmodellen eleganter zu schreiben. Dabei werden alle Properties quasi zu dummen Datenhaltern, derweile die gesamte Logik und Abhängkeieten im Konstruktor definiert werden. Es ist nicht ganz intuitiv zu verstehen und auch etwas ressourcenintensiver, aber wenn man es versteht, sehr elegant.

 

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:AvaEmailArchivar.ViewModels;assembly=AvaEmailArchivar"
        xmlns:imaging="clr-namespace:Avalonia.Media.Imaging;assembly=Avalonia.Visuals"
        Icon="/Assets/main-win-logo.ico"
        Title="E-Mail-Archivar"
        x:Class="AvaEmailArchivar.Views.MainWindow"
        xmlns:cv="clr-namespace:AvaEmailArchivar.Converters"
        xmlns:resources="clr-namespace:AvaEmailArchivar.Resources">
    <Design.DataContext>
        <vm:MainWindowViewModel/>
    </Design.DataContext>
    <Window.Resources>
      <cv:UriToBitmapConverter x:Key="ucv"/>
    </Window.Resources>
      <Grid RowDefinitions="24,Auto,*,50">
         <!--menü-->
        <Menu Name="MainMenu" Items="{Binding MenuItems}" Grid.Row="0">
            <Menu.Styles>
                <Style Selector="MenuItem">
                    <Setter Property="Header" Value="{Binding Header}"/>
                    <Setter Property="Items" Value="{Binding Items}"/>
                    <Setter Property="Command" Value="{Binding Command}"/>
                    <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
                    <Setter Property="IsEnabled" Value="{Binding Enabled}"/>
                    <Setter Property="ToolTip.Tip" Value="{Binding ToolTip}"/>
                </Style>
            </Menu.Styles>
        </Menu>

        <!--hauptview-->
         <UserControl Grid.Column="1"  Content="{Binding ActiveVm}" />
      </Grid>
</Window>