When and why do I need to call StateHasChanged or InvokeAsync(StateHasChanged)? #29318
Replies: 6 comments 1 reply
-
First, as said in the documentation,
When the In a comment recently created by @SteveSandersonMS in #16119, it explains what happens when you have multiple await methods and you are not using Pasted from the discussion When an asynchronous handler involves multiple asynchronous phases@page "/counter"
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private async Task IncrementCount()
{
currentCount++;
// Renders here automatically
await Task.Delay(1000);
currentCount++;
StateHasChanged();
await Task.Delay(1000);
currentCount++;
StateHasChanged();
await Task.Delay(1000);
currentCount++;
// Renders here automatically
}
} The way that tasks are defined in .NET means that a receiver of the End pasted from the discussion More about this information was also documented in #18815 written by @Webreaper.
The difference between using Pasted from the discussion When receiving a call from something external to the Blazor rendering and event handling system
For example, if you have a custom data store that raises C# events, then those C# events are unknown to Blazor. As another example, consider the following "counter" component that uses @page "/counter"
@using System.Timers
@implements IDisposable
<p>Current count: @currentCount</p>
@code {
private int currentCount = 0;
private Timer timer = new Timer(1000);
protected override void OnInitialized()
{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
void IDisposable.Dispose() => timer.Dispose();
} In this example, Similarly, because this callback is invoked outside Blazor's synchronization context, it's necessary to wrap the logic in End pasted from the discussion For a more in depth explanation you can check this article which explains why it's necessary to use References: |
Beta Was this translation helpful? Give feedback.
-
Hi @emimontesdeoca, thank you for this beautiful post. It really helps me to understand this topic much easier. However, I'm still trying to figure out what I'm doing wrong. I have three functions. Which I paste here in right order. The first one is located within my razor component public async Task SearchAsync()
{
loading = true;
cancellationToken.Cancel();
searchResults.Clear();
cancellationToken = new CancellationTokenSource();
await foreach(var article in lieferantenService.SucheLieferantenArtikelAsync(Filter, cancellationToken.Token))
{
if(article != null)
{
searchResults.Add(article);
StateHasChanged();
}
}
loading = false;
} the field loading is used to lock a button on the UI. @if (loading)
{
<button class="btn btn-primary" disabled>Loading...</button>
}
else
{
<button class="btn btn-primary">Search</button>
} As you can see it is calling the function public async IAsyncEnumerable<Lieferantenartikel?> SucheLieferantenArtikelAsync(LieferantenArtikelFilter filter, [EnumeratorCancellation] CancellationToken cancellationToken)
{
string sql = filter.ToSqlQuery(_fbController);
_fbController.AddParameter("@ARLI_N_LIEFNR", filter.Lieferantennummer);
var data = await _fbController.SelectDataAsync(sql);
foreach (DataRow row in data.Rows)
{
if (!cancellationToken.IsCancellationRequested)
{
string articleNumber = row.Field<string>("ARLI_A_ARTIKELNR");
if (!String.IsNullOrWhiteSpace(articleNumber))
{
yield return await Lieferantenartikel.GetLieferantenartikelAsync(articleNumber, filter.Lieferantennummer, _fbController);
}
}
}
} This one just fecthes the IDs of my needed items from the database. For every found ID it calls the function There is also the function public async Task<DataTable> SelectDataAsync(string selectCommand, string tableName = "")
{
DataTable data = new DataTable(tableName);
Command.CommandType = CommandType.Text;
Command.CommandText = selectCommand;
var reader = await Command.ExecuteReaderAsync();
data.Load(reader);
ClearParameters();
return data;
} There are also async methods to just fetch a row instead of a table, which are being used within the All calls are of type It only does when I change my first method into this piece of code: public async Task SearchAsync()
{
loading = true;
await Task.Run(async () =>
{
cancellationToken.Cancel();
searchResults.Clear();
cancellationToken = new CancellationTokenSource();
await foreach(var article in lieferantenService.SucheLieferantenArtikelAsync(Filter, cancellationToken.Token))
{
if(article != null)
{
searchResults.Add(article);
await InvokeAsync(StateHasChanged);
}
}
});
loading = false;
} It works but this not a good solution because the methods are working with a database and you shouldn't put IO heavy work into Task.Run. But why does my UI not update without this change? |
Beta Was this translation helpful? Give feedback.
-
Hey @MarvinKlein1508, nice to know that the answer was helpful, I actually learned a lot while looking for more information 😁 I looked at your code a created a simpler version with some timeouts that contains the keys of you problem, I changed the functions' name to something more undestanable: Also added a bit of style so it's easier to see the updated results while they are yielded. @page "/"
@using System.Threading;
@using System.Runtime.CompilerServices;
<div class="container text-center mt-4 mb-4">
@if (loading)
{
<button class="btn btn-primary" disabled>Loading...</button>
}
else
{
<button class="btn btn-primary" @onclick="SearchAsync">Search</button>
}
<br />
<br />
@if (searchResults != null)
{
@foreach (var item in searchResults)
{
<p>@item</p>
}
}
</div>
@code {
public bool loading { get; set; } = false;
public CancellationTokenSource cancellationToken { get; set; } = new CancellationTokenSource();
public List<string> searchResults { get; set; } = new List<string>();
public async Task SearchAsync()
{
loading = true;
cancellationToken.Cancel();
cancellationToken = new CancellationTokenSource();
await foreach (var article in GetDataFirst(cancellationToken.Token))
{
if (article != null)
{
searchResults.Add(article);
StateHasChanged();
}
}
loading = false;
}
public async IAsyncEnumerable<string?> GetDataFirst([EnumeratorCancellation] CancellationToken cancellationToken)
{
foreach (var value in SelectDataAsync())
{
if (!cancellationToken.IsCancellationRequested)
{
if (!String.IsNullOrWhiteSpace(value))
{
yield return await DoSomethingYieldable(value);
}
}
}
}
public async Task<List<string>> SelectDataAsync()
{
var result = new List<string>();
Random rnd = new Random();
for (int i = 0; i < 10; i++)
{
result.Add($"Item {i} fetched at {DateTime.UtcNow.ToString("hh:mm:ss.fff tt")}");
}
return result;
}
public async Task<string> DoSomethingYieldable(string str)
{
Random rnd = new Random();
var mult = rnd.Next(0, 5);
await Task.Delay(mult * 500);
return str + $" - yielded at {DateTime.UtcNow.ToString("hh:mm:ss.fff tt")}";
}
} This will result on the values being updated as soon as they reach the You can click the search button again Let's see if it helps you to understand how it works so you can make it your code work 😉 |
Beta Was this translation helpful? Give feedback.
-
thanks for your try but I do not see any difference to my code. Except you are not awaiting the results in GetDataFirst. Also I have another await method before the foreach loop in this methods. 🤔 |
Beta Was this translation helpful? Give feedback.
-
My code is more like this: public async IAsyncEnumerable<string?> GetDataFirst([EnumeratorCancellation] CancellationToken cancellationToken)
{
// Something is happening here that becomes awaited
await Task.Delay(3000);
await foreach (var value in SelectDataAsync())
{
if (!cancellationToken.IsCancellationRequested)
{
if (!String.IsNullOrWhiteSpace(value))
{
yield return await DoSomethingYieldable(value);
}
}
}
} Why did you declar |
Beta Was this translation helpful? Give feedback.
-
After the ids are fetched, it loops them and calls my second method GetLieferantenartikelAsync() which is also called with an await statement. This method is loading the data for the ID from the database with await The result gets then yielded back to |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Hey everyone,
I try to figure out when I should call
StateHasChanged();
and awaitInvokeAsync(StateHasChanged);
.Consider the following code:
Why does the UI get not refreshed automatically within the method
SubmitAsyncFailure()
? I thought async and await should keep the UI thread alive so it can update. Why does the UI get refreshed before the firstawait
but not afterwards, except for the end of the method?What is the main difference between
StateHasChanged()
and awaitInvokeAsync(StateHasChanged);
? In which scenario should I call these ? It seems like both of them are working.I uploaded the code here: https://github.com/MarvinKlein1508/AsyncAwaitDemo
Thank you for your help! :)
Beta Was this translation helpful? Give feedback.
All reactions