0

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.

Jorden Q
  • 45
  • 8
  • @HH Currently it's just a dummy function, where I use thread.sleep to wait 2 seconds and then return an error message. This is all just a skeleton of my code, however, as there are a lot of irrelevant bits, but this has all of the biggest parts. – Jorden Q Aug 07 '23 at 18:33
  • Aside from the async/sync issue, are you trying to update the state on each input/key press? Or only when the input loses focus? – RBee Aug 07 '23 at 21:47
  • Can you share your full `MudTextField` component with the properties/attributes you're using. – RBee Aug 07 '23 at 21:57

1 Answers1

1

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.

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • Thank you, although I'm confused. You say I need to call state has changed because the function is not a UI event, but then in your very last comment you say that the text field will render since `StateHasChanged` is coded into the Validation caller. Could you elaborate on that, because I'm a little confused – Jorden Q Aug 08 '23 at 14:44
  • See - https://stackoverflow.com/a/76545126/13065781https://stackoverflow.com/a/76545126/13065781 and https://stackoverflow.com/a/76729783/13065781 – MrC aka Shaun Curtis Aug 08 '23 at 15:43
  • In `MudTextField` the internal onchange/oninput event handler invokes and then awaits the delegate assigned to the `Validation` Parameter. When it completes i.e. `OnValidateValue` in the parent, then the handler [in MudTextField] calls `StateHasChanged` [in MudTextField] which causes a render of `MudTextField` [and any sub-components] but not the parent. Render cascades propagate downwards. not up. – MrC aka Shaun Curtis Aug 08 '23 at 15:58
  • So just to make sure I understand, when passing in the function to the Validation parameter, since it's an object and not an event callback, StateHasChanged() doesn't get called when it's completed. The text field re-renders, but to get the entire parent component that the text field is in to re-render, I have to manually call the StateHasChanged() function? Since the validation function is a delegate, why does a call to StateHasChanged() update the parent, that seems like the call propagating up? – Jorden Q Aug 08 '23 at 19:26
  • 1
    It's confusing at first, but the delegate is just a pointer to the method in the parent. The delegate and thus the method is executed in the context of it's owner - the parent component. The delegate has no access to anything within the class that invokes it other than the objects supplied as arguments. – MrC aka Shaun Curtis Aug 09 '23 at 12:01
  • Okay, that makes a lot of sense, although I am confused on why if I don't run the function asynchronously, as in I remove the `await Task.Delay(2000)` from the function, everything updates without me having to call `StateHasChanged()`. Is there something I'm missing? Because the behviours you describe make sense for async, but then don't still exist if you run a synchronous function. Apologies for the trouble, I don't want to keep taking up your time, If you have links to articles or other SO questions I can look at those. – Jorden Q Aug 09 '23 at 15:52
  • When everything runs as synchronous code, there's no yielding back to the `Synchronisation Context`, so although the code adds render events [actually `RenderFragments`] to the Render Queue], the Renderer doesn't service that queue until all the synchronous code is run i.e. at the end. At this point all the state changes [in both the component and it's parent] have taken place and then component and parent renders with the latest state values. – MrC aka Shaun Curtis Aug 09 '23 at 20:32
  • Having gone though this I realise I need to update my published articles to make this clear. Articles: https://www.codeproject.com/Articles/5277618/A-Dive-into-Blazor-Components and https://www.codeproject.com/Articles/5364401/Building-Blazor-Base-Components – MrC aka Shaun Curtis Aug 09 '23 at 20:36