While WebAssembly has the potential to end our reliance on JavaScript, JavaScript is not going away anytime soon. There are still a lot of things WebAssembly just can't do, most notably DOM manipulation. If you're running server-side Blazor then you don't even have WebAssembly as an option. So how do we handle this problem?

The answer is JavaScript interop. When we can't do what we need using .NET code alone, we can use the IJSRuntime abstraction to make calls into JavaScript functions. We can even get JavaScript functions to make calls into our C# code.

JSRuntime.Static is gone

Before we go any further, I want to point out a recent change in the way JS interop works. Historically, developers could execute their calls to JavaScript using JSRuntime.Current. This was a static version of the JSRuntime and avoided the need to inject anything into components or services.

This worked fine when using Blazor as the app was running in the local browser and there was no state shared with anyone else. However, when running server-side Blazor this caused some serious problems. Because it was static it made the behaviour extremely unpredictable so the team has now removed this static implementation and developers can only use an injected instance of IJSRuntime.

IJSRuntime

This abstraction is our gateway into the JavaScript world. It's a very simple interface which defines 2 methods.

Task<T> InvokeAsync<T>(string identifier, params object[] args);
void UntrackObjectRef(DotNetObjectRef dotNetObjectRef);

The main one is InvokeAsync and is what we use to make calls into JavaScript code. It takes a string, identifier, which is the identifier for the function to call. This identifier is relative to the window object, so if you wanted to call window.Blazored.LocalStorage.setItem you would pass in `Blazored.LocalStorage.setItem. If you have any parameters you need to pass to the JavaScript function, you can do so using the second argument.

You'll have noticed that the method is async. This is important because if you want your code to work in both client-side and server-side Blazor then all JS interop calls must be asynchronous due to the SignalR connection used by server-side Blazor.

IJSInProcessRuntime

If you have client-side only scenarios where you need to invoke a JavaScript call synchronously, then you have the ability to downcast IJSRuntime to IJSInProcessRuntime. This interface defines a single method signature, Invoke.

T Invoke<T>(string identifier, params object[] args);

The method works exactly the same as the InvokeAsync method on the IJSRuntime interface except it will run synchronously.

I can't stress this enough, only use IJSInProcessRuntime when using client-side Blazor. This will not work if you're using server-side Blazor.

How to call a JavaScript function from C#

Now we have covered the tools available to us. Let's look at an example of how we can use them to call a JavaScript function.

We're going setup a component which will interop with the following JavaScript function.

window.ShowAlert = (message) => {
    alert(message);
}

The code above is just wrapping a call to the JavaScript alert function allowing us to pass in a message to be displayed.

Making asynchronous calls

Let's checkout what the code looks like to call this function from a Razor Component.

@inject IJSRuntime jsRuntime

<input type="text" @bind="@message" />
<button @onclick="@ShowAlert">Show Alert</button>

@functions {

    string message = "";

    private async Task ShowAlert()
    {
        await jsRuntime.InvokeAsync<object>("ShowAlert", message);
    }
}

Starting from the top, we're requesting an instance of IJSRuntime from the DI container using the @inject directive. We've got an input which we can use to enter a message then a button which triggers the interop call to the JS function.

Making synchronous calls (Blazor Only)

As I said earlier, you should always default to async calls where ever possible to make sure your code will run in both client and server scenarios. But if you have the need, and you know the code won't be running on the server, then you can make sync calls.

Let's look at the same example again, but this time we'll make some changes to run the code synchronously.

@inject IJSRuntime jsRuntime

<input type="text" @bind="@message" />
<button @onclick="@ShowAlert">Show Alert</button>

@functions {

    string message = "";

    private void ShowAlert()
    {
        ((IJSInProcessRuntime)jsRuntime).Invoke<object>("ShowAlert", message);
    }
}

As you can see, we've downcast the IJSRuntime to IJSInProcessRuntime which has given us access to the synchronous Invoke method. Other than updating the ShowAlert method signature, we haven't had to make any further changes to make our code run synchronously.

Once again, this will not work with server-side Blazor. If you try to run the above code you will end up getting an InvalidCastException. Only use this method when you're sure your code will execute client-side only.

How to call a C# method from JavaScript

Sometimes you need to have JavaScript functions make calls into your C# code. One example of this is when using JavaScript promises. The promise may resolve sometime after the initial call and you need to know the result.

There are two options when calling C# code. The first is calling static methods and the second is calling instance methods.

I'm only going to show asynchronous examples here as that should always be the default but you can use the downcasting method described above to call synchronous methods if required.

Calling static methods

When calling static methods we can either use DotNet.invokeMethod or DotNet.invokeMethodAsync. Much like before, you should always use the async version where ever possible as this will make the code compatible with client and server scenarios. Let's look at an example.

namespace JSInteropExamples
{
    public static class MessageProvider
    {
        [JSInvokable]
        public static Task GetHelloMessage()
        {
            var message = "Hello from C#";
            return Task.FromResult(message);
        }
    }
}

We're going to call the GetHelloMessage method in the class above. When calling static methods they must be public and they must be decorated with the [JSInvokable] attribute.

We're going to call it from the following JavaScript function.

window.WriteCSharpMessageToConsole = () => {
    DotNet.invokeMethodAsync('JSInteropExamples', 'GetHelloMessage')
      .then(message => {
        console.log(message);
    });
}

We're using the DotNet.invokeMethodAsync function which is provided by the Blazor framework. The first argument is the name of the assembly containing the method we want to call. The second argument is the method name. As the call is asynchronous it returns a promise, when the promise resolves we take the message, which comes from our C# code, and log it to the console.

@inject IJSRuntime jsRuntime

<button @onclick="@WriteToConsole">Run</button>

@functions {

    private async Task WriteToConsole()
    {
        await jsRuntime.InvokeAsync<object>("WriteCSharpMessageToConsole");
    }
}

When we execute the above component, clicking the Run button will result in the message "Hello From C#" being printed to the browser console.

Calling instance methods

It's also possible to call instance methods from JavaScript. Let's make a few tweaks to the previous example to see how we can use it.

window.WriteCSharpMessageToConsole = (instance) => {
    instance.invokeMethodAsync('GetHelloMessage')
        .then((message) => {
            console.log(message);
        });
}

The JS function now takes a parameter, instance. This represents the C# instance we are going to pass in. It calls the invokeMethodAsync function on the instance passing the name of the C# method we want to call. As before, when the promise resolves the message passed back from C# is printed to the console.

@inject IJSRuntime jsRuntime

<button @onclick="@WriteToConsole">Run</button>

@functions {
    private async Task WriteToConsole()
    {
        await jsRuntime.InvokeAsync<object>("WriteCSharpMessageToConsole", DotNetObjectRef.Create(this));
    }
            
    [JSInvokable]
    public Task<string> GetHelloMessage()
    {
        var message = "Hello from a C# instance";
        return Task.FromResult(message);
    }
}

The static MessageProvider class is now gone and the GetHelloMessage method now lives on the component, still decorated with the [JSInvokable] attribute. The call to invoke the JS method is slightly different, we're passing in a DotNetObjectRef. This special type stops the value passed to it being serialised to JSON and instead passes it as a reference.

When we run the code above we should now see a message in the console saying "Hello from a C# instance".

Summary

In this post, we've had a detailed look at JavaScript interop. We've covered how to make calls from C# into JavaScript functions as well as how to make calls from JavaScript into C# methods.

I think the big take away here it that async is king when it comes to interop. It should always be your default choice if you want to make your code compatible with both client-side and server-side Blazor.