Similar to aspnet/Blazor#1242 but I can consistently reproduce and it happens on the simplest Component. No Layout involved. Happened on 0.6.0 and kept happening after I upgraded to 0.7.0.
It is a very strange behavior which points to some mishandling of asynchronous actions inside OnInitAsync.
The code below fails as expected. A System.NullReferenceException is shown in the browser console.
@page "/"
<div>
@foreach (var item in Items)
{
<div>@item</div>
}
</div>
@functions {
protected IEnumerable<string> Items { get; set; }
}
Result: Uncaught (in promise) Error: System.NullReferenceException: Object reference not set to an instance of an object.
@page "/"
<div>
@foreach (var item in Items)
{
<div>@item</div>
}
</div>
@functions {
protected override async Task OnInitAsync()
{
await Task.CompletedTask;
Items = new[] { "item1" };
}
protected IEnumerable<string> Items { get; set; }
}
Result: item1 is shown on the screen.
@page "/"
<div>
@foreach (var item in Items)
{
<div>@item</div>
}
</div>
@functions {
protected override async Task OnInitAsync()
{
await Task.Delay(1); // This line is the only difference
Items = new[] { "item1" };
}
protected IEnumerable<string> Items { get; set; }
}
Result: An exception is shown in the console (which I wasn't expecting), and from that point on the browser becomes unresponsive and I need to close the tab and re-run the Blazor application.
Uncaught (in promise) Error: System.NullReferenceException: Object reference not set to an instance of an object.
You have to understand that await in async method means "if the task is not completed temporarily suspend execution of this function and return to the caller (in this case Blazor runtime)". Blazor calls your OnInitAsync method and renders your page first time immediately after first suspension. Then it renders your page again when your method finishes. Run this code and watch the screen. Then comment first await, uncomment await Task.CompletedTask; and note the difference.
@page "/"
<div>
@if (Items == null)
{
<div>List not initialized yet.</div>
}
else
{
@foreach (var item in Items)
{
<div>@item</div>
}
}
</div>
```c#
@functions {
List
protected override async Task OnInitAsync()
{
await Task.Delay(1000);
//await Task.CompletedTask;
Items = new List<int>();
Items.Add(1);
await Task.Delay(1000);
Items.Add(2);
await Task.Delay(1000);
Items.Add(3);
}
}
```
try instantiate a new instance of Items first
@functions {
List<int> Items { get; set; }
protected override void OnInit()
{
Items = new List<int>();
}
protected override async Task OnInitAsync()
{
await Task.Delay(1000);
Items.Add(1);
await Task.Delay(1000);
Items.Add(2);
await Task.Delay(1000);
Items.Add(3);
}
}
@thewebchameleon your solution is based on some Blazor implementation detail and if you really want to create a new Listawait. You can also do this:
c#
List<int> Items { get; set; } = new List<int>();
Both solutions are in my opinion much better than a new function override.
The most important fact to remember is: each time Blazor calls your async method your page can be rendered two times and your rendering code have to be prepared for this. This is true not only in OnInitAsync but also in all other async methods, for example in button's onclick event.
Second important fact: your async method has to have Task return type not void. Otherwise you page will not be rendered second time.
Thank you all for your quick response. I was wondering if this behavior was by-design but it looked bad enough that I thought it wasn't. Finding aspnet/Blazor#1242 didn't help either.
Is there any chance you could include this explanation in the documentation. I went through a lot and couldn't find a single mention of this behavior, plus all the examples I could find are simplistic and don't attempt to load data asynchronously which is realistic and what I was trying to do:
Blazor calls your OnInitAsync method and renders your page first time immediately after first suspension. Then it renders your page again when your method finishes
Keep up the great work. Blazor is an amazing idea!
Most helpful comment
You have to understand that await in async method means "if the task is not completed temporarily suspend execution of this function and return to the caller (in this case Blazor runtime)". Blazor calls your OnInitAsync method and renders your page first time immediately after first suspension. Then it renders your page again when your method finishes. Run this code and watch the screen. Then comment first
await, uncommentawait Task.CompletedTask;and note the difference.```c# Items { get; set; }
@functions {
List
}
```