Un altro aspetto importante dell’interoperabilità è la possibilità di invocare un metodo .NET da JavaScript, indispensabile per poter utilizzare librerie e funzioni JavaScript già pronte con Blazor. Vediamo insieme quali sono i passi da seguire per implementare questa funzionalità.

Invocare metodi statici e metodi di istanza

Affinchè un metodo .NET sia invocabile da JavaScript è necessario che questo sia public e decorato con l’attributo [JSInvokable]. Il modo più semplice è utilizzarlo su un metodo statico, asincrono o non, in una qualsiasi classe di un qualsiasi assembly. Da una funzione JavaScript agganciata sull’oggetto window, possiamo invocare il metodo usando l’oggetto denominato DotNet, che ci espone i metodi invokeMethod e invokeMethodAsync, a cui dobbiamo specificare il nome dell’assembly .NET che contiene il metodo da invocare, il nome del metodo e gli eventuali parametri. L’utilizzo della versione asincrona (invokeMethodAsync) ci restituisce una Promise a cui possiamo specificare la callback da eseguire quando il metodo sarà risolto, mentre la versione sincrona ci restituisce direttamente il risultato:

Supponendo che il progetto corrente si chiami MyBlazorApp, un esempio generico potrebbe essere il seguente:

public class MiaClasse
{
    [JSInvokable]
    public static string[] RecuperaListaStringhe()
    {
        return new string[] { "elemento1", "elemento2", "elemento3" };
    }

    [JSInvokable]
    public static Task<string[]> RecuperaListaStringheAsync()
    {
        return Task.FromResult(new string[] { "elemento1", "elemento2", "elemento3" } )
    }
}
window.chiamaDotNet = () => {
    var elementi = DotNet.invokeMethod('MyBlazorApp', 'RecuperaListaStringhe');

    DotNet.invokeMethodAsync('MyBlazorApp', 'RecuperaListaStringheAsync')
        .then(listastringhe => console.log(listastringhe)); 
}

Come abbiamo visto negli articoli precedenti, un componente è una classe quindi possiamo definire il metodo sia in una Page che in un Blazor Component. In generale comunque, quando si ha a che fare con metodi statici, il consiglio è quello di organizzare il codice per tenere le parti statiche in classi separate o, meglio ancora, di evitarle il più possibile per l’accoppiamento che ne consegue.

Possiamo infatti utilizzare anche metodi di istanza per l’interoperabilità con JavaScript, a patto che quest’ultimo abbia un riferimento all’oggetto su cui vogliamo fare l’invocazione. Il metodo statico T DotNetObjectReference.Create(T value) della libreria JSInterop ci mette disposizione un modo per creare, a partire dall’istanza di un oggetto, un riferimento da poter passare a JavaScript attraverso l’oggetto JSRuntime che abbiamo visto dell’articolo precedente. Possiamo quindi chiamare JavaScript da .NET per passare una istanza di oggetto .NET, su cui poi da JavaScript possiamo invocare metodi non statici. Supponiamo di avere una classe MiaClasse su cui invochiamo il metodo ChiamaJavaScript():

public class MiaClasse
{
    public async Task ChiamaJavaScript()
    {
        await JSRuntime.InvokeVoidAsync(
            "chiamaJavaScript", 
            DotNetObjectReference.Create(this));
    }

    [JSInvokable]
    public string[] RecuperaListaStringhe()
    {
        return new string[] { "elemento1", "elemento2", "elemento3" };
    }

    [JSInvokable]
    public Task<string[]> RecuperaListaStringheAsync()
    {
        return Task.FromResult(new string[] { "elemento1", "elemento2", "elemento3" } )
    }
}
window.chiamaJavaScript = (oggetto) => {
    var elementi = oggetto.invokeMethod('RecuperaListaStringhe');

    oggetto.invokeMethodAsync('RecuperaListaStringheAsync')
        .then(listastringhe => console.log(listastringhe)); 
}

Il metodo ChiamaJavaScript invoca la funzione JavaScript chiamaJavaScript, passando come parametro l’istanza di MiaClasse corrente (usando l’oggetto this), su cui dalla stessa funzione possiamo invocare direttamente i metodi RecuperaListaStringhe o RecuperaListaStringheAsync.

Sia che scegliamo di utilizzare metodi statici che di istanza, è comunque consigliabile l’utilizzo delle versioni asincrone delle invocazioni, che non bloccano l’unico thread JavaScript che abbiamo a disposizione.

Un esempio reale

Supponiamo di voler aggiungere alla nostra applicazione per la gestione di eventi una mappa, su cui posizionare dei marker sul luogo dove l’evento si svolge. Il modo più veloce è usare una mappa di Google, che ci fornisce una API JavaScript completa per necessità di questo tipo. In particolare utilizzeremo due API, quella per la visualizzazione della mappa e quella per il geocoding che, dato un indirizzo, ci restituisce le coordinate (latitudine e logitudine) corrispondenti. Per utilizzare le API di Google c’è bisogno di una registrazione a Google Cloud, essendo il servizio a pagamento superata una determinata soglia di invocazioni. Seguendo le istruzioni che potete trovare qui, otterete il token che ci serve per il nostro codice.

Andiamo quindi sulla nostra SPA, e aggiungiamo sull’oggetto window un metodo mostraMappa, a cui passeremo l’elemento HTML in cui vogliamo che venga visualizzata la mappa, la zona in cui vogliamo centrare la mappa (Italia nei nostri esempi), un livello di zoom, una lista di eventi e l’istanza del componente Blazor su cui vogliamo invocare un metodo specifico:

<script>
    ...
    window.mostraMappa = (contenitore, zona, zoom, eventi, componente) => {
        var geocoder = new google.maps.Geocoder();
        geocoder.geocode({'address': zona}, function(results, status) {
            if (status === 'OK') {
                var map = new google.maps.Map(contenitore, {
                    zoom: zoom,
                    center: results[0].geometry.location
                });
                eventi.forEach(evento => {
                    geocoder.geocode({'address': evento.localita}, function(results, status) {
                        if (status === 'OK') {
                            var marker = new google.maps.Marker({
                                map: map,
                                position: results[0].geometry.location
                            });
                            marker.addListener('click', function() {
                                componente.invokeMethodAsync('SelezionaEvento', evento)
                                    .then(data => { });
                            });
                        } else {
                            console.log('indirizzo non trovato: ' + status);
                        }
                    });
                });
            } else {
                console.log('zona non trovata: ' + status);
            }
        });
    }
</script>
<script async defer src="https://maps.googleapis.com/maps/api/js?key=<APIKEY>"></script>

Il codice è molto semplice e volutamente poco generalizzato per concentrare l’attenzione sul nostro scopo principale. Partendo dal nome della zona su cui vogliamo centrare la mappa, recuperiamo le coordinate e inizializziamo un oggetto mappa. Dato un indirizzo, i risultati possibili della geolocalizzazione possono essere più di uno, ma noi ci sentiamo fortunati e immaginiamo che quello giusto sia il primo. Inizializzata la mappa cicliamo sulla lista degli eventi e sfruttiamo la proprietà localita di ogni elemento per aggiungere dei marker sulla mappa, e su ognuno di essi andiamo ad aggiungere un listener dell’evento click. Quando l’utente cliccherà sul marker invocheremo il metodo di istanza SelezionaEvento del componente .NET, a cui passeremo l’evento corrispndente al marker.

Andiamo adesso a creare un componente Blazor per visualizzare gli eventi sulla mappa, chiamiamolo MappaEventi.razor e posizioniamolo nella cartella Componenti del progetto Blazor. Dal punto di vista del markup abbiamo solo bisogno di un contenitore, un <div></div> per esempio, ma ci serve un modo per recuperare un riferimento a questo componente da poter utilizzare nel codice .NET. Per ottenere questa informazione, possiamo sfruttare la direttiva @ref che il framework ci mette a disposizione, grazie alla quale possiamo riversare il riferimento di un elemento del markup (sia esso un elemento HTML o un ulteriore componente Blazor) in una proprietà di tipo ElementReference:

@inject IJSRuntime JSRuntime;
<div style="width:@Width;height:@Height;" @ref="MapContainerReference"></div>
@code {
    private ElementReference MapContainerReference;
    
    [Parameter]
    public string Width { get; set; } = "100%";
    
    [Parameter]
    public string Height { get; set; } = "400px";

    [Parameter]
    public List<ElementoListaEventi> ListaElementi { get; set; }
   
    [Parameter]
    public EventCallback<ElementoListaEventi> OnSeleziona { get; set; }
    ...
}

A questo punto non dobbiamo fare nient’altro che chiamare la funzione JavaScript mostraMappa, ma mettiamo il codice necessario in un metodo privato perchè, come spesso accade, l’integrazione di librerie JavaScript può richiedere di eseguire alcune chiamate in un determinato momento del ciclo di vita della pagina.

@code {
    ...
    private async Task mostraMappa()
    {
        await JSRuntime.InvokeVoidAsync(
                "mostraMappa", 
                MapContainerReference, 
                "Italia", 
                5, 
                ListaElementi,
                DotNetObjectReference.Create(this));
        StateHasChanged();
    }

    [JSInvokable]
    public void SelezionaEvento(ElementoListaEventi evento)
    {
        this.OnSeleziona.InvokeAsync(evento);
    }
    ...
}

Notiamo che il metodo SelezionaEvento è un metodo di istanza del componente, e questo è necessario per poterci riferire all’evento OnSeleziona o a qualsiasi altra proprietà del componente stesso. Se avesssimo usato qui un metodo statico avremmo avuto il problema di dover trovare un modo per poter accedere alle proprietà dell’istanza del componente, ad esempio generando un evento statico e sottoscrivendolo nel componente.

Notiamo anche il metodo StateHasChanged() del componente Blazor, che ci permette di forzare il meccanismo di change detection del framework per applicare le modifiche al DOM che in questo caso Blazor non è in grado di rilevare. Perchè? Tutte queste invocazioni asincrone lavorano su un thread differente da quello dell’interfaccia su cui Blazor verifica i cambiamenti, quindi in questi casi è necessario indicare al framework che qualcosa è cambiato e che deve eseguire gli algoritmi di verifica e aggiornare l’interfaccia.

Manca un ultimo passaggio: quando eseguiamo questo codice? E’ un aspetto molto interessante, perchè abbiamo già incrociato negli articoli precedenti i metodi di gestione del ciclo di vita di un componente, con l’override dei metodi OnInitialized e OnInitializedAsync. Blazor ci da la possibilità di intervenire in vari momenti del ciclo di vita di un componente, per eseguire delle operazioni custom dopo che determinate operazioni sul componente sono state fatte:

  • OnInitialized e OnInitializedAsync: eseguiti dopo che il componente è stato inizializzato e ha ricevuto i parametri iniziali.
  • SetParametersAsync(ParameterView parameters): eseguito prima che un parametro del componente cambi valore
  • OnParametersSet e OnParametersSetAsync: eseguiti dopo che un parametro del componente ha cambiato valore
  • OnAfterRender(bool firstRender) e OnAfterRenderAsync(bool firstRender): eseguito ogni volta che il componente viene renderizzato e il DOM aggiornato, un parametro booleano ci dice se è la prima volta che viene eseguito.
  • bool ShouldRender(): eseguito prima del rendering per decidere se eseguire oppure no l’aggiornamento dell’interfaccia.

Nel nostro caso abbiamo bisogno di aggiornare la mappa ogni volta che la lista eventi viene aggiornata e appena il DOM è pronto, quindi:

@code {
    ...
    private bool firstRender = true;

    protected override async Task OnParametersSetAsync()
    {
        if(!firstRender)
        {
           await mostraMappa();
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if(firstRender)
        {
            this.firstRender = false;
            await mostraMappa();
        }
    }
    ...
}

Se cerchiamo di aggiornare la mappa alla prima invocazione del OnParametersSetAsync otterremmo un errore perchè il DOM non è ancora pronto, quindi utilizziamo un flag interno per sapere se c’è stata almeno un primo rendering del componente.

Non ci resta che utilizzare il nuovo componente nella pagina degli eventi:

@page "/eventi"
@inject EventManagerDbContext context;
@if(EventoCorrente == null)
{
    <ListaEventi 
        ListaElementi="ListaEventi" OnElimina="EliminaEvento"
        OnCrea="CreaEvento" OnModifica="ModificaEvento" />
    <MappaEventi 
        ListaElementi="ListaEventi"
        OnSeleziona="ModificaEvento" />
}
else
{
    <DettaglioEvento 
        DettaglioElemento="EventoCorrente"
        OnSalva="SalvaEvento" OnAnnulla="AnnullaOperazione" />
}

Ed ecco il risultato:

"Mappa Google in applicazione Blazor"

Potete consultare il codice sorgente qui, nella branch 09-javascript-dotnet.

Conclusioni

In questo articolo abbiamo visto come invocare metodi .NET, statici e non, da JavaScript per integrare librerie di terze parti o nostre funzionalità custom. Abbiamo anche colto l’occasione per analizzare due aspetti avanzati del framework, la direttiva @ref e i cosiddetti hook del ciclo di vita di un componente, molto utili nell’utilizzo di Blazor in applicazioni reali. Come sempre l’unico maestro è la pratica e adesso avete tutti gli elementi per integrare librerie JavaScript nella vostra applicazione.