Articoli

Interfacce dinamiche con il Dynamic Component

da

Una delle cose che più mi piace quando lavoro con Blazor è la forte tipizzazione dei componenti. Questa è una delle caratteristiche principali del framework, infatti ogni Razor Component non è nient’altro che una classe C# e pertanto avrà un Type ad esso associato .

Lavorare con componenti fortemente tipizzati, però, ci vincola a conoscere il loro tipo nel momento in cui andiamo a comporre la nostra pagina. Ma come possiamo fare a rendere le nostre pagine più dinamiche, magari cambiando il tipo di componente di cui fare il render a seconda di regole di business differenti?

Il framework ci offre strade differenti: potremmo riempire il componente di condizioni if/else, potremmo riscrivere un pezzo del render tree, usare la reflection o i RenderFragment. Tutte queste opzioni hanno in comune l’elevata difficoltà di gestione del codice quando le logiche di rendering si fanno complesse.

Ma con .NET 6 il team di ASP.NET Core ha introdotto i DynamicComponent, una funzionalità molto interessante che sembra risolvere l’elevata complessità di gestione per il rendering di componenti dinamici.

Cosa è un Dynamic Component?

I DynamicComponent sono componenti built-in del framework che servono ad eseguire il rendering di componenti dinamici prendendo in ingresso un Type ed una collezione opzionale di parametri.

La definizione è molto semplice:

<DynamicComponent Type="@someType" Parameters="@someParameters" />

@code {
    Type someType = typeof(MyComponent);
    Dictionary<string, object> someParameters = new Dictionary<string,object>() { { "parametro1", "valore1"} };
}

Il codice sopra effettuerà il render del componente con il tipo presente nella variabile someType e verranno passati in input i parametri del dizionario someParameters.

Visto così il codice non sembra poi così interessante, ma vediamo come poter sfruttare questo semplice costrutto per gestire dinamiche più complesse.

Come si usa un Dynamic Component?

Come puro esercizio di stile vediamo un esempio di un form in cui l’utente può selezionare una specifica modalità di contatto e sulla base della modalità selezionata la nostra UI si aggiornerà e mostrerà un componente piuttosto che un altro.

Nella nostra pagina, che per questo esempio sarà l’Index dell’applicazione, andiamo ad inserire una form con una dropdown. Al cambio del valore della dropdown (righe 24-27) cambieremo il componente da visualizzare andando a cambiare a runtime il valore della variabile @selectedType che verrà poi passata in input al DynamicComponent.

Index.razor:

@page "/"

<PageTitle>Form di contatto</PageTitle>
<form>
    
    <div class="form-group">
        <label class="form-label" for="type-change-dropdown">Selezionare una modalità di contatto:</label>
        <select id="type-change-dropdown" @onchange="OnTypeDropdownChange" class="form-control mb-2">
            <option>---</option>
            <option value="@typeof(AddressComponent).FullName">Posta ordinaria</option>
            <option value="@typeof(FaxComponent).FullName">Fax</option>
        </select>
    </div>

    @if (selectedType != null)
    {
        <DynamicComponent Type="@selectedType"></DynamicComponent>
    }
</form>

@code {
    private Type selectedType;

    private void OnTypeDropdownChange(ChangeEventArgs changeEventArgs)
    {
        selectedType = Type.GetType(changeEventArgs.Value.ToString());
    }
}

I componenti AddressComponent e FaxComponent sono dei semplici componenti Razor che mostreranno alcuni input specifici per la modalità di contatto selezionata.

AddressComponent.razor:

<div class="form-group">
    <label for="indirizzo">Indirizzo</label>
    <input class="form-control" id="indirizzo" />
</div>

<div class="form-group">
    <label for="cap">Indirizzo</label>
    <input class="form-control" id="cap" />
</div>

<div class="form-group">
    <label for="citta">Città</label>
    <input class="form-control" id="citta" />
</div>

FaxComponent.razor:

<div class="form-group">
    <label for="numero">Numero</label>
    <input class="form-control" id="numero" />
</div>

Esempio di DynamicComponent
Esempio di DynamicComponent

E se volessimo passare dei parametri di input ai nostri componenti? Aggiungiamo ad esempio in input il “Prefisso” al FaxComponent renderizzato. Possiamo modificare il componente FaxComponent.razor come segue:

<div class="form-group">
    <label for="numero">Prefisso</label>
    <input readonly class="form-control" id="codice-paese" value="@CountryCode"/>
</div>

<div class="form-group">
    <label for="numero">Numero</label>
    <input class="form-control" id="numero" />
</div>

@code {
    [Parameter]
    public string CountryCode { get; set; }
}

Modifichiamo anche la nostra pagina per gestire questo nuovo parametro.

Index.razor:

@page "/"

<PageTitle>Form di contatto</PageTitle>
<form>

    <div class="form-group">
        <label class="form-label" for="type-change-dropdown">Selezionare una modalità di contatto:</label>
        <select id="type-change-dropdown" @onchange="OnTypeDropdownChange" class="form-control mb-2">
            <option>---</option>
            <option value="@typeof(AddressComponent).FullName">Posta ordinaria</option>
            <option value="@typeof(FaxComponent).FullName">Fax</option>
        </select>
    </div>

    @if (selectedType != null)
    {
        <DynamicComponent Type="@selectedType" Parameters="@componentParameters"></DynamicComponent>
    }
</form>

@code {
    private Type selectedType;
    private Dictionary<string, object> componentParameters = new Dictionary<string, object>();

    private void OnTypeDropdownChange(ChangeEventArgs changeEventArgs)
    {
        selectedType = Type.GetType(changeEventArgs.Value.ToString());

        if (selectedType == typeof(FaxComponent))
        {
            componentParameters = new Dictionary<string, object>() { { "CountryCode", "+39" } };
        }
        else
        {
            componentParameters = new Dictionary<string, object>();
        }
    }
}

Esempio di DynamicComponent con parametri
Esempio di DynamicComponent con parametri

Generalizzare il componente

Ma cosa succede se vogliamo aggiungere una nuova modalità di contatto? Dovremmo aggiungere una nuova option alla dropdown, gestirla nell’onchange, inserire nuovi if statement per eventuali nuovi parametri; tutto a discapito della semplicità e della manutenibilità.

Per migliorare la situazione però possiamo portare fuori dalla nostra pagina il “censimento” dei componenti ed iniettare in pagina un servizio che si occupi proprio di restituirci quali sono i componenti di cui possiamo fare il render.

Creiamo quindi una nuova classe ComponentService che andremo ad iniettare in pagina grazie al motore di Dependency Injection di ASP.NET Core. La classe ci esporrà un solo metodo GetRenderComponents() che ci servirà per recuperare l’elenco dei componenti di cui possiamo effettuare il render in pagina.

ComponentService.cs:

using DynamicComponents.SampleApp.Components;

namespace DynamicComponents.SampleApp.Services
{
    public class ComponentService
    {
        public Dictionary<string, RenderComponent> GetRenderComponents()
        {
            return new Dictionary<string, RenderComponent>()
            {
                {"Posta ordinaria", new RenderComponent() { Type = typeof(AddressComponent) } },
                {"Fax", new RenderComponent() { Type = typeof(FaxComponent), Parameters = new(){ { "CountryCode" , "+39"} } } },
            };
        }
    }
}

RenderComponent.cs:

namespace DynamicComponents.SampleApp.Services
{
    public class RenderComponent
    {
        public Type Type { get; set; }
        public Dictionary<string, object> Parameters { get; set; } = new Dictionary<string, object>();
    }
}

Registriamo la classe ComponentService come singleton all’interno del motore di Dependency Injection aggiungendo la seguente riga al file Program.cs.

builder.Services.AddSingleton<ComponentService>();

In ultimo andiamo a modificare il nostro index per utilizzare il nuovo servizio:

@page "/"
@inject ComponentService ComponentService

<PageTitle>Form di contatto</PageTitle>
<form>

    <div class="form-group">
        <label class="form-label" for="type-change-dropdown">Selezionare una modalità di contatto:</label>
        <select id="type-change-dropdown" @onchange="OnTypeDropdownChange" class="form-control mb-2">
            <option value="">---</option>            
            @foreach(var component in componentsToRender)
            {
                <option value="@component.Key">@component.Key</option>
            }
        </select>
    </div>

    @if (selectedType != null)
    {
        <DynamicComponent Type="@selectedType" Parameters="@componentParameters"></DynamicComponent>
    }
</form>

@code {
    private Type selectedType;
    private Dictionary<string, object> componentParameters = new Dictionary<string, object>();
    private Dictionary<string, RenderComponent> componentsToRender;

    protected override void OnInitialized()
    {
        componentsToRender = ComponentService.GetRenderComponents();
    }

    private void OnTypeDropdownChange(ChangeEventArgs changeEventArgs)
    {
        if (componentsToRender.ContainsKey(changeEventArgs.Value.ToString()))
        {
            var selectedComponentMetadata = componentsToRender[changeEventArgs.Value.ToString()];
            selectedType = selectedComponentMetadata.Type;
            componentParameters = selectedComponentMetadata.Parameters;
        }
        else
        {
            selectedType = null;
            componentParameters = new Dictionary<string, object>();
        }
    }
}

Nell’esempio mostrato il ComponentService ha l’elenco dei componenti scritti all’interno del metodo, ma nulla ci vieta di poter andare a recuperare i componenti da una API, da un database locale o remoto, da un file di configurazione o da qualunque altro store in cui vorrete andare a persisterli.

EventCallback come parametro di un DynamicComponent

I componenti possono accettare in input parametri, come abbiamo visto in precedenza, ma anche EventCallback; le EventCallback sono uno dei modi che un componente padre ha di manipolare il comportamento di un componente figlio. In breve sono funzioni che il componente padre passa al componente figlio e che possono venir eseguite all’avvenimento di un determinato evento nel componente figlio (per una disamina più approfondita vi consiglio di leggere l’ottimo articolo “Comunicazione fra Componenti in Blazor” di Alberto pubblicato qui sul blog).

Sempre per puro esercizio di stile, supponiamo di voler aggiungere un button nel nostro componente Fax chiamato “Esporta”. Nella nostra pagina Index vogliamo che quando questo componente viene cliccato il browser mostri un alert all’utente nel quale sarà presente il numero di telefono inserito, formattato con il prefisso.

Per fare ciò aggiungiamo un button al FaxComponent come segue.

<div class="form-group">
    <label for="numero">Prefisso</label>
    <input readonly class="form-control" id="codice-paese" value="@CountryCode" />
</div>

<div class="form-group">
    <label for="numero">Numero</label>
    <input class="form-control" id="numero" @bind-value="@Number" />
</div>

<div class="btn-group mt-1">
    <button type="button" class="btn btn-primary" @onclick="HandleClick">Esporta</button>
</div>

@code {
    [Parameter]
    public string CountryCode { get; set; }

    [Parameter]
    public EventCallback<string> OnExportButtonClick { get; set; }

    private string Number;

    private void HandleClick(MouseEventArgs args) => OnExportButtonClick.InvokeAsync($"{CountryCode} {Number}");
}

La pagina di index non necessiterà di alcuna modifica perché il comportamento dei componenti “renderizzabili” sarà gestito in toto dal nostro ComponentService. Sarà lì che dovremmo andare a modificare il nostro codice come segue.

ComponentService.cs:

using DynamicComponents.SampleApp.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace DynamicComponents.SampleApp.Services
{
    public class ComponentService
    {
        private readonly IJSRuntime iJSRuntime;

        public ComponentService(IJSRuntime iJSRuntime)
        {
            this.iJSRuntime = iJSRuntime;
        }

        public Dictionary<string, RenderComponent> GetRenderComponents()
            => new()
            {
                {"Posta ordinaria", new RenderComponent() { Type = typeof(AddressComponent) } },

                {"Fax", new RenderComponent() {
                    Type = typeof(FaxComponent)
                    , Parameters = new(){
                        { "CountryCode" , "+39"} ,
                        { "OnExportButtonClick", EventCallback.Factory.Create<string>(this, OnExportClick) }
                    }
                }}
            };

        private void OnExportClick(string faxNumberValue) => iJSRuntime.InvokeVoidAsync("alert", faxNumberValue);
    }
}

Nella classe ComponentService andiamo ad iniettare il servizio di IJSRuntime tramite il costruttore in modo da poter inviare un alert all’utente con il numero Fax inserito e compilato. Questo servizio infatti verrà usato nella funzione OnExportClick alla riga 30 che a sua volta viene passata come parametro al componente FaxComponent quando questo verrà renderizzato (riga 25).

Esempio di DynamicComponent con EventCallback come parametro
Esempio di DynamicComponent con EventCallback come parametro

Conclusioni

Il modo in cui vengono passati i parametri in ingresso ad un DynamicComponent potrebbe sembrare quantomeno insolito. Non potevamo applicare un approccio di tipo catch-all sui parametri? Ovvero non potevamo andare a definire un DynamicComponent semplicemente esplicitando tutti i parametri e poi gestirli nel DynamicComponent come un Parameter di tipo catch-all che abbia CaptureUnmatchedValue? Per capirci: <DynamicComponent Type="mioTipo" ParametroA="ValoreA" ParametroB="ValoreB" />.

La risposta alla domanda ce la fornisce direttamente Steve Sanderson, l’ideatore di Blazor, nella issue di Github associata alla creazione del DyamicComponent: “[..] credo sia importante […] permettere di passare soltanto un dizionario di parametri espliciti, se usassimo dei parametri di tipo catch-all, allora ogni altro parametro esplicito del DynamicComponent – esistente o che verrà creato – diventerebbe una parola riservata che non si potrà passare come parametro al componente figlio. Diventerebbe una breaking change aggiungere nuovi parametri al DynamicComponent perché andrebbero a sovrascrivere i parametri passati in ingresso con lo stesso nome. […]”.

I DynamicComponent sono una funzionalità molto interessante di Blazor che ci permettono di rendere la nostra UI dinamica. Questi possono trovare applicazioni disparate nelle nostre applicazioni web, come ad esempio offrire una funzionalità di dashboard personalizzabile con dei widget che l’utente può spostare e nascondere a piacimento; costruire dinamicamente dei form da far vedere all’utente sulla base di diversi parametri; spostare la composizione della UI dalla pagina ad un servizio esterno come abbiamo visto nel precedente esempio.

In questo articolo abbiamo visto cosa sono i DynamicComponent, come si definiscono, come si usano e come è possibile passargli dei parametri in input che siano POCO o funzioni. Per approfondimenti sul tema vi rimando alla pagina ufficiale della documentazione di Microsoft ed al codice sorgente dei DynamicComponent su Github. Il codice mostrato nell’articolo è disponibile su Github al seguente link: https://github.com/fabio-sp/DynamicComponents.SampleApp.

Alla prossima!