Teil 2: Blazor-Syntax, Zustände, Interoperabilität und Libraries

Blazor unter der Haube: Syntax, Zustände und Interoperabilität
Keine Kommentare

Im ersten Teil des Beitrags wurden die architektonischen Unterschiede zwischen Blazor Server und Blazor WebAssembly geklärt. Nun geht es um die Razor-Syntax, Komponentenzustände, Razor Class Libraries und die Interoperabilität mit JavaScript.

Webanwendungsentwickler nutzen in der .razor-Datei einer Razor Component – sowohl in Blazor Server als auch Blazor WebAssembly – im Wesentlichen die gleiche Razor-Syntax wie bei Views im Model-View-Controler-Modell (MVC) und Razor Pages. Im Detail gibt es aber dennoch einige Unterschiede zwischen den Razor-Anwendungsgebieten, siehe dazu die ausführliche Tabelle 1 (Anm.: Bitte anklicken zum vergrößern).

Artikelserie

In Razor Components ist die Syntaxprüfung strenger; während in MVC und Razor Pages ein fehlendes schließendes Tag nur zu einer Warnung führt („Element ‚xy‘ requires end tag“), kommt es in Razor Components zu einem Kompilierungsfehler RZ9980 „Unclosed tag ‚xy‘ with no matching end tag.“

Tabelle 1: Ähnlichkeiten und Unterschiede der Razor-Template-Syntax in MVC, Razor Pages und Razor Components

Zustandshaftigkeit

Klassische Webanwendungen mit Server-side Rendering sind zustandslos: Ein HTTP-Request geht ein, wird verarbeitet und eine Antwort wird erzeugt. Danach werden alle gesetzten Variablen vernichtet. Konkret in ASP.NET und ASP.NET Core wird bei jedem Aufruf immer wieder eine neue Instanz der Page-Klasse (bei Web Forms), der Controller-Klasse (bei MVC) bzw. des Page Models (bei Razor Pages) erzeugt. Man braucht Techniken wie Cookies, Hidden Fields oder URL-Parameter bzw. darauf aufsetzende Abstraktionen wie Session-Variablen oder Viewstate, um Werte von einem zum nächsten HTTP-Aufruf weiterzugeben.

Nichts davon ist in Blazor Server und Blazor WebAssembly notwendig, denn eine Razor Component lebt solange sie angezeigt wird (auch wenn sich nichts rendert oder ihr Inhalt per CSS unsichtbar gemacht wurde). Dass Cookies, Hidden Fields oder URL-Parameter nicht notwendig sind, zeigt schon das Zählerbeispiel (Counter.razor), das Microsoft in der Standardprojektvorlage mitliefert. Wegen der Zustandshaftigkeit wird der Counter bei jedem Klick erhöht. In klassischen Webanwendungen würde er immer nur von 0 bis 1 hochzählen. Wechselt der Benutzer jedoch im Menu zu einer anderen Komponente, wird sie inaktiv und der Zustand der Komponente wird auf dem Server gelöscht.

BASTA! 2020

Entity Framework Core 5.0: Das ist neu

mit Dr. Holger Schwichtenberg (www.IT-Visions.de/5Minds IT-Solutions)

Memory Ownership in C# und Rust

mit Rainer Stropek (timecockpit.com)

Softwarearchitektur nach COVID-19

mit Oliver Sturm (DevExpress)

Delphi Code Camp

Delphi Language Up-To-Date

mit Bernd Ua (Probucon)

TMS WEB Core training

mit Bruno Fierens (tmssoftware.com bvba)

Zwischen Blazor Server und Blazor WebAssembly gibt es aber auch hier wieder einen Unterschied: Bei Blazor WebAssembly legt die Komponenteninstanz im Webbrowser, bei Blazor Server zusammen mit dem Virtual DOM auf dem Webserver.

Eine Komponente verliert ihren Zustand, wenn sie nicht mehr angezeigt wird. Wenn sie später erneut angezeigt wird, wird sie instanziiert. Um den Zustand über mehrere Instanzen hinweg zu verwalten, muss man den Zustand in einer per Dependency Injection erzeugten Instanz halten oder persistieren (auf dem Server oder im Client).

Komponentenverschachtelung

Eine Razor Component in eine andere einzubetten geht ganz leicht: Man verwendet ein Tag im Razor-Template-Code, das dem Namen der Komponente (also dem Dateinamen ohne .razor) entspricht. Für die Razor Component Counter.razor schreibt man also <Counter>. Sofern die Razor Component in einem anderen Namensraum liegt, schreibt man den Namensraumnamen davor <Namensraum.Counter> oder verwendet ein @using:

@using MiracleList_Blazor_RCL;
...
<Counter>

Eingebettete Komponenten können wieder andere Komponenten enthalten. Ein Limit der Verschachtelungstiefe ist nicht dokumentiert und in der Praxis nicht auf den ersten Blick erkennbar. Wenn man aber bei der Verschachtelung eine Rekursion erschafft (z. B. Component1.Razor enthält <Component2> und Component2.Razor <Component1>) hängt sich die Blazor-Anwendung beim Aufruf einer der beiden Komponenten in einer Endlosschleife auf.

Parameter deklariert eine Razor Component mit der Annotation [Parameter]. Parameter müssen Properties sein (Fields sind nicht erlaubt) und die Sichtbarkeit public besitzen. Ebenso können Komponenten Ereignisse auslösen, wenn sie Parameter vom Typ EventCallback<T> besitzen. Listing 1 zeigt eine Razor Component in einer Datei mit einem Parameter StartValue und einem Ereignis ValueHasChanged(), das in der Methode Increment() mit await ValueHasChanged.InvokeAsync(currentValue) ausgelöst wird. Listing 2 zeigt die Verwendung der Razor Component aus Listing 1 inklusive Ereignisbehandlung.

<h4>Counter</h4>

<div class="my-component">
  Diese Razor Component kommt aus der DLL: <strong>@System.Reflection.Assembly.GetExecutingAssembly().GetName().Name</strong> DLL.
</div>

<p>Current count: @currentValue</p>

<button class="btn btn-primary" @onclick="Increment">Klick mich!</button>

@code {
  [Parameter] // Parameter muss Property und public sein!
  public int StartValue { get; set; } = 0;

  private int currentValue { get; set; }

  [Parameter]
  public EventCallback<int> ValueHasChanged { get; set; }

  protected override void OnInitialized()
  {
    this.currentValue = this.StartValue;
  }

  async void Increment()
  {
    currentValue++;
    await ValueHasChanged.InvokeAsync(currentValue);
  }
}
@using MiracleList_Blazor_RCL;
...
<Counter StartValue="10" ValueHasChanged="NewValueArrived" />

@code
{
  public void NewValueArrived(int value)
  {
    if (value % 10 == 0) { Util.Alert("Wert ist nun: " + value); }
  }
}

Razor Class Library

Razor Components kann man zur Kapselung und Wiederverwendung in eigenständige DLL Assemblies auslagern. Dazu erstellt man in Visual Studio ein Projekt vom Typ Razor Class Library. Eine Razor Class Library kann Razor Components (.razor-Dateien) mit oder ohne Code-behind (.razor.cs), einfache Klassen und statische Inhalte (z. B. Grafiken) sowie JavaScript-Dateien im wwwroot-Ordner umfassen.

Eine Razor Class Library referenziert man wie jedes andere .NET-Projekt oder nutzt sie als NuGet-Paket. Die in einer Razor Class Library enthaltenen Razor Components nutzt man über den Namensraum z. B. <RCLName.Counter>.

Bei den statischen Ressourcen muss der Softwareentwickler im Parameter src der Skriptreferenz noch ein „_content“ davor setzen: <script src=“/_content/RCLName/BlazorUtil.js“></script>.

Eine Razor Class Library kann man auch in einer Blazor-Server-Anwendung und einer Blazor WebAssembly-Anwendung gemeinsam nutzen.

Neben Razor Class Libraries kann man in Blazor-Anwendungen auch .NET-Standard-kompatible Klassenbibliotheken referenzieren.

Dependency Injection

Blazor basiert auf ASP.NET Core und nutzt auch das dort integrierte Dependency Injection Framework (Microsoft.Extensions.DependencyInjection). Dienste werden in der Startup-Klasse in der Methode ConfigureServices() registriert mit AddTransient(), AddScoped() oder AddSingleton(). Drei Dienste werden automatisch von Blazor bereitgestellt: NavigationManager, IJSRuntime und HttpClient.

Alle bereitgestellten Dienste können dann in den eigenen Programmcode injiziert werden:

  • in Razor Template mit @Inject
  • in Razor-Code-behind-Dateien mit Konstruktorinjektion oder Property-Injektion mit [Inject]
  • in sonstigen Klassen mit Konstruktorinjektion

Interoperabilität mit JavaScript

Auch wenn es schon einige Erweiterungen für Blazor gibt, hätte die neue Technik sicherlich ein Akzeptanzproblem, wenn es keine Möglichkeit gäbe, hier auch mit JavaScript zu programmieren, um weitere Browser-APIs, JavaScript-Bibliotheken und bestehenden eigenen JavaScript-Code zu integrieren. Die Interoperabilität zwischen dem C#-Programmcode und JavaScript ist vorhanden – sowohl bei Blazor WebAssembly, bei dem beide im Webbrowser laufen, als auch bei Blazor Server, bei dem das JavaScript im Browser und C# auf dem Webserver läuft. Die Aufrufe mit Parametern und die Rückgabewerte werden über ASP.NET SignalR serialisiert.

Die Interoperabilität gibt es in beiden Blazor-Varianten und auch jeweils in beide Richtungen: C#-Code kann JavaScript aufrufen und JavaScript kann wieder C#-Code aufrufen. Zentrale Anlaufstelle für die Interoperabilität ist die Schnittstelle Microsoft.JSInterop.IJSRuntime mit den Methode InvokeAsync() und InvokeVoidAsync() in jeweils drei Überladungen:

  • public static ValueTask<TValue> InvokeAsync<TValue>(this IJSRuntime jsRuntime, string identifier, params object[] args)
  • public static ValueTask<TValue> InvokeAsync<TValue>(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object[] args)
  • public static ValueTask<TValue> InvokeAsync<TValue>(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, params object[] args)
  • public static ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, params object[] args)
  • public static ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object[] args)
  • public static ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, params object[] args)

Die übergebenen Parameter müssen in JSON serialisierbar sein. Die Schnittstelle IJSRuntime lässt man sich per Dependency Injection von der Blazor-Infrastruktur liefern. Es verwundert nicht, dass die beiden Blazor-Formen für die Schnittstelle IJSRuntime verschiedene Implementierungen bereitstellen:

  • Blazor Server: Microsoft.AspNetCore.Components.Server.Circuits.RemoteJSRuntime
  • Blazor Webasssembly: Microsoft.AspNetCore.Blazor.Services.WebAssemblyJSRuntime

Listing 3 zeigt drei in JavaScript geschriebene Hilfsroutinen, die JavaScript-Wrapper um die JavaScript-Funktionen alert() und confirm() für modale Browserdialoge sowie console.log() für die Ausgabe an die Browserkonsole darstellen. In Listing 4 sieht man die zugehörigen C#-Wrapper, die die JavaScript-Funktionen in C#-Methoden verpacken. Die C#-Methoden rufen die JavaScript-Funktionen via IJSRuntime auf. Dieser Interop-Aufruf ist aber bei Log() im Fall von Blazor WebAssembly nicht notwendig, denn hier leitet die Laufzeitumgebung alle Aufrufe von Console.WriteLine() zur Browserkonsole um. Dementsprechend sieht man im C#-Wrapper für Log() eine Fallunterscheidung zwischen Blazor WebAssembly und Blazor Server.

Für den umgekehrten, hier nicht gezeigten Weg stellt die Blazor-JavaScript-Bibliothek (also blazor.server.js bzw. blazor.webassembly.js) das Objekt DotNet mit der Methode invokeMethodAsync() bereit.

function ShowConfirm(text1, text2) {
  var e = confirm(text1 + "\n" + text2);
  console.log("ShowConfirm", text1, text2, e);
  return e;
}
  
window.Log = (s) => {
  console.log(s);
};
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;
using Microsoft.AspNetCore.Components;
 
namespace ITVisions.Blazor
{

public class BlazorUtil
  {

public BlazorUtil(IJSRuntime jsRuntime)
{
  _jsRuntime = jsRuntime;
}

public async Task Alert(string text)
{
  if (_jsRuntime == null) return;
  await _jsRuntime.InvokeVoidAsync("ShowAlert", text);
  //oder: await _jsRuntime.InvokeVoidAsync("alert", "Nachricht: " + text);
}

    public async ValueTask<bool> Confirm(string text1, string text2 = "")
    {
     if (_jsRuntime == null) return false;
     return await _jsRuntime.InvokeAsync<bool>("ShowConfirm", text1, text2);
    }

  public async void Log(object o)
  {
    if (_jsRuntime == null) return;
    // Aufbereitung für Ausgabe
    string s;
    if (o.GetType().IsPrimitive || o is string) s = o.ToString();
    else s = o.GetType().Name + " {" + o.ToNameValueString(attributeSeparator: ";") + "}";

    if (_jsRuntime.GetType().FullName.Contains("WebAssembly"))
    { // ganz einfach in Blazor Webassembly
      Console.WriteLine(s);
    }
    else // JS-Interop in Blazor Server
    {
      try
      {
        await _jsRuntime.InvokeVoidAsync("Log", s);
      }
      catch (Exception)
      {
        // mache nichts, es in Blazor server sein,
        // dass die JSRuntime noch nicht verfügbar ist!
      }
    }
  }
  }
}

Zu beachten ist, dass man grundsätzlich eine JavaScript-Datei nicht in eine einzelne Razor Component einbinden darf, weil Komponenten aus Browsersicht dynamische Bausteine sind, die man wieder entfernen kann. Ein geladenes JavaScript-Skript kann man aber nicht mehr aus dem Browser entfernen. Microsoft hat sich daher entschieden, <script>-Tags in einzelnen Razor Components zu verbieten. Wer es dennoch versucht, erhält den Kompilierungsfehler „Script tags should not be placed inside components because they cannot be updated dynamically. To fix this, move the script tag to the ‚index.html‘ file or another static location.”

Skriptdateien müssen daher in der zentralen Startseite (Masterpage) bereitgestellt werden. Im Fall von Blazor Server ist das die Datei _hosts.razor und die eigene BlazorUtil.js sollte nach der blazor.server.js geladen werden:

<script src="_framework/blazor.server.js"></script>
<script src="BlazorUtil.js"></script>

Fazit: Vor- und Nachteile von Blazor Server

Blazor Server ist eine neue, innovative Architektur für moderne Webanwendungen, die sowohl Vor- als auch Nachteile mit sich bringt. Die Vorteile sind:

  • Eine Blazor Server-Anwendung hat aus der Sicht des Benutzers das Look and Feel einer Single Page Application.
  • Es gibt in den Razor Components eine Zustandsbehaftung auch ohne Workarounds wie Sessions und Cookies.
  • Die Kapselung der Geschäftslogik in eine REST-Web-API-Schicht ist nicht notwendig (gleichwohl dennoch möglich).
  • Es gibt keine Restriktionen durch die Browser-Sandbox, d. h. alle Ressourcen wie Datenbanken und Hardwarekomponenten sind direkt nutzbar.
  • Man kann auf dem Server alle Klassen nutzen, die kompatibel zu .NET Core 3.x und .NET Standard bis einschließlich Version 2.1 sind.
  • Die Anforderungen an den Browser sind gering: Blazor-Server-Apps funktionieren auch mit alten Browsern, die zwar JavaScript, aber kein WebAssembly können.
  • Die Anwendung startet schnell: nur ca. 265 KB JavaScript müssen in den Browser geladen werden.
  • Später ist ein einfaches Migrieren auf Blazor WebAssembly möglich.

Es gibt aber auch signifikante Nachteile von Blazor Server:

  • Eine Blazor-Server-basierte Webanwendung kann nicht offlinefähig werden.
  • Wenn die Verbindung (temporär) abreißt, besteht die Gefahr, dass die Anwendung sich davon nicht mehr erholt und neu geladen werden muss.
  • Es entsteht wesentlich mehr Datenverkehr zwischen Client und Server.
  • Die Anwendung ist bei Netzwerklatenzen größer 250 Millisekunden nicht gut bedienbar.
  • Die Verarbeitungsdauer ist bei Änderungen größer, die eigentlich rein im Browser stattfinden könnten (außer man schreibt dafür doch JavaScript oder TypeScript).
  • Die Skalierbarkeit ist schlechter, da alle Rechenlast auf dem Server liegt und dieser pro Client ein Virtual DOM und den Komponentenzustand im RAM hält. Jeder angeschlossene Browser erhöht den RAM- und CPU-Bedarf des Webservers.

Gerade der letzte Punkt ist bedeutend: Blazor Server ist somit keine Lösung für hochskalierbare Webanwendungen mit sehr hohen Benutzerzahlen, sondern für Anwendungen von kleinen bis mittlere Nutzerzahlen. Eine Blazor-Server-Anwendung kann auch die Vorstufe einer späteren Umstellung auf Blazor WebAssembly sein.

Windows Developer

Windows DeveloperDieser Artikel ist im Windows Developer erschienen. Windows Developer informiert umfassend und herstellerneutral über neue Trends und Möglichkeiten der Software- und Systementwicklung rund um Microsoft-Technologien.

Natürlich können Sie den Windows Developer über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. Außerdem ist der Windows Developer weiterhin als Print-Magazin im Abonnement erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -