Un componente per mantenere lo stato dell'applicazione in Blazor

di Marco De Sanctis, in ASP.NET Core,

Una delle problematiche più comuni nella realizzazione di single page web application, è quella di mantenere lo stato corrente: l'utente tipicamente interagisce con varie pagine, magari modifica impostazioni che vengono poi memorizzate in un server, ma alle quali tutte le pagine e i componenti devono fare riferimento.

Model per application state


Qual è il modo migliore in Blazor per modellare un concetto del genere? Cerchiamo di trovare una soluzione che possiamo facilmente riutilizzare, iniziando da un'implementazione base per il nostro application state, come una classe C#:

public class ApplicationStateBase
{
    public event Action ApplicationStateChanged;

    protected virtual void OnApplicationStateChanged()
    {
        this.ApplicationStateChanged?.Invoke();
    }
}

ApplicationStateBase non presenta ancora alcuna proprietà, ma espone un evento ApplicationStateChanged, tramite cui segnaleremo a tutti i componenti interessati che qualcosa è cambiato nello stato, e che quindi devono riaggiornarsi.

Immaginiamo, allora, di avere a che fare con un semplice esempio, come quello di voler tener traccia del valore del counter nel template di default di Blazor. Possiamo creare una classe MyApplicationState che derivi dalla nostra classe base, simile alla seguente, che aggiunga la proprietà di stato che vogliamo tracciare e sollevi ApplicationStateChanged quando necessario:

public class MyApplicationState : ApplicationStateBase
{
    private int _currentCount;
    public int CurrentCount 
    {
        get { return _currentCount; }
        set 
        {
            if (_currentCount != value)
            {
                _currentCount = value;
                this.OnApplicationStateChanged();
            }
        }
    }
}

Ovviamente, per far sì che la stessa istanza sia condivisa da tutti i componenti, dobbiamo anche registrarla nell'elenco dei servizi:

public class Program
{
    public static async Task Main(string[] args)
    {
        // ... altro codice qui ...
        builder.Services.AddScoped<MyApplicationState>();

        await builder.Build().RunAsync();
    }
}

Esporre lo stato ai componenti Blazor


Come accediamo a questo state dalle nostre pagine, o dai vari componenti? Sicuramente un'opzione potrebbe essere quella di iniettarlo in ogni singola pagina, e di aggiungere la logica per gestire l'evento ApplicationStateChanged. Tuttavia questo ovviamente porterebbe a una certa duplicazione di codice. In realtà possiamo sfruttare in maniera furba un comportamento built-in nell'infrastruttura di Blazor e semplificare il tutto. Stiamo parlando di CascadingValue e CascadingParameter.

L'idea è quella di creare un generico, che chiameremo CascadingState, che esponga lo stato tramite CascadingValue ai suoi figli.

@typeparam TState

<CascadingValue Value="this.ApplicationState">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment @ChildContent { get; set; }
}

Ovviamente il parametro TState non può essere un tipo qualunque, ma deve ereditare da ApplicationStateBase. Pertanto possiamo sfruttare la tecnica che abbiamo visto nello script precedente (https://www.aspitalia.com/script/1378/Specificare-Constraint-TypeParam-Componente-Blazor-Generico.aspx) per definire questo constraint, creando una partial class al cui interno abbiamo aggiunto anche la logica per agganciarsi ad ApplicationStateChanged:

public partial class CascadingState<TState> : IDisposable 
    where TState: ApplicationStateBase
{
    [Inject]
    public TState ApplicationState { get; set; }

    protected override void OnInitialized()
    {
        base.OnInitialized();

        this.ApplicationState.ApplicationStateChanged += this.StateHasChanged;
    }

    public void Dispose()
    {
        this.ApplicationState.ApplicationStateChanged -= this.StateHasChanged;
    }
}

Ora, se vogliamo rendere accessibile lo stato ai component della nostra applicazione, non dobbiamo far altro che aggiungerlo ad App.razor come nell'esempio:

<CascadingState TState="ApplicationState">
    <Router AppAssembly="@typeof(Program).Assembly">
        ...
    </Router>
</CascadingState>

Abbiamo dovuto scrivere un po' di logica infrastrutturale, ma il vantaggio è che agganciarsi allo stato applicativo è ora semplicissimo. Non dobbiamo far altro che aggiungere una proprietà dove necessario, e marcarla come CascadingParameter. Per esempio, nella pagina Counter.razor:

@page "/counter"

<h1>Counter</h1>

<p>Current count: @this.ApplicationState.CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    [CascadingParameter]
    public MyApplicationState ApplicationState { get; set; }

    private void IncrementCount()
    {
        this.ApplicationState.CurrentCount++;
    }
}

Come abbiamo già accennato, il beneficio di usare un CascadingParameter è che non dobbiamo aggiungere alcuna logica per forzare il rendering del componente per mantenerlo in sincrono con lo stato applicativo: invocare StateHasChanged dal parent CascadingState, infatti, scatenerà il refresh automatico di tutti i figli che abbiamo una reference a MyApplicationState. Gli altri elementi del tree, invece, verranno esclusi dal processo di rendering, ottimizzando quindi al massimo le prestazioni.

A riprova di questo fatto, possiamo aggiungere per esempio l'indicazione del valore corrente nel menu laterale NavMenu.razor, e lo vedremo aggiornarsi in automatico agendo sul counter, senza dover scrivere altro codice:

... altro codice qui ...
<li class="nav-item px-3">
    <NavLink class="nav-link" href="counter">
        <span class="oi oi-plus" aria-hidden="true"></span> Counter (value: @this.ApplicationState.CurrentCount)
    </NavLink>
</li>
...

@code {
    [CascadingParameter]
    public ApplicationState ApplicationState { get; set; }

    // ... altro codice qui ...

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi