Reputation: 128
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.
Upvotes: 1
Views: 3010
Reputation: 30001
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 InvokeAsyn
c 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!
Upvotes: 0
Reputation: 273169
You have an async void
in async void SetBool(bool name)
. That explains the difference you see with Task.Run.
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.
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.
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";
}
Upvotes: 1
Reputation: 45586
The following code snippet describes how you should code:
<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;
}
}
<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.
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.
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.
Do not call the StateHasChanged method from the UI event handlers unnecessarily. It is called automatically.
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
Avoid using Task.Run
. I never use. I don't even remember why I shouldn't
use it. I just don't use it.
Upvotes: 4
Reputation: 2256
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);
}
Upvotes: 0