Quando suddividiamo la nostra applicazione in diversi componenti, uno delle domande a cui dobbiamo subito rispondere è “come comunicano fra loro?”. Come sempre la risposta giusta è: dipende!
I componenti in Blazor hanno a disposizione almeno tre tecniche per comunicare fra loro
- EventCallbacks
- Cascading Values
- State Container
A mio avviso non esiste la tecnica migliore, ma appunto, dipende dalla situazione. Vediamo più in dettaglio come funziona ognuna di queste.
EventCallbacks
Questa particolare tecnica è stata introdotta in Blazor con .NET Core 3 Preview 3. EventCallback e EventCallback<T> ci offrono un modo migliore per definire le callbacks rispetto all’uso di Func e Action. La ragione è semplice: l’utilizzo di Func o di Action ci costringe a fare una chiamata a StateHasChanged per comunicare a Blazor che qualcosa è cambiato e poterlo rendere effettivo nel DOM. EventCallback invece fa in modo che questa chiamata sia fatta in maniera automatica dal framework. Ovviamente esistono sia le versioni sincrone che quelle asincrone di EventCallback e EventCallback<T> che si possono utilizzare senza cambiamenti al nostro codice.
Quando vanno usati? Un esempio potrebbe essere il caso in cui avete componenti annidati e avete bisogno che il componente figlio attivi un metodo del componente che lo ospita a seguito di un certo evento.
<!-- Child -->
<button @onClick="@() => OnClick.InvokeAsync("Please Dad .. do domething"))">Do Something for me!</button>
@code {
[Parameter] public EventCallback<string> OnClick { get; set; }
}
<!-- Dad -->
<ChildComponent OnClick="ClickHandler" />
<p>@messageFromChild</p>
@code {
string messageFromChild = string.Empty;
public void ClickHandler(string message) => messageFromChild = message;
}
Il primo componente, ChildComponent, espone un parametro EventCallback, invocato dall’evento OnClick del button. Il componente padre, che ospita il ChildComponent, ha registrato un handler, in particolare il metodo ClickHandler in ascolto sull’evento OnClick del componente ospitato. Quando viene premuto il button, il metodo nel componente padre viene invocato, e grazie al framework che svolge il lavoro sporco per noi, ossia invoca StateHasChanged al posto nostro, il messaggio viene aggiornato nel DOM automaticamente.
Cascading Values
Può capitare di dover passare un valore da un componente padre a tutti i suoi componenti figli. In questo caso Cascading Values è il pattern che fa al caso nostro, senza dover ricorrere i tradizionali parametri. Sono una preziosa opzione nel caso si costruiscano controlli UI che necessitano di gestire alcuni stati comuni, come ad esempio nel caso di form di validazione in Blazor. Il componente EditForm distribuisce a cascata un valore EditContext a tutti i controlli del modulo. Questo valore è quindi utilizzato per coordinare la validazione ed invocare gli eventi del modulo. Facciamo un esempio:
<!—- Tab Container -->
<h1>@SelectedTab</h1>
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
@code {
[Parameter] public RenderFragment ChildContent { get; set; }
public string SelectedTab { get; private set; }
public void SetSelectedTab(string selectedTab)
{
SelectedTab = selectedTab;
StateHasChanged();
}
}
<!-- Tab Component -->
<div @onclick="SetSelectedTab">@Title @(TabContainer.SelectedTab == Title ? "Selected" : "")</div>
@code {
[CascadingParameter] TabContainer TabContainer { get; set; }
[Parameter] public string Title { get; set; }
void SetSelectedTab()
{
TabContainer.SetSelectedTab(Title);
}
}
Il componente TabContainer visualizza la scheda attualmente selezionata e imposta un valore a cascata, ossia il componente TabContainer stesso. Nel componente TabComponent viene ricevuto il valore a cascata [CascadingParameter] che viene utilizzato per chiamare il metodo SetSelectedTab nel componente TabContainer ogni qualvolta viene cliccato l’elemento <div>. Sempre tramite il valore a cascata viene controllato il valore dell’attuale SelectedTab, e se corrisponde al titolo della scheda lo evidenziamo aggiungendo Selected al titolo stesso della scheda.
State Container
La gestione dei cambiamenti tramite uno State Container può essere più o meno complessa, ma è comunque un pattern che necessita di qualche attenzione in più rispetto ai precedenti. Innanzitutto, il modo in cui dobbiamo iniettare questa classe di stato a seconda se stiamo sviluppando un’applicazione Blazor Server o WebAssembly. Nel primo caso utilizzeremo un singleton, nel secondo un servizio scoped. Esitono anche soluzioni maggiormente complesse che sfruttano, ad esempio, Flux.
Questa soluzione ci permette di gestire e coordinare diversi comportamenti in tutta l’applicazione. Vediamo un esempio che implementa una semplice classe AppState. Cominciamo col definire la nostra classe
public class AppState
{
public string SelectedBook { get; private set; } = string.Empty;
public event Action OnChange;
public void SetBookr(string book)
{
SelectedBook = book;
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}
La nostra classe AppState storicizza il libro selezionato. Ma non si ferma a questo, espone un metodo per aggiornare il libro selezionato. C’è anche un evento OnChange, che servirà a qualsiasi componente che desideri mostrare il libro attualmente selezionato. Ora implementiamo i due componenti e vediamo come sfruttare questa classe
<!-- Red Book -->
@inject AppState AppState
<button @onclick="SelectBook">Select RedBook</button>
@code {
void SelectBook()
{
AppState.SetBook("RedBook");
}
}
<!-- Blue Book -->
@inject AppState AppState
<button @onclick="SelectBook">Select BlueBook</button>
@code {
void SelectBook()
{
AppState.SetBook("BlueBook");
}
}
Questi due componenti non fanno altro che aggiornare il libro attualmente selezionato nella nostra classe AppState utilizzando l’evento OnClick del button. Ad ogni click viene chiamato il metodo della classe AppState che imposta il libro selezionato.
Ora uniamo il tutto
@inject AppState AppState
@implements IDisposable
@AppState.SelectedBook
<RedComponent />
<BlueComponent />
@code {
protected override void OnInitialized()
{
AppState.OnChange += StateHasChanged;
}
public void Dispose()
{
AppState.OnChange -= StateHasChanged;
}
#region Dispose
public void Dispose(bool disposing)
{
if (disposing)
{
AppState.OnChange -= StateHasChanged;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~HostedComponent()
{
Dispose(false);
}
#endregion
}
Questo componente sottoscrive l’evento OnChange esposto dalla classe AppState. Ogni volta che il nostro lettore seleziona il libro che vuole leggere dai precedenti componenti, questo componente ne riceve la notifica grazie alla sottoscrizione all’evento OnChange. Invoca quindi StateHasChanged ed il componente viene renderizzato con il colore del libro selezionato.
Quando utilizziamo questo pattern sono molto importanti due cose
- Ricordarsi di sottoscrivere il metodo OnChange nel metodo OnInitialied()
- Ricordarsi di rimuovere la sottoscrizione a OnChange nel metodo Dispose()
Il primo per essere certi di ricevere le notifiche dei cambi di stato, il secondo per evitare di incorrere in problemi di memory leak.
Conclusioni
Abbiamo trattato diversi stili e pattern di comunicazione fra componenti, come anticipato non esiste Il Migliore, ma resta sempre valido il detto di ogni sviluppatore … Dipende!
Alla prossima!