Blazor und Angular via Web Components verheiraten
19.04.2021, 00:00 Uhr
Mischehe
Microsofts neues Webframework Blazor besitzt eine Interoperabilität zu JavaScript und kann über diese auch Web Components aus komplexen Webframeworks wie Angular verwenden.
Ein Aufruf aus einer in C# geschriebenen Razor Component in Blazor zu JavaScript ist recht schnell realisiert: Man lässt sich eine Instanz für die Schnittstelle Microsoft.JSInterop.IJSRuntime per Dependency Injection von der Blazor-Infrastruktur liefern.
Dies geht im C#-Code einer Razor Component per
[Inject]
IJSRuntime jsRuntime { get; set; }
oder im Razor-Template so:
@inject IJSRuntime jsRuntime
Auf dieser Instanz ruft der Entwickler dann mit InvokeVoidAsync() und InvokeAsync<Rückgabetyp>() eine JavaScript-Funktion auf – zum Beispiel die in JavaScript eingebaute confirm()-Funktion:
if (!await js.InvokeAsync<bool>("confirm",
$"Willst Du wirklich den Datensatz #{ID} löschen?"
)) return;
Natürlich kann man auch eigene JavaScript-Funktionen aufrufen, wenn man die entsprechende JavaScript-Datei zuvor geladen hat. In den ersten Blazor-Versionen konnte man die JavaScript-Datei nur global einbinden, nämlich per
<script src="MeineEigeneSkriptdatei.js"></script>
in der Index.html (bei Blazor WebAssembly) oder der _Host.cshtml (bei Blazor Server).
Seit Blazor 5.0 kann man eine JavaScript-Datei auch dynamisch nachladen:
IJSObjectReference skript =
await jsRuntime.InvokeAsync<IJSObjectReference>(
"import", "./MeineEigeneSkriptdatei.js");
Integration mit Angular über Web Components
Spannend wird es, wenn man in Blazor nicht nur einfache Funktionen nutzen will, sondern auch mit komplexen JavaScript-Web-Frameworks wie Angular oder React zusammenarbeiten möchte.
Der Fall tritt beispielsweise ein, wenn man bestehende Lösungsbausteine in einem solchen Web-Framework besitzt oder dort vorhandene Komponenten nutzen möchte, für die es (noch) kein Pendant in Blazor gibt.
Möglich ist eine Integration von Blazor mit JavaScript-Frameworks über den Web-Components-Standard des World Wide Web Consortium (W3C), genauer gesagt über die Custom Elements [1]. Das funktioniert in beiden Blazor-Varianten, also in Blazor WebAssembly und Blazor Server.
Gezeigt werden soll die Integration hier am Beispiel des Web-Frameworks Angular. Bild 1 zeigt das Resultat im Webbrowser. Die Webseite (gelber Bereich) wird von Blazor gerendert. Der grün umrandete Bereich, also die Tabelle und die OK-Schaltfläche, ist mit Angular realisiert. Das Texteingabefeld und die Schaltfläche Load data from Star Trek API sind in Blazor mit C# umgesetzt. Die Textausgaben links oben stammen zum Teil aus C#, zum Teil aus JavaScript-Code, der außerhalb von Angular läuft.
Architektur mit Klebstoff
Damit sind wir auch schon bei einem entscheidenden Punkt: Direkt aus dem C#-Programmcode heraus die mit Angular erstellte Web Component vollständig zu nutzen ist nicht möglich – zumindest solange Microsoft noch nicht das komplette DOM-API (Document Object Model) direkt in Blazor anbietet. Ein solcher Ansatz ist aber in der Entwicklung, siehe Experimental Blazor JS Interop API [2].
Hier sollen aber keine experimentellen, sondern nur stabile Lösungen zum Einsatz kommen. Daher wird es notwendig sein, noch etwas Verbindungscode („Glue Code“) in JavaScript zu schreiben.
Bild 2 veranschaulicht die Architektur der Lösung:
- Ganz rechts sieht man im satten Rotton der rechten Seite des Angular-Logos die Angular-Komponente grid.component.html. Diese Komponente bietet zunächst zwei Eingabeparameter für die Spaltenfestlegung (columnDefsString, im JSON-Format) und die eigentlichen Daten (rowDataString, ebenfalls im JSON-Format) an, sowie ein Ereignis selectedRowsChanged, das gefeuert wird, wenn der Benutzer nach einer Auswahl auf die Schaltfläche OK klickt.
- Die Angular-Komponente grid.component.html wird mithilfe von Angular Elements (NPM-Paket @angular/elements, siehe auch [3]) als eine Web Component mit dem Tag <angular-grid> angeboten und in das Angular-Elements-Bundle ITVElements.js verpackt.
- Links im typischen Lila des .NET-Logos sieht man eine Razor Component in Blazor, die <angular-grid> verwendet. Die Attribute columnDefsString und rowDataString kann Blazor direkt per Datenbindung befüllen. Für die Ereignisbindung ist etwas Glue Code erforderlich.
- Der Verbindungscode (Glue Code) in der Skriptdatei ITVElementsGlueCode.js (in Bild 2 in JavaScript-Gelb gehalten) bindet sich an das selectedRowsChanged-Ereignis und leitet dieses an die Methode NewSelection() in der Razor Component weiter.
Da hier nicht der komplette Programmcode abgedruckt werden kann, steht er unter [4] zum Download bereit.
Implementierung der Web Component
mit Angular
Die folgenden Listingzeilen zeigen Ausschnitte aus der Realisierung der Web Component mit Angular Elements. Das Projekt wird ganz normal mit dem Angular-CLI (hier verwendete aktuelle Version 10.2) via
ng new
erstellt. Dann fügt man das NPM-Paket Angular Elements hinzu:
ng add @angular/elements
Die Tabelle basiert auf der kostenfreien Community-Variante von ag-grid, laut Herstellermarketing „The Best JavaScript Grid in the World“ [5]. Dieses NPM-Paket ergänzt man über folgenden Befehl:
npm install --save ag-grid-community ag-grid-angular
Listing 1 und Listing 2 zeigen Ausschnitte aus der Realisierung der Angular-Komponente in grid.component.html und der zugehörigen TypeScript-Datei grid.component.ts.
Listing 1: grid.component.html
<link
rel="stylesheet"
href="https://unpkg.com/@ag-grid-community/
all-modules@23.0.0/dist/styles/ag-grid.css" />
<link
rel="stylesheet"
href="https://unpkg.com/@ag-grid-community/
all-modules@23.0.0/dist/styles/
ag-theme-alpine.css" />
<button (click)="getSelectedRows()">OK</button>
<ag-grid-angular
#agGrid
style="width: 650px; height: 500px;"
class="ag-theme-alpine"
[rowData]="rowData"
[columnDefs]="columnDefs"
rowSelection="multiple"
>
</ag-grid-angular>
rel="stylesheet"
href="https://unpkg.com/@ag-grid-community/
all-modules@23.0.0/dist/styles/ag-grid.css" />
<link
rel="stylesheet"
href="https://unpkg.com/@ag-grid-community/
all-modules@23.0.0/dist/styles/
ag-theme-alpine.css" />
<button (click)="getSelectedRows()">OK</button>
<ag-grid-angular
#agGrid
style="width: 650px; height: 500px;"
class="ag-theme-alpine"
[rowData]="rowData"
[columnDefs]="columnDefs"
rowSelection="multiple"
>
</ag-grid-angular>
Listing 2: grid.component.ts
import { Component, OnInit, Input, Output,
OnChanges, SimpleChanges, ViewChild,
EventEmitter } from '@angular/core';
import { AgGridAngular } from 'ag-grid-angular';
import { HttpClient } from '@angular/common/http';
import { HttpHeaders } from '@angular/common/http';
@Component({
selector: 'app-grid',
templateUrl: './grid.component.html',
styleUrls: ['./grid.component.css']
})
export class GridComponent implements OnInit,
OnChanges {
public static gridCount : number = 0;
@ViewChild('agGrid') agGrid: AgGridAngular;
constructor(private http: HttpClient) {
GridComponent.gridCount++;
console.log("==== Grid #" +
GridComponent.gridCount)
}
ngOnInit(): void {
console.log("ngOnInit!", this.columnDefs,
this.rowData );
}
@Input()
columnDefsString : string
@Input()
rowDataString : string;
@Output()
selectedRowsChanged = new EventEmitter();
columnDefs : any;
rowData : any;
ngOnChanges() {
console.log("ngOnChanges Beginn in Grid #" +
GridComponent.gridCount + ":",
this.columnDefsString,this.rowDataString);
if (this.columnDefsString) {
try
{
this.columnDefs =
(JSON.parse(this.columnDefsString));
}
catch
{
console.warn("Cannot parse columnDefsString",
this.columnDefsString );
}
}
if (this.rowDataString) {
try
{
this.rowData =
(JSON.parse(this.rowDataString));
}
catch
{
console.warn("Cannot parse rowDataString",
this.rowDataString );
}
}
console.log("ngOnChanges End in Grid #" +
GridComponent.gridCount + ":",
this.columnDefs,this.rowData);
}
getSelectedRows() {
const selectedNodes =
this.agGrid.api.getSelectedNodes();
const selectedData =
selectedNodes.map(node => node.data );
this.selectedRowsChanged.emit(selectedData);
}
}
OnChanges, SimpleChanges, ViewChild,
EventEmitter } from '@angular/core';
import { AgGridAngular } from 'ag-grid-angular';
import { HttpClient } from '@angular/common/http';
import { HttpHeaders } from '@angular/common/http';
@Component({
selector: 'app-grid',
templateUrl: './grid.component.html',
styleUrls: ['./grid.component.css']
})
export class GridComponent implements OnInit,
OnChanges {
public static gridCount : number = 0;
@ViewChild('agGrid') agGrid: AgGridAngular;
constructor(private http: HttpClient) {
GridComponent.gridCount++;
console.log("==== Grid #" +
GridComponent.gridCount)
}
ngOnInit(): void {
console.log("ngOnInit!", this.columnDefs,
this.rowData );
}
@Input()
columnDefsString : string
@Input()
rowDataString : string;
@Output()
selectedRowsChanged = new EventEmitter();
columnDefs : any;
rowData : any;
ngOnChanges() {
console.log("ngOnChanges Beginn in Grid #" +
GridComponent.gridCount + ":",
this.columnDefsString,this.rowDataString);
if (this.columnDefsString) {
try
{
this.columnDefs =
(JSON.parse(this.columnDefsString));
}
catch
{
console.warn("Cannot parse columnDefsString",
this.columnDefsString );
}
}
if (this.rowDataString) {
try
{
this.rowData =
(JSON.parse(this.rowDataString));
}
catch
{
console.warn("Cannot parse rowDataString",
this.rowDataString );
}
}
console.log("ngOnChanges End in Grid #" +
GridComponent.gridCount + ":",
this.columnDefs,this.rowData);
}
getSelectedRows() {
const selectedNodes =
this.agGrid.api.getSelectedNodes();
const selectedData =
selectedNodes.map(node => node.data );
this.selectedRowsChanged.emit(selectedData);
}
}
In dem Angular-Anwendungsmodul (app.module.ts, siehe Listing 3) wird mit dem Aufruf der Funktion createCustomElement() eine Web Component aus einer Angular-Komponente erzeugt.
Listing 3: Ausschnitt aus app.module.ts
import { BrowserModule } from
'@angular/platform-browser';
import { NgModule, Injector } from '@angular/core';
import { createCustomElement } from
"@angular/elements";
import { AppComponent } from './app.component';
import { AgGridModule } from 'ag-grid-angular';
import { GridComponent } from
'./grid/grid.component';
@NgModule({
imports: [
BrowserModule, HttpClientModule,
AgGridModule.withComponents([])
],
providers: [ ],
bootstrap: [],
entryComponents:[
GridComponent
],
declarations: [GridComponent]
})
export class AppModule {
constructor(private injector: Injector) {}
ngDoBootstrap() {
...
const el = createCustomElement(
GridComponent, { injector: this.injector });
customElements.define('angular-grid', el);
}
}
'@angular/platform-browser';
import { NgModule, Injector } from '@angular/core';
import { createCustomElement } from
"@angular/elements";
import { AppComponent } from './app.component';
import { AgGridModule } from 'ag-grid-angular';
import { GridComponent } from
'./grid/grid.component';
@NgModule({
imports: [
BrowserModule, HttpClientModule,
AgGridModule.withComponents([])
],
providers: [ ],
bootstrap: [],
entryComponents:[
GridComponent
],
declarations: [GridComponent]
})
export class AppModule {
constructor(private injector: Injector) {}
ngDoBootstrap() {
...
const el = createCustomElement(
GridComponent, { injector: this.injector });
customElements.define('angular-grid', el);
}
}
Mit dem Aufruf define() legt der Entwickler das Tag für die Web Component fest. Es wäre möglich, hier auch mehrere Angular-Komponenten gleichzeitig als Web Components zu veröffentlichen.
Durch Einsatz eines entsprechenden Build-Skripts (vergleiche package.json und build-script.js; hier nicht abgedruckt, siehe Download unter [4]) wird die Angular-Komponente so verpackt, dass aus den vielen Tausend Dateien in einem Angular-Projekt lediglich eine einzelne, von der Größe her überschaubare JavaScript-Datei für die Web Component entsteht, siehe Bild 3.
Entstandenes Angular-Elements-Bundle (Bild 3)
Quelle: Autor
Das entstandene Bundle kann man nun in jede beliebige HTML-Seite einbetten:
<!-- Einbindung der Web Component -->
<script src="ITVElements.js"></script>
Die Web Component <angular-grid> nimmt die Spaltenliste im Attribut column-Defs-String und die Daten im Attribut row-data-string jeweils im JSON-Format entgegen, zum Beispiel:
<angular-grid id="FirmenGrid"
column-Defs-String='[
{"field":"Firma"},
{"field":"Ort"},
{"field":"Gründungsjahr"}]'
row-data-string='[
{"Firma":"www.IT-Visions.de", "Ort":"Essen",
"Gründungsjahr":1996},
{"Firma":"MAXIMAGO","Ort":"Dortmund",
"Gründungsjahr":2008}]'>
</angular-grid>
Wenn der Benutzer auf OK klickt, löst die Web Component ein Ereignis selectedRowsChanged aus, das die Daten der selektierten Zeilen enthält. Das folgende JavaScript-Fragment zeigt die Ereignisbindung dafür:
<script>
var e = document.querySelector('#FirmenGrid');
e.addEventListener("selectedRowsChanged",
function (eventData) {
const s = eventData.detail.map(
node => node.Firma +
' mit Sitz in ' + node.Ort +
' wurde ' + node.Gründungsjahr +
' gegründet!').join(', ');
document.getElementById("ausgabe").innerText =
("Selected rows = " + s);
});
</script>
Einbindung der Web Component
<angular-grid> in Blazor
Die erstellte Web Component <angular-grid> soll nun im nächsten Schritt in eine Blazor-Anwendung eingebunden werden – hier exemplarisch, um Daten aus dem Star Trek API [6] darzustellen.
Das Listing 4 zeigt die Razor Component, die das <angular-grid> hostet. Auf eine Trennung von Template und Code wurde hier verzichtet. Das passiert in Listing 4:
Listing 4: Angular.Razor – Teil 1
<h4> Web Component <angular-grid></h4>
<span class="row">
<span class="col-xs-4">
<input type="text" @bind="searchName" />
<button @onclick="LoadDataInAngularGrid">Load data
from Star Trek API (STAPI.co)</button>
<angular-grid column-Defs-String='@Header'
row-data-string='@GridData'></angular-grid>
</span>
<span class="col-xs-4">
Ausgabe JS: <span id="gridResultJS"></span><0x000A>
Ausgabe C#: <span id="">@gridResultCS</span>
</span>
</span>
@code
{
string Header = "[{\"field\":\"name\"},
{\"field\":\"alternateReality\"},
{\"field\":\"yearOfDeath\"}]";
string searchName = "";
string GridData = "";
string gridResultCS { get; set; }
IJSObjectReference script1;
IJSObjectReference script2;
protected override async Task
OnAfterRenderAsync(bool firstRender) {
if (firstRender)
{
// Skripte laden!
script1 = await
JSRuntime.InvokeAsync<IJSObjectReference>(
"import",
"/_content/MLBlazorRCL/ITVElements.js");
script2 = await
JSRuntime.InvokeAsync<IJSObjectReference>(
"import",
"/_content/MLBlazorRCL/
ITVElementsGlueCode.js");
_objectReference =
DotNetObjectReference.Create(this);
// Initialisieren Eventhandler für beide Instanzen
await script2.InvokeVoidAsync("init",
_objectReference, c1);
await script2.InvokeVoidAsync("init",
_objectReference, c2);
}
}
public async void LoadDataInAngularGrid() {
util.Log("Loading data...");
// Eventhandler einrichten
var _objectReference = DotNetObjectReference
.Create(this);
await script2.InvokeVoidAsync("initGrid",
_objectReference);
// Daten Laden vom API
var formContent = new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("name",
this.searchName) //, new KeyValuePair<string,
string> //("placeOfBirth", "Earth")
});
HttpResponseMessage response =
await httpClient.PostAsync(
"http://stapi.co/api/v1/rest/character/search",
formContent);
string text =
await response.Content.ReadAsStringAsync();
// Daten filtern
System.Text.Json.JsonDocument json =
System.Text.Json.JsonDocument.Parse(text);
var characters = json.RootElement
.GetProperty("characters").ToString();
// Daten an Grind binden
this.GridData = characters;
this.StateHasChanged(); // WICHTIG!
}
public record StarTrekCharacter {
public string Name { get; init; }
public bool AlternateReality { get; init; }
public int? YearOfDeath { get; init; }
}
// Eventhandler für selectedRowsChanged-Ereignis
// der Web Component <angular-grid>. JSON wird auf
// Record abgebildet!
[JSInvokable]
public void NewSelection(
List<StarTrekCharacter> data) {
util.Log(data);
var selectedDataStringPresentation = String.Join(
", ", data.Select(x => x.Name + ' ' +
x.AlternateReality + ' ' +
x.YearOfDeath).OfType<string>().ToArray());
gridResultCS = selectedDataStringPresentation;
this.StateHasChanged();
util.Log(gridResultCS);
}
// Alternativer Eventhandler für
// selectedRowsChanged-Ereignis der Web Component
// <angular-grid>, mit JSON statt Typen
[JSInvokable]
public void NewSelection2(
List<System.Text.Json.JsonElement> data) {
util.Log(data);
var selectedDataStringPresentation = String.Join(
", ", data.Select(x =>
x.GetProperty("name").ToString() + ' ' +
x.GetProperty("alternateReality").ToString() +
' ' + x.GetProperty("yearOfDeath").ToString())
.OfType<string>().ToArray());
gridResultCS = selectedDataStringPresentation;
this.StateHasChanged();
util.Log(gridResultCS);
}
}
<span class="row">
<span class="col-xs-4">
<input type="text" @bind="searchName" />
<button @onclick="LoadDataInAngularGrid">Load data
from Star Trek API (STAPI.co)</button>
<angular-grid column-Defs-String='@Header'
row-data-string='@GridData'></angular-grid>
</span>
<span class="col-xs-4">
Ausgabe JS: <span id="gridResultJS"></span><0x000A>
Ausgabe C#: <span id="">@gridResultCS</span>
</span>
</span>
@code
{
string Header = "[{\"field\":\"name\"},
{\"field\":\"alternateReality\"},
{\"field\":\"yearOfDeath\"}]";
string searchName = "";
string GridData = "";
string gridResultCS { get; set; }
IJSObjectReference script1;
IJSObjectReference script2;
protected override async Task
OnAfterRenderAsync(bool firstRender) {
if (firstRender)
{
// Skripte laden!
script1 = await
JSRuntime.InvokeAsync<IJSObjectReference>(
"import",
"/_content/MLBlazorRCL/ITVElements.js");
script2 = await
JSRuntime.InvokeAsync<IJSObjectReference>(
"import",
"/_content/MLBlazorRCL/
ITVElementsGlueCode.js");
_objectReference =
DotNetObjectReference.Create(this);
// Initialisieren Eventhandler für beide Instanzen
await script2.InvokeVoidAsync("init",
_objectReference, c1);
await script2.InvokeVoidAsync("init",
_objectReference, c2);
}
}
public async void LoadDataInAngularGrid() {
util.Log("Loading data...");
// Eventhandler einrichten
var _objectReference = DotNetObjectReference
.Create(this);
await script2.InvokeVoidAsync("initGrid",
_objectReference);
// Daten Laden vom API
var formContent = new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("name",
this.searchName) //, new KeyValuePair<string,
string> //("placeOfBirth", "Earth")
});
HttpResponseMessage response =
await httpClient.PostAsync(
"http://stapi.co/api/v1/rest/character/search",
formContent);
string text =
await response.Content.ReadAsStringAsync();
// Daten filtern
System.Text.Json.JsonDocument json =
System.Text.Json.JsonDocument.Parse(text);
var characters = json.RootElement
.GetProperty("characters").ToString();
// Daten an Grind binden
this.GridData = characters;
this.StateHasChanged(); // WICHTIG!
}
public record StarTrekCharacter {
public string Name { get; init; }
public bool AlternateReality { get; init; }
public int? YearOfDeath { get; init; }
}
// Eventhandler für selectedRowsChanged-Ereignis
// der Web Component <angular-grid>. JSON wird auf
// Record abgebildet!
[JSInvokable]
public void NewSelection(
List<StarTrekCharacter> data) {
util.Log(data);
var selectedDataStringPresentation = String.Join(
", ", data.Select(x => x.Name + ' ' +
x.AlternateReality + ' ' +
x.YearOfDeath).OfType<string>().ToArray());
gridResultCS = selectedDataStringPresentation;
this.StateHasChanged();
util.Log(gridResultCS);
}
// Alternativer Eventhandler für
// selectedRowsChanged-Ereignis der Web Component
// <angular-grid>, mit JSON statt Typen
[JSInvokable]
public void NewSelection2(
List<System.Text.Json.JsonElement> data) {
util.Log(data);
var selectedDataStringPresentation = String.Join(
", ", data.Select(x =>
x.GetProperty("name").ToString() + ' ' +
x.GetProperty("alternateReality").ToString() +
' ' + x.GetProperty("yearOfDeath").ToString())
.OfType<string>().ToArray());
gridResultCS = selectedDataStringPresentation;
this.StateHasChanged();
util.Log(gridResultCS);
}
}
- In OnAfterRenderAsync() werden zunächst die JavaScript-Dateien für das Angular-Elements-Bundle (ITVElements.js) und der Glue Code aus der Skriptdatei ITVElementsGlueCode.js nachgeladen. In Blazor WebAssembly könnte dies auch in OnInitializedAsync() erfolgen. Das würde aber in Blazor Server bei der Startkomponente nicht funktionieren, wenn Server-Prerendering aktiv ist.
- In der Zeile <angular-grid column-Defs-String=’@Header’ row-data-string=’@GridData’></angular-grid> sind die Attribute der Web Component an die C#-Properties Header und GridData gebunden.
- Beim Klick auf Load data from Star Trek API (STAPI.co) wird in dem Glue Code die JavaScript-Funktion initGrid() aufgerufen, um die Ereignisbehandlung für das Ereignis selectedRowsChanged der Angular Web Component in JavaScript zu initialisieren. Dabei erhält JavaScript als Parameter eine Referenz auf die aufrufende Razor Component in Form einer Instanz der Klasse DotNetObjectReference, die man mit DotNetObjectReference.Create(this) erzeugen kann. Dies ist notwendig für den Rückruf an Blazor, wenn in der Tabelle eine Auswahl erfolgt ist.
- Anschließend werden die Daten über eine injizierte Instanz der Klasse HttpClient vom Star Trek API aus dem Internet geladen und zur Datenbindung an die Property this.GridData im JSON-Format übergeben.
- Die Callback-Methode NewSelection() wird zu einem späteren Zeitpunkt von dem Glue Code gerufen, wenn die Web Component das Ereignis selectedRowsChanged ausgelöst hat. Die Methode muss mit [JSInvokable] annotiert sein, sonst kann JavaScript sie nicht erreichen. Der Code in Listing 5 zeigt dabei zwei alternative Implementierungen von NewSelection(): Wahlweise kann man als Parameter eine Klasse für die Datenstruktur definieren (hier ist es ein C#-9.0-Record mit dem Namen StarTrekCharacter) oder aber dynamisch mit dem JSON-API System.Text.Json.JsonElement arbeiten.
Listing 5: ITVElementsGlueCode.js
export function initGrid(dotNet) {
// finde das Grid
var e = document.querySelector('angular-grid');
// binde die Ereignisbehandlung
e.addEventListener(
"selectedRowsChanged", function (eventData) {
console.log('selected-Rows-Changed event
fired!', eventData);
// Behandlung des Ereignisses in JS, hier als
// Beispiel: direktes Ändern des DOM
const selectedDataStringPresentation = eventData
.detail.map(node => node.name + ' ' +
node.alternateReality + ' ' +
node.yearOfDeath).join(', ');
document.getElementById("gridResultJS").innerText
= selectedDataStringPresentation;
// Weitergabe des Ereignisses an C# an die
// Methode NewSelection
dotNet.invokeMethodAsync("NewSelection",
eventData.detail).then(data => {console.log(
"JS: Ereignis an .NET gesendet!");});
});
}
// finde das Grid
var e = document.querySelector('angular-grid');
// binde die Ereignisbehandlung
e.addEventListener(
"selectedRowsChanged", function (eventData) {
console.log('selected-Rows-Changed event
fired!', eventData);
// Behandlung des Ereignisses in JS, hier als
// Beispiel: direktes Ändern des DOM
const selectedDataStringPresentation = eventData
.detail.map(node => node.name + ' ' +
node.alternateReality + ' ' +
node.yearOfDeath).join(', ');
document.getElementById("gridResultJS").innerText
= selectedDataStringPresentation;
// Weitergabe des Ereignisses an C# an die
// Methode NewSelection
dotNet.invokeMethodAsync("NewSelection",
eventData.detail).then(data => {console.log(
"JS: Ereignis an .NET gesendet!");});
});
}
Abschließend folgt noch der Programmcode der JavaScript-Funktion initGrid() im Glue Code, siehe Listing 5.
Die Funktion sucht zunächst mit der Funktion document.querySelector(’angular-grid’) das <angular-grid> im aktuellen DOM und bindet sich anschließend an das Ereignis der Web Component:
e.addEventListener(
"selectedRowsChanged", function (eventData) { ... });
Wenn das Ereignis eintritt, erfolgen zwei Aktionen:
- Die selektierten Zeilen werden direkt im <div>-Tag mit der ID gridResultJS ausgegeben. Das wäre der direkte Weg. Aber das ist keine gute Lösung, weil Blazor dann diese Werte gar nicht mitbekommt.
- Besser ist die Weitergabe des Ereignisses an Blazor, namentlich an die Rückrufmethode NewSelection(). Diese wird mit der Funktion invokeMethodAsync() auf dem von Blazor übergebenen .NET-Objekt aufgerufen.
Die obige Implementierung geht davon aus, dass es immer nur eine Instanz der Web Component innerhalb einer Razor Component gibt.
Wenn es mehrere Instanzen geben soll, muss man dem Glue Code neben dem Verweis auf die Razor Component auch noch einen Verweis auf das Web-Component-Element übergeben. Dazu muss man in der Razor Component ein Field vom Typ ElementReference anlegen
private ElementReference grid1;
und der Entwickler muss im Web-Component-Tag mit @ref auf dessen Namen verweisen:
<angular-grid @ref="grid1" column-Defs-String='@Header'
row-data-string='@GridData'></angular-grid>
Damit befüllt die Blazor-Infrastruktur beim Initialisieren der Razor Component das Field mit der Objektreferenz auf das Tag. Diese Objektreferenz grid1 hat man dann als zusätzlichen Parameter an die JavaScript-Methode initGrid() zu übergeben.
Sie erhält dann ein entsprechendes DOM-Objekt vom Typ Element [7] und kann dieses genauso verwenden wie ein selbst per document.getElementById() oder document.querySelector() beschafftes Element.
Wenn man auch noch die Rückrufmethode dynamisch halten will, ist es erforderlich, einen weiteren Parameter methodName bei initGrid() zu ergänzen und diesen beim Aufruf dotNet.invokeMethodAsync(methodName, eventData.detail) zu verwenden.
Fazit
Es waren schon einige Schritte notwendig, um die Mischehe zwischen Blazor und Angular zu realisieren. Aber wer das asynchrone Grundprinzip der Web-Welt verstanden hat, für den ist auch diese Verbindung keine Raketenwissenschaft, sondern durchaus machbar.
Dokumente
Artikel als PDF herunterladen
Fußnoten