7

In the diagram below from this page it shows that when an incomplete task is returned by the call to OnInitializedAsync it will await the task and then render the component.

However it seems that what actual happens when an incomplete task is returned is renders the component immediately, and then renders it again once the incomplete task completes.

enter image description here

An example later in the page seems to confirm this. If the component was not rendered immediately after the call to OnInitializedAsync, and instead only rendered for the first time after the Task returned had been completed you would never see the "Loading..." message.

OnParametersSetAsync behavior appears the same. It renders once immediately when an incomplete task is returned, and then again once that task has completed.

Am I misunderstanding the render lifecycle, or is this an error in the documentation?

Thanks

@page "/fetchdata"
@using BlazorSample.Data
@inject WeatherForecastService ForecastService

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <!-- forecast data in table element content -->
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
    }
}
innominate227
  • 11,109
  • 1
  • 17
  • 20
  • 1
    Your component is rendered twice. The first time when `ForecastService.GetForecastAsync` is awaited. The second time when `ForecastService.GetForecastAsync` returns. when `ForecastService.GetForecastAsync` is awaited, control is yielded to the calling code, and the control is rendered for the first time, displaying the message "Loading..." when `ForecastService.GetForecastAsync` returns, the method StateHasChanged is implicitly called to re-render the component for the second time. See: https://github.com/dotnet/aspnetcore/blob/main/src/Components/Components/src/ComponentBase.cs – enet Jul 20 '22 at 04:29

2 Answers2

7

Short summary

  • Conceptually, Blazor adds two 'free' StateHasChanged calls, one before and one after each lifecycle event and UI event.
  • StateHasChanged only requests an html update, it does not perform one.
  • An update request can only be fulfilled after the event
    or when the main Thread is released by an await
    • not but every await will release the Thread.

So, when you want to make sure the screen gets updated, use

StateHasChanged();
await Task.Delay(1);  // Task.Yield() does not always work

Old answer

when an incomplete task is returned it renders the component immediately, and then renders it again once the incomplete task completes.

Yes, that is a possible sequence.

The flowchart shows the steps for showing a component. What is not so clear from the picture is that the actual rendering is not part of this flow, it happens async on the synchronizationcontext. It can happen when your code awaits something.

So we have this basis non-async sequence:

  • Oninitialzed[Async]

  • OnParametersSet[Async]

  • Render

  • OnAfterRender[Async]

But when there is something async in this code-path then there can be one extra Render during the await. More Renders are possible when you call StateHasChanged during this flow.

H H
  • 263,252
  • 30
  • 330
  • 514
  • `it happens async on the synchronizationcontext` Blazor WebAssembly is single-threaded. – enet Jul 20 '22 at 09:10
  • It seems to me that you fail to see the difference between asynchronous programming and multithreading. – enet Jul 20 '22 at 11:13
  • 1
    @HenkHolterman - [very polite] It's very difficult to get the wording right. "released by an await" is maybe misleading. I know you qualify it later. You can `await` a synchronous block of code wrapped in a Task. It's "yielding" that's important. – MrC aka Shaun Curtis Jul 20 '22 at 12:26
  • @MrCakaShaunCurtis Yes, but "yielding" is synonymous with "releasing the thread", I don't see a contradiction there. But you're right, you could read that first part with await wrong. That does happen all the time. I leave it as is, I think the "not every await ..." part is stronger. – H H Jul 20 '22 at 12:59
  • It's all in the terminology used. Perhaps we need a new term for task yielding as opposed to thread yielding! – MrC aka Shaun Curtis Jul 20 '22 at 14:57
  • Succinct and on the money. Upvoted. – MrC aka Shaun Curtis Jul 20 '22 at 19:59
  • @enet: Yes, one of has no clue whatsoever. – H H Aug 18 '22 at 21:11
4

To fully answer your question we need to delve into the ComponentBase code.

Your code is running in the async world where code blocks can yield and give control back to the caller - your "incomplete task is returned".

SetParametersAsync is called by the Renderer when the component first renders and then when any parameters have changed.

public virtual Task SetParametersAsync(ParameterView parameters)
{
    parameters.SetParameterProperties(this);
    if (!_initialized)
    {
        _initialized = true;
        return RunInitAndSetParametersAsync();
    }
    else
        return CallOnParametersSetAsync();
}

RunInitAndSetParametersAsync is responsible for initialization. I've left the MS coders' comments in which explains the StateHasChanged calls.

private async Task RunInitAndSetParametersAsync()
{
    OnInitialized();
    var task = OnInitializedAsync();

    if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled)
    {
        // Call state has changed here so that we render after the sync part of OnInitAsync has run
        // and wait for it to finish before we continue. If no async work has been done yet, we want
        // to defer calling StateHasChanged up until the first bit of async code happens or until
        // the end. Additionally, we want to avoid calling StateHasChanged if no
        // async work is to be performed.
        StateHasChanged();
        try
        {
            await task;
        }
        catch // avoiding exception filters for AOT runtime support
        {
            if (!task.IsCanceled)
                throw;
        }
        // Don't call StateHasChanged here. CallOnParametersSetAsync should handle that for us.
    }
    await CallOnParametersSetAsync();
}

CallOnParametersSetAsync is called on every Parameter change.

private Task CallOnParametersSetAsync()
{
    OnParametersSet();
    var task = OnParametersSetAsync();
    // If no async work is to be performed, i.e. the task has already ran to completion
    // or was canceled by the time we got to inspect it, avoid going async and re-invoking
    // StateHasChanged at the culmination of the async work.
    var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
        task.Status != TaskStatus.Canceled;

    // We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and
    // the synchronous part of OnParametersSetAsync has run.
    StateHasChanged();

    return shouldAwaitTask ?
        CallStateHasChangedOnAsyncCompletion(task) :
        Task.CompletedTask;
}

In the diagram substitute "Render" for StateHasChanged in the code above.

The diagram uses the work "Render", which is a bit misleading. It implies that the UI re-renders, when what actually happens is a render fragment (a block of code that builds the UI markup for the component) is queued on the Renderer's render queue. It should say "Request Render" or something similar.

If the component code that triggers a render event, or calls StateHasChanged, is all synchronous code, then the Renderer only gets thread time when the code completes. Code blocks need to "Yield" for the Renderer to get thread time during the process.

It's also important to understand that not all Task based methods yield. Many are just synchronous code in a Task wrapper.

So, if code in OnInitializedAsync or OnParametersSetAsync yields there's a render event on the first yield and then on completion.

A common practice to "yield" in a block of synchronous code is to add this line of code where you want the Renderer to render.

await Task.Delay(1);

You can see ComponentBase here - https://github.com/dotnet/aspnetcore/blob/main/src/Components/Components/src/ComponentBase.cs

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31