Blazor WebAssembly: Bidirektionale Kommunikation und Benachrichtigungen

Seite 2: SignalR-Client

Inhaltsverzeichnis

Das NuGet-Paket Microsoft.AspNetCore.SignalR.Client ist in den Blazor-Client zu integrieren:

install-package Microsoft.AspNetCore.SignalR.Client

Im Programmcode einer Razor Component bauen Entwickler bei Bedarf mit der Klasse HubConnectionBuilder unter Verwendung der URL eine Verbindung zum Hub auf:

using Microsoft.AspNetCore.SignalR.Client;
...
public HubConnectionB hubConnection { get; set; }
...
protected override async Task OnInitializedAsync()
{
   var hubURL = new Uri(new Uri(proxy.BaseUrl), "MLHub");
   hubConnection = new HubConnectionBuilder()
       .WithUrl(hubURL)
       .Build();
}

Die URL zum Hub lässt sich aus der Basis-URL des WebAPI-Backends und dem relativen Pfad zum SignalR-Hub/MLHub zusammensetzen, sofern das Backend beides anbietet.

Mit hubConnection.On() registrieren Entwickler ihren Programmcode, der bei eingehenden Nachrichten ausgeführt werden soll (s. Listing 2):

public partial class Index
 {

  #region Infrastruktur-Objekte
  HubConnection hubConnection;
  ClaimsPrincipal user;
  #endregion

...
  /// <summary>
  /// Lebenszyklusereignis: Komponente wird initialisiert
  /// </summary>
  /// <returns></returns>
  protected override async Task OnInitializedAsync()
  {
   Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId);
   user = (await authenticationStateTask).User;
   await ShowCategorySet();

   var hubURL = new Uri(new Uri(proxy.BaseUrl), "MLHub");
   Console.WriteLine("SignalR: Connect to " + hubURL.ToString());
   hubConnection = new HubConnectionBuilder()
       .WithUrl(hubURL)
       .Build();

   // --- eingehende Nachricht
   hubConnection.On<string>("CategoryListUpdate", async (connectionID) =>
   {
    if (hubConnection.ConnectionId != connectionID)
    {
     string s = $"Kategorieliste wurde auf einem anderen System geändert.";
     Console.WriteLine(s);
     await ShowCategorySet();
     StateHasChanged();
    }
   });
   // --- eingehende Nachricht
   hubConnection.On<string, int>("TaskListUpdate", async (connectionID, categoryID) =>
   {
   if (hubConnection.ConnectionId != connectionID)
   {
    string s = $"Aufgaben der Kategorie #{category.CategoryID}: \"{this.category.Name}\" wurden auf einem anderen System geändert.";
    Console.WriteLine(s);
    if (categoryID == this.category.CategoryID) await ShowTaskSet(this.category);
    StateHasChanged();
    }
   });

   // Verbindung zum SignalR-Hub starten
   await hubConnection.StartAsync();
   // Registrieren für Events
   await hubConnection.SendAsync("Register", user.Identity.Name);
  } 
... 
}

Listing 2: Erweiterte Implementierung von OnInitializedAsync() in Index.razor.cs

Dabei legt man Typparameter entsprechend der vom Hub übermittelten Parameter fest. Das ist bei der Nachricht CategoryListUpdate nur die Connection-ID, bei SendTaskListUpdate zusätzlich auch noch die ID der aktualisierten Aufgabenkategorie. Jeder SignalR-Client besitzt eine eindeutige Connection-ID, die SignalR selbst vergibt. Die Hub-Implementierung von AddToGroupAsync() sendet im Standard eine Nachricht an alle Teilnehmer in einer Gruppe, also auch den Absender selbst aus. Durch den Vergleich der Connection-ID kann eine Anwendungsinstanz aber vermeiden, dass sie mit einem Neuladen reagiert, obwohl sie die Daten selbst geändert hat. Daher sieht man in Listing 2 die Bedingung hubConnection.ConnectionId != connectionID. Im Fall der Nachricht "TaskListUpdate" ist eine weitere Bedingung implementiert, dass die Daten nur neu geladen werden, wenn die Instanz der Blazor-Anwendung auch gerade die gleiche Aufgabenkategorie darstellt.

Wichtig ist, dass in der Behandlung der eingehenden Nachricht in der Razor Component StateHasChanged() aufgerufen wird, wenn sich Daten geändert haben, die in der Benutzeroberfläche dargestellt werden sollen. Blazor bekommt die Property-Änderung nicht automatisch mit, da diese im Hintergrund eingehen.

Nach der Registrierung der Ereignisbehandlungen für eingehende Nachrichten mit On() ist einmalig

await hubConnection.StartAsync();

auszuführen. Im folgenden Listing findet sich darunter der folgende Aufruf:

await hubConnection.SendAsync("Register", user.Identity.Name);

Das ist der Aufruf der im Hub auf dem Server implementierten Methode Register(), die dazu dient, alle Clients eines Benutzers zu einer Gruppe zusammenzufassen. Bei hubConnection.SendAsync() werden immer als erster Parameter der Name der aufzurufenden Hub-Methode und als weitere die Parameter dieser Hub-Methode übergeben.

Mit hubConnection.SendAsync() wird der Hub informiert, wann immer es eine Datenänderung gibt. Das bedeutet, dass in Index.razor.cs nun an allen Stellen, an denen sich die Kategorienliste geändert hat (z. B. NewCategory_Keyup() und RemoveCategory()), das Folgende zu ergänzen ist:

await hubConnection.SendAsync("SendCategoryListUpdate", user.Identity.Name);

Ebenso ist an allen Stellen, an denen sich die Aufgabenliste einer Kategorie geändert hat (z. B. NewTask_Keyup(), ReloadTasks() und RemoveTask()), das Folgende zu ergänzen:

await hubConnection.SendAsync("SendTaskListUpdate", user.Identity.Name, this.category.CategoryID);

Hier nicht zu vergessen ist die Änderung des Erledigt-Zustandes einer Aufgabe per Kontrollkästchen. Den Programmcode dafür hatte der Autor in Teil 2 nicht in die Code-Behind-Datei gepackt, sondern direkt inline im Markup-Code hinterlegt. Dass das möglich, aber nicht schön ist, wird jetzt noch deutlicher, wo ein weiterer Befehl zur Informierung des SignalR-Hubs via SendAsync() zu ergänzen ist:

<input type="checkbox" name="@("done" + t.TaskID)" id="@("done" + t.TaskID)" 
checked="@t.Done" class=" MLcheckbox" 
@onchange=@(async(eventArgs) => {                                                                                     t.Done = (bool)eventArgs.Value; 
          await proxy.ChangeTaskAsync(t, am.Token);     
          await hubConnection.SendAsync("SendTaskListUpdate", 
               user.Identity.Name, this.category.CategoryID); 
}) />

Selbstverständlich kann man diesen Inline-Code jederzeit in eine eigene Routine in der Code-Behind-Datei auslagern – das sei hier dem Leser als Übung überlassen.