Call .NET methods from JavaScript functions in ASP.NET Core Blazor

By Javier Calvarro Nelson, Daniel Roth, Shashikant Rudrawadi, and Luke Latham

A Blazor app can invoke JavaScript functions from .NET methods and .NET methods from JavaScript functions. These scenarios are called JavaScript interoperability (JS interop).

This article covers invoking .NET methods from JavaScript. For information on how to call JavaScript functions from .NET, see Call JavaScript functions from .NET methods in ASP.NET Core Blazor.

View or download sample code (how to download)

Static .NET method call

To invoke a static .NET method from JavaScript, use the DotNet.invokeMethod or DotNet.invokeMethodAsync functions. Pass in the identifier of the static method you wish to call, the name of the assembly containing the function, and any arguments. The asynchronous version is preferred to support Blazor Server scenarios. The .NET method must be public, static, and have the [JSInvokable] attribute. Calling open generic methods isn't currently supported.

The sample app includes a C# method to return an int array. The JSInvokable attribute is applied to the method.

Pages/JsInterop.razor:

<button type="button" class="btn btn-primary"
        onclick="exampleJsFunctions.returnArrayAsyncJs()">
    Trigger .NET static method ReturnArrayAsync
</button>

@code {
    [JSInvokable]
    public static Task<int[]> ReturnArrayAsync()
    {
        return Task.FromResult(new int[] { 1, 2, 3 });
    }
}

JavaScript served to the client invokes the C# .NET method.

wwwroot/exampleJsInterop.js:

window.exampleJsFunctions = {
  showPrompt: function (text) {
    return prompt(text, 'Type your name here');
  },
  displayWelcome: function (welcomeMessage) {
    document.getElementById('welcome').innerText = welcomeMessage;
  },
  returnArrayAsyncJs: function () {
    DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync')
      .then(data => {
        data.push(4);
          console.log(data);
      });
  },
  sayHello: function (dotnetHelper) {
    return dotnetHelper.invokeMethodAsync('SayHello')
      .then(r => console.log(r));
  }
};

When the Trigger .NET static method ReturnArrayAsync button is selected, examine the console output in the browser's web developer tools.

The console output is:

Array(4) [ 1, 2, 3, 4 ]

The fourth array value is pushed to the array (data.push(4);) returned by ReturnArrayAsync.

By default, the method identifier is the method name, but you can specify a different identifier using the JSInvokableAttribute constructor:

@code {
    [JSInvokable("DifferentMethodName")]
    public static Task<int[]> ReturnArrayAsync()
    {
        return Task.FromResult(new int[] { 1, 2, 3 });
    }
}

In the client-side JavaScript file:

returnArrayAsyncJs: function () {
  DotNet.invokeMethodAsync('BlazorSample', 'DifferentMethodName')
    .then(data => {
      data.push(4);
      console.log(data);
    });
}

Instance method call

You can also call .NET instance methods from JavaScript. To invoke a .NET instance method from JavaScript:

  • Pass the .NET instance by reference to JavaScript:
    • Make a static call to DotNetObjectReference.Create.
    • Wrap the instance in a DotNetObjectReference instance and call Create on the DotNetObjectReference instance. Dispose of DotNetObjectReference objects (an example appears later in this section).
  • Invoke .NET instance methods on the instance using the invokeMethod or invokeMethodAsync functions. The .NET instance can also be passed as an argument when invoking other .NET methods from JavaScript.

Note

The sample app logs messages to the client-side console. For the following examples demonstrated by the sample app, examine the browser's console output in the browser's developer tools.

When the Trigger .NET instance method HelloHelper.SayHello button is selected, ExampleJsInterop.CallHelloHelperSayHello is called and passes a name, Blazor, to the method.

Pages/JsInterop.razor:

<button type="button" class="btn btn-primary" @onclick="TriggerNetInstanceMethod">
    Trigger .NET instance method HelloHelper.SayHello
</button>

@code {
    public async Task TriggerNetInstanceMethod()
    {
        var exampleJsInterop = new ExampleJsInterop(JSRuntime);
        await exampleJsInterop.CallHelloHelperSayHello("Blazor");
    }
}

CallHelloHelperSayHello invokes the JavaScript function sayHello with a new instance of HelloHelper.

JsInteropClasses/ExampleJsInterop.cs:

public class ExampleJsInterop : IDisposable
{
    private readonly IJSRuntime jsRuntime;
    private DotNetObjectReference<HelloHelper> objRef;

    public ExampleJsInterop(IJSRuntime jsRuntime)
    {
        this.jsRuntime = jsRuntime;
    }

    public ValueTask<string> CallHelloHelperSayHello(string name)
    {
        objRef = DotNetObjectReference.Create(new HelloHelper(name));

        return jsRuntime.InvokeAsync<string>(
            "exampleJsFunctions.sayHello",
            objRef);
    }

    public void Dispose()
    {
        objRef?.Dispose();
    }
}

wwwroot/exampleJsInterop.js:

window.exampleJsFunctions = {
  showPrompt: function (text) {
    return prompt(text, 'Type your name here');
  },
  displayWelcome: function (welcomeMessage) {
    document.getElementById('welcome').innerText = welcomeMessage;
  },
  returnArrayAsyncJs: function () {
    DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync')
      .then(data => {
        data.push(4);
          console.log(data);
      });
  },
  sayHello: function (dotnetHelper) {
    return dotnetHelper.invokeMethodAsync('SayHello')
      .then(r => console.log(r));
  }
};

The name is passed to HelloHelper's constructor, which sets the HelloHelper.Name property. When the JavaScript function sayHello is executed, HelloHelper.SayHello returns the Hello, {Name}! message, which is written to the console by the JavaScript function.

JsInteropClasses/HelloHelper.cs:

public class HelloHelper
{
    public HelloHelper(string name)
    {
        Name = name;
    }

    public string Name { get; set; }

    [JSInvokable]
    public string SayHello() => $"Hello, {Name}!";
}

Console output in the browser's web developer tools:

Hello, Blazor!

To avoid a memory leak and allow garbage collection on a component that creates a DotNetObjectReference, adopt one of the following approaches:

  • Dispose of the object in the class that created the DotNetObjectReference instance:

    public class ExampleJsInterop : IDisposable
    {
        private readonly IJSRuntime jsRuntime;
        private DotNetObjectReference<HelloHelper> objRef;
    
        public ExampleJsInterop(IJSRuntime jsRuntime)
        {
            this.jsRuntime = jsRuntime;
        }
    
        public ValueTask<string> CallHelloHelperSayHello(string name)
        {
            objRef = DotNetObjectReference.Create(new HelloHelper(name));
    
            return jsRuntime.InvokeAsync<string>(
                "exampleJsFunctions.sayHello",
                objRef);
        }
    
        public void Dispose()
        {
            objRef?.Dispose();
        }
    }
    

    The preceding pattern shown in the ExampleJsInterop class can also be implemented in a component:

    @page "/JSInteropComponent"
    @using BlazorSample.JsInteropClasses
    @implements IDisposable
    @inject IJSRuntime JSRuntime
    
    <h1>JavaScript Interop</h1>
    
    <button type="button" class="btn btn-primary" @onclick="TriggerNetInstanceMethod">
        Trigger .NET instance method HelloHelper.SayHello
    </button>
    
    @code {
        private DotNetObjectReference<HelloHelper> objRef;
    
        public async Task TriggerNetInstanceMethod()
        {
            objRef = DotNetObjectReference.Create(new HelloHelper("Blazor"));
    
            await JSRuntime.InvokeAsync<string>(
                "exampleJsFunctions.sayHello",
                objRef);
        }
    
        public void Dispose()
        {
            objRef?.Dispose();
        }
    }
    
  • When the component or class doesn't dispose of the DotNetObjectReference, dispose of the object on the client by calling .dispose():

    window.myFunction = (dotnetHelper) => {
      dotnetHelper.invokeMethod('BlazorSample', 'MyMethod');
      dotnetHelper.dispose();
    }
    

Component instance method call

To invoke a component's .NET methods:

  • Use the invokeMethod or invokeMethodAsync function to make a static method call to the component.
  • The component's static method wraps the call to its instance method as an invoked Action.

In the client-side JavaScript:

function updateMessageCallerJS() {
  DotNet.invokeMethod('BlazorSample', 'UpdateMessageCaller');
}

Pages/JSInteropComponent.razor:

@page "/JSInteropComponent"

<p>
    Message: @message
</p>

<p>
    <button onclick="updateMessageCallerJS()">Call JS Method</button>
</p>

@code {
    private static Action action;
    private string message = "Select the button.";

    protected override void OnInitialized()
    {
        action = UpdateMessage;
    }

    private void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        StateHasChanged();
    }

    [JSInvokable]
    public static void UpdateMessageCaller()
    {
        action.Invoke();
    }
}

When there are several components, each with instance methods to call, use a helper class to invoke the instance methods (as Actions) of each component.

In the following example:

  • The JSInterop component contains several ListItem components.
  • Each ListItem component is composed of a message and a button.
  • When a ListItem component button is selected, that ListItem's UpdateMessage method changes the list item text and hides the button.

MessageUpdateInvokeHelper.cs:

using System;
using Microsoft.JSInterop;

public class MessageUpdateInvokeHelper
{
    private Action action;

    public MessageUpdateInvokeHelper(Action action)
    {
        action = action;
    }

    [JSInvokable("BlazorSample")]
    public void UpdateMessageCaller()
    {
        action.Invoke();
    }
}

In the client-side JavaScript:

window.updateMessageCallerJS = (dotnetHelper) => {
    dotnetHelper.invokeMethod('BlazorSample', 'UpdateMessageCaller');
    dotnetHelper.dispose();
}

Shared/ListItem.razor:

@inject IJSRuntime JsRuntime

<li>
    @message
    <button @onclick="InteropCall" style="display:@display">InteropCall</button>
</li>

@code {
    private string message = "Select one of these list item buttons.";
    private string display = "inline-block";
    private MessageUpdateInvokeHelper messageUpdateInvokeHelper;

    protected override void OnInitialized()
    {
        messageUpdateInvokeHelper = new MessageUpdateInvokeHelper(UpdateMessage);
    }

    protected async Task InteropCall()
    {
        await JsRuntime.InvokeVoidAsync("updateMessageCallerJS",
            DotNetObjectReference.Create(messageUpdateInvokeHelper));
    }

    private void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        display = "none";
        StateHasChanged();
    }
}

Pages/JSInterop.razor:

@page "/JSInterop"

<h1>List of components</h1>

<ul>
    <ListItem />
    <ListItem />
    <ListItem />
    <ListItem />
</ul>

Share interop code in a class library

JS interop code can be included in a class library, which allows you to share the code in a NuGet package.

The class library handles embedding JavaScript resources in the built assembly. The JavaScript files are placed in the wwwroot folder. The tooling takes care of embedding the resources when the library is built.

The built NuGet package is referenced in the app's project file the same way that any NuGet package is referenced. After the package is restored, app code can call into JavaScript as if it were C#.

For more information, see ASP.NET Core Razor components class libraries.

Avoid circular object references

Objects that contain circular references can't be serialized on the client for either:

  • .NET method calls.
  • JavaScript method calls from C# when the return type has circular references.

For more information, see the following issues:

Additional resources