I'm having a strange issue with a Blazor page that is calling JavaScript within the OnAfterRenderAsync override. If within the OnInitializedAsync I include an awaited method I get the error below:
blazor.server.js:1 [2023-09-14T15:14:51.292Z] Error: Microsoft.JSInterop.JSException: Cannot read properties of null (reading 'value')
However if I called a non-async version of the same method it works fine. To confirm I have check that I've used 'await all the way'.
I've been able to repeat this by using await Task.Delay(1000)
and created a simple working example below. If you wanted to try this, just copy it over the FetchData.razor
page from the default templated solution and try it with and without the await Task.Delay(1000)
line.
The example include some checks to ensure that both OnInitializedAsync and OnAfterRenderAsync have completed before it tries to run the JavaScript. I've got another example with this removed but I don't want to confuse things by posting to many similar but different examples. I'm also writing out to the console when certain things happen.
Any ideas or pointers would be great please.
@page "/fetchdata"
@inject IJSRuntime JS
@LogSteps
<br />
@if (tmpExample == null)
{
<p><em>Loading...</em></p>
}
else
{
<EditForm Model="@tmpExample">
<InputNumber id="IntMyProperty" @bind-Value="tmpExample.IntMyProperty" />
<InputText id="StringProperty" @bind-Value="tmpExample.StringProperty" />
</EditForm>
}
@code {
private bool Initialised = false;
private bool Rendered = false;
protected override async Task OnInitializedAsync()
{
LogStep("OnInitializedAsync-Start;");
await Task.Delay(1000); //comment this out and it will work!
tmpExample = new()
{
IntMyProperty = 1,
StringProperty = "hello"
};
LogStep("OnInitializedAsync-End;");
Initialised = true;
await InvokeJavaScript();
}
public string LogSteps = "";
void LogStep(string step)
{
LogSteps += step;
Console.WriteLine(step);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
LogStep("OnAfterRenderAsync-FirstRender;");
Rendered = true;
await InvokeJavaScript();
}
else
{
LogStep("OnAfterRenderAsync-OtherRender;");
}
}
internal Example tmpExample;
internal class Example
{
public int IntMyProperty { get; set; }
public string StringProperty { get; set; }
}
async Task InvokeJavaScript()
{
LogStep("InvokeJavaScript-1-Initialised=" + Initialised.ToString() + "; Rendered=" + Rendered.ToString() + ";");
if (Initialised && Rendered)
{
LogStep("InvokeJavaScript-StartingRun;");
await JS.InvokeVoidAsync("eval", "alert(document.getElementById('StringProperty').value)");
LogStep("InvokeJavaScript-FinishedRun;");
}
}
}
The problem is highlighted by HH in his answer. OnAfterRender{Async}` is a UI event, it's not an intrinsic part of the render process. As such there's no guarantee when it will happen.
If you need to do async await/yielding operations that must execute and complete before a render, then you need to implement them in SetParametersAsync
.
You do need to be careful how you implement an override of SetParametersAsync
. Follow this basic pattern:
public override async Task SetParametersAsync(ParameterView parameters)
{
// Set the parameters in the first line
parameters.SetParameterProperties(this);
// run your async code
// Call the base with an empty ParameterView. You have already set the values
await base.SetParametersAsync(ParameterView.Empty);
}
Your code would look something like this:
private bool _isInitialized;
public override async Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
if (!_isInitialized)
{
await Task.Delay(1000); //comment this out and it will work!
tmpExample = new()
{
IntMyProperty = 1,
StringProperty = "hello"
};
}
_isInitialized = true;
await base.SetParametersAsync(ParameterView.Empty);
}