简体   繁体   中英

StateHasChanged() re-render the component one time out of two

I'm making a Blazor Server Side project and I wanted to make a button that desactivate after a click, but without using the disabled attribute of <button> . The code is pretty simple:

@functions {

    LogInForm logInForm = new LogInForm();
    bool IsDisabled;
    SignInResult result;

    protected override void OnInitialized()
    {
        IsDisabled = false;
    }

    async Task TryLogIn()
    {
        IsDisabled = true;
        StateHasChanged();
        result =  await _LogInService.TryLogIn(logInForm);
        Console.WriteLine("Logging status : " + (result.Succeeded ? "Sucess" : "Failure"));
        IsDisabled = false;
        StateHasChanged();
    }

}

For odd reasons, the first StateHasChanged isn't triggering but the second does re-render the page. It can be quite easily testes by going in Debug mode and entering into the StateHasChanged() method. On second time call it does stop on the HTML code after going into the method but not the first time.

Why so?

NB: I'm not looking for any workaround using Task.Delay or Task.Run(...) only, as it exist a race condition between those threads and the UI refresh thread, and so it is not a reliable solution. I'm looking for answers on the StateHasChanged() behaviour or a workaround by using events like PropertyChanged or EventCallback and putting the button as a child component.

Edit: After some testing, it seems that StateHasChanged() only triggers the re-render of the component after an await operation on a Task . It can be easily tested by putting in commentary the result = await _LogInService.TryLogIn(logInForm); line or changing IsDisabled =... to await new Task.Run(() => { IsDisabled =...}) . I have some workaround now, but I still wonder why this is a feature. Shouldn't StateHasChanged() re-render after any operations? Or it consider that only async operations (so mostly server-calls) can change something in the UI?

The Blazor team is about to publish documentation about how StateHasChanged() works.

You can track it here here: https://github.com/aspnet/AspNetCore/issues/14591

For the time being, I think this explanation taken from a github comment is an excellent explanation:

Adding a call to StateHasChanged simply queues the component to be rendered. The renderer decides when the renders happen.

This can be triggered by 4 circumstances:

  • Initial render where the bootstrap process triggers the initial render of the root component and all its children.
  • An event, in which the component that handles the event automatically triggers a new render after the event, and potentially its children if it renders new children or change their parameters.
  • As a result of calling StateHasChanged from an InvokeAsync call (marshalling back into the UI thread, essentially)
  • As a result of the parent component changing the parameters for the child component, which happens as part of the diffing process when the renderer calls SetParametersAsync on the child component.

To be very clear, calling StateHasChanged only queues a Render for the component or "marks it as dirty".

It's the renderer the one that decides when and how to produce the renders. BuildRenderTree does not result in new rendered output, only in a new definition of the "V-DOM" for the component at the time it's being called.

Normally, a component gets rendered once per render batch (which is a collection of components that are rendered/diffed together and sent to the UI for update). There are only two situations in which a component renders more than once in a batch:

  • You have a component that directly implements IComponent and calls RenderHandle.Render
  • You have a circular dependency between a child and a parent component that might cause a parent to re-render as part of a children invoking some callback parameter from the parent as part of its initialization

Source: https://github.com/aspnet/AspNetCore/issues/15175#issuecomment-544890549

The following is the flow of execution to describe how re-rendering occurs:

  1. Parent component calls StateHasChanged
  2. A new render tree is produced
  3. The diff between the old tree and the new tree is happening
  4. The values being passed to your child component are considered different to the ones it currently hold.
  5. SetParameters is being called on the child component to update them with the values the parent passed to it.

Now, when you call StateHasChanged after assigning a value to the local variable IsDisabled, does not really change the state of the component, and there is no reason why calling StateHasChanged will yield a re-rendering. When you call StateHasChanged, it simply queues a render request for that component. But there is no reason for re-rendering...

Or it consider that only async operations (so mostly server-calls) can change something in the UI?

It has nothing to do whether the type of the operation is async or not, except of the OnInitializedAsync method, in which case, the StateHasChanged method is automatically called when the OnInitializedAsync method completes to re-render the UI once again, with the new data retrieved by the async calls perform in the OnInitializedAsync method.

Update:

What you want can be done in various ways, the simplest of which is demonstrated here:

   <input type="button" value="Click me now"  disabled="@IsDisabled" @onclick="TryLogIn" />


@code{ 

    bool IsDisabled;

    protected override void OnInitialized()
    {
        IsDisabled = false;
    }

    async Task TryLogIn()
    {
        IsDisabled = true;

        // Do some async work here...
        // Note: Replace your async method with Task.Delay 
        await Task.Delay(5000);

        IsDisabled = false;

    }

}

This should work... Note: The only way you can disable your button control is by using the disabled property

No need to call the StateHasChanged method. It is automatically called by the code the compiler insert into your source code when it (the compiler) creates an EventCallback 'delegate' for your component.

The StateHasChanged method is automatically called after a UI event is triggered.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM