Tarta
Tarta

Reputation: 2063

Blazor rendering after computation or call

making my first steps in Blazor!

I have my page Test.razor with a simple grid:

<table class="table">
    <thead>
        <tr>Message</tr>
    </thead>
    <tbody>
        @foreach (var exception in Exceptions)
        {
            <tr>exception.Message</tr>
        }
    </tbody>
</table>

and my logic:

public partial class Test

{
    public List<TestEventModel> Exceptions { get; set; }


    protected override async void OnInitialized()
    {
        var exceptionsResponse = (await http.GetAsync("TestController")).Content.ReadAsStringAsync();
        Exceptions = JsonConvert.DeserializeObject<List<TestEventModel>>(await exceptionsResponse);

    }
}

Problem: Unhandled exception rendering component: Object reference not set to an instance of an object.

Exception occurring on the line :

@foreach (var exception in Exceptions)

But according to the lifecycle it should start rendering only after the execution of OnInitialized:

enter image description here

If indeed I initialize the list in a constructor for example, problem is not there, but of course the list will be empty and not showing the result of my http call.

Upvotes: 0

Views: 9270

Answers (4)

Connor Low
Connor Low

Reputation: 7226

Why isn't Exceptions initialized?

async OnInitialized (and OnInitializedAsync; you should use this instead of OnInitialized if you are doing async work like HTTP requests!) begins before render, but await unblocks the execution chain allowing the page to render before an asset (i.e. Exceptions from the question) has loaded.

Example

I created a test page that logs the chronological order of each lifecycle event. In particular, note the OnInitialized method:

protected override async void OnInitialized()
{
    Record("-> OnInitialized");
    // Note: I am not advocating you use Task.Run... 
    //   this is to simulate an asynchronous call to an external source!
    Data = await Task.Run(() => new List<string> { "Hello there" });
    StateHasChanged(); // not always necessary... see link below
    base.OnInitialized();
    Record("<- OnInitialized");
}

I got this output:
enter image description here

But if we change OnInitialized so that it does not contain any awaits:

protected override async void OnInitialized()
{
    Record("-> OnInitialized");
    base.OnInitialized();
    Record("<- OnInitialized");
}

Console, no await

As you can see, await will unblock the process that called OnInitialized allowing for the next steps in the life-cycle method to be called. In your case, you await your Exceptions to set it, but this allows the component to continue down the lifecycle, rendering before the awaited task completes, assigning Exceptions.

Blazor's own default app demonstrates knowledge of this and how to address it:

@if (forecasts == null) @* <-- forecasts is null, initially *@
{
    <p><em>Loading...</em></p>
}
else
{
    @* render forecasts omitted *@
}

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await 
            Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
    }

    // WeatherForecast implementation omitted
}

Why do I need StateHasChanged()?

See When to call StateHasChanged for an explanation.

...it might make sense to call StateHasChanged in the cases described in the following sections of this article:

  • An asynchronous handler involves multiple asynchronous phases
  • Receiving a call from something external to the Blazor rendering and event handling system
  • To render component outside the subtree that is rerendered by a particular event

Original Answer

Removed because I looked into it and decided it wasn't really correct. See the revision history.

Upvotes: 4

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30310

As your looking for Why?

You call:

protected override async void OnInitialized()
{
        var exceptionsResponse = (await http.GetAsync("TestController")).Content.ReadAsStringAsync();
}

OnInitialized is the synchronous version of OnInitializedAsync - and should only run synchronous code. You've turned it into a fire-and-forget async void event handler.

It runs synchronously until it hits some real yielding code - http.GetAsync. At which point it yields back to the component SetParametersAsync. This runs to completion. By the time Exceptions =.. gets run SetParametersAsync has completed, the component has completed rendering and errored because Exceptions is still null.

You need to run your code in OnInitializedAsync(). This returns a task to SetParametersAsync which it can wait on, and only continues when OnInitializedAsync completes by returning a completed Task.

    protected override async Task OnInitializedAsync()
    {
        var exceptionsResponse = (await http.GetAsync("TestController")).Content.ReadAsStringAsync();
        Exceptions = JsonConvert.DeserializeObject<List<TestEventModel>>(await exceptionsResponse);
    }

You still need to check if Exceptions is null in the display code and display a Loading message because the initial component render may occur before OnInitializedAsync completes. A second render event which occurs once SetParametersAsync has completed will render the component correctly.

Something like:

<table class="table">
    <thead>
        <tr>Message</tr>
    </thead>
    <tbody>
@if (Exceptions != null)
{
        @foreach (var exception in Exceptions)
        {
            <tr><td>exception.Message</td></tr>
        }
}
else 
{
            <tr><td>Loading....</td></tr>
}
    </tbody>
</table>

When you call StateHasChanged in your code you need to ask why? There are valid reasons, but often it's to recover from a earlier coding mistake.

Upvotes: 0

Henk Holterman
Henk Holterman

Reputation: 273601

Where you see ...await task... in the picture a Render action can/will execute.

If you want to confirm this experimentally you can use this:

@foreach (var item in Items)  
{

}
public List<string> Items { get; set; } = new();

protected override async Task OnInitializedAsync()
{
    Console.WriteLine("OnInitializedAsync start");
    await Task.Delay(100);
    Console.WriteLine("OnInitializedAsync done");
    Items = new List<string> { "aa", "bb", "cc", };        
}

protected override Task OnAfterRenderAsync(bool firstRender)
{
    Console.WriteLine($"OnAfterRenderAsync {firstRender}");
    return base.OnAfterRenderAsync(firstRender);
}

This will print "OnInitializedAsync done" after the first rendering. Note that this is before Items is assigned.

When you remove the = new(); part you get your original NRE error again. When you then remove the Task.Delay() as well it will run w/o an error. That follows the non async path you first assumed: "according to the lifecycle it should start rendering only after the execution of OnInitialized".

It is all about the await, it has nothing to do with server prerendering.

Upvotes: 1

Bennyboy1973
Bennyboy1973

Reputation: 4246

Blazor renders in multiple passes, and what you're experiencing is absolutely normal. My understanding is that this is so most of your page will load even if it's waiting for async data to get filled in (complex database search etc.)

There are two ways I've used to avoid throwing a null reference on async init:

1. Check for null. Don't worry, your component will initialize everything and render properly on the second pass.

@if (Exceptions is not null){
    foreach (var exception in Exceptions)
        {
            <tr>exception.Message</tr>
        }
}

2. Initialize your List in the declaration. This empty List will be available on the first pass:

@code{
    public List<TestEventModel> Exceptions { get; set; } = new List<TestEventModel>();
}

Upvotes: 1

Related Questions