Articoli

Creare una SPA: gestione Form

da

In un’applicazione enterprise l’inserimento dati da parte dell’utente è una attività fondamentale, per la quale ogni framework di front-end mette a disposizione librerie apposite che permettono sia la cattura dell’input che la sua validazione formale, attività spesso noiosa e ripetitiva. Blazor ci fornisce tutta una serie di componenti già pronti a questo scopo, nonchè la possibilità di sfruttare le tecniche di validazione del .NET Framework che già conosciamo.

I componenti Form

La cattura dell’input è un’attività facilmente isolabile in un componente e Blazor ha già fatto questo lavoro per noi per tutti i casi semplici. Questa tipologia di componenti sfrutta la possibilità di specificare un modello come tipo di dati su cui lavorare, facilitandoci non poco il compito della realizzazione di una form e supportandoci con la forte tipizzazione di cui gode .NET.

Partiamo dunque dalla definizione del dettaglio di un Evento, nel quale andiamo a inserire tutte le proprietà che vogliamo utilizzare nella form di dettaglio. Nella cartella Models andiamo ad aggiungere una classe Evento con la seguente definizione:

public class Evento
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public string Localita { get; set; }
    public DateTime Data { get; set; }
    public string Descrizione { get; set; }
    public string Note { get; set; }
}

Nella cartella Components andiamo a creare un componente per la gestione del dettaglio evento, creando il file DettaglioEvento.razor. La definizione di una form parte da un elemento contenitore denominato <EditForm></EditForm>, sul quale possiamo specificare il nome di una proprietà che rappresenta il nostro modello dati e la callback da invocare quando l’utente richiederà la sottomissione della form:

<h2>Evento @DettaglioElemento.Nome</h2>
<EditForm 
    Model="@DettaglioElemento"
    OnValidSubmit="@(e => OnSalva.InvokeAsync(DettaglioElemento))">
    
    <button type="submit" class="btn btn-primary">Salva</button>
    <button type="button" class="btn btn-warning" @onclick="OnAnnulla">Annulla</button>
</EditForm>
@code {
    [Parameter]
    public Evento DettaglioElemento { get; set; }

    [Parameter]
    public EventCallback<Evento> OnSalva { get; set; }

    [Parameter]
    public EventCallback OnAnnulla { get; set; }
}

Il componente utilizza il tipo del modello specificato (nel nostro caso la classe Evento) per tipizzare la nostra form. Aggiungiamo due pulsanti, uno per il salvataggio e l’altro per annullare l’operazione. Per il salvataggio definiamo il solo markup del pulsante, dato che, utilizzando un pulsante di tipo submit, l’operazione richiesta sarà direttamente collegata alla sottomissione della form. Per il pulsante di annullamente invece andiamo a catturare l’evento click, collegandolo a un paramentro EventCallback che chiamiamo OnAnnulla, in modo che chi utilizza il nostro componente possa reagire all’evento. Per completezza aggiungiamo in cima al componente un titolo, riportando il nome dell’evento su cui stiamo lavorando.

Non ci resta che aggiungere i vari campi, utilizzando un componente specifico per ogni tipologia di Input che vogliamo visualizzare. Se vogliamo ad esempio visualizzare una classica casella di testo, possiamo utilizzare il componente InputText, che renderizzerà per noi un elemento <input type="text" />. Allo stesso modo abbiamo un componente per la textarea (InputTextArea), per la select (InputSelect), la input di tipo numerica (InputNumber), checkbox (InputCheckBox) e date (InputDate). Su questi elementi possiamo applicare le classi CSS che preferiamo, quindi anche le classi di Bootstrap per la gestione delle form:

<EditForm 
    Model="@DettaglioElemento" 
    OnValidSubmit="@(e => OnSalva.InvokeAsync(DettaglioElemento))">
    
    <div class="form-group">
        <label for="nome">Nome:</label>
        <InputText id="nome" @bind-Value="DettaglioElemento.Nome" class="form-control" />
    </div>

    <div class="form-group">
        <label for="localita">Località:</label>
        <InputText id="localita" @bind-Value="DettaglioElemento.Localita" class="form-control" />
    </div>

    <div class="form-group">
        <label for="data">Data:</label>
        <InputDate id="data" @bind-Value="DettaglioElemento.Data" class="form-control" />
    </div>

    <div class="form-group">
        <label for="descrizione">Descrizione:</label>
        <InputText id="descrizione" @bind-Value="DettaglioElemento.Descrizione" class="form-control" />
    </div>

    <div class="form-group">
        <label for="note">Note:</label>
        <InputTextArea id="note" @bind-Value="DettaglioElemento.Note" class="form-control" />
    </div>
    
    <button type="submit" class="btn btn-primary">Salva</button>
    <button type="button" class="btn btn-warning" @onclick="OnAnnulla">Annulla</button>
</EditForm>

Un po’ di classi Bootstrap con i componenti di Blazor e il gioco è fatto! Da notare il binding del valore dell’input con la corrispondente proprietà del modello, utilizzando la direttiva @bind-Value, che è l’unico caso di binding bidirezionale nel framework, nonchè l’unico caso in cui abbia senso visto che solo nelle form l’utente può modificare i dati.

Andiamo adesso nella pagina Eventi e aggiungiamo questo nuovo componente alla gestione della CRUD. Per dare un po’ di interattività, aggiungiamo una proprietà EventoCorrente di tipo Evento, che rappresenta l’evento su cui vogliamo lavorare. Quindi nel caso l’utente richieda la creazione di un nuovo evento, questa proprietà sarà una nuova istanza della classe, mentre nel caso l’utente chieda di modificare un evento esistente possiamo riversare i dati dell’evento selezionato nell’evento corrente.

Aggiungiamo al componente ListaEventi un pulsante di creazione e un pulsante di modifica da poter catturare e gestire:

<h2>@Titolo</h2>
<button class="btn btn-primary mb-4" 
    @onclick="OnCrea">Crea Evento</button>
<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>
        <th>
            <button class="btn btn-warning" 
                @onclick="e => OnModifica.InvokeAsync(evento)">Modifica</button>
            <button class="btn btn-danger" 
                @onclick="e => OnElimina.InvokeAsync(evento)">Elimina</button>
        </th>
    </tr>
    }
</table>
@code {
    ...

    [Parameter]
    public EventCallback OnCrea { get; set; }
    
    [Parameter]
    public EventCallback<ElementoListaEventi> OnModifica { get; set; }
    
    ...
}

A questo punto un semplice if ci permetterà di passare dalla visualizzazione lista a quella dettaglio, dandoci la sensazione di navigare da una all’altra, aiutati dalla cattura degli eventi di entrambi i componenti, utilizzati per valorizzare l’evento corrente:

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

    public Evento EventoCorrente { get; set; }

    List<ElementoListaEventi> ListaEventi { 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)}
            };

    public void CreaEvento()
    {
        this.EventoCorrente = new Evento();
    }

    public void ModificaEvento(ElementoListaEventi evento)
    {
        this.EventoCorrente = new Evento()
        {
            Id = evento.Id,
            Nome = evento.Nome,
            Localita = evento.Localita,
            Data = evento.Data
        };
    }

    public void SalvaEvento(Evento evento)
    {
        if(evento.Id == 0)
        {
            this.ListaEventi.Add(new ElementoListaEventi()
            {
                Id = this.ListaEventi.Count() > 0 ? this.ListaEventi.Max(x=> x.Id) + 1 : 1,
                Nome = evento.Nome,
                Localita = evento.Localita,
                Data = evento.Data
            });
        }
        else
        {
            var eventoDaModificare = this.ListaEventi.Single(x => x.Id == evento.Id);
            eventoDaModificare.Nome = evento.Nome;
            eventoDaModificare.Localita = evento.Localita;
            eventoDaModificare.Data = evento.Data;
        }
        this.EventoCorrente = null;
    }

    public void AnnullaOperazione()
    {
        this.EventoCorrente = null;
    }

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

Ed ecco il risultato dei nostri sforzi:

La validazione dei dati

Guardando il modo in cui abbiamo utilizzato EditForm, notiamo che il submit della form è collegato al parametro OnSalva utilizzando un attributo denominato OnValidSubmit, che effettivamente invocherà la callback solo se la form è valida. Ma che significa valida? Questa è probabilmente uno dei più grossi vantaggi dell’utilizzo di Blazor: possiamo sfruttare le Data Annotations di .NET per definire le regole di validazione del nostro modello, lasciando ai componenti di Blazor l’onere di utilizzarle per verificare la validità dei dati ed eventualmente visualizzare gli errori.

Torniamo sulla classe Evento è aggiungiamo qualche annotazione per le validazioni:

public class Evento
{
    public int Id { get; set; }

    [Required(ErrorMessage="Il nome è obbligatorio")]
    public string Nome { get; set; }

    [Required(ErrorMessage="La località è obbligatoria")]
    [StringLength(50, ErrorMessage="La lunghezza può essere al massimo di {1} caratteri")]
    public string Localita { get; set; }
    
    public DateTime Data { get; set; }
    public string Descrizione { get; set; }
    public string Note { get; set; }
}

A questo punto dobbiamo solo abilitare l’utilizzo delle Data Annotation nella form, utilizzando il componente <DataAnnotationsValidator /> e visualizzare l’elenco degli errori con il classico <ValidationSummary /> oppure uno specifico errore di validazione, ad esempio per la proprietà Nome, con <ValidationMessage For="@(() => DettaglioElemento.Nome)" />:

<EditForm Model="@DettaglioElemento" OnValidSubmit="@(e => OnSalva.InvokeAsync(DettaglioElemento))">
    <DataAnnotationsValidator />
    <ValidationSummary />
    
    <div class="form-group">
        <label for="nome">Nome:</label>
        <InputText id="nome" @bind-Value="DettaglioElemento.Nome" class="form-control" />
        <ValidationMessage For="@(() => DettaglioElemento.Nome)" />
    </div>

    <div class="form-group">
        <label for="localita">Località:</label>
        <InputText id="localita" @bind-Value="DettaglioElemento.Localita" class="form-control" />
        <ValidationMessage For="@(() => DettaglioElemento.Localita)" />
    </div>

    ...
    
    <button type="submit" class="btn btn-primary">Salva</button>
    <button type="button" class="btn btn-warning" @onclick="OnAnnulla">Annulla</button>
</EditForm>

Con queste semplici modifiche abbiamo una gestione completa delle validazioni:

Potete consultare il codice descritto qui, nella branch 05-gestione-form.

Conclusioni

Con questo articolo abbiamo ultimato la gestione della nostra prima CRUD con Blazor, utilizzando i componenti già pronti per l’inserimento dati e sfruttando le Data Annotations per la definizione delle regole di validazione. Per esigenze didattiche abbiamo lavorato con dei dati in memoria, ma nel prossimo articolo vedremo come possiamo facilmente inviare questi dati al back-end per poterli salvare in un repository persistente come un database.