zurück zum Artikel

Blazor WebAssembly, Teil 4: Zustandsverwaltung und Nachladen von Modulen

Dr. Holger Schwichtenberg

In diesem Teil des Tutorials werden Informationen lokal im Browser persistiert, sodass sie einen Neustart der MiracleList-Webanwendung überleben.

Klassische Webanwendungen mit serverseitigem Rendering sind zustandslos: Ein HTTP-Request geht auf dem Webserver ein, wird verarbeitet und irgendwann wird eine Antwort zum Browser gesendet. Nach Absenden der Antwort werden alle auf dem Webserver gesetzten Variablen vernichtet. Das ist in Blazor anders: Der Komponentenzustand mit allen gesetzten Fields und Properties bleibt erhalten, solange eine Razor Component aktiv ist. Eine Komponente ist aktiv, wenn sie

Blazor-Tutorial

Für die Bewahrung des Komponentenzustands über die oben genannten Fälle hinaus gibt es mehrere Alternativen in Blazor, die sowohl in Blazor WebAssembly als auch Blazor Server funktionieren (s. Tabelle).

Strategie Nicht mehr Teil der aktuellen Ansicht Reload und Browserneustart Verbindungsverlust (nur bei Blazor Server)
Sitzungszustand innerhalb von Blazor hilft hilft nicht hilft nicht
Session Storage des Browsers bzw. Session Cookies hilft hilft nicht hilft
Local Storage des Browsers bzw. persistente Cookies hilft hilft hilft
Persistierung in Datenbank oder anderem persistenten Speicher auf dem Server hilft hilft hilft

Strategien für die Zustandsbewahrung in Blazor

Die in den ersten drei Teilen des Tutorials mit Blazor WebAssembly erstellte Single-Page-Webanwendung "MiracleList" [6] soll nun so erweitert werden, dass Benutzer sich nach einem Neustart des Webbrowsers (oder des ganzen Client-Systems) nicht noch mal erneut anmelden müssen, sondern wieder direkt beim Aufrufen der URL in die Aufgabenverwaltung gelangen. Eine Persistierung im Server würde für diesen Fall nicht helfen; vielmehr braucht es eine Information im lokalen Speicher des Webbrowsers. Diese Information sollte nicht Benutzername und Kennwort sein (das wäre ein Sicherheitsrisiko), sondern das zeitlich begrenzte Token, das das Backend-System [7] bei der WebAPI-Operation /Login vergibt (vgl. Teil 1 dieser Serie [8]).

Im Browserspeicher kann man mehr Daten (mehrere Megabyte, je nach Browser) ablegen, als in Cookies erlaubt ist. Auch für die Arbeit mit Cookies gibt es in Blazor keine eingebaute Funktion; man müsste auf die JavaScript-Interoperabilität oder eine entsprechende Wrapper-Komponente zurückgreifen.

Für den Zugriff auf den lokalen persistenten Speicher (Local Storage) des Webbrowsers gibt es bisher keine direkt in das Blazor-Framework integrierte Lösung, sondern einige Zusatzpakete bei NuGet.org:

Das Paket Blazored.LocalStorage ist zum Zeitpunkt der Erstellung dieses Beitrags aktueller gewartet, hat mehr GitHub-Sterne und deutlich mehr NuGet-Downloads (330.700) als Blazor.Extensions.Storage (36.100). Daher fällt die Wahl auf ersteres. Zunächst ist das Paket Blazored.LocalStorage per grafischer NuGet-Benutzeroberfläche in Visual Studio oder in der NuGet Package Manager Console zu einem Blazor-Projekt hinzuzufügen:

Install-Package Blazored.LocalStorage

Abhängig von der verwendeten Blazor-Version müssen die Leser gegebenenfalls eine bestimmte, kompatible Version des Pakets installieren. Da sich das Paket unabhängig von Blazor versioniert, lässt sich hier nicht vorhersagen, welche Version das beim Erscheinen dieses Beitrags sein wird.

Dann ist der Local Storage Service der Komponente im Dependency-Injection-Container in Program.cs zu registrieren:

using Blazored.LocalStorage;
...
services.AddBlazoredLocalStorage();

Nun können sich Softwareentwickler in jeder Razor Component oder anderen Klassen eine Klasse injizieren lassen, die Blazored.LocalStorage.ILocalStorageService bereitstellt. Diese Schnittstelle bietet einige Methoden für das Speichern von Name-Wert-Paaren an:

In diesem Fall wird eine Zeichenkette (das o. g. Token) persistiert; es lassen sich aber auch komplexe Objekte übergeben, sofern diese in JSON serialisierbar sind, denn der Zugriff auf die Web Storage API erfolgt über JavaScript.

Die Injizierung einer Klasse für die Schnittstelle Blazored.LocalStorage.ILocalStorageService wird in AuthenticationManager.cs benötigt:

public class AuthenticationManager : AuthenticationStateProvider 
 {
  MiracleListAPI.MiracleListProxy proxy { get; set; }
  Blazored.LocalStorage.ILocalStorageService storage { get; set; }

  public AuthenticationManager(MiracleListAPI.MiracleListProxy proxy, Blazored.LocalStorage.ILocalStorageService storage )
  {
   this.proxy = proxy;
   this.storage = storage;
  }
...
}

In der gleichen Klasse ergänzt man nun eine Methode CheckLocalTokenValid(), die mit GetItemAsync() versucht, ein Token aus dem Local Storage des Webbrowsers zu laden. Falls ein Token vorhanden ist, wird es durch einen Aufruf der flexiblen WebAPI-Operation /LoginAsync geprüft. Hier können Nutzer statt Benutzernamen und Kennwort auch ein Token übergeben. Wenn das Token noch gültig ist, liefert die WebAPI den Benutzernamen zurück, und durch Aufruf der eigenen Methode Notify() informiert der Code die Blazor-Infrastruktur über die erfolgreiche Benutzeranmeldung. Abbildung 1 zeigt in der Browserkonsole die Ausgaben des Authentication Manager beim Aufruf von CheckLocalTokenValid() bei vorhandenem Token im Local Storage.

Das noch gültige Token im Local Storage sorgt dafür, dass Benutzer sich nicht nochmals neu anmelden müssen (Abb. 1).
public async Task<bool> CheckLocalTokenValid()
  {
   bool result = false;
   string token = await localStorage.GetItemAsync<string>(STORAGE_KEY);
   if (!String.IsNullOrEmpty(token))
   {
    // Es gibt ein Token im Local Storage. Nachfrage beim Server, ob noch gültig.
    Console.WriteLine($"{nameof(AuthenticationManager)}.{nameof(CheckLocalTokenValid)}: Checking local token {token}...");
    var l = new LoginInfo() {Token=token, ClientID = AuthenticationManager.ClientID };
    this.CurrentLoginInfo = await proxy.LoginAsync(l);
    if (this.CurrentLoginInfo == null || String.IsNullOrEmpty(CurrentLoginInfo.Token))
    { // Token ungültig!
     Console.WriteLine($"{nameof(AuthenticationManager)}.{nameof(CheckLocalTokenValid)}: Token not valid: {CurrentLoginInfo?.Message}!");
     CurrentLoginInfo = null;
    }
    else
    { // Token gültig!
     Console.WriteLine($"{nameof(AuthenticationManager)}.{nameof(CheckLocalTokenValid)}: Found valid Token: {CurrentLoginInfo.Token} for User: {CurrentLoginInfo.Username}");
     Notify();
     result = true;
    }
   }
   else
   {
    Console.WriteLine($"{nameof(AuthenticationManager)}.{nameof(CheckLocalTokenValid)}: No local token!");
   }
   Notify();
   return result;
  }

Listing 1: Erweiterung der Klasse AuthenticationManager.cs

Diese neue Methode CheckLocalTokenValid() aus Listing 1 lässt sich dann in der Razor Component "Login" in der Code-Behind-Datei aufrufen (s. Listing 2). Die Prüfung, ob ein Token vorhanden ist, sollte in der Lebenszyklusmethode OnInitializedAsync() erfolgen; hier kann man dann vor dem Rendern von Inhalten Benutzer mit dem NavigationManager schon auf die Hauptansicht leiten. Allerdings muss die Prüfung des Tokens im Programmcode erst nach der Abfrage folgen, ob die Benutzer eine Abmeldung wünschen; sonst würde diese niemals erreicht werden.

  protected override async System.Threading.Tasks.Task OnInitializedAsync()
  {
   // Reaktion auf die URL /logout
   if (this.NavigationManager.Uri.ToLower().Contains("/logout"))
   {
    await ((AuthenticationManager)asp).Logout(); return;
   }

   // Direkt zur Hauptseite, falls ein Token im Local Storage ist
   if (await (asp as AuthenticationManager).CheckLocalTokenValid())
   {
    this.NavigationManager.NavigateTo("/main");
   }
  }

Listing 2: Erweiterung von Login.razor.cs

Eine direkte Interaktion mit dem Local Storage ist noch an zwei weiteren Stellen in der Klasse AuthenticationManager erforderlich:

Nun sollte die Anmeldung bei der Webanwendung rundlaufen:

Wenn Benutzer auf Logout klicken, wird per <a>-Tag die relative URL /Logout gerufen und die Komponente Login.razor reagiert darauf mit Abmeldung beim Backend und Löschen des Tokens im Local Storage.

Die bisher erstellte Blazor-WebAssembly-Anwendung ist nicht sehr groß, aber lädt dennoch schon rund 70 Dateien und rund 10 MByte in den Webbrowser. Schon seit der ersten Version Blazor WebAssembly 3.2 ist es möglich, Razor Components und sonstige C#-Klassen sowie auch klassische Webinhalte wie HTML-, CSS-, JavaScript- und Grafikdateien in sogenannte Razor Class Libraries (RCL) auszulagern. Das diente bisher aber nur der Wiederverwendbarkeit und nicht dem Nachladen bei Bedarf (Lazy Loading). Diese schmerzlich vermisste Funktion hat Microsoft in Blazor 5.0 nachgerüstet: Damit ist es möglich, die Downloadlast beim Start der Anwendung zu reduzieren.

In Fall des MiracleList-Frontends soll bei Anwendungsstart nur noch die Anmeldemaske geladen werden. Alle andere Razor Components sollen erst nach dem erfolgreichen Anmelden vom Webserver geholt werden. Weder Microsofts Standardprojektvorlage noch das bisher erstellte MiracleList-Projekt sind auf Lazy Loading vorkonfiguriert. Um das zu integrieren, sind folgende Schritte notwendig:

<ItemGroup>
  <BlazorWebAssemblyLazyLoad Include="MiracleListRCL.dll" />
 </ItemGroup> 
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@inject LazyAssemblyLoader LazyAssemblyLoader

<CascadingAuthenticationState>
 <Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="lazyLoadedAssemblies" OnNavigateAsync="@OnNavigateAsync">
  <Navigating>
   <div>
    <p>Loading the requested page...</p>
   </div>
  </Navigating>
  <Found Context="routeData">
   <AuthorizeRouteView RouteData="@routeData"
                       DefaultLayout="@typeof(MainLayout)">
    <NotAuthorized>
     Sie sind nicht angemeldet und können den Inhalt dieser Seite daher nicht sehen. Bitte melden Sie sich an in der <a href="/Login">Anmeldenmaske</a>
     @{
      NavigationManager.NavigateTo("/");
     }
    </NotAuthorized>
   </AuthorizeRouteView>
  </Found>
  <NotFound>
   <LayoutView Layout="typeof(MainLayout)">
    <p>Sorry, there is nothing at this address.</p>
   </LayoutView>
  </NotFound>
 </Router>
</CascadingAuthenticationState>

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

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

Listing 3: App.razor mit Lazy Loading

Nach diesen umfangreichen Änderungen sollte die MiracleList-Projektmappe so aussehen, wie sie Abbildung 2 zeigt. Die Anwendung verhält sich beim Start wie bekannt. Nur wenn man in die Netzwerkansicht der Entwicklerwerkzeuge im Browser schaut, sieht man, dass MiracleListRCL.dll erst nach der Benutzeranmeldung nachgeladen wird. Der Einspareffekt beim Anwendungsstart ist in diesem Fall nicht groß, weil die MiracleListRCL.dll nur rund 13 KByte groß ist. Aber wenn die Anzahl der Razor Components in der Webanwendung wächst, vergrößert sich der Effekt. Zudem kann man durch einen möglichst spartanischen Anmeldedialog auch Grafiken unter anderem Ressourcen erst später zu laden.

Umgestaltung der Projektmappe für das Lazy Loading (Abb. 2)

Das Lazy Loading funktioniert: MiracleListRCL.dll wird erst nach der Benutzeranmeldung nachgeladen (Abb. 3).

Die Erweiterung des Beispiels um die lokale Persistierung war einfach zu realisieren und verbessert das Benutzererlebnis dennoch ganz erheblich. Die nachträgliche Umstellung auf das Nachladen von Anwendungsteilen bei Bedarf war hinweg leider ein umfangreiches Prozedere, weil die Modularisierung nicht von Anfang an berücksichtigt wurde.

Im fünften und letzten Teil des Tutorials wird es um die Integration der bidirektionalen Kommunikation mit ASP.NET Core SignalR. Damit lässt der Inhalt mehrerer Browserfenster synchronisieren, worüber der Benutzer auch optische benachrichtigt werden soll.

Dr. Holger Schwichtenberg
ist Chief Technology Expert bei der MAXIMAGO-Softwareentwicklung [14]. Mit dem Expertenteam bei www.IT-Visions.de [15] bietet er zudem Beratung und Schulungen im Umfeld von Microsoft-, Java- und Web-Techniken an. Er ist Autor zahlreicher Bücher, u. a. "ASP.NET Core Blazor: Moderne Single-Page-Web-Applications mit .NET, C# und Visual Studio".

(ane [16])


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

Links in diesem Artikel:
[1] https://www.heise.de/ratgeber/Webprogrammierung-mit-Blazor-WebAssembly-Teil-1-Web-API-Aufrufe-und-Rendering-4932237.html
[2] https://www.heise.de/ratgeber/Blazor-WebAssembly-Teil-2-Eingabesteuerelemente-JavaScript-Interoperabilitaet-4971874.html
[3] https://www.heise.de/ratgeber/Blazor-WebAssembly-Teil-3-Authentifizierung-und-Autorisierung-4988529.html
[4] https://www.heise.de/ratgeber/Blazor-WebAssembly-Teil-4-Zustandsverwaltung-und-Nachladen-von-Modulen-5036983.html
[5] https://www.heise.de/ratgeber/Blazor-WebAssembly-Bidirektionale-Kommunikation-und-Benachrichtigungen-5069045.html
[6] http://miraclelist-bw.azurewebsites.net/
[7] http://miraclelistbackend.azurewebsites.net/
[8] https://www.heise.de/ratgeber/Webprogrammierung-mit-Blazor-WebAssembly-Teil-1-Web-API-Aufrufe-und-Rendering-4932237.html
[9] https://github.com/BlazorExtensions/Storage
[10] https://github.com/Blazored/LocalStorage
[11] https://github.com/Blazored/SessionStorage
[12] https://github.com/aspnet/AspLabs
[13] https://www.heise.de/ratgeber/Blazor-WebAssembly-Teil-3-Authentifizierung-und-Autorisierung-4988529.html
[14] https://www.maximago.de
[15] https://www.it-visions.de
[16] mailto:ane@heise.de