Dopo aver visto come utilizzare la Reflection di .NET per generalizzare una griglia e come creare dei Templated Components utilizzando proprietà RenderFragmente i Generics messi a disposizione da C#, continuiamo il nostro percorso sulla generalizzazione affrontando la gestione del contesto di un componente generico.

Gestire il contesto di un componente generico

Nell’articolo precedente abbiamo generalizzato un contenitore per le nostre form sfruttando la possibilità di proiettare contenuto custom grazie ai RenderFragment. I campi in binding erano definiti nella stessa pagina dove utilizzavamo il componente, quindi quando abbiamo aggiunto il generico TItem non abbiamo avuto nessun problema perchè il valore di Item e quello dei @bind-Value coincideva e il tipo del generico veniva dedotto dal valore:

<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>

In realtà possiamo specificare a livello di componente quale sia il tipo del Generico definito, come facciamo di solito quando usiamo le classi generiche, utilizzando un attributo con lo stesso nome che abbiamo dato al generico:

<Details TItem="WeatherForecast" Item="newWeatherForecast" OnCancel="Cancel" OnSave="Save">
    <FormFields>
        ...
    </FormFields>
</Details>

L’altra cosa interessante è che i RenderFragment sono disponibili in versione generica, grazie alla quale possiamo definire un tipo su cui lavorare. Possiamo quindi, ad esempio, dirgli che il tipo su cui lavorerà il frammento FormFields sarà WeatherForecast, oppure decidere di mantenere anche qui una implementazione generica e riutilizzare TItem:

@typeparam TItem
<EditForm Model="@Item" OnValidSubmit="@(e => OnSave.InvokeAsync(Item))">
    ...
    @FormFields(Item)
    ...
</EditForm>
@code 
{
    [Parameter] public RenderFragment<TItem> FormFields { get; set; }
    [Parameter] public TItem Item { get; set; }
    ...
}

Nella definizione della proprietà aggiungiamo quindi il tipo, mentre quando indichiamo il punto in cui il frammento sarà renderizzato, andiamo a specificare l’istanza da utilizzare. Tutto questo è molto utile perchè quell’oggetto diventa il cosiddetto context del frammento, e possiamo usarlo lato utilizzatore del componente sfruttando la parola riservata context:

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

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

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

Se non vi piace la parola context, potete specificarne una di vostro gradimento grazie all’attributo Context sul delimitatore del vostro frammento:

<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>

Tenete presente che in questo caso il tipo TItem verrebbe comunque dedotto dal tipo di Item, quindi non è necessario specificarlo, ma potendolo indicare non siete legati a una proprietà in particolare e in contesti più complessi, come ad esempio con più di un generico o con più di un RenderFragment, potete controllare ogni aspetto del vostro componente.

Potete trovare il codice completo sul repository della community.

Conclusioni

In questo articolo abbiamo visto come sia possibile in Blazor gestire il contesto di un RenderFragment, che ci permette di gestire casi complessi di proiezione di contenuto in un componente. Nel prossimo articolo sfrutteremo tutto quello che abbiamo imparato per capire come funziona un componente form e come generalizzarlo per rendere uniforme la user experience della nostra applicazione.