Search code examples
async-awaitblazortaskmudblazor

MudBlazor Async Validation Not Updating State


I'm using MudBlazor's MudTextField component to take in a string and I'm trying to run some validation on it asynchronously as I expect it to take a long time to validate. Other components in my app are disabled based off the results of the validation, so I use a flag to keep track of whether the input is valid. My general code setup is this:

<MudTextField Validation=ValidateValue />

@code{
    private async Task<string?> ValidateValue(string newVal){
        /* Do some prep */

        string? ErrorMessage = await Task.Run(() => SomeLongValidation());
        StateHasChanged() // Without this, the state doesn't update and the page doesn't see the updated flag
        return ErrorMessage;
    }
    
    private string? SomeLongValidation(){
        Thread.Sleep(2000);
        return "Dummy Error Text";
    }
}

If I run the validate value as a synchronous operation where I don't put the long validation as a task, I don't need the state has changed call and everything updates properly.

With some research, I've seemed to have found places that suggest when you await something, the StateHasChanged in the parent that would get called at the end of the operation gets called early, and thats why we have to call it ourselves, but I didn't see anything completely confirming that, and I found a few sources that almost disagreed. I believe it has something to do with MudBlazor specifically, but I'm not sure.


Solution

  • You current code block is invalid - MudTextField requires a bind.

    The bind callback [to set the bind value] is a UI event and as such triggers the UI event handler which calls StateHasChanged. Valdation isn't, it's a standard assignment of a delegate to a Parameter Property. You therefore need to call StateHasChanged when the delegate completes to update the UI.

    Here's some demo code that disables the control and button during validation.

    @page "/"
    
    <PageTitle>Index</PageTitle>
    
    <MudText Typo="Typo.h3" GutterBottom="true">Mud Text Valdation</MudText>
    <MudPaper Class="pa-2">
        <MudTextField Disabled="_validating" @bind-Value="_value" Validation="OnValidateValue" Label="Enter a Value" />
    </MudPaper>
    <MudPaper Class="pa-2">
        <MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="_validating">A Button</MudButton>
    </MudPaper>
    
    @code {
        private string? _value;
        private bool _validating;
    
        private async Task<string?> OnValidateValue(string? value)
        {
            _validating = true;
            // fake an async call to do some async work
            await Task.Delay(2000);
            // This yields back to the Synchronisation Context.  
             // The renderer gets time to service it's queue
            // and execute the render requested by the bind event
            // capturing _validating = true in the render.
            _validating = false;
            // Need to call StateHasChanged as this is not a UI event 
            // and all the UI event driven rendering is complete.
            this.StateHasChanged();
            return value is null || value.Length < 10 ? "Definitely Not Valid": null;
            // The MudTextField will render as 
            // StateHasChanged is coded into the `Validation` caller
            // when the returned Task completes.
        }
    }
    

    Additional explanation

    It's important to understand the context and the difference between an event and a UI event.

    ComponentBase implements a UI event handler that calls StateHasChanged automatically. Any UI events such as button clicks, input onchange are called through this handler. This includes any component EventCallbacks registered as parameters.

    In MudTextField the Validation parameter is declared as an object. It's not an EventCallback.

    [Parameter] public object? Validation { get; set; }
    

    It uses pattern matching to work out what you supply and how to invoke it correctly.

    The basic invocation for a Task Function(type value) looks like this:

    await invokeasync(delegate);
    StateHasChanged(); 
    

    So once OnValidateValue in the parent has completed, MudTextField renders. THIS DOES NOT render the owner of the delegate. OnValidateValue was not called as a UI event. There's no automatic call to StateHasChanged. The owner needs to call StateHasChanged seperately to render and display any state changes.