Avalonia UI – Cross-Platform WPF Anwendungen mit .NET Core entwickeln

von | 16.05.2020

Wenn Sie nach dem Begriff „Cross-Platform“ – besonders im Zusammenhang mit Desktop-Anwendungen – suchen, finden Sie schnell verschiedene Anbieter. Einer der prominensten ist Electron. Mit Electron wurden viele nützliche Tools wie bspw. Visual Studio Code, Slack, Microsoft Teams oder auch Discord umgesetzt. Als großer Vorteil gilt, dass in einer Chromium-Browser-Umgebung mit Javascript entwickelt wird, während gleichzeitig Zugang zu nativen Teilen des Hostsystems besteht. Allerdings wird Electron-Apps nachgesagt, dass sie im Vergleich zu nativen Anwendungen ressourcenintensiv sind. Kurz gesagt: Electron ist sicherlich eine gute Lösung, aber kein Allheilmittel. Das trifft natürlich auch auf viele andere Lösungen zu, und obwohl ich schon verschiedene Lösungen ausprobieren konnte, der Markt für Cross-Platform-Development ist zu groß, um alle Anbieter und Möglichkeiten zu kennen. Vor kurzem hat einer meiner Kollegen bspw. beschrieben, wie man mit Flutter eine App für Android und iOS auf einer gemeinsamen Codebasis erstellt.¹ Das war ziemlich einfach und elegant.

.NET Core 3.0 – Desktop Applications und WPF

Natürlich können Sie mit .NET Core schon seit Anbeginn plattformübergreifende Anwendungen entwickeln. Diese werden jedoch primär auf Servern in Form von Web- oder Konsolenanwendungen eingesetzt. Mit der neuen Version 3.0 hat Microsoft nun offiziell Support für WinForms und WPF mit .NET Core geliefert. WPF ist das Akronym für Windows Presentation Foundation; ein Framework, das die Entwicklung von Desktopanwendungen unter Windows mit hardwarebeschleunigter Grafikdarstellung und Vektorgrafiken erlaubt. Ein wesentliches Feature des Frameworks ist die strikte Trennung von Oberflächen und Businesslogik, die durch so genannte Bindings realisiert werden. Damit lassen sich MVVM oder MVC Pattern sehr gut umsetzen.

Aus meiner Sicht ist .NET Core 3.0 ein großer Schritt, um für bestehende Anwendungen die Vorteile von .NET Core zu nutzen, dabei aber gleichzeitig die Desktopanwendung selbst nur unter Windows laufen zu lassen.

Was ist Avalonia UI?

Avalonia UI verbindet die Plattformunabhängigkeit von .NET Core mit einer an WPF stark angelegten Neuentwicklung. Es handelt sich also um eine echte plattformunabhängige Desktopanwendung mit Oberfläche, die sich auf Windows, Mac und Linux nutzen lässt.

Wenn Sie sich gut mit WPF auskennen, werden Sie sich sofort heimisch fühlen. Aktuell ist Avalonia UI in der Version 0.9 als nuget-Paket verfügbar und funktioniert so gut, dass einige Entwicklungen schon größere Anwendungen damit realisieren. Das Projekt wird auf offiziell von der .NET Foundation unterstützt und ist komplett Open Source und auf Github verfügbar. Da ich in der Vergangenheit schon WPF-Anwendungen in verschiedensten Größen und Varianten implementieren durfte, habe ich einen genaueren Blick auf die Bibliothek geworfen und ausprobiert, wie sich das ganze „anfühlt“. Los geht’s!

Projekt anlegen und starten

Derzeit ist es am einfachsten, mit Visual Studio ein Projekt anzulegen. Man installiert sich die Avalonia UI Visual Studio Extension und kann dann direkt ein Projekt vom Typ „Avalonia Application“ anlegen. Wenn man das erstellte Projekt danach startet, erscheint ein leeres Fenster. Der Xaml-Code sieht bis auf die xml-namespaces sehr vertraut aus:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Button>Hello World!</Button>
</Window>

Der Aufbau des Programms hat sich im Vergleich zu WPF etwas verändert. Nun gibt es ähnlich wie bei ASP.NET Core Startup Code, bei dem man ähnlich wie bei der Startup-Class eine App-Class registriert:

class Program
    {
        // Main-Methode, welche eine Destkop Anwendung startet
        public static void Main(string[] args) => BuildAvaloniaApp()
            .StartWithClassicDesktopLifetime(args);

        // Anwendung konfigurieren.
        public static AppBuilder BuildAvaloniaApp()
            => AppBuilder.Configure<App>()
                .UsePlatformDetect()
                .LogToDebug();
    }

ViewModel erstellen und Binden

Hier hat sich nicht viel verändert. Ohne weitere Bibliotheken implementiert eine Klasse das INotifyPropertyChanged() Interface, stellt Propertys für die Bindings bereit und kann auf Kommandos durch die Implementierung des ICommand Interfaces auf die Oberfläche wie bspw. das Drücken eines Buttons reagieren:

public class RelayCommand : ViewModel, ICommand
{
    private readonly Action _execute;

    public RelayCommand(Action execute)
    {
        _execute = execute;
    }

    public bool CanExecute(object parameter) => true;

    public void Execute(object parameter) => _execute();

    public event EventHandler CanExecuteChanged;
}

public class MainWindowViewModel : INotifyPropertyChanged
{
    private int _counter;
    public event PropertyChangedEventHandler PropertyChanged;

    public MainWindowViewModel()
    {
        IncrementCommand = new RelayCommand(() => Counter++);
    }

    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public int Counter
    {
        get => _counter;
        set
        {
            if (_counter == value) return;
            _counter = value;
            OnPropertyChanged();
        }
    }

    public ICommand IncrementCommand { get; }
}

Der entsprechende Code im MainWindow.xaml sieht wie folgt aus:

<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"
        mc:Ignorable="d" 
        d:DesignWidth="800" 
        d:DesignHeight="450"
        x:Class="Test.MainWindow"
        Title="TestWindow">
  <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
    <Button Content="Increase Value" Command="{Binding IncrementCommand}" />
    <TextBlock Text="{Binding Counter}" />
  </StackPanel>
</Window>

Und im Code-behind des Fensters setzen Sie das erstellte ViewModel als DataContext:

namespace Test
{
    public class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
#if DEBUG
            this.AttachDevTools();
#endif
            DataContext = new MainWindowViewModel();
        }

        private void InitializeComponent()
        {
            AvaloniaXamlLoader.Load(this);
        }
    }
}

Wenn Sie nun die Anwendung starten und den Button klicken, wird jedes Mal der Wert der Variable um eins erhöht und die Oberfläche entsprechend aktualisiert.

DataTemplates

Wenn Sie das MVVM Pattern gut umsetzen möchten, empfehle ich Ihnen DataTemplates. Sie sind eine gute Möglichkeit, um für ein ViewModel einen View nach Datentyp bereitzustellen. Alles was Sie tun müssen, ist ein DataTemplate bereitzustellen und den Datentyp festzulegen:

<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"
        mc:Ignorable="d" 
        d:DesignWidth="800" 
        d:DesignHeight="450"
        x:Class="Test.MainWindow"
        Title="TestWindow">
  <Window.DataTemplates>
    <DataTemplate DataType="{x:Type MainWindowViewModel">
      <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button Content="Increase Value" Command="{Binding IncrementCommand}" />
        <TextBlock Text="{Binding Counter}" />
      </StackPanel>
    </DataTemplate>
  </Window.DataTemplates>
  <ContentControl Content="{Binding}" />
</Window>

Hier ist schon einer der ersten Unterschiede zu WPF zu sehen: WPF bietet auf jeder Ebene an, beliebige Ressourcen zu hinterlegen. Das funktioniert bei Avalonia UI auch, aber mit spezialisierten Collections für das Hinterlegen von DataTemplates. Ein weiterer Unterschied ist, dass jetzt für die Typangaben auch abgeleitete Klassen und Interfaces beachtet werden. Dadurch gewinnen Sie natürlich Freiraum, aber Sie müssen bei der Reihenfolge der Definitionen der DataTemplates aufpassen.

Styles

Das Styling von WPF Controls ist ein sehr mächtiges, aber auch sehr komplexes Werkzeug. Ich habe damit schon sehr gute, aber auch schon sehr schlechte Erfahrungen damit gemacht, insbesondere bei der Anpassung von Steuerelementen von Drittanbietern. Vermutlich aus diesen Gründen haben sich die Entwickler von Avalonia UI  dagegen entschieden, das alte Styling-System zu übernehmen. Das neue erinnert mehr an CSS und wird mit Selektoren beschrieben. Hier ein kleines Beispiel:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Window.Styles>
        <Style Selector="TextBlock.h1">
            <Setter Property="FontSize" Value="24"/>
            <Setter Property="FontWeight" Value="Bold"/>
        </Style>
    </Window.Styles>

    <TextBlock Classes="h1">I'm a Heading!</TextBlock>
</Window>

Weitere Unterschiede zu WPF

Im Folgenden möchte ich die – aus meiner Sicht – spannendsten Änderungen gegenüber WPF kurz vorstellen. Viele davon empfinde ich als äußerst erfrischend und eine gute Ergänzung zu den beibehaltenen Features:

Vereinfachtes Angeben von Grid Zeilen- und Spaltendefinionen

Hat mich auf Anhieb sehr begeistert, da ich Grids sehr oft verwende. Über eine Kurzsyntax können Sie die Definitionen für die Spalten und Zeilen zusammen schreiben.

Üblich:

<Grid Margin="4">
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="100" />
    <ColumnDefinition Width="1.5*" />
    <ColumnDefinition Width="4*"/>
  </Grid.ColumnDefinitions>
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
  </Grid.RowDefinitions>
  <!—Controls... --> 
</Grid>

Avalonia UI:

<Grid ColumnDefinitions="100,1.5*,4*" RowDefinitions="Auto,Auto,Auto"  Margin="4">
<!—Controls... -->
</Grid>

Bindings an Methoden anstelle von ICommand

Es ist nun möglich, direkt eine Methode mit einem Parameter zu binden, ohne ein ICommand zu implementieren. Letztendlich kann das insbesondere eine Menge Code sparen, wenn Sie in Ihrer Architektur ViewModelCommands separat von den ViewModels implementieren und im ICommand.Execute() öffentliche Methoden der ViewModels aufrufen. Die öffentlichen ViewModel-Methoden können Sie jetzt direkt einbinden:

namespace Example
{
    public class MainWindowViewModel : ViewModelBase
    {
        public void RunTheThing(string parameter)
        {
            // Code for executing the command here.
        }
    }
}
<Window xmlns="https://github.com/avaloniaui">
  <Button Command="{Binding RunTheThing}" CommandParameter="Hello World">Do the thing!</Button>
</Window>

Bindings definieren

Bindings an ein Property des Vater-Controls oder an ein benanntes Element werden mittels einer verkürzten Syntax ermöglicht: 

<!—Binding an den Text der Textbox -->
<TextBox Name="other">
<TextBlock Text="{Binding #other.Text}"/>

<!—Binding an das Vatercontrol -->
<Border Tag="Hello World!">
  <TextBlock Text="{Binding $parent.Tag}"/>
</Border>

<!—Binding an ein Vatercontrol eines bestimmten Typs -->
<Border Tag="Hello World!">
  <Decorator>
    <TextBlock Text="{Binding $parent[Border].Tag}"/>
  </Decorator>
</Border>

Meine Verbesserungswünsche

Trotz der vielen Verbesserungen gibt es einige Eckpunkte, die aus meiner Sicht noch verbessert werden könnten:

Unterstützung anderer IDEs: Derzeit ist die Entwicklung mit Avalonia UI lediglich mit Visual Studio sehr einfach. Bei der Arbeit mit Visual Studio Code oder Rider, wäre mehr Codeunterstützung hilfreich und wünschenswert. Dort müssen einige Dinge manuell installiert werden und das ist wirklich schade, zumal ich gerne unter Linux mit Rider entwickeln wollte und sich derzeit unter Windows alles deutlich stimmiger anfühlt.

Dokumentation verbessern: Die Dokumentation ist alles in allem mehr als hinreichend, um eine Anwendung auf die Beine zu stellen. Dennoch würde ich mir an der einen oder anderen Stelle mehr Informationen wünschen. Es ist beispielsweise nicht offensichtlich, wie DataTemplates zentral in einer Anwendung über ResourceDictionaries bereitstellt werden. Die in der Dokumentation beschriebene Lösung hat bei mir leider nicht funktioniert.

Unterstützung von Drittherstellern: Viele WPF-Anwendungen verwenden externe Controls, bspw. von der Firmy DevExpress oder Telerik. Diese Controls können alle nicht verwendet werden, das schränkt generell ein.

Fazit

Ich bin von Avalonia UI begeistert. Letztendlich haben sich sehr viele Entwickler gewünscht, dass WPF auch für Mac und Linux verfügbar ist – das ist nun endlich der Fall. Ich habe während meiner Experimente keine Probleme gehabt und konnte in den meisten Fällen wie gewohnt eine WPF-Anwendung entwickeln, in den wenigen verbleibenden Fällen hat sich einfach nur das Verhalten geändert.

Auch wenn der Trend zu Webanwendungen geht, sind native Desktopanwendungen durchaus eine gute Option und ich kann Ihnen nur wärmstens empfehlen, sich das Avalonia UI-Projekt anzuschauen. Für mich hat es sich gelohnt.

 

Hinweise:

Interessieren Sie sich für weitere Tipps aus der Praxis? Testen Sie unseren wöchentlichen Newsletter mit interessanten Beiträgen, Downloads, Empfehlungen und aktuellem Wissen.

Wir suchen Softwareentwickler. Berufseinsteigerinnen, Entwickler mit einigen und Expertinnen mit vielen Jahren Erfahrung.

[1] Smartphone-Anwendungen mit Flutter erstellen

Peter Friedland hat hier im t2informatik Blog weitere Beiträge veröffentlicht, u. a.

t2informatik Blog: Performance Optimierung bei WPF Anwendungen

Performance Optimierung bei WPF Anwendungen

t2informatik Blog: CI/CD Pipeline auf einem Raspberry Pi

CI/CD Pipeline auf einem Raspberry Pi

t2informatik Blog: Warum ich bei Godot gelandet bin

Warum ich bei Godot gelandet bin

Peter Friedland
Peter Friedland

t2informatik GmbH

Peter Friedland ist bei der t2informatik GmbH als Software Consultant tätig. In verschiedenen Kundenprojekten entwickelt er innovative Lösungen in enger Zusammenarbeit mit Partnern und den Ansprechpartnern vor Ort. Und ab und an bloggt er auch.