A JavaScript-to-.NET call allows notifying C# code of an event produced by a JavaScript API or library.
For example, a click on a Leaflet marker happens in JavaScript, but it might need to update the state of a Blazor component.
We need JavaScript to be able to call C# methods. Blazor offers us two ways to do this:
- Static Methods: Global calls that do not depend on any component.
- Instance Methods: Calls directed to a specific object or component in memory.
| Feature | Static Method | Instance Method |
|---|---|---|
| Attribute | [JSInvokable] | [JSInvokable] |
| State Access | No (only what you pass as arguments) | Yes (accesses component variables, services, etc.) |
| Usage in JS | DotNet.invokeMethodAsync(...) | myReference.invokeMethodAsync(...) |
| Complexity | Low | Medium (requires passing reference and Dispose) |
| Use Case | Pure helpers, Calculations, Factories | UI Events, Library Callbacks, Notifications |
Calling Static Methods
This is the simplest way. It is used for utility functions that do not need to access application state (variables, dependency injection, etc.).
We must mark the method with the [JSInvokable] attribute. Additionally, the method must be static.
using Microsoft.JSInterop;
public class Utilities
{
[JSInvokable]
public static string CalculateSum(int a, int b)
{
return $"The sum is {a + b} (calculated in .NET)";
}
}In JS, we use the global DotNet object.
The key method is DotNet.invokeMethodAsync. It requires:
- The assembly name (your project’s .dll name).
- The method name.
- The arguments.
// Script in your index.html or app.razor
function requestCalculationFromDotNet() {
// 'MyBlazorProject' is your assembly name
DotNet.invokeMethodAsync('MyBlazorProject', 'CalculateSum', 10, 20)
.then(result => {
console.log(result); // "The sum is 30..."
});
}Limitation: Since it’s static, inside CalculateSum you cannot access this, nor StateHasChanged, nor services injected into a component. It is isolated code.
Calling Instance Methods with DotNetObjectReference
In this case, an event managed by JS needs to update a specific instance of a component.
To do this, we pass a managed reference to JavaScript using DotNetObjectReference.
Workflow
C#: We create a DotNetObjectReference pointing to this (the current component).
C#: We send it to a JS initialization function.
JS: Stores that reference.
JS: When the event occurs, it uses the reference to call the C# method.
Implementation
1. The JavaScript Code (The Listener)
// window.myHelperJS
window.myHelperJS = {
subscribeToEvent: function (dotNetHelper) {
// Simulate an event that occurs every 2 seconds
return setInterval(() => {
// Call the "UpdateTime" method of the instance passed to us
dotNetHelper.invokeMethodAsync('UpdateTime');
}, 2000);
},
cancelSubscription: function (intervalId) {
clearInterval(intervalId);
}
};2. The Blazor Component (The Receiver)
@implements IAsyncDisposable
@inject IJSRuntime JS
<h3>Current time: @currentTime</h3>
@code {
private string currentTime = "Waiting...";
// Store the reference to clean it up later
private DotNetObjectReference<MyComponent>? objRef;
private int? intervalId;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// 1. Create the reference pointing to 'this'
objRef = DotNetObjectReference.Create(this);
// 2. Pass the reference to JS
intervalId = await JS.InvokeAsync<int>(
"myHelperJS.subscribeToEvent", objRef);
}
}
// 3. This is the method JS will call
[JSInvokable]
public Task UpdateTime()
{
currentTime = DateTime.Now.ToLongTimeString();
// The call is not a Razor event, so we notify the change
return InvokeAsync(StateHasChanged);
}
public async ValueTask DisposeAsync()
{
try
{
if (intervalId is not null)
{
await JS.InvokeVoidAsync(
"myHelperJS.cancelSubscription", intervalId);
}
}
catch (JSDisconnectedException)
{
// The circuit is already disconnected
}
finally
{
objRef?.Dispose();
}
}
}Memory Management and Dispose
This is where serious mistakes are often made.
When you create DotNetObjectReference.Create(this), you are telling the .NET Garbage Collector (GC): “Hey! Don’t delete this object, because JavaScript has a reference to it.”
If the component is removed and you don’t release objRef, the interop reference table might still hold onto it, causing a memory leak. You should also cancel timers or JavaScript listeners before disposing of the reference.
If you hold a DotNetObjectReference, release the reference when it’s no longer needed. If you need to unregister JS resources first, implement IAsyncDisposable as in the example.