Nell’articolo precedente abbiamo visto come creare una griglia generica sfruttando la Reflection di .NET Core, uno strumento potente che ci capiterà spesso di usare quando dobbiamo generalizzare i comportamenti dei nostri componenti. Quando però la nostra esigenza è quella di fornire all’utente un meccanismo per personalizzare il markup con cui viene visualizzato una parte del contenuto nel nostro compontente, la Reflection non ci basta. Facciamo un esempio.

Creare una nuova previsione del tempo

Supponiamo di voler aggiungere un nuovo elemento alla lista di previsioni del tempo che visualizziamo. Concentriamoci su come farlo in locale, lasciando ad esempi reali la necessità di andare poi a salvare questa modifica su un database.

Per poter inserire i dati abbiamo bisogno di una form con cui permettere all’utente di specificare i valori da inserire. Creiamoci nella cartella Components un componente Details.razor con il classico codice di una form di dettaglio che usa i componenti già pronti di Blazor:

<EditForm Model="@Item" OnValidSubmit="@(e => OnSave.InvokeAsync(Item))">
    <DataAnnotationsValidator />

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

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

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

    <button type="submit" class="btn btn-primary">Save</button>
    <button type="button" class="btn btn-warning" @onclick="OnCancel">Cancel</button>
</EditForm>
@code 
{
    [Parameter]public WeatherForecast Item { get; set; }
    [Parameter]public EventCallback OnCancel { get; set; }
    [Parameter]public EventCallback OnSave { get; set; }
}

A questo punto possiamo modificare la pagina FetchData.razor, aggiungendo un pulsante per la creazione di un nuovo elemento e un semplice if per gestire la modalità di inserimento:

@page "/fetchdata"
@inject IWeatherForecastService WeatherForecastService

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (WeatherForecastModel.Rows == null)
{
    <p><em>Loading...</em></p>
}
else
{
    @if(newWeatherForecast == null)
    {
        <button class="btn btn-primary mb-3" @onclick="CreateWeatherForecast">
            Create Weather Forecast
        </button>
        <Grid Model="WeatherForecastModel" />
    }
    else
    {
        <Details Item="newWeatherForecast" OnCancel="Cancel" OnSave="Save" />
    }
}
@code {
    private GridModel WeatherForecastModel = new GridModel();
    private WeatherForecast[] weatherForecasts = null;
    private WeatherForecast newWeatherForecast = null;

    public void CreateWeatherForecast()
    {
        newWeatherForecast = new WeatherForecast();
    }

    public void Cancel()
    {
        newWeatherForecast = null;
    }

    public void Save()
    {
        Array.Resize(ref weatherForecasts, weatherForecasts.Length + 1);
        weatherForecasts[weatherForecasts.Length - 1] = newWeatherForecast;
        WeatherForecastModel.Rows = weatherForecasts;
        newWeatherForecast = null;
    }

    protected override async Task OnInitializedAsync()
    {
        WeatherForecastModel.UseAnnotations = true;
        weatherForecasts = await WeatherForecastService.GetForecastAsync(DateTime.Now);
        WeatherForecastModel.Rows = weatherForecasts;
    }
}

Eseguendo l’applicazione vedremo il risultato ottenuto:

Form dettaglio previsione del tempo

Niente che non abbiamo già visto negli articoli di base di come creare una Single Page Application. Come però potete notare, nonostante il contenuto della form sia specifico per il modello che stiamo gestendo, ci sono gli elementi EditForm, DataAnnotationsValidation e i pulsanti per il salvataggio e l’annullamento dell’operazione che potrebbero essere riutilizzati in tutte le form di una applicazione.

Templated Components

Blazor ci permette di chiedere all’utente di specificare una parte del contenuto del nostro componente, utilizzando un parametro di tipo RenderFragment. Questo significa che possiamo trasformare il nostro Details.razor in questo modo:

<EditForm Model="@Item" OnValidSubmit="@(e => OnSave.InvokeAsync(Item))">
    <DataAnnotationsValidator />

    @FormFields

    <button type="submit" class="btn btn-primary">Save</button>
    <button type="button" class="btn btn-warning" @onclick="OnCancel">Cancel</button>
</EditForm>
@code 
{
    [Parameter]public RenderFragment FormFields { get; set; }
    [Parameter]public WeatherForecast Item { get; set; }
    [Parameter]public EventCallback OnCancel { get; set; }
    [Parameter]public EventCallback<WeatherForecast> OnSave { get; set; }
}

La proprietà FormFields, di tipo RenderFragment può essere usata come segnaposto nel markup, in modo che la pagina FetchData possa specificare il contenuto di quel punto esatto del nostro componente:

...
@if(newWeatherForecast == null)
{
    ...
}
else
{
    <Details Item="newWeatherForecast" OnCancel="Cancel" OnSave="Save">
        <FormFields>
            <div class="form-group">
                <label for="Date">Date:</label>
                <InputDate id="Date" @bind-Value="newWeatherForecast.Date" class="form-control" />
                <ValidationMessage For="@(() => newWeatherForecast.Date)" />
            </div>

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

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

Rieseguendo l’applicazione vedrete lo stesso risultato precedente, ma questa volta è la pagina a decidere il contenuto della form. Visto che il RenderFragment è un parametro del nostro componente, ne possiamo inserire anche più di uno. Un componente che contiene uno o più parametri di questo tipo è detto Templated Component.

Utilizzare i Generics

Per poter però riutilizzare il nostro componente in scenari differenti è necessario rompere il legame con il tipo WeatherForecast, ma questa volta vogliamo farlo senza usare la Reflection. Il framework .NET ci permette fin dalle prime versioni di usare i Generics per poter rendere generici i tipi su cui una classe può lavorare e dato che un file Razor, in fase di compilazione, viene convertito in una classe, non ci dovrebbe meravigliare la possibilità di poter utilizzare i Generics anche con i componenti.

Per poterlo fare ci basta utilizzare la direttiva @typeparam, con cui specifichiamo il nome che vogliamo dare al nostro tipo generico:

@typeparam TItem
<EditForm Model="@Item" OnValidSubmit="@(e => OnSave.InvokeAsync(Item))">
    <DataAnnotationsValidator />

    @FormFields

    <button type="submit" class="btn btn-primary">Save</button>
    <button type="button" class="btn btn-warning" @onclick="OnCancel">Cancel</button>
</EditForm>
@code 
{
    [Parameter]public RenderFragment FormFields { get; set; }
    [Parameter]public TItem Item { get; set; }
    [Parameter]public EventCallback OnCancel { get; set; }
    [Parameter]public EventCallback<TItem> OnSave { get; set; }
}

Come potete vedere il nostro parametro Item adesso è di tipo TItem e il nostro codice continua a funziona esattamente come prima: provate voi stessi scaricando il codice dal repository della community.

Conclusioni

In questo articolo abbiamo visto come sia possibile in Blazor proiettare contenuto all’interno di un componente, utilizzando parametri di tipo RenderFragment che lo rendono Templated. Abbiamo anche visto come utilizzare i Generics di .NET per poter riutilizzare in contesti differenti il nostro form di dettaglio. Nei prossimi articoli vedremo come unire le due cose per semplificare e generalizzare ancora di più il nostro codice.