Unexpected UI binding behavior in Blazor WASM

Viewed 169

I have run into an unexpected (and inexplicable) difference in UI binding behavior between Blazor WASM and Blazor server. Given the following razor component, the "Send" button is correctly disabled during the request, in both Blazor Server and Blazor WASM:

<MudButton Disabled="isProcessing" OnClick="Submit">Send</MudButton>

@code {
    private bool isProcessing;

    private async Task Submit()
    {
        isProcessing = true;
        var contacts = await Mediator.Send(getContacts);
        isProcessing = false;
    }    
}

After adding a second awaited call (IsValidAsync()) to the Submit-method, the button now stays enabled during the request in Blazor WASM, which I consider wrong. In Blazor Server it still gets disabled as expected:

private async Task Submit()
{
    if (await validator.IsValidAsync())
    {
        isProcessing = true;
        var contacts = await Mediator.Send(getContacts);
        isProcessing = false;
    }
}

Is there a clear reason as to why this kind of binding would not work in Blazor WASM? Or is this a clear bug in Blazor?

2 Answers

The rule is that you get 2 implied StatehasChanged() calls for free, 1 before and 1 after your eventhandler.

So you get a 'free update' in the first await but when you have more steps in your code (that require output) you need to help:

private async Task Submit()
{
   if (await validator.IsValidAsync())  // consumes the first StateHasChanged
   {
      isProcessing = true;

      StateHasChanged();    // request an update

      var contacts = await Mediator.Send(getContacts); // update UI in background
      isProcessing = false;     
   }
   // one implied StateHasChanged for free again
}

This pattern should be used on Server and Wasm. The reason why the original code worked for you on Server is unclear. It probably is a fluke from IsValidAsync() having a non-async code path.

I'm suprised it behaves differently, but there is a significant different between Server and WASM that may explain what's going on. I'm not fully familiar with MudBlazor, so I'm not sure how the MudBlazor internals work exactly.

The difference being that the browser has only a single thread for all operations. Server as as many as are available on the server instance, which is almost all cases is more than one.

Try:

private async Task Submit()
{
    isProcessing = true;
    if (await validator.IsValidAsync())
    {
        var contacts = await Mediator.Send(getContacts);
    }
    isProcessing = false;
}

or

private async Task Submit()
{
    if (await validator.IsValidAsync())
    {
        isProcessing = true;
        await InvokeAsync(StateHasChanged);
        var contacts = await Mediator.Send(getContacts);
        isProcessing = false;
    }
}

The event handler basically does this:

var task = Submit();
StateHasChanged();
if (!task.IsCompleted || !task.IsCanceled)
{
  await task;
  StateHasChanged();
}

You can see this pattern in ComponentBase. It says if the task yields then we update the UI, wait for it to complete and then update the UI again. If it doesn't yield then we update the UI once it completes. Both my Server and WASM codes behave the same -nit sure whay yours is different.

Your code yields in if (await validator.IsValidAsync()) and thus the first StateHasChanged gets called before isProcessing is set.

Related