Gestire il timing sugli eventi all'interno di un'applicazione Blazor

di Morgan Pizzini, in ASP.NET Core,

In Blazor, per presentare a video i dati presenti in un modello e salvare ciò che l'utente inserisce all'interno di un input, viene utilizzata una logica definita Two-way data binding. Questi processi permettono di mantenere una sincronia in tempo reale tra modello logico e dati rappresentati: come mostrato nell'esempio che troviamo all'interno di ogni nuova applicazione, se premiamo il bottone, nella pagina Counter, il numero mostrato viene immediatamente incrementato.

Perchè se è una funzionalità così performante, abbiamo bisogno di inserire un debounce, ossia qualcosa che dilazioni il numero di aggiornamenti? Vi sono dei contesti, anche molto comuni, in cui l'elevato numero di aggiornamenti genera del rumore non voluto, rendendo i processi della pagina lenti e volubili ad errori.

Per capire meglio utilizziamo la seconda pagina di esempio, la Weather forecast, e immaginiamo di voler aggiungere un input in cima alla tabella per filtrare i dati. La logica è molto semplice: prevede l'utilizzo di una proprietà di tipo string, che al cambiamento aggiorni una lista che sarà composta da tutti gli elementi della lista originale che hanno superato la scrematura.

<input class="form-control" @bind-value="SearchText" @bind-value:event="oninput"/>
<Virtualize Items="@FilteredList" Context="weather">
@weather.Location
</Virtualiza>

@code{
    public string SearchText = "";

    IList<WeatherForecast> FilteredList
    {
        get => originalList.Where( weather => weather.Location.Contains(SearchText)).ToList();
    }

    IList<WeatherForecast> originalList = new List<WeatherForecast>();
}

Per motivi dimostrativi abbiamo ridotto il codice mostrato, ma nel caso reale ci sarebbe stata una tabella con molte righe e colonne, che vengono create e distrutte ad ogni input dell'utente. Se inserissimo una parola come "MILANO", la logica di aggiornamento ricreerà la tabella 6 volte. Sarebbe dunque più funzionale se, prima di lanciare l'evento che aggiorni la proprietà SearchText, si attendesse del tempo, in modo da lasciare all'utente la possibilità di inserire più lettere.

Per arrivare a ciò non abbiamo proprietà o nulla di automatico, ma possiamo crearci i nostri metodi, ricordandoci che tutto quello che lega ciò che scriviamo all'interno del browser, al codice C#, sono eventi javascript, che possono essere intercettati e gestiti.

Partiamo proprio dalla scrittura di una funzione che ci permetterà di rimandare di un certo numero di secondi l'emissione dell'evento e che allo stesso tempo permetta di annullare l'evento precedente se viene richiamata una seconda volta entro il tempo prefissato. Aggiungiamo un file nella seguente posizione wwwroot/js/events.js

// funzione principale che verrà chiamata da Blazor fornendo 
// l'elemento html
// la tipologia di evento da gestire
// l'intervallo di tempo
export function debounceEvent(htmlElement, eventName, delay) {
    registerEvent(htmlElement, eventName, delay, debounce);
}

function registerEvent(htmlElement, eventName, delay, filterFunction) {
    let raisingEvent = false;

    // creazione dell'evento
    let eventHandler = filterFunction(function (e) {
        raisingEvent = true;
        try {
            // se si supera il tempo pre-impostato, viene eseguito il dispatch dell'evento
            htmlElement.dispatchEvent(e);
        } finally {
            raisingEvent = false;
        }
    }, delay);

    // registrazione dell'evento sull'elemento
    htmlElement.addEventListener(eventName, e => {
        if (!raisingEvent) {
            e.stopImmediatePropagation();
            eventHandler(e);
        }
    });
}

// funzione che permette di incapsulare la funzione all'interno di un timer
function debounce(func, wait) {
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => { func.apply(this, args); }, wait);
    };
}

Quello che abbiamo creato pocanzi è un modulo javascript, che andrà opportunamente importato alla bisogna, utilizzando l'istanza dell'interfaccia IJSRuntime, come mostrato in seguito

public static class EventExtensions
{
    public static async Task DebounceEvent(this IJSRuntime jsRuntime, ElementReference element, string eventName, TimeSpan delay)
    {
        await using var module = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/events.js");
        await module.InvokeVoidAsync("debounceEvent", element, eventName, (long)delay.TotalMilliseconds);
    }
}

Uniamo i pezzi del puzzle nella pagina Weather forecast, all'interno della quale occorrerà ottenere una referenza all'input HTML di ricerca e richiamare l'extension method per impostare la logica con cui controllare gli eventi di input

@inject IJSRuntime JSRuntime
<input @ref="searchInput" class="form-control" @bind-value="SearchText" @bind-value:event="oninput"/>

<Virtualize Items="@FilteredList" Context="weather">
@weather.Location
</Virtualiza>

@code{
    ElementReference searchInput;
    
    public string SearchText = "";

    IList<WeatherForecast> FilteredList
    {
        get => originalList.Where( weather => weather.Location.Contains(SearchText)).ToList();
    }

    IList<WeatherForecast> originalList = new List<WeatherForecast>();

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JSRuntime.DebounceEvent(searchInput, "input", TimeSpan.FromMilliseconds(500));
        }
    }
}

Grazie alla struttura modulare e all'extension method basato sull'interfaccia JSRuntime, possiamo applicare il debounce a qualsiasi tipologia di evento di un qualsiasi elemento, senza appesantire il codice o l'esecuzione.

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