In Blazor, why does invoking NavigationManager.NavigateTo(string) sometimes result in an extra invocation of OnParametersSetAsync call with old values?
I have a page that reacts to a click with a CallbackEvent, the parent calls NavigationManager.NavigateTo to set up a new URL, which causes the Parameters of the parent to update, then the component responds to the new value, via OnParametersSetAsync. But before it does, there also was an invocation of OnParametersSetAsync with the old values--apparently from before they changed. It seems that often my second invocation finishes before the first (bad) invocation, and thus the first invocation finishes last, and I am left with a bad result.
So, is this first seemingly spurious and incorrect invocation call to OnParametersSetAsync just happening because a property may have changed when the handler of the event awaits? And how do I determine that it is a spurious call?
Here's an example that triggered it:
@page "/demo"
@page "/demo/{SelectedOrderId:int}"
@using Microsoft.Extensions.Configuration
@inject IHttpClientFactory clientFactory
@inject IToastService toastService
@inject IConfiguration Configuration
@inject NavigationManager navigationManager
<div class="mt-4 container-fluid">
<div class="row">
<div class="col-12 col-md-8 order-md-2">
Order @OrderDetail?.OrderId @OrderDetail?.ProductCode
</div>
<div class="col-12 col-md-4 order-md-1">
@foreach (var order in Orders)
{
<button @onclick="async () => await OnSelectedOrderIdChanged(order.OrderId)">@order.OrderId</button> }
</div>
</div>
</div>
@code {
[CascadingParameter]
public AppState State { get; set; } = null!;
public int? CustomerId { get; set; } = 8010;
[Parameter]
public int? SelectedOrderId { get; set; } = null;
private List<Order> Orders { get; set; } = new List<Order>();
private OrderDetail? OrderDetail { get; set; } = null;
protected override async Task OnInitializedAsync()
{
await LoadOrdersList();
}
protected override async Task OnParametersSetAsync()
{
Console.WriteLine($"params changed to order {SelectedOrderId}");
await LoadOrderDetail();
Console.WriteLine($"done loading order {SelectedOrderId}");
}
private async Task LoadOrdersList()
{
string serviceEndpoint = Configuration["MyServiceUrl"];
string url = $"{serviceEndpoint}/orders?customerId={CustomerId}";
Orders = await clientFactory.CreateClient().GetFromJsonAsync<List<Order>>(url);
}
private async Task LoadOrderDetail()
{
string serviceEndpoint = Configuration["MyServiceUrl"];
string url = $"{serviceEndpoint}/orders/{SelectedOrderId}";
OrderDetail = await clientFactory.CreateClient().GetFromJsonAsync<OrderDetail>(url);
}
private async Task OnSelectedOrderIdChanged(int? newOrderId)
{
SelectedOrderId = newOrderId;
await Task.Yield(); // could be anything
navigationManager.NavigateTo($"/demo/{newOrderId}");
}
}
When I run this and then click on one of the button for order 48026528, I get in the console:
params changed to order 48026500
params changed to order 48026528
done loading order 48026528
And the display in the page shows order 48026500! Presumably the UI update that should result happened early. So how do I determine that the invocation for 48026500 is bogus (or that it doesn't represent a real change to the parameters)?
I tried tracking the previous value and comparing it to the current value, but that doesn't help because the "new" value is processed before the old value--in other words, LoadOrderDetail() above runs twice, and they race, and often LoadOrderDetail with the new value runs first, then LoadOrderDetail with the old value completes and overwrites the new data. So how do I avoid calling LoadOrderDetail on these spurious events?