Webprogrammierung mit Blazor WebAssembly, Teil 1: Web-API-Aufrufe und Rendering

In diesem Tutorial lernen Entwickler, wie sie schrittweise eine moderne Single-Page-Webanwendung mit C# und ASP.NET Core Blazor WebAssembly programmieren.

In Pocket speichern vorlesen Druckansicht 2 Kommentare lesen
Lesezeit: 13 Min.
Von
  • Dr. Holger Schwichtenberg
Inhaltsverzeichnis

Im Gegensatz zu Blazor Server führt die WebAssembly-Variante von ASP.NET Core Blazor den C#-Programmcode im Webbrowser als echte Single-Page-Webanwendung aus. Eine Blazor WebAssembly kann somit im Gegensatz zu der Blazor-Server-Variante auch offline arbeiten – sie muss den Server nur kontaktieren, um Daten zu laden und zu persistieren. Abbildung 1 zeigt den Vergleich der beiden Architekturen. Details zu den Architekturunterschieden gab es im Artikel "Alternativen zu JavaScript?".

Blazor-Tutorial

Architekturalternativen: Vergleich Blazor Server mit Blazor Webassembly (Abb. 1)

(Bild: Holger Schwichtenberg)

Die erste Version Blazor WebAssembly ist unter der Versionsnummer 3.2 am 19. Mai 2020 erschienen. Das Tutorial behandelt schon den verbesserten Nachfolger, der die Versionsnummer 5.0 trägt. Dazu benötigt man folgende Software:

Grundlage für das Tutorial ist der Release Candidate 2 von .NET 5.0. Auch wenn Microsoft dafür eine "Go-Live"-Lizenz für den produktiven Einsatz vergeben hat, ist es möglich, dass sich APIs noch ändern und der hier dargestellte Programmcode mit Folgeversionen nicht ohne Modifikationen läuft.

Während das .NET 5.0 SDK dauerhaft kostenfrei ist, gibt es bei Visual Studio nur die Preview-Versionen komplett kostenfrei. Von der stabilen Version ist nur die Community-Version kostenlos. Diese ist technisch für die Tutorial-Artikel hinreichend, unterliegt aber der rechtlichen Einschränkung. Das Erstellen von Open-Source-Anwendungen sowie der Einsatz für Online- oder Präsenzschulungen oder für akademische Forschungszwecke sind aber auch in größeren Unternehmen erlaubt. Das Tutorial könnte man als "Forschung" verstehen. Wer keine Preview-Versionen installieren möchte, kann auch die stabile Version 16.7 von Visual Studio mit .NET Core 3.1 SDK nutzen. Auf wenige, dann nicht verfügbare Funktionen wird das Tutorial hinweisen. Inhaltlich setzt es voraus, dass Entwickler mit .NET Core, C#, Web-APIs beziehungsweise REST-Diensten, HTML, CSS und JavaScript vertraut sind.

Mehr zu .NET 5.0

betterCode() präsentiert: .NET 5.0 – Das Online-Event am 3. Dezember 2020

Das können Sie lernen:

  • Von .NET Framework über .NET Core zu .NET 5.0: Was bedeutet das für die Migration, und wie groß sind die Aufwände?
  • Was ist neu in .NET 5.0?
  • Neue Features: ASP.NET Core 5.0 und Blazor 5.0 kennen lernen
  • Die wichtigsten Sprachneuerungen in C# 9
  • Mobile Entwicklung mit .NET 5
  • OR-Mapping mit Entity Framework Core 5.0
  • WinUI 3 als Alternative zu WPF und UWP
  • Ausblick auf .NET 6.0

Im Rahmen eines dreiteiligen iX-Tutorials (siehe [1], [2] und [3]) ist 2017 die Single-Page-Webanwendung "MiracleList" zur Aufgabenverwaltung mit TypeScript, Angular und Twitter Bootstrap sowie diversen frei verfügbaren Steuerelementen entstanden. "MiracleList" ist angelehnt die Webanwendung und App "WunderList", die die Firma Microsoft 2015 für über 100 Millionen Dollar kaufte und dann 2020 zugunsten von "Microsoft ToDo" beerdigte.

Dieses Tutorial wird nun die gleiche Webanwendung mit ASP.NET Core Blazor WebAssembly nachprogrammieren. Das Ergebnis zeigt Abbildung 2, man kann es sich live im Internet ansehen. Den Quellcode (Ausgangszustand, Zwischenstände und Endstand jeweils in Branches) finden Leser auf GitHub.

MiracleList besitzt Aufgabenkategorien (s. links in Abb. 2), eine Liste von Aufgaben in jeder Kategorie (Mitte) und einen Aufgabenbearbeitungsbereich rechts, der aber nur fallweise eingeblendet wird.

MiracleList mit Blazor WebAssembly (Abb. 2)

Das Tutorial wird auf das in der Cloud vorhandene Backend zugreifen. Um das nutzen zu können, müssen Anwender dort eine "Client ID" beantragen.

Microsoft liefert im .NET SDK jeweils eine Projektvorlage für Blazor Server und Blazor WebAssembly mit. Diese Vorlagen kann man über die Kommandozeile (dotnet new) oder Visual Studio verwenden. Das Tutorial soll aber nicht mit dieser Projektvorlage beginnen, sondern mit einer eigens dafür erstellten, die Leser auf GitHub herunterladen können. Der Vorteil ist, dass hier die notwendigen Gestaltungselemente (CSS, Grafiken) schon eingebunden und alle nicht erforderlichen Beispiele aus der Standardprojektvorlage entfernt sind. Das wären sonst viele einzelne, fehleranfällige Schritte.

Anwender öffnen die Datei MiracleList.sln über File | Open | Project/Solution in Visual Studio und sehen dann im Solution Explorer (s. rechts Abb. 3) eine Projektmappe "MiracleList" mit drei Projekten:

  • "MiracleListBW" (Target Framework: .NET 5.0): das Blazor-WebAssembly-basierte Frontend
  • "MiracleListAPI_Proxy" (.NET Standard 2.0): Proxy-Klassen für das MiracleList-Backend, generiert aus der Open API Specification (OAS) mit NSwagStudio.
  • "BO" (.NET Standard 2.0): gemeinsame Geschäftsobjekte, die im Backend und im Frontend zum Einsatz kommen

Wer nicht .NET 5.0, sondern das alte Blazor WebAssembly 3.2 nutzen mag, muss die Projektdatei MiracleListBW32.csproj anstelle von MiracleListBW.csproj in die Projektmappe einbinden.

Nun empfiehlt es sich, die Anwendung zum Test zu übersetzen (Build/Build Solution) und dann zu starten, entweder mit Debug/Start Debugging[/cpas] (Taste F5) oder [caps]View in Browser im Kontextmenü des Projekts. Wenn alles auf dem PC korrekt arbeitet, sieht man die Webanwendung mit zwei Menüpunkten (Abb. 3).

Ausgangsbasis für das Tutorial (Abb. 3)

Zu beachten ist, dass WebAssembly grundsätzlich nur in modernen Webbrowser implementiert ist – also nicht im Internet Explorer. Für Softwareentwickler erfordert ein Debugging aus Visual Studio heraus derzeit einen Chromium-basierten Webbrowser, also Google Chrome oder Microsoft Edge der neuesten Generation. Den Browser für das Debugging wählen Entwickler in dem in Abbildung 4 gezeigten Menü in der Visual-Studio-Symbolleiste.

Wahl des Webbrowsers, der das Debugging starten soll (Abb. 4)

Das gelieferte Vorlagenprojekt ist wie folgt aufgebaut:

  • Es gibt einen Ordner wwwroot mit CSS- und Grafikdateien (u. a. Bootstrap und Open Iconic), der statischen Startdatei index.html (mit CSS-basierter Ladeanimation) sowie einem Manifest und einem Service-Worker für eine Progressive Web App (PWA). Zu sehen ist daher in Abbildung 5 am Ende der Chrome-Adressenleiste ein Pluszeichen, um die Anwendung als PWA zu installieren.

Start der Ausgangsbasis für das Tutorial im Chrome-Browser (Abb. 5)
  • Program.cs und App.razor bilden den Startcode der Anwendung.
  • Der Ordner /Shared enthält die Grundstruktur des Layouts mit weißer Kopfzeile inklusive Hamburger-Menü und gelbem Hauptbereich.
  • Die beiden Razor Components (.razor-Dateien) finden sich unter /Pages. Index.Razor (realisiert als Template mit Code-Behind-Datei) gibt bisher nur einen statischen Text aus. About.Razor (realisiert als Template mit Inline-Code) liefert die Versionsnummernanzeige, die in Abbildung 5 zu sehen ist.
  • Imports.Razor enthält die Einbindungen von Namensräumen mit @using, die für alle Razor-Component-Dateien gelten.

Ein erstes Beispiel, wie man den generierten Proxy-Code des Backends aufruft, sieht man schon in der Implementierung von About.Razor in Abbildung 3. Die generierte Klasse MiracleListAPI.MiracleListProxy wird per Dependency Injection mit @inject in die Razor Component injiziert. Im Lebenszyklusereignis OnInitializedAsync() erfolgt dann der Aufruf der entsprechend generierten Methode mit await, zum Beispiel await proxy.AboutAsync(). Voraussetzung für die Dependency Injection ist wie üblich in .NET Core/.NET 5, dass die Klasse im Dependency-Injection-Container registriert wurde. Das erfolgt in Blazor-WebAssembly-Anwendungen in der Program.cs-Datei. Das Listing 1 gibt den Kern der Startklasse wieder.

Listing 1: Ausschnitt aus Program.cs

public class Program
 {
 var builder = WebAssemblyHostBuilder.CreateDefault(args);
 builder.RootComponents.Add<App>("app");
 IServiceCollection services = builder.Services;  
 services.AddSingleton(new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
 services.AddScoped<MiracleListAPI.MiracleListProxy>();
 await builder.Build().RunAsync();
}

Wer keine generierte Proxyklasse verwenden will oder kann, würde direkt die Methoden der Klasse HttpClient nutzen, zum Beispiel:

@inject HttpClient Http
var daten = await Http.GetJsonAsync<List<Typ>>("https://Server/URL");

Die Web-API-Operation /About, die die Methode AboutAsync() in Abbildung 3 aufruft, ist eine der wenigen Methoden im MiracleList-Backend, die keine Authentifizierung erfordert, denn diese Operation liefert nur allgemeine Daten über das Backend-System. Für einen konkreten Zugriff auf die Datenbank ist eine Authentifizierung erforderlich, wobei dieses auch in Schulungen verwendete Backend bewusst ein extrem gnädiges Benutzerverwaltungssystem besitzt: Wenn es zu einer Kombination aus Benutzernamen und Kennwort noch kein Konto gibt, wird automatisch ein solches mit Testdaten erstellt. Zur Authentifizierung brauchen Entwickler allerdings eine Client-ID, die zuvor zu beantragen ist (s. o.).

An dieser frühen Phase der Entwicklung soll noch keine Anmeldemaske erstellt werden. Vielmehr hinterlegen Entwickler die Anmeldedaten statisch in einer Klasse, sodass die Anmeldung bei jedem Seitenaufruf automatisch erfolgen kann (s. Listing 2). In der dritten Folge dieses Tutorials wird die Klasse AuthenticationManager dann zum AuthenticationStateProvider für Blazor ausgebaut.

Listing 2: Statisch hinterlegte Authentifizierung

using MiracleListAPI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Web
{
 /// <summary>
 /// Authentifizierung zum Debugging
 /// </summary>
 public class AuthenticationManager
 {
  MiracleListAPI.MiracleListProxy proxy { get; set; }

  public AuthenticationManager(MiracleListAPI.MiracleListProxy proxy)
  {
   this.proxy = proxy;
  }

  public const string DebugUser = "iXTutorial";
  public const string DebugPassword = "Sehr+Geheim"; // :-)
  public const string ClientID = "0000-0000-0000";
  public LoginInfo CurrentLoginInfo = null;
  public string Token { get { return CurrentLoginInfo?.Token; } }

  public async Task<bool> Login()
  {
   var l = new LoginInfo() { Username = AuthenticationManager.DebugUser, Password = AuthenticationManager.DebugPassword, ClientID = AuthenticationManager.ClientID };
   try
   {
    CurrentLoginInfo = await proxy.LoginAsync(l);
    if (String.IsNullOrEmpty(CurrentLoginInfo.Token))
    {
     Console.WriteLine("Anmeldung NICHT erfolgreich: " + this.CurrentLoginInfo.Username);
     return false;
    }
    else
    {
     Console.WriteLine("Anmeldung erfolgreich: " + this.CurrentLoginInfo.Username);
     return true;
    }
   }
   catch (Exception ex)
   {
    Console.WriteLine("Anmeldefehler: " + ex.Message);
    return false;
   }
  }
 }
}

Die /Login-Operation auf dem Server erwartet als Datenstruktur den Typ LoginInfo mit Benutzername, Kennwort und Client-ID. Anwender sollten unbedingt in Listing 2 die dort abgedruckte, nicht funktionierende Client-ID durch eine beantragte persönliche Client-ID ersetzen. Wenn die Client-ID korrekt war und Benutzername beziehungsweise Kennwort zueinander passten (oder neu waren), erhalten Aufrufer wieder ein LoginInfo-Objekt, in dem sich nun ein Token befindet, das bei jedem Aufruf einer Web-API-Operation zu übergeben ist. Die Übergabe erfolgt in Wirklichkeit im HTTP-Header; das verbirgt der von NSwag Studio generierte Wrapper. Nicht umsonst, hat er doch für rund ein Dutzend Web-API-Operationen bereits über 1400 Zeilen C#-Programmcode erzeugt, die den Fingerkuppenabrieb deutlich reduzieren und das Budget der Auftraggeber schonen.

Die Klasse AuthenticationManager nutzt die aus Konsolenanwendungen bekannte statische Methode Console.WriteLine() für Protokollausgaben. In Blazor WebAssembly landen diese Ausgaben in der Entwicklerkonsole des Webbrowsers. In Blazor Server würden sie aber auf dem Webserver landen. Außerdem ist zu beachten, dass Console.WriteLine() in C# nicht wie console.log in JavaScript für Objektparameter die Werte der einzelnen Objekteigenschaften ausgibt, sondern in den meisten Fällen nur den Klassennamen ausgibt; Entwickler müssen selbst für einen "Dump" des Objekts sorgen.

In Listing 2 ist noch zu beachten, dass hier die Dependency Injection weder mit @inject noch mit [Inject] erfolgen kann, da es sich um keine Razor Component, sondern nur um eine "einfache" Klasse handelt. Hier bekommt man die Instanzen per Konstruktorparameter. Damit das funktioniert, ist diese Klasse auch dem Dependency-Injection-Container bekannt zu machen und deren Instanzen dann über den Dependency-Injection-Container zu beschaffen:

services.AddScoped<AuthenticationManager>();

Auf dieser Basis lässt sich nun schon der Hauptteil der Benutzeroberfläche rendern. Die Listings 3 und 4 zeigen die Implementierung von Index.razor.cs und der zugehörigen Template-Datei Index.Razor.

Listing 3: Index.razor.cs

using Microsoft.AspNetCore.Components;
using MiracleListAPI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;


namespace Web.Pages
{
 public partial class Index
 {
  [Inject]
  AuthenticationManager am { get; set; }
  [Inject]
  MiracleListAPI.MiracleListProxy proxy { get; set; }

  #region Einfache Properties zur Datenbindung
  List<BO.Category> categorySet { get; set; }
  List<BO.Task> taskSet { get; set; }
  BO.Category category { get; set; }
  BO.Task task { get; set; }
  #endregion

  /// <summary>
  /// Lebenszyklusereignis: Komponente wird initialisiert
  /// </summary>
  /// <returns></returns>
  protected override async Task OnInitializedAsync()
  {
  // TODO: Muss später im Anmeldebildschirm erfolgen
   if (await am.Login())
   {
    await ShowCategorySet();
   }
  }

  public async Task ShowCategorySet()
  {
   categorySet = await proxy.CategorySetAsync(am.Token);
   // zeige erste Kategorie an, wenn es Kategorien gibt!
   if (this.categorySet.Count > 0) await ShowTaskSet(this.categorySet[0]);
  }

  public async Task ShowTaskSet(BO.Category c)
  {
   this.category = c;
   this.taskSet = await proxy.TaskSetAsync(c.CategoryID, am.Token);
  }

  public async Task ShowTaskDetail(BO.Task t)
  {
   this.task = t;
  }


 } // end class Index
}

Hier werden die ersten beiden Spalten aus Abbildung 2 erzeugt. Für die dritte Spalte wird im zweiten Teil des Tutorials die Bearbeitungsansichtskomponente TaskEdit.razor mit dem Tag <TaskEdit> eingebunden. Das Razor-Template in Listing 4 beginnt mit der Deklaration der Route "/". Hier folgt später mit der Direktive @attribute [Authorize] die Festlegung, dass nur angemeldete Benutzer diese Komponente aufrufen dürfen.

Listing 4: Index.razor.cs

@page "/"

<div class="row">

 <!-- ### Spalte 1: Kategorien-->
 @if (categorySet != null)
 {
 <div class="WLPanel col-xs-4 col-sm-4 col-md-3 col-lg-2 @(this.task!=null ? "hidden-sm hidden-xs": ""  )">
  <!-- ---------- Überschrift Spalte 1-->
  <h4> @(categorySet.Count()) <span>Categories</span></h4>
  <!-- ---------- neue Kategorie eingeben-->
  @*TODO*@
  <!-- ---------- Kategorieliste ausgeben-->

  <ol class="list-group scroll">
   @foreach (var c in categorySet)
   {
    <li class="list-group-item" @onclick="() => ShowTaskSet(c)" title="Task Category #@c.CategoryID" style="Background-color:@(this.category != null && c.CategoryID == this.category.CategoryID ? "#E0EEFA" : "white")">
     @c.Name
     <span id="Remove" style="float:right;" class="glyphicon glyphicon-remove-circle"></span>
    </li>
   }
  </ol>
 </div>

  <!-- ### Spalte 2: Aufgabenliste-->
  <div class="WLPanel @(this.task==null ? "col-xs-8 col-sm-8 col-md-9 col-lg-10 ": "hidden-xs col-sm-6 col-md-5 col-lg-6"  )">
   <!-- ---------- Überschrift Spalte 2-->
   <h4 id="TaskHeadline">@(taskSet == null ? 0 : taskSet.Count()) <span>Tasks in</span><i> @category?.Name</i></h4>
   <!-- ---------- neue Aufgaben eingeben-->
   @*TODO*@
   <!-- ---------- Aufgabenliste ausgeben-->
   @if (taskSet != null)
   {<ol id="TaskSet" class="list-group scroll">
     @foreach (var t in taskSet)
     {
      <li class="list-group-item" style="Background-color: @((t.TaskID == this.task?.TaskID) ? "#E0EEFA" : "white")" title="Task #@t.TaskID">
       <span id="Remove" style="float:right;" class="glyphicon glyphicon-remove-circle" ></span>

       <input type="checkbox" name="@("done" + t.TaskID)" id="@("done" + t.TaskID)" checked="@t.Done" class="MLcheckbox"  />

       <b>@t.Title</b>

       @if (t.Due.HasValue)
        if (t.Due.Value < DateTime.Now)
        {
         <div style="color:red">Due since @t.Due.Value.ToShortDateString()</div>
        }
        else
        {
         <div>Due at @t.Due.Value.ToShortDateString()</div>
        }
      </li>
     }
    </ol>
   }
  </div>
 }
@*TODO <TaskEdit> *@
</div>

Im Programmcode findet man eine Reihe von @if-Bedingungen, die fallweise ganze Blöcke nur dann rendern, wenn die Daten auch verfügbar sind. Das ist in Blazor grundsätzlich wichtig, da viele Operationen asynchron sind (s. die Web-API-Aufrufe) und es möglich ist, dass die Daten beim ersten Rendern noch nicht verfügbar sind, sodass es zu Null-Referenz-Fehlern kommen kann. Wenn die Daten dann etwas später eintreffen, stößt Blazor automatisch ein erneutes Rendern an.

Fallunterscheidungen mit dem bedingten C#-Ausdruck ( Bedingung ? ja : nein ) findet man auch innerhalb einzelner HTML-Attribute, zum Beispiel bei style für die farbliche Hervorhebung (hellblau) der aktuellen Kategorie und der aktuellen Aufgabe sowie bei class für das fallweise Ausblenden ganzer Spalten.

Mit @foreach bildet man in Razor eine Schleife, zum Beispiel über die Liste der Kategorien und die Liste der Aufgaben. Die Code-Behind-Datei (s. Listing 4) stellt zur Datenbindung zwei Properties für die Listen von Aufgabenkategorie (List<BO.Category> categorySet) und die Listen der Aufgaben innerhalb einer Kategorie (List<BO.Task> taskSet) bereit. In der Vorlage wird jeweils mit @foreach über diese Listen iteriert, um <li>-Tags zu erzeugen, die mithilfe von Bootstrap-CSS-Klassen (list-group-item) als große, leicht auch mit dem Finger ansteuerbare Blöcke erscheinen. Bei einem Klick (@onclick) auf einen der Blöcke wird jeweils eine Methode ShowTaskSet(c) und ShowTaskDetail(t) erzeugt, die das aktuelle Objekt in den Properties Category beziehungsweise im Task vermerkt. In OnInitializedAsync() ist festgelegt, dass beim Initialisieren der Razor Component die ShowCategorySet() gerufen wird, die die Kategorienliste lädt und die erste Kategorie anzeigt, wenn eine vorhanden ist.

In der Template-Datei gibt es noch Platzhalter-Kommentare (@*TODO*@). Hier folgen im nächsten Teil dann die Texteingabesteuerelemente für neue Kategorien und neue Aufgaben sowie das Bearbeitungsformular. Ebenso fehlt noch die Ereignisbehandlung für die kleinen "x" im Kreis, mit denen man Elemente löschen kann.

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

  1. Holger Schwichtenberg; Die 100-Millionen-Dollar-App. Webentwicklung mit Angular, Teil 1: Grundgerüst, Zugriff auf Webservices, Templates; iX 5/2017
  2. Holger Schwichtenberg; Formulargedöns: Browser-Programmierung mit Angular, Teil 2; iX 6/2017
  3. Holger Schwichtenberg; Feinschliff: Web-Programmierung mit Angular, Teil 3; iX 7/2017

(ane)