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
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
Approfondimenti
Utilizzare Model as a Service su Microsoft Azure
Utilizzare un numero per gestire la concorrenza ottimistica con SQL Server ed Entity Framework
Esporre i propri servizi applicativi con Semantic Kernel e ASP.NET Web API
Gestire gli accessi con Token su Azure Container Registry
Generare la software bill of material (SBOM) in GitHub
Eseguire una ricerca avanzata per recuperare le issue di GitHub
Generare un hash con SHA-3 in .NET
Proteggere le risorse Azure con private link e private endpoints
Migliorare i tempi di risposta di GPT tramite lo streaming endpoint in ASP.NET Core
Utilizzare Azure Cosmos DB con i vettori
Gestire la cancellazione di una richiesta in streaming da Blazor
Effettuare il log delle chiamate a function di GPT in ASP.NET Web API
I più letti di oggi
- Simulare Azure Cosmos DB in locale con Docker
- Utilizzare il metodo Index di LINQ per scorrere una lista sapendo anche l'indice dell'elemento
- ecco tutte le novità pubblicate sui nostri siti questa settimana: https://aspit.co/wkly buon week-end!
- .NET Conference Italia 2024 - Milano
- .NET Conference Italia 2023 - Milano e Online