I have a file drop zone implemented in one of the pages of my blazor application, which is handled using the javascript runtime interface. In order to avoid memory leaks, I have a javascript method, which removes all the event listeners. This is called from the dispose function as follows:
public ValueTask DisposeAsync()
{
ViewModel.PropertyChanged -= OnPropertyChangedHandler;
GC.SuppressFinalize(this);
return JSRuntime.InvokeVoidAsync("deInitializeDropZone");
}
This works, however if I reload the page in my browser (F5, or reload button), I get the following exceptions:
fail: Microsoft.AspNetCore.Server.Kestrel[13]
Connection id "0HMI59N5RRGP7", Request id "0HMI59N5RRGP7:0000000E": An unhandled exception was thrown by the application.
System.InvalidOperationException: JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered. When prerendering is enabled, JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle method.
at Microsoft.AspNetCore.Components.Server.Circuits.RemoteJSRuntime.BeginInvokeJS(Int64 asyncHandle, String identifier, String argsJson, JSCallResultType resultType, Int64 targetInstanceId)
at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, CancellationToken cancellationToken, Object[] args)
at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, Object[] args)
at Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(IJSRuntime jsRuntime, String identifier, Object[] args)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.<>c__DisplayClass69_0.<<Dispose>g__HandleAsyncExceptions|1>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Components.Rendering.HtmlRenderer.HandleException(Exception exception)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.<>c__DisplayClass69_0.<Dispose>g__NotifyExceptions|2(List`1 exceptions)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.<>c__DisplayClass69_0.<<Dispose>g__HandleAsyncExceptions|1>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Components.RenderTree.Renderer.DisposeAsync()
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.<DisposeAsync>g__Await|22_0(Int32 i, ValueTask vt, List`1 toDispose)
at Microsoft.AspNetCore.Http.Features.RequestServicesFeature.<DisposeAsync>g__Awaited|9_0(RequestServicesFeature servicesFeature, ValueTask vt)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.<FireOnCompleted>g__ProcessEvents|227_0(HttpProtocol protocol, Stack`1 events)
warn: Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer[100]
Unhandled exception rendering component: JavaScript interop calls cannot be issued at this time. This is because the circuit has disconnected and is being disposed.
Microsoft.JSInterop.JSDisconnectedException: JavaScript interop calls cannot be issued at this time. This is because the circuit has disconnected and is being disposed.
at Microsoft.AspNetCore.Components.Server.Circuits.RemoteJSRuntime.BeginInvokeJS(Int64 asyncHandle, String identifier, String argsJson, JSCallResultType resultType, Int64 targetInstanceId)
at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, CancellationToken cancellationToken, Object[] args)
at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, Object[] args)
at Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(IJSRuntime jsRuntime, String identifier, Object[] args)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.<>c__DisplayClass69_0.<<Dispose>g__HandleAsyncExceptions|1>d.MoveNext()
fail: Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111]
Unhandled exception in circuit 'blLlOrw1UtfEHUoPBbO_N3peh7u3Or5Uk51p5RbR5xA'.
Microsoft.JSInterop.JSDisconnectedException: JavaScript interop calls cannot be issued at this time. This is because the circuit has disconnected and is being disposed.
at Microsoft.AspNetCore.Components.Server.Circuits.RemoteJSRuntime.BeginInvokeJS(Int64 asyncHandle, String identifier, String argsJson, JSCallResultType resultType, Int64 targetInstanceId)
at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, CancellationToken cancellationToken, Object[] args)
at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, Object[] args)
at Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(IJSRuntime jsRuntime, String identifier, Object[] args)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.<>c__DisplayClass69_0.<<Dispose>g__HandleAsyncExceptions|1>d.MoveNext()
This will only happen on reload, if I switch to another page, the dispose function is called as well, but no exception. I'm not entirely sure what the reason for this problem is. Maybe it could relate to the initialization as well, which happens after first render:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var authState = await AuthenticationStateTask;
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
if (await ViewModel.LoadSelectedDatabase(DatasetID))
{
await JSRuntime.InvokeVoidAsync("initializeDropZone");
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "The selected Dataset does not exist!");
NavigationManager.NavigateTo($"datasets");
}
}
}
await base.OnAfterRenderAsync(firstRender);
return;
}
Edit: Some more testing, exception is thrown before the await JSRuntime.InvokeVoidAsync("initializeDropZone"); is called after reload.
Edit#2: I also switched up the JS function:
public ValueTask DisposeAsync()
{
ViewModel.PropertyChanged -= OnPropertyChangedHandler;
GC.SuppressFinalize(this);
return JSRuntime.InvokeVoidAsync("console.log", "testilein");
//return JSRuntime.InvokeVoidAsync("deInitializeDropZone");
}
This will result in the same errors on reload.
Since it is impossible to call JavaScript when the SignalR connection is disconnected, I believe the recommended way is to wrap it in a try-catch and catch the JSDisconnectedException as shown below.
Since event listeners stop existing after a page reload there are no memory leaks.
Having said that, I agree this isn't very "elegant" though...
async ValueTask IAsyncDisposable.DisposeAsync()
{
try
{
ViewModel.PropertyChanged -= OnPropertyChangedHandler;
GC.SuppressFinalize(this);
await JSRuntime.InvokeVoidAsync("deInitializeDropZone");
}
catch (JSDisconnectedException ex)
{
// Ignore
}
}
Side note I made the implementation explicit since it is only accessed on an instance of the interface.
Adding to Jesse's answer: I'm having the same issue as OP and currently looking for a proper solution. Seems there's a statement in the official docs, that confirm Jesse's answer:
Basically:
JavaScript interop calls without a circuit
This section only applies to Blazor Server apps.
JavaScript (JS) interop calls can't be issued after a SignalR circuit is disconnected. Without a circuit during component disposal or at any other time that a circuit doesn't exist, the following method calls fail and log a message that the circuit is disconnected as a JSDisconnectedException:
JS interop method calls IJSRuntime.InvokeAsync JSRuntimeExtensions.InvokeAsync JSRuntimeExtensions.InvokeVoidAsync) Dispose/DisposeAsync calls on any IJSObjectReference.
In order to avoid logging JSDisconnectedException or to log custom information, catch the exception in a try-catch statement.
For the following component disposal example:
The component implements IAsyncDisposable. objInstance is an IJSObjectReference. JSDisconnectedException is caught and not logged. Optionally, you can log custom information in the catch statement at whatever log level you prefer. The following example doesn't log custom information because it assumes the developer doesn't care about when or where circuits are disconnected during component disposal.
C#
async ValueTask IAsyncDisposable.DisposeAsync() { try { if (objInstance is not null) { await objInstance.DisposeAsync(); } } catch (JSDisconnectedException) { } }If you must clean up your own JS objects or execute other JS code on the client after a circuit is lost, use the MutationObserver pattern in JS on the client.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With