12

We are noticing that in our Blazor server-side application, users are able to click a button more than once unintentionally. How can I prevent this using options available in Blazor framework?

Anything as simple as changing the text of the button to "processing..." as soon as they click the button will suffice our requirements but I am not sure how to achieve this. Handler for click event takes few seconds to process.

Any pointers?

biostat
  • 423
  • 1
  • 4
  • 12
  • I've tried to click a button more than once, but to no avail... It's amazing... not the very fact that your users can click more than once, just at their leisure, but the very fact that you folks have noticed this cryptic phenomena – enet Dec 05 '19 at 20:46
  • @biostat: What does the Handler look like, is it async or not? – H H Dec 05 '19 at 22:24
  • "Handler for click event takes few seconds to process." So what ? Let them take their time. You seem to have failed to understand the processing flow: The issue is not how much time does it take for the event handler to execute, but the span of time elapsing immediately after clicking the button and encountering the first await operator which yields control to the calling code that proceed with re rendering of the component, and thus disabling the button. This takes a fraction of a second. No human hand can elicit a second click before the button is disabled. – enet Dec 06 '19 at 12:38
  • "async or not", not relevant. What are you striving at ? – enet Dec 06 '19 at 12:39
  • 2
    my issue is resolved and i understood the concept so let's close this thread. As Henk guessed I had several synchronous operations before the actual await statement which caused delay in handing the control back to calling code. I learned from my mistake. – biostat Dec 06 '19 at 17:27

4 Answers4

12
<button class="btn" disabled="@isTaskRunning" @onclick="DispatchTask">Click me</button>

This works best with an async Task handler method, as

async Task DispatchTask()   // avoid async void
{
    isTaskRunning = true;
    await Task.Delay(1);    // don't rely on DoLongWork() executing async
    await DoLongWork();

    isTaskRunning = false;
    // StateHasChanged();  // only needed in an async void
}

Suppose the DoWork() method looks like

async Task DoLongWork()
{
    Thread.Sleep(6000);   // synchronous
}

Then it executes synchronously, despite the async. The Task.Delay(1) remedies that.

H H
  • 263,252
  • 30
  • 330
  • 514
  • 5
    await Task.Delay(1) did a whole world of difference. I am unable to click more than once, so, does that mean that awaitable method is performing some sort of synchronous operation? Can you please explain what does introducing delay do behind the scenes? – biostat Dec 06 '19 at 00:03
  • 1
    It does an implict StateHasChanged() and allow the Render engine to run. – H H Dec 06 '19 at 06:02
  • 1
    "allow the GUI to catch up", what in the world does that mean ? Care to explain how calling await Task.Delay(1) makes any difference. Whether you use it or not, the yielding of control to the calling code that continues to re-render the component is taken place immediately after the first await operator is encountered. await DoLongWork(); is enough. what if you call Task.Delay(100000000). Will that make the re-rendering longer ;) – enet Dec 06 '19 at 12:54
  • It "did a whole world of difference". – H H Dec 08 '19 at 14:28
  • 2
    @biostat Same here. I must use `Task.Delay(1)` to show a "spinner" button while the work happens. It feels like a hack but I can't see another way. The `Task.Delay(1)` is actually 15 milliseconds on Windows and because the method is `async`, the UI is free and has time in that 15 ms to update the spinner button. And I need the `await Taks.Delay(1)` because I need at least one `await` to make the method `async`. Everything I'm calling is synchronous. – CoderSteve Apr 16 '21 at 10:35
7

You likely have one of two problems or both:

  1. Latency is an issue. Make sure you are hosting your application with Web Sockets enabled. The setting is dependent on the host, ex: Azure App Servers has a simple on/off knob for Web Sockets under settings. See the very last step in this blog post http://blazorhelpwebsite.com/Blog/tabid/61/EntryId/4349/Deploying-A-Server-Side-Blazor-Application-To-Azure.aspx
  2. You have a long running task.

For a long running task you can try the following solution. I have not tested it, mileage may vary.

<button disabled=@IsTaskRunning @onclick="DispatchTask">Submit</button>

@code {

    bool IsTaskRunning = false;

    async void DispatchTask()
    {
        IsTaskRunning = true;

        await DoLongWork();

        IsTaskRunning = false;
        StateHasChanged();
    }

    Task DoLongWork()
    {
        return Task.Delay(6000);
    }

}
Ed Charbeneau
  • 4,501
  • 23
  • 23
  • second option did not help (still able to click multiple times). am looking into first option. – biostat Dec 05 '19 at 19:39
  • Updated my answer with a blog resource. – Ed Charbeneau Dec 05 '19 at 19:50
  • The `async void` is dangerous and unnecessary. Blazor accepts `async Task` habdlers. – H H Dec 05 '19 at 22:28
  • async void can cause unhandled exceptions to "get lost". However, this is just a rough example and not meant to be used as-is. I wouldn't advise anyone to name a method DoLongWork either. Also, async void is actually acceptable (not my pref) in Blazor. See the documentation at Blazor.net Microsoft uses it as well. – Ed Charbeneau Dec 06 '19 at 16:20
  • Add `StateHasChanged()` just after `IsTaskRunning = true` otherwise you're setting the variable but not refreshing the UI to pay attention to it. – ataraxia Aug 17 '20 at 17:32
  • Microsoft's doc uses an `async void` example [here](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/azure-active-directory-groups-and-roles?view=aspnetcore-5.0#authorization-configuration). Also, @ataraxia I've found that `StateHasChanged()` does not always update the UI. It notifies the UI but does not seem to guarantee it will get refreshed. – CoderSteve Apr 16 '21 at 10:46
2

How about a generic solution? SpinnerButton

It could easily be edited to use a standard button. Generate a SVG animation here.

(both Radzen and Loading.io are free)

<RadzenButton Text="@(IsProcessing ? null : Text)" Image="@(IsProcessing ? Spinner : null)" Disabled="@IsProcessing" @attributes="@AdditionalAttributes" />

@code {
    [Parameter]
    public string Text { get; set; } = string.Empty;

    [Parameter]
    public string Spinner { get; set; } = "/images/spinner-button.svg";

    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }

    public bool IsProcessing { get; private set; }

    [Parameter]
    public EventCallback OnSubmit { get; set; }

    public async Task FormSubmitAsync()
    {
        if (IsProcessing) { return; }

        IsProcessing = true;
        try
        {
            await OnSubmit.InvokeAsync(null);
        }
        finally
        {
            IsProcessing = false;
        }
    }
}

Use like this

<EditForm OnValidSubmit="@SubmitAsync">
    <SpinnerButton @ref="ButtonRef" style="width:150px" Text="Login" 
        ButtonType="@((Radzen.ButtonType)ButtonType.Submit)" 
        ButtonStyle="@((Radzen.ButtonStyle)ButtonStyle.Primary)"
        OnSubmit="LogInAsync" />
</EditForm>

@code {
    public SpinnerButton? ButtonRef { get; set; }
    public async Task SubmitAsync() => await ButtonRef!.FormSubmitAsync().ConfigureAwait(false);

    public async Task LogInAsync()
    {
        // Get stuff done!
    }
}
Etienne Charland
  • 3,424
  • 5
  • 28
  • 58
-2
private Dictionary<string, string> control = new Dictionary<string, string>();

function click(){
    lock (control)
        {
            if (!control.ContainsKey("click"))
            {
            control.Add("click", "executing");
            }
            else
            {
            if (control["click"] == "executing")
            {
                //Double click detected
                return;
            }
            else
            {
                control["click"] = "executing";
            }
            }
        }


//Do usual stuffs here


    lock (control)
    {
        control["click"] = "";
    }

}