Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 16 Min.

UIs für Linux

Es gibt viele UI-Frameworks für .NET, doch nur sehr wenige davon unterstützen Linux. Avalonia schafft als etabliertes Open-Source-Projekt Abhilfe.
© dotnetpro
Es gibt eine lange Liste von UI-Frameworks für C#/.NET. Allein Microsoft hat mit Windows Forms, WPF, WinUI,
.NET MAUI und ASP.NET Core Blazor eine lange Liste im Programm. Wollen wir eine Applikation für Windows entwickeln, haben wir somit die Qual der Wahl. Unter mac­OS wird die Microsoft-Liste bereits kleiner, mit .NET MAUI und ASP.NET Core Blazor stehen von den oben Genannten aber noch zwei Frameworks zur Auswahl.In diesem Artikel möchten wir uns damit beschäftigen, wie wir UIs für Linux entwickeln können. Hier purzeln mit Ausnahme von ASP.NET Core Blazor alle anderen genannten Microsoft-Frameworks aus der Liste. Selbst bei ASP.NET Core Blazor kann noch etwas fehlen, da es einen Browser benötigt, innerhalb dessen der HTML-Content gerendert werden kann. ASP.NET Core Blazor Hybrid als Container ist hierfür eine gängige Variante, unter Linux kann diese jedoch nicht verwendet werden. Wir müssten uns somit selbst um einen Container für die ASP.NET-Core-Blazor-Applikation kümmern oder ein entsprechendes Open-Source-Projekt suchen.ASP.NET Core Blazor basiert auf Webtechnologie. Ist damit Webtechnologie der einzige Weg, wie wir in C# eine Benutzeroberfläche für Linux bauen können? Glücklicherweise nicht. Mit Avalonia existiert ein seit mehr als elf Jahren gereiftes UI-Framework für C# / .NET, das neben Windows und macOS auch Linux klar im Fokus hat. Seit Version 11 werden zudem Android, iOS und Browser (via WASM) unterstützt.Avalonia wurde bereits im dotnetpro-Artikel „Das bessere WPF“ von Fabian Hügle [1] vorgestellt. Daher beschäftigen wir uns in diesem Artikel nur kurz mit allgemeinen Informationen zu Avalonia: Avalonia ist ein von der Community getriebenes Open-Source-Projekt. Es hat eine hohe Ähnlichkeit zu WPF, nutzt XAML, präferiert das MVVM-Pattern und setzt auf ein eigenes Rendering aller Controls. All diese Punkte führen dazu, dass wir uns bei der Arbeit mit Avalonia schnell wohlfühlen, wenn wir bereits WPF-Kenntnisse oder Erfahrung mit XAML mitbringen.Der Ansatz des eigenen Renderings führt dazu, dass es aus technischer Sicht einfach ist, Avalonia auf weitere Plattformen zu bringen – das Framework benötigt nur eine Canvas, in die gerendert werden kann. Hervorgehoben wird dieser Umstand, weil es ein völlig anderer Weg ist als etwa bei .NET MAUI. .NET MAUI nutzt die Controls und damit auch das Rendering der jeweiligen Plattform. Neue Plattformen zu unterstützen ist damit für .NET MAUI deutlich aufwendiger.WPF verfolgt einen ähnlichen Ansatz wie Avalonia – alle Controls werden selbst gerendert. Anders als bei Avalonia wurde in WPF nie die Unterstützung für weitere Betriebssysteme eingebaut. Möchte man eine WPF-Applikation für weitere Plattformen fit machen, könnte das Produkt Avalonia XPF spannend sein [2], siehe auch den Kasten Avalonia XPF.

Avalonia XPF

Neben Avalonia selbst stellt das Avalonia-Team auch das kostenpflichtige Produkt Avalonia XPF bereit. Avalonia XPF ist ein Fork von WPF, der Unterbau wurde hier aber durch die Rendering-Logik von Avalonia ersetzt. Dadurch wird erreicht, dass auch WPF-Applikationen plötzlich Cross-Plattform-fähig sein können.
Zurück zum Thema Linux: Wenn wir von Linux-UIs sprechen, ist die Welt deutlich komplexer als etwa unter Windows oder macOS. Unter Linux unterscheiden wir beispielsweise mehrere Distributionen (etwa Ubuntu oder Fedora), mehrere Desktop-Umgebungen (zum Beispiel Gnome, KDE Plasma oder Xfce) und mehrere Display-Server-Protokolle (X11, Wayland). Und das ist noch nicht einmal alles, soll aber für diesen Artikel genügen, um einen Eindruck zu bekommen. Wir müssen uns an dieser Stelle also damit auseinandersetzen, was davon unsere konkrete Applikation unterstützen soll. Das Avalonia-Team selbst tätigt dazu folgende Aussagen:
  • Offiziell unterstützt werden die Distributionen Debian 9+, Ubuntu 16.04+ und Fedora 30+.
  • Weitere Voraussetzung ist .NET, etwa eine aktuelle .NET-Core-Version.
Einige andere Distributionen funktionieren ebenfalls „out of the box“, werden jedoch nicht offiziell unterstützt. Bei anderen Distributionen wiederum muss etwas nachgeholfen werden, so etwa bei Raspberry Pi OS. Das Avalonia-Team beschreibt die dafür notwendigen Schritte auf seiner Website [3]. Primär geht es darum, dass alle weiteren technischen Abhängigkeiten für Avalonia für das jeweilige Betriebssystem zur Verfügung stehen. Insbesondere bei sparsam ausgestatteten Distributionen kann davon etwas fehlen. Das Avalonia-Team selbst bietet an, im Rahmen eines kostenpflichtigen Supportvertrags bei weiteren Linux-Distributionen zu unterstützen.

Entwickeln unter Linux

Bevor wir uns Avalonia genauer anschauen, stellt sich die Frage, wie wir überhaupt entwickeln. Dabei geht es zunächst um die Wahl des Betriebssystems, auf dem entwickelt wird. Für Linux-UIs bietet sich an dieser Stelle entsprechend auch eine Linux-Umgebung für die Entwicklung an. „Wie entwickelt man C# unter Linux?“, wird sich der eine oder andere Leser sicher fragen. Ganz einfach, tatsächlich ist es Stand 2025 nicht einmal mehr so selten, dass C#-Entwickler ein Linux verwenden. Es gibt zwei sehr bekannte Entwicklungsumgebungen für C#, die hervorragend unter Linux laufen:
  • Rider von JetBrains,
  • Visual Studio Code von Microsoft.
Rider steht in dieser Liste bewusst ganz oben. Es unterstützt Avalonia bereits ohne zusätzliche Plug-ins und bringt eine der besten Integrationen dafür mit. Hintergrund ist, dass die Firma JetBrains selbst Avalonia verwendet hat, um Tools wie dotTrace für mehrere Plattformen fit zu machen. dotTrace, um bei diesem Beispiel zu bleiben, wurde ursprünglich mit WPF gebaut und war deswegen in der Vergangenheit nur auf Windows lauffähig. Die jetzige Version baut auf Avalonia auf, wodurch dotTrace nun auch macOS und Linux unterstützt.JetBrains Rider wird vom Avalonia-Team als IDE empfohlen [4]. Diese Empfehlung deckt sich auch mit den Erfahrungen des Autors: Er nutzt die IDE schon seit mehreren Jahren unter Windows, Linux und macOS. Bild 1 zeigt, wie das unter einer aktuellen Version von Ubuntu aussieht.
Entwickeln unter Linux mit JetBrains Rider (Bild 1) © Autor
Visual Studio Code ist als Alternative ebenso möglich. Das Avalonia-Team stellt die Extension Avalonia for Visual Studio Code bereit, beschreibt diese dabei aber selbst so: „While functional, the development experience is not as rich as what you’ll find in Rider or Visual Studio.“

Entwickeln unter Windows und WSL

Entscheiden wir uns für Windows als Betriebssystem bei der Entwicklung, so haben wir auch hier die Möglichkeit, un­sere Avalonia-Applikation aus Visual Studio heraus auf Linux auszuführen und zu debuggen. Das Zauberwort an dieser Stelle ist WSL (Windows Subsystem for Linux) beziehungsweise WSLg (Windows Subsystem for Linux GUI). WSLg wurde in der dotnetpro bereits von Tam Hanna vorgestellt [5]. WSLg ermöglicht die Ausführung von Linux-Anwendungen mit grafischer Bedienoberfläche und bindet diese in den Windows-Desktop ein.Visual Studio erlaubt es, Applikationen innerhalb des WSL zu starten und zu debuggen. Machen wir das mit einer Avalonia-Applikation, so sorgt WSLg dafür, dass die Applikation in WSL hochfährt und in einem Fenster auf dem Windows-Desktop dargestellt wird. Bild 2 zeigt, wie das für die Applikation MessageCommunicator aussieht, ein Open-Source-Projekt auf Basis von Avalonia. Die Applikation kann dabei ähnlich einer nativen Windows-Applikation bedient werden. Auch der Debugger aus Visual Studio heraus funktioniert wie gewohnt. Somit erhalten wir eine ideale Grundlage zur Entwicklung von Linux-Applikationen unter Windows.
Avalonia-Applikation MessageCommunicator, ausgeführt innerhalb der WSL 2 unter Windows (Bild 2) © Autor
Sollte man sich auf der Entwicklungsmaschine für Windows entscheiden, so dürfen an dieser Stelle einige Hinweise nicht fehlen. Linux macht verschiedene grundlegende Dinge anders als Windows:
  • Pfade werden durch Slashes (/) getrennt, nicht durch Back­slashes (\).
  • Zeilenumbrüche innerhalb von Textdateien werden mit Line Feed ausgeführt, nicht mit Carriage Return + Line Feed.
  • Pfadangaben sind Case-sensitiv.
Vielen Leserinnen und Lesern ist das sicherlich nicht neu. Wenn wir unter Windows für Linux entwickeln, bedeutet das aber, dass wir auf solche Unterschiede besonders achten müssen. Es empfiehlt sich daher, regelmäßig mit der WSL zu testen.

Beispielapplikation für diesen Artikel

Eine Beispielapplikation, die Sie in den Downloads zum Artikel finden, hilft uns dabei, einen tieferen Einblick in die Entwicklung mit Avalonia zu bekommen. Wir bauen dazu ein kleines Monitoring-Tool für einen Temperatursensor. Die Logik ist sehr einfach: Wir haben einen Sensor, von dem wir im Sekundentakt Temperaturwerte auslesen. Wir visualisieren den zuletzt gelesenen Wert und die historischen Werte anhand einer Tabelle. Damit das Tool einfach selbst getestet werden kann, binden wir keinen echten Temperatursensor an. Stattdessen generieren wir die Werte im Hintergrund zufällig. Das Endergebnis ist in Bild 3 zu sehen.
Screenshot der Beispielapplikation für diesen Artikel (Bild 3) © Autor

Mit dem richtigen Template starten

Avalonia stellt mehrere Templates bereit, mit denen wir starten können. Diese werden vom Avalonia-Team in einem separaten GitHub-Repository gepflegt [6]. Mit folgendem Befehl in der Kommandozeile werden sie installiert:

dotnet new install Avalonia.Templates 
Zum Zeitpunkt dieses Artikels erhalten wir folgende Templates:
  • Avalonia .NET App,
  • Avalonia .NET MVVM App,
  • Avalonia Cross Platform Application.
Da wir für Linux entwickeln wollen, blicken wir womöglich zuerst auf die letzte Variante Avalonia Cross Platform Application. Tatsächlich wäre aber die erste oder die zweite Variante richtiger. Hintergrund ist, dass es sich dabei um die Templates für reine Desktop-Applikationen handelt. Solche sind bei Avalonia von vornherein Cross-Plattform, also unter Windows, Linux und macOS lauffähig. Der Begriff „Cross Platform“ im Namen des letztgenannten Templates geht noch weiter, denn es zielt zusätzlich auf Android, iOS und den Browser. Für diesen Artikel starten wir mit dem Template Avalonia .NET App.Es gibt noch einen weiteren Punkt, den es an dieser Stelle zu klären gilt: Wir können Compiled Bindings für unsere Applikation nutzen. Standardmäßig ist das Häkchen dafür beim Template aktiv. Es handelt sich um keine endgültige Entscheidung, doch es empfiehlt sich, diese Einstellung immer aktiv zu lassen. Compiled Bindings sorgen dafür, dass die Bindings im XAML-Code kompiliert werden – anders als etwa bei WPF. Bei WPF werden Bindings mittels Reflection zur Laufzeit aufgelöst. Compiled Bindings bringen uns eine bessere Performance. Zusätzlich bekommen wir beim Kompilieren bereits Feedback in Form von Compiler-Fehlern, falls es etwa Tippfehler gibt. Weitere Infos dazu enthält der Kasten Compiled Bindings.

Compiled Bindings

Anders als WPF unterscheidet Avalonia zwischen Compiled Binding und Reflection Binding. Für beide Varianten werden gleichnamige Markup Extensions bereitgestellt. Die Markup Extension <span class="_4_Kursiv-im-Kasten">Binding</span> nutzt die Variante, die als Standard in der XAML-Datei oder global in der <span class="_4_Kursiv-im-Kasten">.csproj</span>-Datei hinterlegt ist. Per Default wird Letztere durch die Avalonia-Templates so eingestellt, dass Compiled Binding der Standard ist.

Ein MVVM-Framework auswählen

MVVM (Model-View-ViewModel) ist bei XAML-Frameworks nach wie vor das beliebteste Pattern; Gleiches gilt auch für Avalonia. Bei der Auswahl des MVVM-Frameworks sind folgende zwei Varianten für Avalonia-Projekte am häufigsten anzutreffen.
  • CommunityToolkit.Mvvm,
  • ReactiveUI.
Aus Sicht des Autors ist es meist das Einfachste, mit CommunityToolkit.Mvvm zu starten. ReativeUI bietet zwar mehr Features und hat mit dem NuGet-Paket Avalonia.ReactiveUI eine sehr gute Integration in Avalonia. Der Nachteil ist die höhere Einstiegshürde aufgrund der Tatsache, dass ReactiveUI auf den Reactive Extensions for .NET aufbaut. In diesem Artikel wird aus Gründen des Umfangs nicht tiefer darauf eingegangen. Es wäre aber ratsam, ReactiveUI nur dann zu nutzen, wenn ein Verständnis über die Reactive Extensions for .NET vorliegt.CommunityToolkit.Mvvm kommt leichtgewichtig daher und kümmert sich im Wesentlichen um die für das MVVM-Pattern notwendige Implementierung von INotifyPropertyChanged sowie INotifyDataErrorInfo, und es bringt Klassen
für Commands mit. Eine Besonderheit des CommunityToolkit.Mvvm ist die Nutzung von Roslyn Source Generators, um typischen Boilerplate-Code rund um INotifyPropertyChanged-Implementierungen zu generieren.

ViewModel und Service erstellen

Zuerst erstellen wir den Service, der für die Ermittlung der Temperaturdaten zuständig ist. Hierzu definieren wir das Interface IMeasurementService und die Implementierung RandomMeasurementService. Letztere simuliert den Temperatursensor, damit wir in diesem Beispiel nicht von einem physischen Sensor abhängig sind. Listing 1 enthält den Code aus dem Beispiel.
Listing 1: Service zur Ermittlung von Werten vom simulierten Temperatursensor
TemperatureMeasurement.cs&lt;br/&gt;public record TemperatureMeasurement(DateTimeOffset &lt;br/&gt;  TimeStamp, double TemperatureInDegrees);&lt;br/&gt;IMeasurementService.cs&lt;br/&gt;public interface IMeasurementService&lt;br/&gt;{&lt;br/&gt;  IAsyncEnumerable&amp;lt;TemperatureMeasurement&amp;gt; &lt;br/&gt;    StartMeasurement(&lt;br/&gt;    CancellationToken cancellationToken);&lt;br/&gt;}&lt;br/&gt;RandomMeasurementService.cs&lt;br/&gt;public class RandomMeasurementService : &lt;br/&gt;    IMeasurementService&lt;br/&gt;{&lt;br/&gt;  public async IAsyncEnumerable&amp;lt;&lt;br/&gt;      TemperatureMeasurement&amp;gt; StartMeasurement(&lt;br/&gt;      [EnumeratorCancellation] CancellationToken &lt;br/&gt;      cancellationToken)&lt;br/&gt;  {&lt;br/&gt;    var random = new Random();&lt;br/&gt;    while (!cancellationToken.IsCancellationRequested)&lt;br/&gt;    {&lt;br/&gt;      yield return new TemperatureMeasurement(&lt;br/&gt;        DateTimeOffset.UtcNow,&lt;br/&gt;        16 + random.NextDouble() * 16);&lt;br/&gt;      &lt;br/&gt;      try&lt;br/&gt;      {&lt;br/&gt;        await Task.Delay(1000, cancellationToken)&lt;br/&gt;          .ConfigureAwait(false);&lt;br/&gt;      }&lt;br/&gt;      catch (OperationCanceledException)&lt;br/&gt;      {&lt;br/&gt;        break;&lt;br/&gt;      }&lt;br/&gt;    }&lt;br/&gt;  }&lt;br/&gt;} 
Ein besonderer Hinweis gilt an dieser Stelle dem Configure­Await(false) bei Task.Delay in der Methode StartMeasurement. ConfigureAwait(false) ist hier nicht zwingend notwendig, dafür aber gute Praxis, um die Hintergrundverarbeitung aus dem UI-Thread herauszuhalten. Avalonia ist ähnlich wie andere UI-Frameworks single-threaded und setzt auch einen SynchronizationContext ein. Das await würde damit automatisch nach der Wartezeit im UI-Thread weitermachen. Innerhalb des ViewModels oder allgemein in der View-Logik ist das zwar meist das gewünschte Verhalten, bei Hintergrundverarbeitung wie hier aber nicht. ConfigureAwait(false) unterdrückt den Rücksprung in den UI-Thread und macht standardmäßig mit einem Thread aus dem ThreadPool weiter.Dann erstellen wir das ViewModel unserer Applikation, hier einfach MainWindowViewModel genannt. Listing 2 zeigt, wie es mithilfe des CommunityToolkit.Mvvm gebaut wird. Wir erben von ObservableObject und definieren die Klasse als partial, damit der Roslyn Source Generator des Toolkits ­arbeiten kann. Der Member _currentMeasurement wird mit dem Attribut ObservableProperty markiert. Daraufhin generiert das CommunityToolkit.Mvvm automatisch die zugehörige Eigenschaft mit trigger für das NotifyPropertyChanged-Ereignis. Ähnlich bei der Methode StartMeasurementAsync: Das Attribut ReleayCommand sorgt hier dafür, dass das CommunityToolkit.Mvvm das zugehörige Command generiert. Das Command ist dabei vom Typ AsyncRelayCommand. Dieser Typ sorgt dafür, dass das Command nur einmal parallel laufen kann und dass wir es abbrechen können.
Listing 2: Das ViewModel für unsere Beispielapplikation
MainWindowViewModel.cs&lt;br/&gt;public partial class MainWindowViewModel(&lt;br/&gt;    IMeasurementService measurementService) : &lt;br/&gt;    ObservableObject&lt;br/&gt;{&lt;br/&gt;  [ObservableProperty]&lt;br/&gt;  private TemperatureMeasurement? _currentMeasurement;&lt;br/&gt;  &lt;br/&gt;  public ObservableCollection&amp;lt;TemperatureMeasurement&amp;gt;&lt;br/&gt;    Measurements { get; } = new();&lt;br/&gt;  [RelayCommand]&lt;br/&gt;  private async Task StartMeasurementAsync(&lt;br/&gt;      CancellationToken cancellationToken)&lt;br/&gt;  {&lt;br/&gt;    await foreach (var actMeasurement in &lt;br/&gt;        measurementService.StartMeasurement(&lt;br/&gt;        cancellationToken))&lt;br/&gt;    {&lt;br/&gt;      this.CurrentMeasurement = actMeasurement;&lt;br/&gt;      &lt;br/&gt;      this.Measurements.Add(actMeasurement);&lt;br/&gt;      if (this.Measurements.Count &amp;gt; 20)&lt;br/&gt;      {&lt;br/&gt;        this.Measurements.RemoveAt(&lt;br/&gt;          this.Measurements.Count - 1);&lt;br/&gt;      }&lt;br/&gt;    }&lt;br/&gt;  }&lt;br/&gt;} 
Zuletzt weisen wir das ViewModel noch unserer View zu. Es gibt viele Wege dafür; der für dieses Beispiel einfachste verläuft über die App.axaml.cs. Hier modifizieren wir die Methode OnFrameworkInitializationCompleted so, dass dort das ViewModel erzeugt und dem MainWindow zugewiesen wird.

App.axaml.cs
public partial class App : Application
{
  public override void Initialize()
  {
    AvaloniaXamlLoader.Load(this);
  }
  public override void OnFrameworkInitializationCompleted()
  {
    if (ApplicationLifetime is 
        IClassicDesktopStyleApplicationLifetime desktop)
    {
      var mainWindow = new MainWindow();
      mainWindow.DataContext = 
        new MainWindowViewModel(
          new RandomMeasurementService());
      desktop.MainWindow = mainWindow;
    }
    base.OnFrameworkInitializationCompleted();
  }
} 

Die Benutzeroberfläche definieren

Im nächsten Schritt können wir uns mit der Benutzeroberfläche selbst beschäftigen. Da die Applikation klein ist, reicht uns als einzige View ein Hauptfenster. Darin definieren wir das Layout und die Inhalte.In Listing 3 sehen wir den XAML-Code. Hervorzuheben ist hier das x:DataType in der achten Zeile. Damit sagen wir dem Compiler, welches ViewModel dieser View zugrunde liegt. Erst mit dieser Information funktionieren die Compiled Bindings, mit denen wir uns weiter oben im Artikel beschäftigt haben.
Listing 3: Inhalt der MainWindow.axaml, zuständig für das Layout der Beispielapplikation
&amp;lt;Window xmlns="https://github.com/avaloniaui"&lt;br/&gt;        xmlns:x="http://schemas.microsoft.com/&lt;br/&gt;          winfx/2006/xaml"&lt;br/&gt;        xmlns:d="http://schemas.microsoft.com/&lt;br/&gt;          expression/blend/2008"&lt;br/&gt;        xmlns:mc="http://schemas.openxmlformats.org/&lt;br/&gt;          markup-compatibility/2006"&lt;br/&gt;        xmlns:local=&lt;br/&gt;          "clr-namespace:HappyCoding.TemperatureViewer"&lt;br/&gt;        mc:Ignorable="d" d:DesignWidth="800" &lt;br/&gt;          d:DesignHeight="450"&lt;br/&gt;        x:Class=&lt;br/&gt;          "HappyCoding.TemperatureViewer.MainWindow"&lt;br/&gt;        x:DataType="local:MainWindowViewModel"&lt;br/&gt;        Title="Temperature Measurement"&amp;gt;&lt;br/&gt;  &amp;lt;DockPanel&amp;gt;&lt;br/&gt;    &lt;br/&gt;    &amp;lt;!-- Menu bar --&amp;gt;&lt;br/&gt;    &amp;lt;Menu DockPanel.Dock="Top"&amp;gt;&lt;br/&gt;      &amp;lt;MenuItem Name="MnuStartMeasuring" &lt;br/&gt;                Header="Start Measuring"&lt;br/&gt;                Command="{Binding Path=&lt;br/&gt;                  StartMeasurementCommand}" /&amp;gt;&lt;br/&gt;      &amp;lt;MenuItem Name="MnuStopMeasuring"&lt;br/&gt;                Header="Stop Measuring"&lt;br/&gt;                Command="{Binding Path=&lt;br/&gt;                  StartMeasurementCommand.Cancel}"&lt;br/&gt;                IsEnabled="{Binding Path=&lt;br/&gt;                  StartMeasurementCommand.&lt;br/&gt;                  CanBeCanceled}" /&amp;gt;&lt;br/&gt;    &amp;lt;/Menu&amp;gt;&lt;br/&gt;    &lt;br/&gt;    &amp;lt;Grid Margin="15"&lt;br/&gt;          ColumnDefinitions="300,10,*"&amp;gt;&lt;br/&gt;        &lt;br/&gt;      &amp;lt;!-- Current temperature --&amp;gt;&lt;br/&gt;      &amp;lt;HeaderedContentControl Grid.Column="0" &lt;br/&gt;                              Header="Current"&amp;gt;&lt;br/&gt;        &amp;lt;TextBlock Name="TxtCurrentTemperature" &lt;br/&gt;                   Text="{Binding CurrentMeasurement.&lt;br/&gt;                     TemperatureInDegrees, &lt;br/&gt;                     StringFormat='{}{0:F2}°'}" &lt;br/&gt;                   HorizontalAlignment="Center" &lt;br/&gt;                   VerticalAlignment="Center"&lt;br/&gt;                   FontSize="40"/&amp;gt;&lt;br/&gt;      &amp;lt;/HeaderedContentControl&amp;gt;&lt;br/&gt;    &lt;br/&gt;      &amp;lt;GridSplitter Grid.Column="1"&lt;br/&gt;                    HorizontalAlignment="Stretch" &lt;br/&gt;                    VerticalAlignment="Stretch" /&amp;gt;&lt;br/&gt;        &lt;br/&gt;      &amp;lt;!-- Details --&amp;gt;&lt;br/&gt;      &amp;lt;HeaderedContentControl Grid.Column="2" &lt;br/&gt;                              Header="Details"&amp;gt;&lt;br/&gt;        &amp;lt;Border Classes="GridBorder"&amp;gt;&lt;br/&gt;          &amp;lt;DataGrid ItemsSource="{Binding Measurements}"&lt;br/&gt;                    CanUserResizeColumns="True"&lt;br/&gt;                    CanUserSortColumns="False"&lt;br/&gt;                    IsReadOnly="True"&amp;gt;&lt;br/&gt;            &amp;lt;DataGrid.Columns&amp;gt;&lt;br/&gt;              &amp;lt;DataGridTextColumn Header="Timestamp"&lt;br/&gt;                 Binding="{Binding TimeStamp, &lt;br/&gt;                 StringFormat=&lt;br/&gt;                 'yyyy-MM-dd hh:mm:ss.fff zzz'}"&lt;br/&gt;                 Width="275"/&amp;gt;&lt;br/&gt;              &amp;lt;DataGridTextColumn Header="Temperature" &lt;br/&gt;                 Binding="{Binding TemperatureInDegrees, &lt;br/&gt;                 StringFormat='{}{0:F2}°'}"&lt;br/&gt;                 Width="125"/&amp;gt;&lt;br/&gt;            &amp;lt;/DataGrid.Columns&amp;gt;&lt;br/&gt;          &amp;lt;/DataGrid&amp;gt;&lt;br/&gt;        &amp;lt;/Border&amp;gt;&lt;br/&gt;      &amp;lt;/HeaderedContentControl&amp;gt;&lt;br/&gt;    &amp;lt;/Grid&amp;gt;&lt;br/&gt;  &amp;lt;/DockPanel&amp;gt;&lt;br/&gt;&amp;lt;/Window&amp;gt; 
Auch die beiden MenuItems sind einen Blick wert. Hier sehen wir, wie wir auf einfache Weise mit dem AsyncRelayCommand des CommunityToolkit.Mvvm arbeiten können. Die erste Schaltfläche Start Measuring bindet direkt gegen das Command und würde die Verarbeitung damit starten. Das Command sorgt im Hintergrund automatisch dafür, dass diese Schaltfläche deaktiviert ist, solange die Verarbeitung im Hintergrund läuft. Die zweite Schaltfläche Stop Measuring bindet direkt gegen die Methode Cancel des AsyncRelayCommand. Mehr als diese beiden Schaltflächen brauchen wir nicht, um den asynchronen Vorgang im Hintergrund starten und bei Bedarf wieder abbrechen zu können.Die Anzeige des zuletzt erhaltenen Temperaturwerts ist sehr einfach über ein zentriertes TextBlock-Element gehalten. Spannender dürfte die Anzeige der Historie sein, da wir dafür das bei Avalonia mitgelieferte DataGrid nutzen. Wir binden das DataGrid an die ObservableCollection aus dem ViewModel. Dadurch reagiert das DataGrid sofort auf neue oder gelöschte Einträge. Die Spalten des DataGrids können wir direkt an die Eigenschaften in der Datenquelle binden und dabei auch die bekannte Binding-Syntax nutzen (Listing 4).
Listing 4: Inhalte der Applikation – Anzeige der aktuellen Temperatur und der Historie
CurrentTemperatureView.axaml&lt;br/&gt;&amp;lt;UserControl xmlns="https://github.com/avaloniaui"&lt;br/&gt;             xmlns:x="http://schemas.microsoft.com/&lt;br/&gt;               winfx/2006/xaml"&lt;br/&gt;             xmlns:d="http://schemas.microsoft.com/&lt;br/&gt;               expression/blend/2008"&lt;br/&gt;             xmlns:mc="http://schemas.openxmlformats.&lt;br/&gt;               org/markup-compatibility/2006"&lt;br/&gt;             xmlns:local="clr-namespace:HappyCoding.&lt;br/&gt;               TemperatureViewer"&lt;br/&gt;             mc:Ignorable="d" d:DesignWidth="800" &lt;br/&gt;               d:DesignHeight="450"&lt;br/&gt;             x:Class="HappyCoding.TemperatureViewer.&lt;br/&gt;               Views.CurrentTemperatureView"&lt;br/&gt;             x:DataType="local:MainWindowViewModel"&amp;gt;&lt;br/&gt;  &amp;lt;TextBlock Text="{Binding CurrentMeasurement.&lt;br/&gt;                  TemperatureInDegrees, &lt;br/&gt;                  StringFormat='{}{0:F2}°'}" &lt;br/&gt;             HorizontalAlignment="Center" &lt;br/&gt;             VerticalAlignment="Center"&lt;br/&gt;             FontSize="40"/&amp;gt;&lt;br/&gt;&amp;lt;/UserControl&amp;gt;&lt;br/&gt;DetailsView.axaml&lt;br/&gt;&amp;lt;UserControl xmlns="https://github.com/avaloniaui"&lt;br/&gt;             xmlns:x="http://schemas.microsoft.com/&lt;br/&gt;               winfx/2006/xaml"&lt;br/&gt;             xmlns:d="http://schemas.microsoft.com/&lt;br/&gt;               expression/blend/2008"&lt;br/&gt;             xmlns:mc="http://schemas.openxmlformats.&lt;br/&gt;               org/markup-compatibility/2006"&lt;br/&gt;             xmlns:local="clr-namespace:HappyCoding.&lt;br/&gt;               TemperatureViewer"&lt;br/&gt;             mc:Ignorable="d" d:DesignWidth="800" &lt;br/&gt;               d:DesignHeight="450"&lt;br/&gt;             x:Class="HappyCoding.TemperatureViewer.&lt;br/&gt;               Views.DetailsView"&lt;br/&gt;             x:DataType="local:MainWindowViewModel"&amp;gt;&lt;br/&gt;  &amp;lt;Border Classes="GridBorder"&amp;gt;&lt;br/&gt;    &amp;lt;DataGrid ItemsSource="{Binding Measurements}"&lt;br/&gt;              CanUserResizeColumns="True"&lt;br/&gt;              CanUserSortColumns="False"&lt;br/&gt;              IsReadOnly="True"&amp;gt;&lt;br/&gt;      &amp;lt;DataGrid.Columns&amp;gt;&lt;br/&gt;        &amp;lt;DataGridTextColumn Header="Timestamp"&lt;br/&gt;                            Binding="{Binding &lt;br/&gt;                              TimeStamp}"&lt;br/&gt;                            Width="225"/&amp;gt;&lt;br/&gt;        &amp;lt;DataGridTextColumn Header="Temperature" &lt;br/&gt;                            Binding="{Binding &lt;br/&gt;                              TemperatureInDegrees, &lt;br/&gt;                              StringFormat='{}{0:F2}°'}"&lt;br/&gt;                            Width="125"/&amp;gt;&lt;br/&gt;      &amp;lt;/DataGrid.Columns&amp;gt;&lt;br/&gt;    &amp;lt;/DataGrid&amp;gt;&lt;br/&gt;  &amp;lt;/Border&amp;gt;&lt;br/&gt;&amp;lt;/UserControl&amp;gt; 

Das mitgelieferte DataGrid

Wie bereits angesprochen liefert Avalonia im Standard ein DataGrid. Darauf soll an dieser Stelle gesondert eingegangen werden, da es für professionelle Applikationen ein nahezu unverzichtbares Feature ist. Man darf hier kein DataGrid wie von den großen Komponentenherstellern erwarten, bekommt aber immerhin einen guten Basisumfang an Features. So unterstützt es etwa Virtualisierung, damit nur der sichtbare Teil des DataGrids gerendert wird.Eine einfache Anwendung des DataGrids sehen wir im Beispiel in den Downloads zum Artikel. Man kann es gegen eine beliebige Auflistung binden und Spalten definieren (oder automatisch erzeugen lassen). Es gibt mitgelieferte Spaltentypen für Text, für Booleans und eine DataGridTemplate­Column, bei der die UI-Elemente in den Zellen selbst per Template bestimmt werden können.Gruppier- und Filterfunktionen werden vom DataGrid nicht direkt als Features angeboten, dafür aber über die Klasse Data­GridCollectionView bereitgestellt. Letztere wird im ViewModel erzeugt; das DataGrid bindet anschließend dagegen.Ein weiteres Feature, die Alternating Row Colors, können wir nicht direkt am DataGrid aktivieren. Stattdessen nutzen wir dafür das mächtige, an CSS angelehnte Styling-System von Avalonia. Es reicht, den Style aus Listing 5 dafür zu definieren.
Listing 5: DataGrid-Style für Alternating Row Colors
&amp;lt;Style Selector="DataGridRow:nth-child(odd)"&amp;gt;&lt;br/&gt;  &amp;lt;Setter Property="Background" Value="#20AAAAAA"/&amp;gt;&lt;br/&gt;&amp;lt;/Style&amp;gt; 

Avalonia DevTools

Features wie Hot Reload oder als Alternative einen visuellen Designer, wie wir ihn etwa noch aus den guten alten Win­dows-Forms-Zeiten kennen, fehlen in Avalonia. Mit Avalonia Accelerate (siehe den Kasten Avalonia Accelerate) arbeitet das Avalonia-Team zum Zeitpunkt der Abfassung dieses Artikels genau an diesen Punkten [7].

Avalonia Accelerate

Avalonia selbst ist Open Source und damit frei verfüg- und verwendbar, auch für kommerzielle Projekte. Darauf aufbauend bietet das Avalonia-Team seit April 2025 mit dem Produkt ­Avalonia Accelerate diverse Erweiterungen an, die unter einer Lizenzgebühr verkauft werden.
Im Open-Source-Paket von Avalonia werden schon seit ­geraumer Zeit integrierte DevTools angeboten, die über das ­NuGet-Paket Avalonia.Diagnostics bereitgestellt werden. Die beschriebenen Templates binden das NuGet-Paket direkt mit ein. Ein Tastendruck auf [F12] genügt, um das DevTools-Fenster zu öffnen. Die DevTools helfen bei mehreren Aufgaben; so lassen sich Logical Tree und Visual Tree prüfen, ob sie die erwarteten Inhalte aufweisen. Der Logical Tree bildet dabei die Baumstruktur aus den XAML-Dateien ab. Der Visual Tree zeigt, was tatsächlich am Bildschirm gerendert wird – aufgrund von Templating in der Regel deutlich mehr. Bild 4 zeigt, wie die DevTools im vorgestellten Beispiel aussehen.
Avalonia DevTools am gezeigten Beispiel (Bild 4) © Autor
Noch ein Hinweis an alle Leserinnen und Leser, die Hot Reload oder einen Designer vermissen. In den DevTools können Werte von Eigenschaften direkt live geändert werden. Damit sieht man sofort, welche Änderungen zu welchen Effekten auf der Benutzeroberfläche führen.Neben der Anzeige der Baumstrukturen bieten die DevTools auch weitere Features wie ein Tracking der von den Controls ausgelösten Events. Performance-relevante Kennzahlen wie Rendering-Zeit und Layout-Zeit können ebenfalls eingeblendet werden. Unterm Strich sind die DevTools auf ­jeden Fall mehr als nur einen Blick wert.

Testautomatisierung mit Avalonia.Headless

Um die Beispielapplikation ordentlich abzuschließen, fehlen noch automatisierte Tests. Hierfür gibt uns Avalonia mit Avalonia.Headless ein besonderes Werkzeug in die Hand. Avalonia.Headless ermöglicht es, eine Avalonia-Applikation innerhalb von Tests auszuführen, ohne dafür den Overhead des Renderings zu benötigen.In Listing 6 sehen wir als Beispiel einen Test auf Basis des bekannten Test-Frameworks xUnit und Avalonia.Headless. Hierin erstellen wir zunächst einen Mock des IMeasurementService, öffnen dann das Hauptfenster und simulieren einen Klick auf den Button Start Measurement. Anschließend prüfen wir, ob der erwartete Temperaturwert auf dem UI angezeigt wird.
Listing 6: Testautomatisierung mit Avalonia.Headless am Beispiel des aktuellen Messwerts
apublic class MainWindowTests&lt;br/&gt;{&lt;br/&gt;  [AvaloniaFact]&lt;br/&gt;  public void StartTemparatureMeasurement()&lt;br/&gt;  {&lt;br/&gt;    // Arrange&lt;br/&gt;    var testData = new[]&lt;br/&gt;    {&lt;br/&gt;      new TemperatureMeasurement(&lt;br/&gt;        DateTimeOffset.UtcNow, 10.5)&lt;br/&gt;    };&lt;br/&gt;    &lt;br/&gt;    var mockedMeasurementService = &lt;br/&gt;      Substitute.For&amp;lt;IMeasurementService&amp;gt;();&lt;br/&gt;    mockedMeasurementService.StartMeasurement(&lt;br/&gt;      Arg.Any&amp;lt;CancellationToken&amp;gt;())&lt;br/&gt;      .Returns(_ =&amp;gt; testData.ToAsyncEnumerable());&lt;br/&gt;    &lt;br/&gt;    var mainWindow = new MainWindow();&lt;br/&gt;    mainWindow.DataContext = new &lt;br/&gt;      MainWindowViewModel(mockedMeasurementService);&lt;br/&gt;    mainWindow.Show();&lt;br/&gt;    &lt;br/&gt;    // Act&lt;br/&gt;    var mnuStartMeasuring = mainWindow.GetControl&lt;br/&gt;      &amp;lt;MenuItem&amp;gt;("MnuStartMeasuring");&lt;br/&gt;    mnuStartMeasuring.SimulateClick();&lt;br/&gt;    &lt;br/&gt;    // Assert&lt;br/&gt;    var txtCurrentTemperature = mainWindow.GetControl&lt;br/&gt;      &amp;lt;TextBlock&amp;gt;("TxtCurrentTemperature");&lt;br/&gt;    Assert.Equal("10.50°", &lt;br/&gt;      txtCurrentTemperature.Text);&lt;br/&gt;  }&lt;br/&gt;} 
Im Beispiel nutzen wir zusätzlich das NuGet-Paket Avalonia.Headless.XUnit, das sich um das allgemeine Setup des Test-Frameworks und um den Umgang mit dem UI-Thread kümmert. Das Attribut AvaloniaFact kommt aus diesem Paket und markiert Tests, die mit UI-Bestandteilen ausgeführt werden sollen.

Weitere Features

In diesem Artikel haben wir nur einen Bruchteil der Features von Avalonia behandeln können. Speziell für Linux bietet Avalonia noch ein weiteres spannendes Feature an: Es ist möglich, unter Linux direkt in den Framebuffer zu rendern. Dadurch können UI-Applikationen bereitgestellt werden, die keine Desktop-Umgebung wie Gnome oder KDE benötigen. Dieses Feature ist insbesondere für Entwicklungen auf Embedded Linux relevant.Mit diesem und weiteren Features werden wir uns in ausschließlich online veröffentlichten Folgeartikeln beschäftigen, siehe Kasten Sie möchten gerne mehr erfahren?

Sie möchten gerne mehr erfahren?

Das freut uns, denn ab Erscheinen des vorliegenden Artikels veröffentlichen wir auf unserer Webseite in unregelmäßigen Abständen zusätzlich Beiträge, die den Artikel fortsetzen und ausschließlich online zur Verfügung stehen. Derzeit geplant sind Beiträge zu Themen wie:

Sie möchten gerne mehr erfahren?

Das freut uns, denn ab Erscheinen des vorliegenden Artikels veröffentlichen wir auf unserer Webseite in unregelmäßigen Abständen zusätzlich Beiträge, die den Artikel fortsetzen und ausschließlich online zur Verfügung stehen. Derzeit geplant sind Beiträge zu Themen wie:

Fazit

Avalonia gibt uns alle notwendigen Werkzeuge in die Hand, um Benutzeroberflächen nicht nur für Linux, sondern auch für andere Plattformen zu entwickeln. Insbesondere Entwickler mit Erfahrung in XAML-basierten Frameworks finden sich schnell zurecht. Aber auch der Umstieg von anderen Frameworks wie dem nach wie vor verbreiteten Windows Forms ist kein Ding der Unmöglichkeit – hier muss allerdings Zeit zum Erlernen der XAML-Syntax und des MVVM-Patterns hinzugerechnet werden.Avalonia gibt uns alle notwendigen Werkzeuge in die Hand, um Benutzeroberflächen nicht nur für Linux, sondern auch für andere Plattformen zu entwickeln. Insbesondere Entwickler mit Erfahrung in XAML-basierten Frameworks finden sich schnell zurecht. Aber auch der Umstieg von anderen Frameworks wie dem nach wie vor verbreiteten Windows Forms ist kein Ding der Unmöglichkeit – hier muss allerdings Zeit zum Erlernen der XAML-Syntax und des MVVM-Patterns hinzugerechnet werden.Gerade der hier im Artikel in den Fokus gerückte Support für Linux dürfte für viele Leserinnen und Leser spannend sein. Linux spielt insbesondere in der Embedded-Welt eine sehr große Rolle. Avalonia kann hier als mächtiges UI-Framework mit ebenso mächtigem .NET-Unterbau punkten. Gerade der hier im Artikel in den Fokus gerückte Support für Linux dürfte für viele Leserinnen und Leser spannend sein. Linux spielt insbesondere in der Embedded-Welt eine sehr große Rolle. Avalonia kann hier als mächtiges UI-Framework mit ebenso mächtigem .NET-Unterbau punkten.Das Avalonia-Team stellt sich aus Sicht des Autors sinnvoll für die Zukunft auf. Man erweitert das Open-Source-Paket mit Avalonia Accelerate um kommerzielle Zusatzkomponenten, die für professionelle Entwickler zusätzliche Produkti­vität und Features liefern. Auch XPF kann gerade bei einer größeren Migration eines WPF-Projekts spannend sein. Ganz nebenbei finanzieren diese Produkte die Weiterentwicklung am Open-Source-Paket Avalonia selbst, das auch für kommerzielle Projekte kostenlos zur Verfügung steht.Das Avalonia-Team stellt sich aus Sicht des Autors sinnvoll für die Zukunft auf. Man erweitert das Open-Source-Paket mit Avalonia Accelerate um kommerzielle Zusatzkomponenten, die für professionelle Entwickler zusätzliche Produkti­vität und Features liefern. Auch XPF kann gerade bei einer größeren Migration eines WPF-Projekts spannend sein. Ganz nebenbei finanzieren diese Produkte die Weiterentwicklung am Open-Source-Paket Avalonia selbst, das auch für kommerzielle Projekte kostenlos zur Verfügung steht.Zum Abschluss dieses Artikel sei noch auf den App-Showcase auf der Website von Avalonia [8] verwiesen. Darin listet das Avalonia-Team eine längere Liste von Applikationen, die auf Basis von Avalonia realisiert wurden. Hier bekommt man einen guten Eindruck darüber, was mit Avalonia möglich ist.Zum Abschluss dieses Artikel sei noch auf den App-Showcase auf der Website von Avalonia [8] verwiesen. Darin listet das Avalonia-Team eine längere Liste von Applikationen, die auf Basis

Fussnoten

  1. Fabian Hügle, Das bessere WPF, dotnetpro 1/2024, Seite 34 ff., http://www.dotnetpro.de/A2401Avalonia
  2. Avalonia – Cross-platform WPF, https://avaloniaui.net/xpf
  3. Avalonia – Running on Raspberry Pi, http://www.dotnetpro.de/SL2506-07Avalonia1
  4. Avalonia – Set Up an Editor, http://www.dotnetpro.de/SL2506-07Avalonia2
  5. Tam Hanna, Grafisches UI für WSL, dotnetpro 2/2022, Seite 29 ff., http://www.dotnetpro.de/A2202WSLUI
  6. Avalonia-Templates auf GitHub, http://www.dotnetpro.de/SL2506-07Avalonia3
  7. Avalonia Accelerate, https://avaloniaui.net/accelerate
  8. Avalonia – App Showcase, https://avaloniaui.net/showcase

Neueste Beiträge

DWX hakt nach: Wie stellt man Daten besonders lesbar dar?
Dass das Design von Websites maßgeblich für die Lesbarkeit der Inhalte verantwortlich ist, ist klar. Das gleiche gilt aber auch für die Aufbereitung von Daten für Berichte. Worauf besonders zu achten ist, erklären Dr. Ina Humpert und Dr. Julia Norget.
3 Minuten
27. Jun 2025
DWX hakt nach: Wie gestaltet man intuitive User Experiences?
DWX hakt nach: Wie gestaltet man intuitive User Experiences? Intuitive Bedienbarkeit klingt gut – doch wie gelingt sie in der Praxis? UX-Expertin Vicky Pirker verrät auf der Developer Week, worauf es wirklich ankommt. Hier gibt sie vorab einen Einblick in ihre Session.
4 Minuten
27. Jun 2025
„Sieh die KI als Juniorentwickler“
CTO Christian Weyer fühlt sich jung wie schon lange nicht mehr. Woran das liegt und warum er keine Angst um seinen Job hat, erzählt er im dotnetpro-Interview.
15 Minuten
27. Jun 2025
Miscellaneous

Das könnte Dich auch interessieren

Mythos Motivation - Teamentwicklung
Entwickler bringen Arbeitsfreude und Engagement meist schon von Haus aus mit. Diesen inneren Antrieb zu erhalten sollte für Führungskräfte im Fokus stehen.
13 Minuten
19. Jan 2017
Evolutionäres Prototyping von Business-Apps - Low Code/No Code und KI mit Power Apps
Microsoft baut Power Apps zunehmend mit Features aus, um die Low-Code-/No-Code-Welt mit der KI und der professionellen Programmierung zu verbinden.
19 Minuten
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige