Articoli

Creare una SPA: componenti riutilizzabili

da

Nel precedente articolo abbiamo parlato di componenti e di come andrebbero progettati, passando poi alla pratica e a come realizzare un primo componente in Blazor. Abbiamo sottolineato come uno dei fattori di successo nella realizzazione di un componente sia la sua riusabilità, ma questo argomento richiede un approfondimento perchè impatta notevolmente su come realizziamo il nostro progetto.

Fateli, ma non accoppiateli

Il basso accoppiamento tra i componenti è l’ingrediente fondamentale per la riusabilità in contesti differenti, che vanno dalla stessa applicazione ma casi d’uso differenti, fino alla creazione di librerie di componenti riutilizzabili in diverse applicazioni. Per fare in modo che questo sia possibile è necessario che un componente sia parametrizzabile, infatti se possiamo personalizzarlo per contesti differenti, potremo riutilizzarlo dove vogliamo. A breve vedremo come questo sia possibile in Blazor, ma è necessario sottolineare che la parametrizzazione non garantisce necessariamente il disaccoppiamento tra due componenti, che potrebbero usare i parametri per passarsi un riferimento alla propria istanza.

Se due componenti si trovano in relazioni padre-figlio, il padre potrebbe passare al figlio, come parametro, l’oggetto this, un riferimento a se stesso. In questo modo il figlio ha visibilità su tutti gli elementi pubblici della classe del padre, e potrebbe invocare metodi o impostare valori alle proprietà esposte. Perchè dovremmo fare un cosa del genere? Perchè i componenti a un certo punto avranno bisogno di comunicare tra loro e la facilità con cui questa tecnica risolve il problema potrebbe rendere la tentazione molto forte. In questo modo però l’unico modo di riutilizzare il componente figlio e portarsi dietro anche il padre.

Come risolviamo allora il problema? Basta progettare i nostri componenti per essere trattati come delle Black Box che prendono in Input i dati con cui devono lavorare e forniscano in Output eventi a cui sottoscriversi per poter reagire all’interazione dell’utente. In questo modo è il componente stesso a esporre il contratto di comunicazione, rendendosi autonomo dal suo utilizzatore. Vediamo come questo può essere fatto in Blazor.

Parametrizzazione: dati in ingresso, eventi in uscita

In un componente Blazor i parametri sono delle proprietà pubbliche decorate con l’attributo [Parameter], senza differenza tra parametri di input e parametri di output in termini dichiarativi (come ad esempio in Angular con i decoratori @Input() e Output()): la differenza la fa il tipo della proprietà che andiamo a definire.

Partiamo dall’esempio dell’ultima volta, la nostra lista di eventi. Per come è definito questo componente visualizzerà sempre la stessa lista di elementi, quindi se abbiamo bisogno di visualizzare esattamente quella lista, possiamo utilizzare il selettore in tutti i punti dell’applicazione in cui ci serve. Se invece vogliamo poter cambiare la lista degli elementi visualizzati possiamo rendere la proprietà ListaElementi un parametro, e magari rendere personalizzabile anche il titolo ma fornire un valore di default:

<h2>@Titolo</h2>
<table class="table">
    <tr>
        <th>Id</th>
        <th>Nome</th>
        <th>Località</th>
        <th>Data</th>
    </tr>
    @foreach(var evento in ListaElementi)
    {
    <tr>
        <td>@evento.Id</td>
        <td>@evento.Nome</td>
        <td>@evento.Localita</td>
        <td>@evento.Data</td>
    </tr>
    }
</table>
@code {

    [Parameter]
    public string Titolo { get; set; } = "Lista Eventi";

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

A questo punto potremmo, ad esempio, mostrare gli eventi passati e gli eventi futuri in due liste differenti, utilizzando le proprietà definite come attributi del selettore del componente:

@page "/"
<h1>Event Manager</h1>
<p>Benvenuti nella Single Page Application scritta in Blazor per la gestione degli eventi.</p>
<p>Selezionare dal menu laterale l'opzione desiderata.</p>

<ListaEventi ListaElementi="ListaEventi" />
<ListaEventi Titolo="Eventi Passati" ListaElementi="ListaEventiPassati" />
@code {
    List<ElementoListaEventi> ListaEventiPassati { get; set; } 
        = new List<ElementoListaEventi>
            {
                new ElementoListaEventi() { Id = 1, Nome="DevDay Benevento - Blazor", Localita="Benvento", Data = new DateTime(2020, 2,8)},
                new ElementoListaEventi() { Id = 2, Nome="DotNetSide Bari - Blazor", Localita="Bari", Data = new DateTime(2020, 2, 21)}
            };

    List<ElementoListaEventi> ListaEventi { get; set; } 
        = new List<ElementoListaEventi>
            {
                new ElementoListaEventi() { Id = 1, Nome="Mercoledi del Palazzo - Blazor", Localita="Salerno", Data = new DateTime(2020, 3, 11)},
                new ElementoListaEventi() { Id = 2, Nome="Visual Studio Tour - Blazor", Localita="Cagliari", Data = new DateTime(2020, 3, 21)}
            };
}

Ecco il risultato:

Supponiamo adesso di volere aggiungere, per ogni riga, un pulsante per eliminare l’evento corrispondente. Aggiungiamo l’HTML necessario e andiamo a eseguire il binding dell’evento click sul pulsante con un metodo EliminaElemento(), che semplicemente rimuove l’elemento dalla lista:

<h2>@Titolo</h2>
<table class="table">
    <tr> ... </tr>
    @foreach(var evento in ListaElementi)
    {
    <tr>
        <td>@evento.Id</td>
        <td>@evento.Nome</td>
        <td>@evento.Localita</td>
        <td>@evento.Data</td>
        <td><button class="btn btn-danger" @onclick="e => EliminaElemento(evento)">Elimina</button></td>
    </tr>
    }
</table>
@code {

    [Parameter]
    public string Titolo { get; set; } = "Lista Eventi";

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

    public void EliminaElemento(ElementoListaEventi evento) 
    {
        this.ListaElementi.Remove(evento);
    }
}

Niente di complesso, la direttiva @onclick ci permette di andare a catturare il click del mouse e di invocare un metodo, che in questo caso è una lambda expression, perchè abbiamo bisogno di passare un parametro. L’evento di click ci fornisce un oggetto MouseEventArgs (e =>) con le informazioni sull’evento scatenante l’azione, in questo caso non lo utilizziamo ma potrebbe tornare utile in scenari differenti (volendo possiamo sostiture e => con () =>). Eseguendo il codice vedremo funzionare il nostro pulsante:

Ma c’è qualcosa che non va, non dal punto di vista funzionale, ma dal punto di vista progettuale. Sto facendo accesso diretto alla lista che mi è stata passata per eseguire l’eliminazione, lista di cui, come componente, non so nulla. Sto eseguendo una operazione predefinita, senza dare la possibilità all’utilizzatore del componente di poter intervenire e magari chiedere all’utente se è sicuro di voler continuare con l’operazione, oppure fare una chiamata al back-end invece di eliminare localmente l’elemento.

Il problema qui è che mi sto sostituendo al mio utilizzatore, quando invece potrei semplicemente fargli sapere che l’utente ha cliccato sul pulsante elimina, in modo da lasciarli la possibilità di intervenire come meglio crede. Posso ottenere questo risultato creando un altro parametro del componente, utilizzando il tipo EventCallback<T>, una struttura offerta dal framework che permette al componene di sollevare un evento e al suo utilizzatore (il componente padre) di sottoscriversi al verificarsi dello stesso:

<h2>@Titolo</h2>
<table class="table">
    <tr> ... </tr>
    @foreach(var evento in ListaElementi)
    {
    <tr>
        <td>@evento.Id</td>
        <td>@evento.Nome</td>
        <td>@evento.Localita</td>
        <td>@evento.Data</td>
        <td>
            <button class="btn btn-danger" 
                @onclick="e => OnElimina.InvokeAsync(evento)">
                Elimina
            </button>
        </td>
    </tr>
    }
</table>
@code {

    ...

    [Parameter]
    public EventCallback<ElementoListaEventi> OnElimina { get; set; }
}

E’ consuetudine utilizzare la nomenclatura On<nome evento> per definire gli eventi del componente, nel nostro caso OnElimina. Il tipo EventCallback<T> ci espone il metodo InvokeAsync(T) grazie al quale al click del pulsante possiamo sollevare l’evento definito, in questo modo tutti i sottoscrittori saranno invocati:

@page "/"

<h1>Event Manager</h1>
<p>Benvenuti nella Single Page Application scritta in Blazor per la gestione degli eventi.</p>
<p>Selezionare dal menu laterale l'opzione desiderata.</p>

<ListaEventi ListaElementi="ListaEventi" 
    OnElimina="EliminaEvento" />

<ListaEventi Titolo="Eventi Passati" ListaElementi="ListaEventiPassati" 
    OnElimina="EliminaEventoPassato" />
@code {

    ...

    public void EliminaEvento(ElementoListaEventi evento) 
    {
        this.ListaEventi.Remove(evento);
    }

    public void EliminaEventoPassato(ElementoListaEventi evento) 
    {
        this.ListaEventiPassati.Remove(evento);
    }
}

Eseguendo il codice vedremo che il risultato non cambia, ma dal punto di vista della parametrizzazione il nostro componente ListaEventi è sempre più vicino a un generico elemento lista che possiamo riutilizzare in più contesti. Il codice descritto è disponibile qui, nella branch 03-componenti-riutilizzabili.

Conclusioni

Per oggi ci fermiamo qui, abbiamo messo un po’ di carne sul fuoco ma c’è ancora tanto da scoprire sugli strumenti messi a disposizione da Blazor per generalizzare i componenti e renderli sempre più riutilizzabili. Non preoccupatevi, ne parleremo, ma per il momento procediamo con gli strumenti base per la nostra Single Page Application e in particolare nel prossimo articolo vedremo come gestire la navigazione delle pagine pur utilizzando una Single Page.