Blazor Server und Blazor WebAssembly: Alternativen zu JavaScript?

Seite 2: Razor Components

Inhaltsverzeichnis

Blazor Server und Blazor WebAssembly teilen sehr viele Gemeinsamkeiten, insbesondere das gleiche Komponentenmodell (Razor Components), sodass die Benutzerschnittstellenbeschreibung und -steuerungen gleich und damit zwischen beiden Architekturmodellen austauschbar sind, sofern keine Eigenarten der jeweiligen Architektur zum Einsatz kommen. Razor Components darf man nicht verwechseln mit Razor Views (in ASP.NET Core Model View Controller – MVC) und Razor Pages (in ASP.NET Core Razor Pages). Views und Pages verwenden auch die Razor-Syntax, sind aber ältere Modelle für die serverseitige Programmierung von Multi-Page Applications.

Exemplarisch zeigen Listing 1 und 2 eine Razor Component, die in beiden Architekturen einsetzbar ist. Sie besteht aus einer Template-Datei (CodeBehindPartielleKlasse.razor) mit HTML-Markup und eingestreuten C#-Ausdrücken zur Datenbindung (@...) sowie einer rein in C# geschriebenen Code-Behind-Datei (CodeBehindPartielleKlasse.cs). Auch in der .razor-Datei sind Inline-Code-Blöcke (@code) mit Methoden möglich, da der Compiler aus der .razor- auch eine C#-Code-Datei erzeugt, die er anschließend mit der Code-Behind-Datei über das Sprachkonstrukt der partiellen Klassen (partial class) zu einer einzigen .NET-Klasse vereint. In diesem Fall wird exemplarisch zur Erläuterung des Konzeptes gezeigt, dass die Code-Behind-Datei die Lebenszyklusereignisse der Komponente sowie die Ereignisbehandlungen für Benutzerinteraktionen realisiert, von dort aber wieder eine Methode aus dem @code-Block der .razor-Datei aufruft. Solch einen Mischmasch der Codepositionierung sollte man in der Praxis vermeiden, um die Übersicht zu behalten.

Eine Razor Component kann auch auf eine Code-Behind-Datei verzichten (also nur aus Inline-Code-Blöcken bestehen – so zeigt es Microsoft in den eigenen Projektvorlagen, die Visual Studio und .NET SDK mitliefern) oder allein aus einer C#-Datei bestehen. In letzterem Fall schreibt man eine C#-Klasse, die von der Basisklasse Microsoft.AspNetCore.Components.ComponentBase erbt. Wenn eine .razor-Datei vorhanden ist, findet man diese notwendige Vererbung in der aus dieser Datei generierten C#-Code-Datei.

Eine Razor Component kann über eine URL direkt ansteuerbar sein (s. @page in Listing 1).

@page "/Demos/CodeBehindPartielleKlasse"
@using ITVisions.Blazor
@inject BlazorUtil Util

<h2>Code-Behind mit partieller Klasse</h2>
X:
<input id="x" type="number" @bind="X" />
Y:
<input id="y" type="number" @bind="Y" />
<button @onclick="AddAsync">Add x + y</button>
Sum: @Sum

@code
{
 // Hier ist auch Code m�glich, wenn man unbedingt will ;-)
void LogURL()
 {
  Util.Log("LogURL(): " + NavigationManager.Uri.ToString());
 }
}

Listing 1: Razor-Template-Datei der Razor Component (CodeBehindPartielleKlasse.razor)

Ohne die Direktive @page ist eine Komponente nur über ihren Namen als Tag in anderen Komponenten einbettbar, in diesem Fall als <CodeBehindPartielleKlasse>. Parameter müssen Entwickler in Properties, die mit [Parameter] annotiert sind, festlegen und dann im Tag als Attribut belegen: <CodeBehindPartielleKlasse X="1" Y="2">. Komponenten können andere Komponenten per Ereignis über Zustandsänderungen informieren, siehe EventCallback<decimal> ValueHasChanged in Listing 2 und die Auslösung des Ereignisses mit await ValueHasChanged.InvokeAsync(). Nutzer der Komponente können dann eine Ereignisbehandlungsroutine im Tag angeben: <CodeBehindPartielleKlasse X="1" Y="2" ValueHasChanged="BehandleEreignis">.

using System;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using ITVisions.Blazor;
using System.Threading.Tasks;

namespace MLBlazorRCL.Komponentendateien
{
 public partial class CodeBehindPartielleKlasse
 {
  // Schon injiziert in Razor-Seite
  //[Inject]
  //public BlazorUtil Util { get; set; } = null;
  [Inject]
  public IJSRuntime JSRuntime { get; set; } = null;
  [Inject]
  public NavigationManager NavigationManager { get; set; } = null;

  [Parameter]
  public decimal X { get; set; } = 1.23m;
  [Parameter]
  public decimal Y { get; set; } = 2.34m;
  [Parameter]
  public EventCallback<decimal> ValueHasChanged { get; set; }

  public decimal Sum = 0;

  #region Standard-Lebenszyklus-Ereignisse
  protected override void OnInitialized()
  {
   Util.Log(nameof(CodeBehindPartielleKlasse) + ".OnInitialized()");
  }

  protected async override Task OnInitializedAsync()
  {
   Util.Log(nameof(CodeBehindPartielleKlasse) + ".OnInitializedAsync()");
  }

  protected override void OnParametersSet()
  {
   Util.Log(nameof(CodeBehindPartielleKlasse) + ".OnParametersSet()");
  }

  protected async override Task OnParametersSetAsync()
  {
   Util.Log(nameof(CodeBehindPartielleKlasse) + ".OnParametersSetAsync()");
  }

  protected override void OnAfterRender(bool firstRender)
  {
   Util.Log(nameof(CodeBehindPartielleKlasse) + ".OnAfterRender(firstRender=" + firstRender + ")");
   // this.StateHasChanged(); // --> Endlosschleife !!! :-(
  }

  protected async override Task OnAfterRenderAsync(bool firstRender)
  {
   Util.Log(nameof(CodeBehindPartielleKlasse) + ".OnAfterRenderAsync(firstRender=" + firstRender + ")");
  }

  public void Dispose()
  {
   Util.Log(nameof(CodeBehindPartielleKlasse) + ".Dispose()");
  }
  #endregion

  #region Reaktionen auf Benutzerinteraktionen
  public async Task AddAsync()
  {
   Sum = X + Y;
   Util.Log($"{nameof(CodeBehindPartielleKlasse)}.Add(). x={X} y={Y} sum={Sum}");
   await ValueHasChanged.InvokeAsync(Sum);
   LogURL(); // Aufruf von Code in der .razor-Datei -> nur exemplarisch!
  }
  #endregion
 }
} 

Listing 2: Code-Behind-Datei der Razor Component (CodeBehindPartielleKlasse.cs/code])

Sowohl Template-Dateien ([code]@inject) als auch Codedateien ([Inject]) können Instanzen per Dependency Injection beziehen. Razor Components können Softwareentwickler in sogenannte Razor Class Libraries als DLL verpacken und sowohl gleichzeitig in mehreren Blazor-Anwendungen verwenden als auch zwischen Blazor-Server- und Blazor-WebAssembly-basierten Anwendungen teilen.