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 WebAssembly as the app was running in the local browser and there was no state shared with anyone else. However, when running Blazor Server 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 gives us 2 methods we can use to call JavaScript functions.

ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object[] args);
ValueTask InvokeVoidAsync(string identifier, params object[] args);

The first is InvokeAsync, we use this when we are expecting the JavaScript function we're calling to return something to us. 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.

The second is InvokeVoidAsync and as you can probably tell, we use this when we want to call a JavaScript function that doesn't return anything. Just as with InvokeAsync, the first argument is the function we want to call and the second allows us to pass in various arguments.

You'll have noticed that both methods are 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 offers us the same two methods, only this time they are synchronous.

T Invoke<T>(string identifier, params object[] args);
void InvokeVoid(string identifier, params object[] args);
I can't stress this enough, only use IJSInProcessRuntime when using Blazor WebAssembly. This will not work if you're using Blazor Server.

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>

@code {

    string message = "";

    private async Task ShowAlert()
    {
        await jsRuntime.InvokeVoidAsync("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 WebAssembly 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>

@code {

    string message = "";

    private void ShowAlert()
    {
        ((IJSInProcessRuntime)jsRuntime).InvokeVoid("ShowAlert", message);
    }
}

As you can see, we've downcast the IJSRuntime to IJSInProcessRuntime which has given us access to the synchronous InvokeVoid 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 Blazor Server. 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>

@code {

    private async Task WriteToConsole()
    {
        await jsRuntime.InvokeVoidAsync("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 = (dotnetHelper) => {
    dotnetHelper.invokeMethodAsync('GetHelloMessage')
        .then(message => console.log(message));
}

The JS function now takes a parameter, dotnetHelper. We can use this helper, specifically its invokeMethodAsync function, to call our C# method. 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>

@code {
    private async Task WriteToConsole()
    {
        await jsRuntime.InvokeVoidAsync("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.