Gib 8–Blazor united

Da jubeln sie alle. Auf Twitter und in den Blog Artikeln. Die Version 8 von :NET core und Blazor ändert alles. Es wird zusammengeführt was zusammen gehört. Egal ob WASM oder Blazor Server- alles das selbe, mal Männchen, mal Weibchen, grad wie es man sich wünscht.

Unzählige Stunden habe ich mit Visual Studio 2022 Preview zugebracht um herauszufinden wo wo was. Und ich kann euch sagen, tiefe Ernüchterung hat sich breit gemacht. Es fängt damit an das der Standard Rendermode einer Blazor .Razor Seite nun “statisch” ist. Es wird einfach eine strunzdumme HTML Seite gerendert und per HTTP Get zwischen Server und Browser hin und her geschickt. Das gabs mit ASP (ja Active Server Pages) schon mal. Aber der Trend alles neu zu erfinden …

Mein erster Versuch bzw Idee: Eine ServerRendert Blazor Page enthält eine Component die als WASM im Browser ausgeführt wird.

Mit dem neueen einheitlichen Projekt Template für Balzor Apps ist man ja schon mal ganz nah dran. Man erhält zwei Projekte. Ein WASM und ein Server.

Blazor8a

Die WASM Variante wird als Client bezeichnet. Dort liegt eine Counter.razor die ich nun nutzen will.

Blazor8b

Zunächst modifiziere ich die home.razor. Zeile 2 sollte in der finalen Version von .net 8 obsolet sein. Erst mit Zeile 3 wird das eine Server Blazor Page wie wir alle das kennen.

   1:  @page "/"
   2:  @using static Microsoft.AspNetCore.Components.Web.RenderMode
   3:  @rendermode RenderMode.InteractiveServer
   4:  <button @onclick="click">click</button>
   5:  @ausgabe
   6:  @code
   7:  {
   8:      string ausgabe;
   9:      void click()
  10:      {
  11:          ausgabe = DateTime.Now.ToString();
  12:      }
  13:  }

 

Die Counter Component läuft in einem extra Projekt und damit auch in einem extra Adressraum. Hier kommt ein neues Attribut RenderMode Auto zum tragen. Diese Counter Komponente kann WASM oder Server sein. Selbst eine dynamische Zustandsänderung von Server zu WASM ist möglich und wurde von mir auch beobachtet.

   1:  @page "/counter"
   2:  @attribute [RenderModeInteractiveAuto]

Die Syntax in Zeile 2 wird in der finalen Version dem Beispiel in Listing ein folgen.
Nun weis man aber nicht was mit Counter genau passiert. Bist du WASM oder Server?
Theoretisch kann man den Rendermode bei Deklaration der Komponente mit geben.

Blazor8c

Das mag Blazor bzw Visual Studio aber nicht. Kompiliert nicht mehr, mit dem Hinweis, das der Rendermode in der Komponente oder in der Deklaration zu erfolgen hat. Also aus der Counter.Razor Zeile 2 (von vorigen Listing) entfernen.

Wirft im Browser allerdings auch schnell eine Fehlermeldung

Blazor8d

Die Fehlermeldung in der F12 Browser Console ist (noch) nichtssagend. Es geht schlicht nicht, WASM und Server in der Hierarchie zu mischen. Wenn man allerdings den Rendermode von Counter in home.razor auf Interactive Server ändert, dann klappt es, weil beide Komponenten bzw die ganze Page Server rendert ist

   1:  @rendermode RenderMode.InteractiveServer
   2:  <button @onclick="click">click</button>
   3:  @ausgabe
   4:  <hr />
   5:  <BlazorApp8Try3.Client.Pages.Counter 
   6:  @rendermode="RenderMode.InteractiveServer">
   7:  </BlazorApp8Try3.Client.Pages.Counter>

Tatsächlich kann Benutzer nun beide Buttons klicken und es funktioniert wiw erwartet.

Blazor8e

Der Kontrollblick in die F12 Browser Tools, bzw den Netzwerk Tab offenbart, das beide Komponenten nun eine SignalR, respektive eine Websocket Connection nutzen.

Blazor8f

Nun stellt sich die Frage, wie können die beiden Komponenten miteinander kommunizieren? Der erste Versuch führt zu einem Parameter in der Counter Component. Nennen wir ihn Count und weisen ihn in der Home.razor zu
<BlazorApp8Try3.Client.Pages.Counter Count="23"

Das Ergebnis liegt im Rahmen der Erwartungen. Klappt das aber auch per Referenz? Dazu wird in der Deklaration der Kind Komponente eine @ref gesetzt. Dieser Wert wird als Objekt pseudo mäßig deklariert. Beim Zugriff auf das Obekt Client1 erscheint dieses als Null und wirft eine Exception.

Blazor8g

Das Problem lässt sich lösen indem man einfach ? Null-Conditional Operator einfügt. Ansonsten klappt das aber wie erwartet.

Shake it Baby-WASM und Server in einer Blazor Page

Wir wissen nun, das solange man den Rendermode ident hält, man doch WASM und Server wechseln kann. Ich habe natürlich nicht alle Fälle ausgetestet. Ich wüsste auch nicht warum man das tun sollte. Das Feature Set von WASM und Server Blazor unterscheidet sich zu deutlich, als das man dort United entwickeln sollte.

Unser fiktiver Anwendungsfall mischt nun beides in einer Page. Eine WASM Komponente die per Webcam einen QR Code auslesen kann und den als Filterparameter in einer EF Datenbank Abfrage serverseitig nutzt. Soweit die Idee.

Der Ansatz: eine statische Blazor Page in der beide Komponenten parallel existieren.

Blazor8h

Die statisch.razor Page mit den Components aus dem Client und dem Server Projekt.

   1:  @page "/statisch"
   2:  @using static Microsoft.AspNetCore.Components.Web.RenderMode

3: <CounterServer @rendermode="RenderMode.InteractiveServer">

</CounterServer>

   4:  <h3>Statisch</h3>
   5:  <BlazorApp8Try3.Client.Pages.Counter @rendermode="RenderMode.InteractiveWebAssembly">
   6:  </BlazorApp8Try3.Client.Pages.Counter>

Tatsächlich klappt das im Browser ohne das der Benutzer im Ansatz erkennen kann, was im Hintergrund passiert.

Blazor8i

Ein Blick in den Netzwerk Trace der WebSocket Nachrichten offenbart allerdings, das beide Komponenten Websocket verwenden. Testweise wird die Server Counter Component auf WASM gesetzt. Keine Fehlermeldung und trotzdem Blazor Server. Erwartungsgemäß kann das auch nicht funktionieren, weil eine WASM Anwendung im Browser laufen muss und folglich auch binär extra und anders kompiliert sein muss.

Es wird nun der Rendermode der WASM Komponente aus dem letzten Listing in Zeile 5 entfernt. Würde man das auch in Zeile 3 tun, wäre der dortige Counter statisch und würde auf keinen Button click mehr reagieren. Wird nun in der Client Counter Component der Rendermode direkt auf Auto gestellt, wird wiederum von Blazor für beide Components der Server Render Mode gewählt.

@page "/counter"
@attribute [RenderModeInteractiveAuto]

Verwirrend nicht?

Tatsächlich tauchen nun die WASM Button Clicks nicht mehr im Netzwerk Trace als Websocket Messages auf. Leider fehlt mir eine andere Möglichkeit im Browser anzuzeigen wie Blazor nun tatsächlich rendert (hinweise erbeten).

Wie kommen die beiden nun zusammen und können Daten austauschen. Dazu die freundliche Erinnerung das WASM und Server nicht nur in getrennten Adressräumen sondern in der Regel Physikalisch getrennt agieren.

Blazor8j

Mir unbekannt ist eine Art Automatismus die in Blazor den Datenaustausch zwischen den Komponenten erlauben würde. Da der Rahmen Statisch ist fällt alles weg wie CascadingParameter oder eine per DI injizierte Variable. Mir bleibt nur die Mittel des Webs. Cookie, Url, LocalBrowser Storage. Alles bisher ungetestet.

Ich habe mir die Frage gestellt ob man die Websocket SignalR Verbindung der Server Component kapern kann und aus der Client WASM Komponente nutzen. Vorweg, das klappt, ob das eine Gute Idee ist, wage ich heute noch nicht zu beurteilen.

Wohin mit dem JavaScript? Um es ganz einfach zu machen, stecke ich das in die “neue” App.Razor, die das gesamte HTML Konstrukt der Page in Blazor 8 hosted (vorher _hosts.cshtml).

Blazor8k

Zunächst muss die Server Rendered Component, CallServerJS aufrufen um eine Instanz eines JavaScript Zugriffsobjektes auf diese Blazor Page zu erhalten. Dieses wird global als “bridge” definiert und zugewiesen. Auf dieser Bridge kann die WASM Rendered Component einen Aufruf basierend auf der existierenden Signalr Connection an den Server senden, wo in der Blazor Component eine Methode CallFromWasm wartet. Der JavaScript Code in App.razor.

   1:  <Routes />
   2:  <script src="_framework/blazor.web.js"></script>
   3:  <script>
   4:      var bridge;
   5:      window.wasmJS = () => {
   6:          bridge.invokeMethodAsync('CallFromWASM', '42')
   7:      };
   8:      window.CallServerJS = (dotNetHelper, parameter) => {
   9:          bridge = dotNetHelper;
  10:      };     
  11:  </script>

Um mit JavaScript arbeiten zu können braucht man in Blazor schon immer das passende Objekt per Dependency Injection.   @inject IJSRuntime JSRuntime

Weiters wird in einer Server Component mit dem Namen CounterServer.razor eine Object Reference erzeugt auf das in JavaScript genutzt “DotNet” Objekt. Dies wird in einem JS Methoden Aufruf (CallServerJS) mit übergeben.

   1:  private DotNetObjectReference<CounterServer>? objRef;
   2:  protected override void OnInitialized()
   3:  {
   4:      objRef = DotNetObjectReference.Create(this);
   5:  }
   6:  protected override void OnAfterRender(bool firstRender)
   7:  {
   8:     if (firstRender)
   9:      {
  10:     JSRuntime.InvokeVoidAsync("CallServerJS",objRef);
  11:       }
  12:  }

Der vorige Code könnte im OnInitalized zusammengefasst werden, wenn das Prerendering deaktiviert ist. Das ginge theoretisch in .NET 8 mit  @rendermode="new InteractiveAutoRenderMode(false)".

In der selben Datei muss die Methode CallFromWASM lauern, die dann von der WASM Seite initiert werden kann. Es gäbe auch noch die Variante diese Methode Statisch auszuführen um den Kontext der Blazor Server Seite nicht zu brauchen. Das klappt aber nicht, weil hier eben spezifisch das JS DotNet Zugriffsobjekt auf die Page verweist.

   1:   [JSInvokable]
   2:   public void CallFromWASM(string name)
   3:   {
   4:       ausgabe = "fromwasm "+name;
   5:       StateHasChanged();
   6:    }

In der Counter.razor Page Component des “Client” Projektes muss dann nur noch eine Zeile JavaScript Code initiert werden, WasmJS, aus dem Listing der app.razor. Ich wähle dazu den Button Event der den Counter hochzählt, incrementcount und ergänze mit folgender Zeile C# Code.

 JSRuntime.InvokeVoidAsync("wasmJS");


Das klappt tatsächlich. Damit werde ich im nächsten Schritt einen QRCode Scanner Prototypen bauen.

Kommentare sind geschlossen