Dopo aver visto come gestire il contesto di un componente generico, facciamo un ulteriore passo concentrandoci su come funziona un componente form e come possiamo generalizzarlo per rendere uniforme la user experience della nostra applicazione.

Uniformare e centralizzare la UX

Se andiamo a riguardare la nostra form di dettaglio noteremo che vengono utilizzate le classi di Bootstrap per l’impaginazione:

<Details TItem="WeatherForecast" Item="newWeatherForecast" OnCancel="Cancel" OnSave="Save">
    <FormFields Context="weather">
        <div class="form-group">
            <label for="Date">Date:</label>
            <InputDate id="Date" @bind-Value="weather.Date" class="form-control" />
            <ValidationMessage For="@(() => weather.Date)" />
        </div>

        <div class="form-group">
            <label for="Temperature">Temperature:</label>
            <InputNumber id="Temperature" @bind-Value="weather.TemperatureC" class="form-control" />
            <ValidationMessage For="@(() => weather.TemperatureC)" />
        </div>

        <div class="form-group">
            <label for="Summary">Summary:</label>
            <InputTextArea id="Summary" @bind-Value="weather.Summary" class="form-control" />
            <ValidationMessage For="@(() => weather.Summary)" />
        </div>
    </FormFields>
</Details>

Si tratta di una scelta abbastanza comune, ma l’idea di base è che solitamente scelto o realizzato un insieme di regole CSS per l’impaginazione grafica, si tende ad usare sempre le stesse regole per mantenere uniforme l’aspetto della nostra applicazione. Inoltre stiamo utilizzando i componenti di Blazor per la gestione dei singoli campi di Input, ma questa scelta potrebbe cambiare. Se decidiamo ad esempio di usare una libreria di terze parti dovremo modificare tutte le form della nostra applicazione.

Potremmo quindi crearci dei nostri componenti per i campi di Input in modo da centralizzare l’utilizzo di uno specifico componente e di determinate classi CSS. Nella cartella Components creiamo ad esempio un file MyInputDate.razor, in cui copiamo questo pezzo di markup:

<div class="form-group">
    <label for="Date">Date:</label>
    <InputDate id="Date" @bind-Value="weather.Date" class="form-control" />
    <ValidationMessage For="@(() => weather.Date)" />
</div>

Esaminandolo è chiaro che dobbiamo parametrizzare il valore di id, l’etichetta utilizzata, il valore di bind-Value e l’espressione utilizzata nell’attributo For del componente ValidationMessage. Le prime due diventano dei semplici parametri di tipo stringa, mentre per le restanti possiamo fare qualcosa di più interessante.

L’espressione @bind-Value, utilizzata per il binding bidirezionale con la proprietà Value di InputDate, non può essere utilizzata su qualsiasi elemento, ma è necessario che il componente esponga tre parametri:

  • Il Value con cui sarà eseguito il binding.
  • Un EventCallback chiamato ValueChanged, invocato quando il valore in binding viene modificato.
  • Una espressione chiamata ValueExpression di tipo Expression<Func<T>> che permette di invocare una funzione per recuperare il valore in binding.

Il tipo gestito nel binding, e quindi il tipo di tutti e tre i pametri di binding, dipendono dal tipo del valore che vogliamo gestire: nel caso della data un DateTime. I componenti Input di Blazor fanno esattamente questo lavoro, anche se in maniera più generica, quindi possiamo rimappare questi parametri esattamente come sono, a patto di specificare il tipo su cui lavorare attraverso il generico TValue.

Il nostro codice diventa quindi:

@using System.Linq.Expressions
<div class="form-group">
    <label for="@Id">@Label</label>
    <InputDate id="@Id" 
        TValue="DateTime" 
        Value="@Value" 
        ValueChanged="@ValueChanged" 
        ValueExpression="@ValueExpression" class="form-control" />
    <ValidationMessage For="@ValueExpression" />
</div>
@code
{
    [Parameter] public string Id { get; set; }
    [Parameter] public string Label { get; set; }
    [Parameter] public DateTime Value { get; set; }
    [Parameter] public EventCallback<DateTime> ValueChanged { get; set; }
    [Parameter] public Expression<Func<DateTime>> ValueExpression { get; set; }
}

Un fantastico effetto collaterale di questi parametri è che possiamo riutilizzare il ValueExpression anche sul componente ValidationMessage, in modo da bindare e validare il valore in un colpo solo: il ValidationMessage userà l’espressione per recuperare il messaggio di errore dalle DataAnnotation della proprietà in binding.

Facendo un lavoro simile anche sugli altri tipi di Input, la nostra form di dettaglio diventa semplicissima:

<Details TItem="WeatherForecast" Item="newWeatherForecast" OnCancel="Cancel" OnSave="Save">
    <FormFields Context="weather">
        <MyInputDate Id="Date" Label="Date:" @bind-Value="weather.Date" />
        <MyInputNumber Id="Temperature" Label="Temperature:" @bind-Value="weather.TemperatureC" />
        <MyInputTextArea Id="Summary" Label="Summary:" @bind-Value="weather.Summary" />
    </FormFields>
</Details>

In questo modo abbiamo semplificato le nostre form e contemporaneamente anche centralizzato eventuali modifiche ai componenti di Input. Come sempre potete testare il codice completo sul repository della community.

Conclusioni

In questo articolo abbiamo visto come funziona in Blazor il binding bidirezionale di componente form e come generalizzarlo per rendere uniforme la user experience della nostra applicazione. Questo ci ha portati a vedere più da vicino come un componente Input viene gestito, cosa che ci tornerà molto utile nel prossimo articolo, quando creeremo un componente personalizzato più complesso.