zurück zum Artikel

Web-Frontend-Framework: Eine Blazor-App für alle Plattformen

Dr. Holger Schwichtenberg

(Bild: Semisatch/Shutterstock.com)

Das Webframework Blazor gibt es in Varianten, die allesamt Vor- und Nachteile haben. Durch Code-Sharing lässt sich das Beste aus allen Blazor-Welten verbinden.

Das populäre .NET-basierte Web-Frontend-Framework ist in Wahrheit keine einheitliche Plattform, sondern es gibt mittlerweile vier Varianten davon. Blazor WebAssembly läuft komplett eigenständig in allen modernen Browsern, Blazor Server benötigt kontinuierliche Interaktion zwischen Browser und einem Webserver, Blazor Desktop erstellt native Windows-Anwendungen, die auf Windows Forms oder Windows Presentation Foundations (WPF) basieren, Blazor MAUI schließlich läuft auf Windows, macOS, iOS, Android oder Tizen innerhalb einer .NET-MAUI-Rahmenanwendung. Blazor MAUI und Blazor Desktop werden auch zusammen als Blazor Hybrid bezeichnet.

Die einzige bei Blazor bisher nicht angebotene Plattform ist der Linux-Desktop: Der Host .NET MAUI hat diese Plattform bisher vernachlässigt. Das GitHub-Projekt [1]eines Microsoft-Mitarbeiters, das .NET MAUI auf Linux bringen sollte, ist in der Zwischenzeit eingeschlafen.

Auch wenn nur der Browser angesprochen werden soll, wünschen sich Entwickler und Entwicklerinnen oft, dass eine Anwendung gleichzeitig mit Blazor WebAssembly und Blazor Server läuft, denn die Entscheidung zwischen Blazor WebAssembly und Blazor Server fühlt sich in einigen Fällen an wie die Wahl zwischen Pest und Cholera.

Blazor WebAssembly muss zu Beginn große Datenmengen in den Webbrowser laden und die Binärdateien werden von manchen Firewalls und Virenscanner blockiert. Danach läuft Blazor WebAssembly aber eigenständig und funktioniert auch, wenn die Verbindung zum Server schlecht ist oder abbricht. Blazor Server hingegen lädt zu Beginn ausgesprochen schnell, hat keine Probleme mit Sicherheitssoftware und bietet zudem einen guten Schutz vor Spionage, denn der C#-Programmcode verlässt den Webserver nicht. Allerdings braucht Blazor Server eine konstant gute Netzwerkverbindung zwischen Client und Webserver sowie pro Benutzer eine gewisse Menge an RAM und Rechenzeit auf dem Webserver, denn dort liegen der Zustand und eine Kopie des Document Object Model (DOM) für alle aktiven Nutzer.

Daher hat es Vorteile, eine Anwendung gleichzeitig mit Blazor WebAssembly und Blazor Server anzubieten, beispielsweise um Blazor Server als Fallback zu nutzen, wenn Blazor WebAssembly von Sicherheitssoftware geblockt ist. Für .NET 8.0 plant Microsoft Verbesserungen: Blazor WebAssembly-Anwendungen können im neuen WEBCIL-Format übertragen werden [2], das nicht geblockt werden sollte. Zudem soll es mit Blazor United die Möglichkeit eines automatischen Fallbacks von Blazor WebAssembly auf Blazor Server geben. Blazor MAUI und Blazor Desktop werden aber nicht Teil von Blazor United sein.

Der Dotnet-Doktor – Holger Schwichtenberg

Dr. Holger Schwichtenberg ist technischer Leiter des Expertennetzwerks www.IT-Visions.de, das mit 53 renommierten Experten zahlreiche mittlere und große Unternehmen durch Beratungen und Schulungen sowie bei der Softwareentwicklung unterstützt. Durch seine Auftritte auf zahlreichen nationalen und internationalen Fachkonferenzen sowie mehr als 90 Fachbücher und mehr als 1500 Fachartikel gehört Holger Schwichtenberg zu den bekanntesten Experten für .NET und Webtechniken in Deutschland.

Auch in einem weiteren Szenario wünschen sich Nutzer mehr als eine Anwendungsart. Eine Browser-Anwendung läuft immer in einer Sandbox ohne Zugang zu Ressourcen auf dem lokalen Rechner oder im Netzwerk. Sowohl in technischen als auch in kaufmännischen Anwendungen gibt es aber manchmal den Wunsch, solche Ressourcen direkt ansprechen zu können – etwa dann, wenn Messgeräte oder spezielle Ausgabegeräte wie 3D-Drucker per USB- oder serieller Schnittstelle angeschlossen sind.

Für solch eine Cross-Platform-Anwendung, die nicht nur in der Browser-Sandbox, sondern auch als native Anwendung mit vollem Zugriff auf allen Ressourcen laufen soll, muss das Entwicklerteam dann zumindest Blazor MAUI (für Windows, macOS, iOS, Android oder Tizen) und entweder Blazor WebAssembly oder Blazor Server anbieten. Letztlich entsteht daraus in der Praxis oft eine der folgenden drei Konstellationen:

Ein interessanter Ansatz besteht darin, eine Webanwendung so zu kapseln, dass sie sowohl in Blazor WebAssembly als auch Blazor Server, Blazor Desktop und Blazor MAUI verwendet werden kann.

Die vielen Gemeinsamkeiten zwischen allen Blazor-Varianten erlauben es, Programmcode leicht zwischen Blazor WebAssembly und Blazor Server sowie Blazor Hybrid auszutauschen beziehungsweise den gemeinsamen Code und gemeinsame Oberflächen in allen Blazor-Varianten zu verwenden.

Das Grundkonzept für Codesharing zwischen mehreren Blazor-Projekten oder zwischen mehreren Blazor-Arten sind Razor Class Libraries. Eine Razor Class Library ist eine spezielle Art von Klassenbibliothek in .NET, die nicht nur Programmcode und statische Ressourcen, sondern auch Razor Components enthalten kann. Razor Components sind eine Mischung aus HTML, CSS und C#-Programmcode, die der normale C#-Compiler nicht verarbeiten kann. Dafür brauchen Entwickler und Entwicklerinnen den speziellen Razor-Compiler.

Eine Razor Class Library lässt sich aber nicht eigenständig starten. Zum Start einer Blazor-Anwendung ist zusätzlich ein entsprechendes Blazor-Projekt nötig, in das sich die Razor Class Library als Abhängigkeit einbinden lässt. Eine Razor Class Library kann gleichzeitig in mehreren Blazor-Projekten genutzt werden. Sie kann direkt auf Projektebene oder als NuGet-Paket eingebunden sein, das zuvor aus dem Razor Class Library-Projekt kompiliert wurde. In eine Razor Class Library packt man möglichst alle Razor Components. Die Blazor-Projekte fungieren als "Kopf-" oder "Startprojekte", die nur noch den Startcode bereitstellen (Abb. 1). Eine solche Architektur mit Kopfprojekten gab es vor .NET MAUI auch in Xamarin für verschiedene Plattformen.

Bild 1: Code Sharing zwischen allen vier Blazor-Arten in vier Kopfprojekten mit gemeinsamer Razor Class Libraries und einer gemeinsamen normalen .NET-Klassenbibliothek

(Bild: Dr. Holger Schwichtenberg)

Eine Razor Class Library können Entwicklerteams in Visual Studio mit der Projektvorlage "Razor Class Library" oder an der Kommandozeile mit dotnet new razorclasslib erstellen. An dieser Stelle wäre es falsch, "Class Library" (dotnet new classlib) zu wählen, da es nicht möglich ist, in eine normale .NET-Bibliothek eine Razor Components einzubinden. Auch das Einbinden statischer Webelemente wie Grafiken und CSS-Dateien ist nicht elegant möglich.

In dem folgenden Optionsdialog ist die .NET-Version auszuwählen. Hier wählt der Entwickler oder die Entwicklerin die gleiche .NET-Version, die in den Kopfprojekten zum Einsatz kommt. Falls dort verschiedene .NET-Versionen laufen, gilt es, die Razor Class Library auf die kleinste dieser Versionsnummern festlegen. Aktuell besitzen nur die .NET-Versionen 6.0 (bis November 2025) und 7.0 (bis Mai 2024) offizielle Unterstützung von Microsoft, wie Abbildung 2 zeigt.

Bild 2: Wählbare Versionen für eine Razor Class Library in Visual Studio 2022 Version 17.6

(Bild: Dr. Holger Schwichtenberg)

Beim Anlegen einer Razor Class Library dürfen Entwickler und Entwicklerinnen nicht das Häkchen "Support pages and views" aktivieren, denn damit würde eine andere Art von Klassenbibliothek für Razor Views (für ASP.NET Core MVC) und Razor Pages (für ASP.NET Core Razor Pages) entstehen. Microsoft stellt dies noch nicht als eine Option dar, obwohl dabei zwei verschiedene Projektvorlagen zum Einsatz kommen.

Eine Razor Class Library kann folgende Elemente enthalten:

In der Projektvorlage der Razor Class Library (siehe unterer Teil in Abb. 3) befinden sich eine Grafik (wwwroot/background.png), eine JavaScript-Datei (wwwroot/exampleJsInterop.cs), ein zugehöriger C#-Wrapper (ExampleJsInterop.cs) sowie eine Razor Component (Component1.razor) mit zugehöriger CSS-Datei Component1.razor.css.

Eine Abhängigkeit zu einer Razor Class Library kann innerhalb einer Visual Studio-Projektmappe per Drag&Drop erstellt werden, wobei der Zielast "Dependencies" sein muss (siehe Pfeile in Abb. 3). Danach erscheint das Projekt im Unterast "Projects". Ein direktes Fallenlassen auf "Projects" ist kurioserweise nicht möglich. Alternativ können Entwickler und Entwicklerinnen im Kontextmenü von "Dependencies" oder "Projects" auch die Funktion "Add Project Reference" auswählen.

Bild 3: Eine Abhängigkeit zu einer Razor Class Library erstellen Entwickler und Entwicklerinnen in Visual Studio per Drag&Drop, wobei der Zielast "Dependencies" sein muss.

(Bild: Dr. Holger Schwichtenberg)

Die in einer Razor Class Library enthaltenen Razor Components lassen sich über ihren vollständigen Klassennamen inklusive Namensraum in Komponenten einbetten, zum Beispiel mit <RCLNamensraum.Counter>. Den Namensraum kann das Entwicklerteam per @using einbinden und damit das Tag auf den Klassennamen verkürzen:

@using SamplesRCL;

<Counter>

Razor Components in einer Razor Class Library können eine oder mehrere Routen in Form der @page-Direktive besitzen, wie Listing 1 veranschaulicht:

@page "/SamplesList"
@page "/Samples"

<h3>Weitere Blazor-Beispiele aus dem Buch <a href="https://it-visions.de/blazorbuch">'ASP.NET Core Blazor'</a></h3>


Listing 1: Ausschnitt aus SampleList.razor im Projekt SamplesRCL.csproj

Razor Components können auf eine solche Route verzichten. Razor Components ohne Route sind nur als Unterkomponenten oder eingebettete Komponenten in anderen Komponenten verwendbar. Für solche Unterkomponenten reicht die Projekt- oder NuGet-Referenz auf die Razor Class Library-Assembly-Komponenten, die eine eigene, mit @page deklarierte Route besitzen, können wahlweise in andere Komponenten eingebettet oder direkt aufgerufen werden.

Falls zumindest ein Razor Component in einer Razor Class Library eine Route besitzt, ist neben der Referenz auf die Razor Class Library ein weiterer Schritt notwendig: Die Razor Component-Assembly muss beim Router angemeldet werden. Dies erfolgt in der Datei App.razor durch das Attribut AdditionalAssemblies beim XML-Tag <Router> (Listing 2).

Beim Attribut AdditionalAssemblies muss nicht der Namen der Assembly sondern ein System.Assembly-Objekt übergeben werden. Typsicher gelingt dies, indem die Entwicklerin sich mit dem C#-Operator typeof() das Typobjekt einer beliebigen Klasse aus der Razor Class Library holt und dort auf das Property Assembly zugreift. Dies zeigt Listing 2:

<CascadingAuthenticationState>
 <Router AppAssembly="typeof(Startup).Assembly"
         AdditionalAssemblies="new[] { typeof(SamplesRCL.SampleList).Assembly }"
         >
  <Found Context="routeData">
   <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
  </Found>
  <NotFound>
   <h1>Page not found</h1>
   <p>Sorry, but there's nothing here!</p>
  </NotFound>
 </Router>
</CascadingAuthenticationState>


Listing 2: App.razor mit einem Zusatz bei AdditionalAssemblies

Bei der Einbindung von Routen aus Razor Class Library-Assemblies gilt, was auch für die Routen innerhalb eines Projekts gilt: Routen dürfen nicht doppelt vergeben sein. Dies führt bereits beim Start der Anwendung zu einem Laufzeitfehler. Abbildung 4 zeigt dies am Beispiel der Fehlerausgabe einer Blazor WebAssembly-Anwendung in der Konsole des Webbrowsers.

Bild 4. Die Konsole des Webbrowsers zeigt, dass in dieser Blazor WebAssembly-Anwendung eine Route doppelt vergeben wurde.

(Bild: Dr. Holger Schwichtenberg)

Bei den statischen Ressourcen, die nicht im lokalen /wwwroot-Ordner des Blazor-Projekts, sondern im /wwwroot-Ordner einer referenzierten Razor Class Library liegen, muss der Parameter src der Skriptreferenz den Wert "_content" gefolgt von der Package-Id haben. Wurde diese nicht explizit festgelegt, ist der AssemblyName zu setzen. Wenn es kein Tag <AssemblyName> in der Projektdatei gibt, ist der Assemblyname dem Projektnamen voranzustellen:

<script src="/_content/<PackageID>/Datei.js"></script>

Abbildung 5 zeigt dies an zwei Beispielen:

Beiden Dateien müssen in der Startdatei der nutzenden Blazor-Anwendung (hier: index.html, weil Blazor WebAssembly) per <script>- beziehungsweise <link>-Tag eingebunden werden.

Bild 5: Nutzung von statischen Webelementen aus einer Razor Class Library

(Bild: Dr. Holger Schwichtenberg)

Blazor Server, Blazor MAUI und Blazor Desktop laden die Razor Class Libraries erst bei Bedarf, also wenn eine Route aus der Razor Class Library verwendet oder eine Komponente aus der Razor Class Library eingebettet wird. Blazor WebAssembly lädt im Standard immer alle Assemblies beim Anwendungsstart vom Webserver, damit die Anwendung auch offline arbeiten kann. Dies können Entwickler ändern, indem sie Razor Class Library erst bei Bedarf nachladen. Damit ist es möglich, die Downloadlast beim Start der WebAssembly-Anwendung zu reduzieren. Diese Lazy Loading-Funktion gibt es seit .NET 5.0.

Bild 6: Erst beim Klick auf "Blazor-Beispiele außerhalb der MiracleList" wurde die SamplesRCL.dll per Lazy Loading nachgeladen, was beim Anwendungsstart die Ladelast um immerhin 116 KByte reduziert hat.

(Bild: Dr. Holger Schwichtenberg)

Die Standardprojektvorlage für Blazor WebAssembly-Anwendungen, die Microsoft ausliefert, enthält noch kein Lazy Loading. Um dies zu integrieren, sind folgende Schritte notwendig: Entwickler rufen ein neues Blazor-WebAssembly-Projekt (Target Framework .NET 5.0 oder höher) ins Leben, legen dann eine Razor Class Library an und verschieben die Datei Counter.razor aus dem Blazor WebAssembly-Projekt in die Razor Class Library. Zum Abschluss erhält in der Projektdatei (.csproj) des Blazor WebAssembly-Projekts der Name der nachzuladenden DLL die Dateiendung ".dll".

<ItemGroup>
<BlazorWebAssemblyLazyLoad Include="RCL.dll" />
</ItemGroup>

Im Blazor WebAssembly-Projekt muss die App.razor-Datei mit dem Inhalt des Listings 3 ersetzt werden. Die Klasse LazyAssemblyLoader und das Ereignis OnNavigateAsync() gibt es seit Blazor 5.0.

@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@inject LazyAssemblyLoader LazyAssemblyLoader

<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="lazyLoadedAssemblies" OnNavigateAsync="@OnNavigateAsync">
 <Navigating>
  <div>
   <p>Loading the requested page…</p>
  </div>
 </Navigating>
 <Found Context="routeData">
  <RouteView RouteData="@routeData" DefaultLayout="typeof(MainLayout)" />
 </Found>
 <NotFound>
  <LayoutView Layout="typeof(MainLayout)">
   <p>Sorry, there is nothing at this address.</p>
  </LayoutView>
 </NotFound>
</Router>

@code {
 private List<Assembly> lazyLoadedAssemblies =
    new List<Assembly>();

 private async Task OnNavigateAsync(NavigationContext args)
 {
  Console.WriteLine("OnNavigateAsync: " + args.Path);
  if (args.Path.EndsWith("counter"))
  {
   Console.WriteLine("Lazy Loading RCL.dll...");
   var assemblies = await LazyAssemblyLoader.LoadAssembliesAsync(new string[] { "RCL.dll" });
   lazyLoadedAssemblies.AddRange(assemblies);
  }
 }
}
}


Listing 3: App.razor mit Lazy Loading

Wer das Tag <BlazorWebAssemblyLazyLoad> in der Projektdatei des Blazor WebAssembly-Projekts vergisst, erhält die Fehlermeldung: "Unhandled exception rendering component: No assemblies have been marked as lazy-loadable. Use the 'BlazorWebAssemblyLazyLoad' item group in your project file to enable lazy loading an assembly."

Dieser Beitrag zeigt, wie Entwicklerinnen und Entwickler gemeinsame Razor Components und statische Webartefakte in mehrere Blazor-Anwendungen nutzen können, auch wenn diese auf verschiedenen Blazor-Arten basieren.

Allerdings verbleibt noch ein Thema, das immer dann zum Tragen kommt, wenn Entwickler Razor Components, die Datenbanken oder andere Ressourcen nutzen, zwischen Anwendungen in verschiedenen Blazor-Arten teilen will: Blazor Server, Blazor Desktop und Blazor MAUI können als 2-Tier-Anwendung mit direktem Datenbankzugriff umgesetzt werden. Blazor WebAssembly benötigt aber immer einen Application Server (3-Tier-Architektur) und auch bei den Mobilplattformen, die Blazor MAUI anspricht, ist ein direkter Datenbankzugriff mangels Treibern nicht immer möglich oder zumindest verpönt. In einem späteren Beitrag wird es daher darum gehen, wie Entwickler und Entwicklerinnen Razor Components so schreiben können, dass sie Daten sowohl direkt von einem Datenbankmanagementsystem als auch via Webservice lesen und schreiben können.

(fms [3])


URL dieses Artikels:
https://www.heise.de/-8987210

Links in diesem Artikel:
[1] https://github.com/jsuarezruiz/maui-linux
[2] https://github.com/dotnet/runtime/issues/80807
[3] mailto:fms@heise.de