1

I recently asked a question regarding the difference between await Task.Run(StateHasChanged); and await InvokeAsync(StateHasChanged); in Blazor wasm here.

The conclusion was that await Task.Run(StateHasChanged); was incorrect and should be avoided; using it would produce the same results as await InvokeAsync(StateHasChanged); however would fall over when threads are available (The accepted answer explains in detail).

I've updated my codebase to use await InvokeAsync(StateHasChanged);, however I've discovered there is actually a difference in outcome between the two.

Here's a minimal reproduction of the issue in my application:

Parent

<h1>Parent: @title</h1>

<button class="btn btn-primary" @onclick="() => SetBool(true)">True</button>
<button class="btn btn-primary" @onclick="() => SetBool(false)">False</button>

<Child Bool="@Bool" @ref="Child"></Child>

@code {
    private bool Bool = false;
    private Child Child;
    private string title;

    private async void SetBool(bool name)
    {
        Bool = name;
        title = Bool ? "True" : "False";
        // NOTE: This will work as expected; Child will be updated with the Parent
        // await Task.Run(StateHasChanged);
        // NOTE: This will only update the Child the second time it is clicked
        await InvokeAsync(StateHasChanged);
        await Child.Trigger();
    }

}

Child

<h3>Child: @title</h3>

@code {
    [Parameter]
    public bool Bool { set; get; } = false;
    private string title;

    public async Task Trigger()
    {
        title = Bool ? "True" : "False";
        await InvokeAsync(StateHasChanged);
    }
}

Clicking either the True or False button in the parent should update the Bool value in both the parent and child. Note that the title variable is only used for a visual display of the Bool value.

Using await Task.Run(StateHasChanged); will cause both the parent and child's state to be updated at the same time. On the other hand await InvokeAsync(StateHasChanged); will update the parent, but not the child; it takes two clicks of the a button to get the respective value in the child component.

Is there an issue with how I'm passing this value to the child component?

Note that using await Task.Run(StateHasChanged); isn't an option; doing so means I can't test the component with bUnit.

The reproduction code is available here.

Sam Carswell
  • 129
  • 12

4 Answers4

4

The following code snippet describes how you should code:

Parent.razor

<h1>Parent: @title</h1>

<button class="btn btn-primary" @onclick="() => SetBool(true)">True</button>
<button class="btn btn-primary" @onclick="() => SetBool(false)">False</button>

<Child Bool="@Bool" @ref="Child"></Child>

@code {
    private bool Bool = false;
    private Child Child;
    private string title;

    protected override void OnInitialized()
    {
        title = Bool ? "True" : "False";
    }

    private async Task SetBool(bool name)
    {
        Bool = name;
        title = Bool ? "True" : "False";
        
        await Task.CompletedTask;
    }

}

Child.razor

<h3>Child: @title</h3>

@code {
    [Parameter]
    public bool Bool { set; get; } = false;
    private string title;

    private bool _Bool;

    public async Task Trigger()
    {
        title = _Bool ? "True" : "False";
        await InvokeAsync(StateHasChanged);
    }

    protected override async Task OnParametersSetAsync()
    {
        _Bool = Bool;
        await Trigger();
      
    }
}

Note: The issues you're facing is not because of using Task.Run(StateHasChanged); and await InvokeAsync(StateHasChanged);, though the result is different. You simply don't follow the Blazor Component Model rules.

  1. Do you realize that when you click on the SetBool button, the Parent component is re-rendered, as its state has changed. Consequently, the Child component is re-rendered with Bool == true, but you don't see that on the screen as the code of changing the title in the child component is placed in the Trigger method, which is only called from the Parent component. Your assumption was wrong.

  2. Do not modify or change the State of the Bool parameter property. Define a local variable to store the value of the parameter property, whose value you may manipulate as you wish. In other words, the component parameter properties should be defined as automatic properties... They are a DTO for the Blazor framework to pass values from a parent component to its children.

  3. Do not call the StateHasChanged method from the UI event handlers unnecessarily. It is called automatically.

  4. Do not use async void... use async Task. When you use async void, Blazor doesn't figure out when the an async method completes, and thus it does not call the StateHasChanged method

  5. Avoid using Task.Run. I never use. I don't even remember why I shouldn't use it. I just don't use it.

enet
  • 41,195
  • 5
  • 76
  • 113
  • Wouldn't `@key` just solve all of the problems? – Grizzlly Dec 02 '21 at 09:50
  • No, the `@key` can't solve any problem, as for instance, displaying `True` in the child component, when you click on the `SetBool(true)` button, if you do not design your child component to display this, when the Bool variable changes in the parent component. Please, read my answer, and try to understand what is the issue with the question code, which has nothing to do with how you call the StateHasChanged method; you actually do not call this method at all, with Task.Run or InvokeAsync. See my code copy and test. – enet Dec 02 '21 at 11:11
  • 1
    Not enough emphasis on the OnParametersSetAsync, seems like the magic OP is looking for is there, as new values to its parameter will trigger this, avoiding the need for the parent to trigger any or all children it might have. I like this. – Shuryno Dec 02 '21 at 14:11
  • 1
    Well, yeah, it is a good answer, and you got my upvote. For me, whenever I had issues similar to this, I was able to use `@key` and it worked. – Grizzlly Dec 02 '21 at 15:38
  • Thanks for the detailed explanation @enet. My only issue still is that Trigger is called by the user in the Parent component; not every time the values change. Is there a way to call Trigger from the parent and have the function use the expected value for _bool? – Sam Carswell Dec 07 '21 at 03:38
  • The Trigger method is not called from the parent component. It is triggered from the OnParametersSetAsync method because the value of the Bool parameter has changed as a result of clicking on the SetBool pair of buttons. "Is there a way to call Trigger from the parent and have the function use the expected value for _bool?" This is exactly what you do in your original code, right ? You can do that, but it is wrong. You may call a method in the child component to perform an action, such as opening a dialogue box. – enet Dec 07 '21 at 13:40
  • You can also assign a value to a local variable that enables the display of the dialogue, as for instance: `openDialogue = true;` But you should not modify the parameter Bool passed from the parent component. In other words, you should code as demonstrated in the answer code. – enet Dec 07 '21 at 13:40
1

You have an async void in async void SetBool(bool name). That explains the difference you see with Task.Run.

  1. You will (almost) never need async void in Blazor. Blazor supports awaitable eventhandlers. Use async Task SetBool(bool name) ... and the results should become deterministic.

  2. You don't need the InvokeAsync() here. Everything runs on the main thread.
    You will only need it with Blazor Server in code that is executed by Task.Run(). Or in an async void but that shouldn't happen.

  3. You don't need StateHasChanged() everywhere. Only use it to display intermediate results in a method. In the sample code you would kinda need it in Trigger but currently Trigger() itself is not needed. The Child should rerender when Bool changes without any help from you. The async void may have led you to believe otherwise.

You are trying too hard. The whole sample can be simplified to

Parent

<h1>Parent: @title</h1>

<button class="btn btn-primary" @onclick="() => SetBool(true)">True</button>
<button class="btn btn-primary" @onclick="() => SetBool(false)">False</button>

<Child Bool="@Bool" ></Child>

@code {
    private bool Bool = false;
   // private Child Child;
    private string title;

    private void SetBool(bool name)  // no async
    {
        Bool = name;   // Child will rerender if this is a change
        title = Bool ? "True" : "False";
    }  // auto StateHasChanged

}

Child

<h3>Child: @title</h3>

@code 
{
    [Parameter]
    public bool Bool { set; get; } = false;
    private string title => Bool ? "True" : "False";
}
H H
  • 263,252
  • 30
  • 330
  • 514
0

You aren't passing the new value into the Child component. This is one way you could make it work.

private async void SetBool(bool name)
{
    ...
                
    await InvokeAsync(StateHasChanged);
    await Child.Trigger(Bool); // pass the new value
}

public async Task Trigger(bool newValue)
{
    title = newValue ? "True" : "False";
    await InvokeAsync(StateHasChanged);
}
Steve Wong
  • 2,038
  • 16
  • 23
0
await Task.Run(StateHasChanged);

is a NONO. It says, run StateHasChanged on a threadpool thread, which is the exact opposite of reality - StateHasChanged must be run on the UI Thread.

It "works" in single threaded environments because Task.Run doesn't switch threads - there's only the UI thread. In Web Assembly that's the case at the moment. Run it in any multi-threaded environment - Blazor Server or bUnit testing - and it goes bang.

Blazor provides two InvokeAsync methods on ComponentBase to run methods on the UI Thread. One for an Action and one for a Func. Here's the Action version.

protected Task InvokeAsync(Action workItem)
    => _renderHandle.Dispatcher.InvokeAsync(workItem);

RenderHandle is a struct that the Renderer passes to the component when it calls Attach. Dispatcher is the Thread Dispatcher for the UI thread. InvokeAsync ensures whatever Action or Func it's passed, it gets run on the UI Thread. In our case StateHasChanged.

The answer to the behaviour differences in your code is answered by both Enet and Henk above. I would just re-iterate Enet's comment on async and void. My personal rule is: no async and void together in a Blazor Component event handler.

You will find a significant number of questions on this specific subject on here (Stack Overflow), and many answers by one of the three of us!

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • "The answer to the behaviour differences in your code is answered by both Enet and Henk above. You broke a golden rule - async and void on the same line in a Blazor Component event handler." [Politely] Not true. Please, read my answer. – enet Dec 02 '21 at 19:41
  • Copy and test the code by the asker... Now change `async void` to `async Task` Do you see any change ? I think not. There shouldn't be an issue here with using `async void`, and it is not the root of the issue, though it is good to remember not to use this pair, as under some circumstances such usage may prevent the Blazor framework to re-render a given component. Now run my code, and change `async Task SetBool` to `async void SetBool` It still works, right. YOUR quoted statement is wrong... – enet Dec 02 '21 at 20:13
  • "Just because it works this time doesn't mean it's good practice." Have I said that it is a good practice. Please, read my answer and the comment above. Do I use `async void SetBool` instead of `async Task SetBool` in my answer ? I'm just trying to emphasize that this is not the issue in this question. – enet Dec 02 '21 at 21:02
  • Just because it works in certain circumstances doesn't mean it's good practice. The asker will think this is good, use it elsewere, and Bang! Add a `Task.Delay ` or `Task.Yield` as line 1 and and see what happens. `Async void BlazorComponentEventHandler()` is a fundamentally flawed pattern. I'll stick with my Golden Rule and I recommend others do. – MrC aka Shaun Curtis Dec 02 '21 at 21:05
  • @enet. :-) I edited my comment a little - it was a bit wordy. My quote "async and void should never appear on the same line in a Blazor Component event handler" is good practice, and yes I did note you sticking to it! I didn't say any method written with this pattern wouldn't work! – MrC aka Shaun Curtis Dec 02 '21 at 21:09
  • "Just because it works in certain circumstances doesn't mean it's good practice." Again, why do you say that ? Why do you dwell on it ? Read my comments again. This is from my answer: Do not use async void... use async Task. When you use async void, Blazor doesn't figure out when the an async method completes, and thus it does not call the StateHasChanged method – enet Dec 02 '21 at 21:09