Francesco Cristallo
Francesco Cristallo

Reputation: 3064

Delay a task in Blazor without blocking the UI

I created a .razor Notification Component in Blazor, and I'm trying to autoclose the notification div after xx seconds.

So far it works with this Method

 private async Task CloseToast(Guid Id, bool autoclose = false)
{
    if (autoclose)
    {
        await Task.Delay(TimeSpan.FromSeconds(5));
    }

   //Code to remove the notification from list
    StateHasChanged();
}

The problem is that for 5 seconds the UI data binding is stuck, any one way or two way binding update to variables (text fields etc..) is on hold until the Notification is closed and the Task resumes.

How can I launch a method or code block after xx seconds without blocking the main UI task in Blazor?

Upvotes: 16

Views: 18587

Answers (5)

Mirko MyRent
Mirko MyRent

Reputation: 211

async void Onchange()
{
    await Task.Delay(100);
    await js.InvokeVoidAsync("javascriptFN");
}

Delay before execute javascript function

Upvotes: 0

Ogglas
Ogglas

Reputation: 69958

There is an issue for this in .NET 8 Planning to Enable debounce / throttling for events but there is currently no native support.

https://github.com/dotnet/aspnetcore/issues/10522

Currently I use a timer like @ZsoltBendes but I have implemented it like @JeremyLikness does for his debounce example. Meaning I use timer.Elapsed to remove notifications or anything else that needs to be handled after a certain time.

https://blog.jeremylikness.com/blog/an-easier-blazor-debounce/#the-perfect-time

TimerComplete.razor:

@inject HttpClient Http

@using System.Timers;

<CompletionTemplate Calls="@calls"
                    TotalItemsFetched="@totalItems"
                    Reset="() => Reset()"
                    @bind-Text="@Text"
                    FoodItems="@foodItems" />

@code {
    private Timer timer = null;
    private string text;
    private FoodItem[] foodItems = new FoodItem[0];
    private int calls;
    private int totalItems;

    private string Text
    {
        get => text;
        set
        {
            if (value != text)
            {
                text = value;
                DisposeTimer();
                timer = new Timer(300);
                timer.Elapsed += TimerElapsed_TickAsync;
                timer.Enabled = true;
                timer.Start();
            }
        }
    }

    private async void TimerElapsed_TickAsync(object sender, EventArgs e)
    {
        DisposeTimer();
        await OnSearchAsync();
    }

    private void DisposeTimer()
    {
        if (timer != null)
        {
            timer.Enabled = false;
            timer.Elapsed -= TimerElapsed_TickAsync;
            timer.Dispose();
            timer = null;
        }
    }

    private void Reset()
    {
        calls = 0;
        totalItems = 0;
        DisposeTimer();
        text = string.Empty;
        foodItems = new FoodItem[0];
        StateHasChanged();
    }

    private async Task OnSearchAsync()
    {
        if (!string.IsNullOrWhiteSpace(text))
        {
            foodItems = await Http.GetFromJsonAsync<FoodItem[]>(
                $"/api/foods?text={text}");
            calls++;
            totalItems += foodItems.Length;
            await InvokeAsync(StateHasChanged);
        }
    }
}

CompletionTemplate.razor:

<div class="container">
    <div class="row">
        <div class="col-6">
            Calls: @Calls (Fetched @TotalItemsFetched items)
            <button class="btn btn-sm btn-primary"
                    @onclick="() => Reset()">
                Reset
            </button>
        </div>
        <div class="col-6">
            Enter search text: <input @bind-value="Text" @bind-value:event="oninput" />
        </div>
    </div>
    <div class="row">
        <div class="col-6">
            @FoodItems.Length item(s).
        </div>
        <div class="col-6">
            @if (FoodItems.Length < 1)
            {
                <p>No items found.</p>
            }
            else
            {
                foreach (var foodItem in FoodItems)
                {
                    <p @key="foodItem.Id">@foodItem.Description</p>
                }
            }
        </div>
    </div>
</div>

@code {
    private string text;

    [Parameter]
    public int Calls { get; set; }

    [Parameter]
    public int TotalItemsFetched { get; set; }

    [Parameter]
    public Action Reset { get; set; }

    [Parameter]
    public string Text
    {
        get => text;
        set
        {
            if (value != text)
            {
                text = value;
                InvokeAsync(async () => await TextChanged.InvokeAsync(text));
            }
        }
    }

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

    [Parameter]
    public FoodItem[] FoodItems { get; set; }
}

Source:

https://github.com/JeremyLikness/BlazorDebounce/blob/main/BlazorDebounce/Client/Shared/TimerComplete.razor

Upvotes: 0

M.M
M.M

Reputation: 141554

The official Blazor Server EFCore sample project includes this as an example, in TextFilter.razor. The essence of the code is:

Timer? timer;

// ... code in a function to start the timer
timer?.Dispose();
timer = new(DebounceMs);
timer.Elapsed += NotifyTimerElapsed;
timer.Enabled = true;

private async void NotifyTimerElapsed(object? sender, ElapsedEventArgs e)
{
    timer?.Dispose();
    timer = null;

    // SomeMethodAsync will need to call StateHasChanged()
    InvokeAsync(() => SomeMethodAsync());
}

and a Dispose() function for the page to dispose any timer in progress when user navigates away.

Upvotes: 1

Major
Major

Reputation: 6658

You can use .NET Timer from System.Timers as well and set the Delay in milisec. When it elapsed event will triggered and you can put your logic into the event handler. If you don't want to bother with all the config and Disposing of Timer you can use this Nuget package. It is a very convenient wrapper for the Timer with many extra features see docs.

<AdvancedTimer Occurring="Times.Once()" IntervalInMilisec="@_closeInMs" AutoStart="true" OnIntervalElapsed="@(e => { IsVisible = false; })" />
@code {
  private int _closeInMs = 5000;
  ...
}

Upvotes: 1

Zsolt Bendes
Zsolt Bendes

Reputation: 2569

A component with a timer that counts back


<h3>@Time</h3>

@code {
    [Parameter] public int Time { get; set; } = 5;

    public async void StartTimerAsync()
    {
        while (Time > 0)
        {
            Time--;
            StateHasChanged();
            await Task.Delay(1000);
        }
    }

    protected override void OnInitialized()
        => StartTimerAsync();
}

Usage:

<Component />
<Component Time="7"/>

Tested on client side Blazor. Should behave the same way in server-side Blazor. Hope this helps

Upvotes: 30

Related Questions