FlaUI, Erfahrung aus dem Projekt

Vorwort

Automatisiertes Testen von grafischen Oberflächen ist ein wichtiges Thema. Für jede GUI-Technologie gibt es mehrere Bibliotheken, die es sorgsam auszuwählen gilt, um zügig ein qualitativ hochwertiges und akkurates Ergebnis zu bekommen.  

Im Bereich Web-Technologien gibt es viele bekannte Frameworks wie Selenium, Playwright, Cypress und viele mehr. Aber auch im Bereich WPF oder Winforms gibt es passende Vertreter und ich möchte heute FlaUI vorstellen.

FlaUI ist eine .NET-Klassenbibliothek für automatisiertes Testen von Windows Apps, insbesondere der UI. Sie baut auf den hauseigenen Microsoft Bibliotheken für UI Automation auf.

Abbildung: Pyramide der Test-Automatisierung

Geschichte

Am 20. Dezember 2016 hat Roman Roemer die erste Fassung von FlaUI auf Github veröffentlicht. Unter der Version 0.6.1 wurden die ersten Schritte in Richtung Klassenbibliothek für das Testen von .NET Produkten gelegt. Seitdem wurde diese konsequent und mit großem Eifer weiterentwickelt, um die Bibliothek mit neuen und besseren Funktionen zu erweitern. Die neueste Version trägt die Versionsnummer 4.0.0 und bietet Features wie z.B. das Automatisieren von WPF und Windows Store App Produkten sowie das Tool FlaUI Inspect, welches die Struktur von .NET Produkten ausliest und wiedergibt.

Installation

Über GitHub oder NuGet kann FlaUI geladen und installiert werden. Zudem werde ich für diesen Artikel und das nachfolgende Beispiel noch weitere Plugins / Frameworks sowie Klassenbibliotheken verwenden wie:

  • C# von OmniSharp
  • C# Extensions von Jchannon
  • NuGet Package Manager von Jmrog
  • .NET Core Test Explorer von Jun Han
  • Die neueste Windows SDK
  • NUnit Framework

Beispiel

In diesem Beispiel werde ich mit mehreren unterschiedlichen Methoden eine typisches Windows-App, hier den Taskmanager, maximieren bzw. den Ausgangszustand wiederherstellen. Außerdem sollen unterschiedliche Elemente markiert werden.

Bei der Arbeit an diesem Artikel ist mir aufgefallen, dass Windows ein besonderes Verhalten aufweist: Wird ein Programm maximiert, ändert sich nicht nur der Name und andere Eigenschaften des Buttons, sondern auch die AutomationID. Das führte dazu, dass ich den Methodenaufrufen zwei verschiedene Übergabestrings für die AutomationID, „Maximize“ und „Restore“, mitgeben musste, die beide denselben Button ansprechen.

Code (C#)

Zuerst starten wir die gewünschte Anwendung und legen uns eine Instanz des Fensters für die weitere Verwendung an:

var app = Application.Launch(@"C:WindowsSystem32Taskmgr.exe");
var automation = new UIA2Automation();
var mainWin = app.GetMainWindow(automation);

Desweiteren benötigen wir noch die Helper Class ConditionFactory:

ConditionFactory cf = new ConditionFactory(new UIA2PropertyLibrary());

Diese Helper Class bietet uns die Möglichkeit, Objekte nach bestimmten Bedingungen zu suchen. Wie zum Beispiel, die Suche nach einem Objekt mit einer bestimmten ID.

In den folgenden Methoden wollen wir, wie oben erwähnt, das Programm maximieren und wieder den Ausgangszustand herstellen. Außerdem wollen wir Elemente hervorheben:

Für die erste Methode werden wir mit FindFirstDescendant und FindAllDescendant arbeiten. FindAllDescendant sucht sich alle Elemente, die unterhalb des Ausgangselements liegen.  FindFirstDescendant findet das erste Element unterhalb des Ausgangselements, das mit der übergebenen Suchbedingung übereinstimmt und mit DrawHighlight wird ein roter Rahmen um das Element gelegt.

        static void FindWithDescendant(Window window, string condition, string expected)
        {
            window.FindFirstDescendant(cf => cf.ByAutomationId(condition)).AsButton().Click();
            var element = window.FindFirstDescendant(cf =>
            cf.ByAutomationId("TmColumnHeader")).FindAllDescendants();            
            foreach(var Item in element)
            {
                if(Item.IsOffscreen != true)
                {
                    Item.DrawHighlight();
                }
            }
            Assert.IsNotNull(window.FindFirstDescendant(cf => cf.ByAutomationId(expected)));
        }

In der zweiten Methode nutzen wir FindFirstChild und FindAllChildren. Beide funktionieren fast gleich wie Descendant, nur dass hier nicht alle Elemente gefunden werden, sondern nur die, die direkt unter dem Ausgangselement liegen.

        static void FindWithChild(Window window, string condition, string expected)
        {
            window.FindFirstChild(cf => cf.ByAutomationId("TitleBar")).FindFirstChild(cf =>
            cf.ByAutomationId(condition)).AsButton().Click();
            var element = window.FindAllChildren();
            foreach(var Item in element)
            {
                    Item.DrawHighlight();
            }
            Assert.IsNotNull(window.FindFirstDescendant(cf => cf.ByAutomationId(expected)));
        }

Und für die dritte Methode nehmen wir FindFirstByXPath und FindAllByXPath. Hier müssen wir, wie der Name sagt, den Pfad angeben. Bei First sollte es der genaue Pfad zum gewünschten Element sein und bei FindAll werden alle Elemente gesucht, die in dem Pfad gefunden werden können. Solltet ihr ein Programm untersuchen, welches ihr nicht kennt, hilft es, FlaUI Inspect zu nutzen, welches Eigenschafen wie den Pfad anzeigen, aber auch andere Informationen über Elemente von Windows-Apps darstellen kann.

        static void FindWithXPath(Window window, string expected)
        {
            window.FindFirstByXPath("/TitleBar/Button[2]").AsButton().Click();
            var element = window.FindAllByXPath("//TitleBar/Button");
            foreach(var Item in element)
            {
                    Item.DrawHighlight();
            }
            Assert.IsNotNull(window.FindFirstDescendant(cf => cf.ByAutomationId(expected)));
        }

Als letztes müssen wir nur noch die Methoden aufrufen und ihnen die gewünschten Werte übergeben. Zum einen das Fenster was wir uns am Anfang angelegt haben und zum anderen die AutomationID des Maximieren-Buttons, welche sich wie erwähnt ändert, sobald der Button betätigt wurde.

     FindWithDescendant(mainWin,"Maximize", "Restore");
       FindWithChild(mainWin,"Restore", "Maximize");
       FindWithXPath(mainWin,"Restore");

Im Code sieht das bei mir wie folgt aus:

Schwachpunkte

Ein Problem sind selbst gebaute Objekte z.B. hatten wir in einem Projekt mit selbsterstellten Polygonen Buttons angelegt. Diese konnten weder von FlaUI Inspect noch von FlaUI selbst gefunden werden, was die Nutzung dieser in unseren Autotests stark eingeschränkt hat. Für solche Objekte muss ein AutomationPeer (stellt Basisklasse bereit, die das Objekt für die UI-Automatisierung nutzbar macht) angelegt werden, damit diese gefunden werden können.

Fazit und Zusammenfassung

FlaUI unterstützt mit UIA2 Forms und Win 32 Anwendungen sowie mit UIA3 WPF und Windows Store Apps. Es ist einfach zu bedienen und übersichtlich, da es mit relativ wenig Grundfunktionen auskommt. Außerdem kann es jederzeit mit eigenen Methoden und Objekten erweitert werden.

Auch die Software-Entwickler:innen sind zufrieden, da sie keine extra Schnittstellen und damit keine potenziellen Fehlerquellen für die Testautomatisierung einbauen müssen. Gerade weil FlaUI uns die Möglichkeit gibt, direkt die Objekte des zu testenden Programmes abgreifen zu können, brauchen wir keine zusätzliche Arbeitszeit darauf zu verwenden, für das Testing größere und fehleranfällige Anpassungen an der bestehenden Programmstruktur vorzusehen und zu verwalten.

Andererseits muss man um jedes Objekt automatisiert ansprechen zu können, seine AutomationID im Testcode mindestens einmal hinterlegen, damit sie für den Test auch nutzbar ist. Dies bedeutet, dass man praktisch die ungefähre Struktur des zu testenden Programmes nachbilden muss, was gerade bei komplexeren Programmen aufwändig sein kann. Und um dabei die Übersichtlichkeit zu erhalten, sollte man diese geclustert in mehreren, namentlich Aussagekräftigen, Klassen hinterlegen.

Wir werden es auf alle Fälle weiterverwenden und auch unseren Kolleg:innen empfehlen.

Test Driven Development für Blazor-Webkomponenten mit bUnit

Im vorherigen Artikel wurde das Blazor-Framework vorgestellt, mit dem mit C# Web-UIs erstellt werden können. In diesem zweiten Artikel wollen wir uns eine Test-Bibliothek ansehen, die in den Top10 der NuGet-Downloads für Blazor rangiert [1].

bUnit ist eine Bibliothek, mit der Unit Tests für Blazor-Komponenten erstellt werden. Sie ist seit 2021 als stabile Version verfügbar [2] und wird in der offiziellen Blazor-Dokumentation für Komponententests empfohlen [3].

Die Dokumentation für bUnit findet sich unter https://bunit.dev/

Razor-Syntax für eine Web-UI mit C# statt JavaScript

Alle Bausteine einer Blazor-App sind Razor-Komponenten mit der Dateiendung .razor. Darin wird die Website oder ein Teil davon in der sogenannten Razor-Syntax beschrieben. Das ist ein Mix aus HTML, speziellem Razor-Markup und C#. CSS wird zum Stylen verwendet.

Die klassische Blazor-Demo-Seite mit einem Zähler sieht so aus:

@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

Abbildung 1: Seite Counter.razor

In diesem Beispiel ist HTML und C# in einer Datei enthalten. Für komplexere Komponenten ist es hilfreich, beides voneinander zu trennen, indem eine code-behind-Datei angelegt wird. Die Datei für unsere Komponente Counter.razor heißt dann Counter.razor.cs. Der Compiler verknüpft die beiden Dateien automatisch miteinander, sodass uns IntelliSense beim Programmieren auf die Sprünge hilft.

Um die Code-Ausschnitte in diesem Artikel kürzer zu halten, wurde für alle Beispiele Markup und C# mit einer code-behind-Datei getrennt.

Komponenten in Blazor: Razor Components

Um Elemente zu kapseln und wiederverwendbar zu machen, realisiert man Funktionalitäten in Komponenten. Diese enthalten nur das Markup und den Code für eine einzelne Funktionalität. In der Website werden dann nur Komponenten eingefügt und eventuell Parameter und Callbacks angebunden. Komponenten können in eine Library verschoben und als NuGet-Pakete bereitgestellt werden.

Das Beispiel zeigt den gleichen Counter, nachdem er in einer Komponente CounterComponent.razor gekapselt wurde. Das visuelle Ergebnis im Browser ist gleich.

@page "/counter"
@using BlazorWasmTemplateApp.Components
<PageTitle>Counter</PageTitle>

<h1>CounterComponent</h1>
<CounterComponent/>

Abbildung 2: Seite Counter.razor mit Komponente

<p id="currentCount" role="status">Current count: @currentCount</p>

<button id="incrementCountButton" class="btn btn-primary" @onclick="IncrementCount">Click me</button>

Abbildung 3: Markup für Counter-Komponente CounterComponent.razor

using Microsoft.AspNetCore.Components;

namespace BlazorWasmTemplateApp.Components
{
    public partial class CounterComponent : ComponentBase
    {
        private int currentCount = 0;

        private void IncrementCount()
        {
            currentCount++;
        }
    }
}

Abbildung 4: Code für Counter-Komponente CounterComponent.razor.cs

Unit Tests für die UI

Üblicherweise wird die UI einer Anwendung mit Ende-zu-Ende-Tests (E2E) überprüft. In der Web-Entwicklung kommen dafür Frameworks wie Selenium oder Playwright zum Einsatz. Diese Tests starten die gesamte Anwendung, rendern sie in einem Browser und führen dann die Testschritte durch. In solchen Tests wird eine Funktionalität oft durch die gesamte Anwendung hindurch getestet. Aufgrund der Komplexität sind E2E-Tests oft wartungsintensiv und langsam. Daher ist die allgemein anerkannte Empfehlung, nur einige wenige E2E-Tests und viele Komponententests zu schreiben (Testpyramide, siehe [4]).

Wenn man die UI testgetrieben nach den Prinzipien von Test Driven Development (TDD) entwickelt oder eine Benutzeroberfläche mit umfangreicher Logik programmiert, benötigt man viele Tests, die in kurzer Zeit ausgeführt werden. Daher hat es in solchen Fällen Nachteile, wenn man ausschließlich auf E2E-Tests setzt. bUnit-Tests sind das Gegenteil zu E2E-Tests, sie sind übersichtlich, isoliert voneinander und vor allem schnell. Ein Test benötigt oft nur einige Millisekunden für die Ausführung und fühlt sich an wie ein Unit Test im Backend.

bUnit ist auch eine gute Wahl, wenn man eine Bibliothek von Razor-Komponenten aufbaut. Nicht umsonst finden sich unter den Hauptsponsoren von bUnit Firmen wie Progress Telerik und Syncfusion, die u.a. Bibliotheken von Blazor-UI-Komponenten vertreiben.

Unit Tests für die UI mit bUnit sind also eine gute Ergänzung zu E2E-Tests, um Details der UI zu testen.

Erste Schritte mit bUnit

Vorbereitungen

bUnit ist kein Framework, sondern auf die Test Runner xUnit, NUnit oder MSTest angewiesen. Für die Beispiele in diesem Artikel wird xUnit verwendet. Die Ausführung der Tests erfolgt wie gewohnt über die IDE oder über die Kommandozeile mit dotnet test.
Da wir mit bUnit Razor-Komponenten testen wollen, müssen wir das SDK in der Projektdatei unseres Testprojekts auf Microsoft.NET.Sdk.Razor ändern. Details dazu finden sich in der bUnit-Dokumentation unter [5].

bUnit ist mit Test-Projekten in der Version .NET Standard 2.1 und mit allen .NET Core-Versionen ab 3.1 kompatibel.

Tests können in reinem C# oder in Razor-Syntax programmiert werden. Aus Gründen der Verständlichkeit wird ausschließlich die Variante mit C#-Code gezeigt. Mit Razor-Syntax ist es etwas einfacher, Parameter und Callbacks zu übergeben und HTML direkt mit einem erwarteten Markup zu vergleichen. Da ein Vergleich von HTML aber zu schlecht wartbaren Tests führt, ist dieser Vorteil der Razor-Syntax in der Praxis unwichtig.

Der erste Test

Wie könnte ein Test unserer CounterComponent aussehen? Wir wollen überprüfen, ob unser Zähler im Ausgangszustand 0 und nach einem Klick 1 anzeigt.

using Bunit;

namespace Tests
{
    public class CounterComponentTests : TestContext
    {

        [Fact]
        public void CurrentCount_IsZero_ByDefault()
        {
            //Arrange
            var componentUnderTest = RenderComponent<CounterComponent>();

            //Act
            AngleSharp.Dom.IElement paragraph = componentUnderTest.Find("#currentCount");

            //Assert
            Assert.Equal("Current count: 0", paragraph.InnerHtml);
        }

        [Fact]
        public void CurrentCount_Increases_WhenButtonClicked()
        {
            //Arrange
            var componentUnderTest = RenderComponent<CounterComponent>();
            AngleSharp.Dom.IElement button = componentUnderTest.Find("#incrementCountButton");

            //Act
            button.Click();

            //Assert
            AngleSharp.Dom.IElement paragraph = componentUnderTest.Find("#currentCount");
            Assert.Equal("Current count: 1", paragraph.InnerHtml);
        }
    }
}

Abbildung 5: Einfacher Test der CounterComponent

Die generische Methode RenderComponent<TComponent>() mit TComponent als Typ der zu testenden Komponente erstellt ein Rendering. Dieses Rendering unserer CounterComponent mit seinen Attributen, Methoden und Eigenschaften entspricht der Darstellung der Komponente in einem Browser.

Um Interaktionen durchzuführen und den Inhalt des Renderings zu überprüfen, können wir Elemente mit der Methode Find() identifizieren. Diese Methode erwartet einen CSS-Selektor vom Typ string als Parameter. Ein CSS-Selektor ist ein Filter für die Elemente in einem HTML-DOM, der nach CSS-Klassen, Attributen, IDs oder HTML-Element-Typen filtern kann. Eine gute Referenz für CSS-Selektoren findet sich unter [6].

Der CSS-Selektor ‚#‘ filtert nach der ID des Elements. Der Selektor #currentCount filtert also das Element mit id=“currentCount“ heraus, das in unserer Beispielkomponente den aktuellen Wert des Zählers enthält.

Um zu ermitteln, ob unser Test erfolgreich ist, vergleichen wir den HTML-Inhalt mit einem erwarteten Text. Solche Tests sind fragil und wartungsintensiv, aber ein gutes Demo-Beispiel.

Komponenten mit Parametern testen

Wie sieht ein Test für eine Komponente mit Parametern aus? Dazu erweitern wir unsere CounterComponent um einen Parameter, mit dem wir den Startwert setzen können. Da wir unsere Komponente testgetrieben entwickeln wollen, fügen wir als erstes einen neuen Test hinzu. Darin nutzen wir eine Überladung der Methode RenderComponent() mit der man per Lambda-Ausdruck Parameter an einen ParameterBuilder übergibt.

Da wir testgetrieben vorgehen und es den Parameter StartValue in unserer CounterComponent noch gar nicht gibt, ist unser Test jetzt noch nicht kompilierbar.

[Fact]
public void CurrentCount_IsNotZero_WhenStartValueSet()
{
    //Arrange
    int startValue = 42;
    var componentUnderTest = RenderComponent<CounterComponent>(
        parameterBuilder => parameterBuilder.Add(param => param.StartValue, startValue));

    //Act
    AngleSharp.Dom.IElement paragraph = componentUnderTest.Find("#currentCount");

    //Assert
    Assert.Equal("Current count: " + startValue, paragraph.InnerHtml);
}

Abbildung 6: Test für unsere Komponente mit einem Parameter

Damit der Test kompiliert, müssen wir unsere Komponente CounterComponenterweitern. Im ersten Schritt fügen wir das Property StartValue vom Typ inthinzu und versehen es mit dem Attribut Parameter. Jetzt kann unsere Komponente kompiliert werden. Anschließend führen wir unseren neuen Test aus und sehen, dass er fehlschlägt. Wir befinden uns also noch in der roten Phase des Red-Green-Refactor-Zyklus von TDD und müssen die Implementierung anpassen. Dazu initialisieren wir unsere Variable currentCountin der Methode OnParametersSet(). Diese Methode wird von Blazor aufgerufen, nachdem die Parameter an eine Komponente übergeben wurden. Jetzt können wir den Test erneut ausführen und sehen, dass er erfolgreich ist. Wir sind also in der grünen Phase unseres TDD-Zyklus. Wir verzichten hier auf Änderungen in der Refactoring-Phase eines TDD-Zyklus.

Die code-behind-Datei unserer Komponente sieht nun so aus:

using Microsoft.AspNetCore.Components;

namespace BlazorWasmTemplateApp.Components
{
    public partial class CounterComponent : ComponentBase
    {
        [Parameter]
        public int StartValue { get; set; } = 0;

        private int currentCount = 0;

        protected override void OnParametersSet()
        {
            currentCount = StartValue;
        }

        private void IncrementCount()
        {
            currentCount++;
        }
    }
}

Abbildung 7: Die um einen Parameter erweiterte Komponente

Komponenten mit Events testen

Ein weiteres typisches Element von Razor-Komponenten sind Events. Nehmen wir an, dass unsere Website informiert werden möchte, wenn sich der Wert unseres Zählers ändert. Dazu erweitern wir unsere Tests abermals um den folgenden Test:

[Fact]
public void OnCountIncremented_WasInvoked_WhenButtonClicked()
{
    //Arrange
    bool isEventInvoked = false;
    var componentUnderTest = RenderComponent<CounterComponent>(
        parameterBuilder => parameterBuilder
.Add(param => param.OnCountIncremented, () => isEventInvoked = true));

    AngleSharp.Dom.IElement button = componentUnderTest.Find("#incrementCountButton");

    //Act
    button.Click();

    //Assert
    Assert.True(isEventInvoked);
}

Abbildung 8: Test für unsere Komponente mit einem Event

Auch hier ist unser Test noch nicht kompilierbar. Daher ergänzen wir unsere Komponente. Wir fügen das neue Property OnCountIncremented vom Typ EventCallback hinzu und versehen es wie gehabt mit dem Attribute Parameter. Jetzt ist unser Test kompilierbar, schlägt aber noch fehl. Damit im nächsten Schritt unser neuer Test erfolgreich ist, rufen wir den Callback in der Methode IncrementCount() unserer Component auf.

using Microsoft.AspNetCore.Components;

namespace BlazorWasmTemplateApp.Components
{
    public partial class CounterComponent : ComponentBase
    {
        [Parameter]
        public int StartValue { get; set; } = 0;

        [Parameter]
        public EventCallback OnCountIncremented { get; set; }

        private int currentCount = 0;

        protected override void OnParametersSet()
        {
            currentCount = StartValue;
        }

        private async Task IncrementCount()
        {
            currentCount++;
            await OnCountIncremented.InvokeAsync();
        }
    }
}

Abbildung 9: Die um einen Event erweiterte Komponente

Subkomponenten

Razor-Komponenten können in sich wieder Razor-Komponenten enthalten (Subkomponenten). Mit bUnit ist es ist möglich, diese Subkomponenten im DOM zu finden, mit Parametern zu versehen, erneut zu rendern und Asserts durchzuführen. Ob solche Tests eine gute Idee sind, hängt vom Szenario ab. Um Abhängigkeiten zu verringern, ist es mit bUnit möglich, Komponenten durch Test Doubles zu ersetzen. Das kann vor allem beim Einsatz von Bibliotheken eines Drittanbieters nützlich sein.

Dependency Injection

Neben Parametern und Events ist es in Razor-Komponenten üblich, Abhängigkeiten durch Dependency Injection zu injizieren. Das Blazor-Framework nutzt dafür die Dependency Injection von ASP.NET Core. bUnit macht deshalb die Services-Kollektion in der Basisklasse TestContext verfügbar, die aus ASP.NET Core bekannt ist. Dort können Test Doubles registriert werden, bevor die Komponente gerendert wird. Für Abhängigkeiten wie IJsRuntime, HttpClient, NavigationManager etc., die den Komponenten normalerweise automatisch durch ASP.NET Core bereitgestellt werden, gibt es Anleitungen für Test Doubles in der Dokumentation von bUnit.

Fazit

Mit bUnit gibt es auch für Blazor eine Möglichkeit, um Komponenten isoliert, schnell und ohne E2E-Tests unter die Lupe zu nehmen. Damit steht für Blazor-Entwickler ein wichtiges Tool zur Verfügung, um mit Tests die Wartbarkeit der Blazor-App sicherzustellen. Außerdem wird es möglich, auch die UI testgetrieben zu entwickeln. Damit steht Blazor auch in diesem Aspekt anderen SPA-Frameworks in nichts nach.

Quellen

[1] nuget.org. [Online]. Available: https://www.nuget.org/packages?q=Tags%3A%22Blazor%22&sortby=totalDownloads-desc. [Zugriff am 22 Juni 2023].

[2] E. Hansen, „bUnit Releases,“ GitHub, [Online]. Available: https://github.com/bUnit-dev/bUnit/releases?page=3. [Zugriff am 22 Juni 2023].

[3] L. Latham, R. Anderson und GitHubPang, „Test Razor components in ASP.NET Core Blazor,“ Microsoft Learn, 04 April 2023. [Online]. Available: https://learn.microsoft.com/en-us/aspnet/core/blazor/test. [Zugriff am 22 Juni 2023].

[4] M. Cohn, Succeeding with Agile: Software Development Using Scrum, Addison-Wesley, 2009.

[5] E. Hansen, „Creating a new bUnit test project,“ bUnit, 7 April 2023. [Online]. Available: https://bunit.dev/docs/getting-started/create-test-project.html. [Zugriff am 22 Juni 2023].

[6] Refsnes Data, „CSS Selector Reference,“ W3Schools, [Online]. Available: https://www.w3schools.com/cssref/css_selectors.php. [Zugriff am 22 Juni 2023].

[7] E. Hansen, „bUnit Documentation,“ 2023. [Online]. Available: https://bunit.dev/docs/getting-started/index.html. [Zugriff am 22 Juni 2023].

MAUI – Mehr als eine Insel

Im Mai 2020 hat Microsoft eine neue, vereinheitlichte UI-Plattform für alle Systeme unter dem klangvollen Namen MAUI bzw. in voller Länge „.NET Multi-Platform App UI“ vorgestellt, die voraussichtlich im November 2021 auf den Markt kommen wird. Doch was genau verbirgt sich hinter diesem Namen? Diese Frage soll in diesem Artikel beantwortet werden. Dabei sollen die Plattform vorgestellt, die technischen Hintergründe erläutert und vor allem auch das Potential der Technologie herausgestellt werden.  

Um MAUI als Plattform zu verstehen, muss man zunächst Xamarin kennen. Xamarin (und im speziellen Xamarin.Forms) ist eine Plattform zur Entwicklung von nativen Apps für iOS, Android und UWP mit C#, XAML und .NET. Man kann dabei aus einer Code-Basis für alle unterstützten Betriebssysteme eine App erzeugen. Somit ist der Aufwand für die Entwicklung für verschiedene Betriebssysteme im Vergleich zur nativen Codierung der Anwendungen deutlich geringer. Aktuell sind tatsächlich die diversen SPA-Frameworks für den Browser die einzige Technologie, welche vergleichbare Portabilität bei ähnlichem Gesamtaufwand bieten. 

Schematische Darstellung von MAUI
Abbildung 1: Mit Xamarin.Forms kann man aus einer Code-Basis native Apps für alle unterstützten Betriebssysteme erzeugen. MAUI wird der direkte Nachfolger von Xamarin.Forms.

Aber was ist nun dieses geheimnisvolle MAUI und was hat es mit Xamarin zu tun? Diese Frage lässt sich recht simpel beantworten: MAUI ist das neue Xamarin oder, präziser ausgedrückt, dessen direkter Nachfolger, der erstmals mit .NET 6 ausgeliefert wird. 

Genau wie mit Xamarin.Forms lassen sich mit MAUI aus einem Projekt und mit derselben Code-Basis Apps für alle unterstützten Betriebssysteme erstellen. Aus dem Code werden Installationspakete generiert, die dann auf den verschiedenen Plattformen installiert werden können. Offiziell werden von Microsoft Android ab Version 10, iOS, macOS und natürlich Windows, sowohl nativ als auch als UWP-App, unterstützt. Zudem soll es eine Community-Implementierung für Linux-Betriebssysteme sowie eine von Samsung zur Verfügung gestellte Implementierung für deren Tizen-Plattform geben.  Das Projekt- und Build-System wird für alle Plattformen vereinheitlicht und das Erzeugen der Apps wird sowohl über Visual Studio als auch das .NET CLI möglich sein. 

Ein weiteres Feature wird die geteilte Nutzung von Ressourcen wie Bildern oder Übersetzungen sein. Diese sollen von MAUI automatisch in die entsprechenden nativen Formate umgewandelt und in die erstellten Pakete integriert werden können. Außerdem wird man jederzeit auf die APIs des jeweiligen Betriebssystems zugreifen können. Hierzu soll es im Projekt einen speziellen Ordner geben, unter dem man die nativen Code-Hooks ablegt und die dann beim Kompilieren automatisch ins Paket integriert werden.  

Alle im .NET-Standard verfügbaren Funktionalitäten wie zum Beispiel Dependency Injection sollen dabei auch für eine MAUI-App genutzt werden können. Durch die Verwendung von C# und XAML wird auch die Nutzung entsprechender Entwurfsmuster wie dem viel genutzten MVVM-Pattern möglich sein. Neu ist zudem der Support für das Model-View-Update-Pattern, ein von Elm entliehenes Muster, mit dem man einen unidirektionalen Datenfluss analog zu Redux abbilden können soll. Auch Microsofts webbasierte Client-Technologie Blazor soll unterstützt werden. 

Leider wird MAUI erst mit der Einführung von .NET 6 im November 2021 offiziell zur Verfügung stehen. Zudem wurden Teile des Frameworks nach .NET 7 und damit ins Jahr 2022 verschoben. Hier sind vor allem der offizielle Support für Blazor und das MVU-Pattern zu nennen. Da MAUI der offizielle Nachfolger von Xamarin ist, wird dieses auch mit dem Release von .NET 6 noch für ein Jahr unterstützt und dann eingestellt.  

Damit scheint Microsofts Strategie für die Zukunft der UI-Entwicklung mit dem .Net-Framework klar zu sein: MAUI ist der neue „First Class Citizen“, wenn es um die Erstellung von nativen User Interfaces geht.  

Das klingt zunächst alles danach, dass Microsoft eine native Cross Platform-Unterstützung ins .NET-Framework integrieren möchte. Dieser Plan wird jedoch nur aufgehen, wenn MAUI von den Entwicklerinnen und Entwicklern akzeptiert wird. Durch das sehr späte Release und einer Menge hervorragender Open Source-Alternativen wie Uno könnte sich MAUI am Ende eventuell nicht etablieren. Aktuell kann man deshalb nur abwarten und sehen, was die Zukunft bringt. Die Migration von bestehenden WPF-Anwendungen gestaltet sich beispielsweise schwierig, da der ersehnte XAML-Standard, der eine Vereinheitlichung der Elemente und Tags für alle XAML-Dialekte definieren sollte, scheinbar wieder verworfen wurde und sich MAUI und WPF wegen unterschiedlicher Elemente nicht übergangslos austauschen lassen werden.

Wenn die Technologie aber tatsächlich hält, was sie verspricht, die Entwicklerinnen und Entwickler sie breitflächig einsetzen und MAUI so hochklassig wird, wie Microsoft es in Aussicht stellt, könnte hier eine Cross Platform-Revolution unter .NET in den Startlöchern stehen.