Search code examples
.netblazor.net-5

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.


Solution

  • 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.