InteropServices.JavaScript

🟥 Not applicable to Blazor Server

InteropServices.JavaScript is a feature exclusive to Blazor WebAssembly, which offers its own unique advantages over IJSRuntime. In this tutorial, you will learn about:

  • The benefits of using InteropServices.JavaScript over IJSRuntime
  • Understanding the marshalling process
  • Enabling InteropServices.JavaScript
  • Build a wrapper
  • Import a JavaScript function
  • Export a C# method
  • Marshalling data
You can download the example code used in this topic on GitHub.

The benefits of using InteropServices.JavaScript over IJSRuntime

There are three primary advantages of using InteropServices.JavaScript over IJSRuntime:

  1. It is ideal for modularized JavaScript code.
  2. Developers can control the marshalling process without needing to write additional code.
  3. The lifetimes of C# instances in JavaScript and JavaScript instances in C# are managed.

However, InteropServices.JavaScript cannot be used with unpredictable JavaScript functions.

Better support for modularized JavaScript code

While IJSRuntime enables the invocation of a JavaScript function that is attached to a module or the window object by its name, InteropServices.JavaScript maps a JavaScript function to a C# method. For instance, consider the following example that maps the JavaScript function console.log to a C# method:

Using IJSRuntime:

public class ConsoleWrapper
{
    private readonly IJSRuntime _js;

    public ConsoleWrapper(IJSRuntime js)
	{
        _js = js;
    }

    public async Task Log(string message)
    {
        await _js.InvokeVoidAsync("console.log", message);
    }
}

Using InteropServices.JavaScript:

public static partial class ConsoleWrapper
{
    [JSImport("globalThis.console.log")]
    public static partial void Log(string message);
}

Control the marshalling process

IJSRuntime is a versatile tool that can be used to invoke any JavaScript function from .NET. Whereas, one limitation of InteropServices.JavaScript is that it can only invoke JavaScript functions with a single return type. This means that the code structure of the JavaScript function must be carefully considered when using InteropServices.JavaScript.

Using IJSRuntime:

public async Task MyMethod() 
{
    await MyModule.Value.InvokeVoidAsync("MyFunction", 5);
}

If we want to convert the received number into a BigInt, we need to write additional JavaScript code:

export function MyFunction(number)
{
    let bigintNumber = BigInt(number);
}

On the other hand, by using InteropServices.JavaScript, we can simplify the process and eliminate the need for additional code:

[JSImport("MyFunction", "MyModule")]
public static partial void MyMethod([JSMarshalAs<JSType.BigInt>] long number);

With this approach, the received parameter on the JavaScript side is already a BigInt, making the code more efficient and easier to maintain.

Managed lifetime

One of the major advantages of using C# is its garbage collection (GC) feature. However, when using IJSRuntime, the GC is unable to collect the module instance of C# that is referenced by JavaScript code. To overcome this issue, InteropServices.JavaScript can be used so that you don't need to worry about when a JavaScript module is destroyed. You just need to import the module every time you require it. Importing the JavaScript module multiple times does not impact performance as the module is automatically cached.


Understanding the marshalling process

JavaScript and C# have their own distinct data types. When integrating between them, it's necessary to marshal the parameters to the other data type because C# cannot recognize the JavaScript type, and vice versa. The following image provides a clear illustration of the marshalling process.

marshalling-process-blazor.png

Here are the table of interchangeable common data type between JavaScript and C#.

JavaScript type .NET type
Boolean Boolean
String Char, char, String, string
Number Int16, short, Int32, int, Int64, long, Single, float, Double, double, IntPtr, Byte, byte
BigInt Int64, long
Date DateTime, DateTimeOffset
Error Exception
Object JSObject
Any Object
Promise Task
Function Action, Action<T>, Action<T1, T2>, Action<T1, T2, T3>, Func<TResult>, Func<T, TResult>, Func<T1, T2, TResult>, Func<T1, T2, T3, TResult>

Enabling InteropServices.JavaScript

Enabling unsafe code is necessary when integrating with another programming language, including JavaScript. To integrate with JavaScript, you'll need to enable unsafe code in your project by following these steps:

  1. Right-click on your project and select Properties to open the project properties.

open-project-properties.png

  1. Navigate to Build General and select the checkbox for Unsafe code.

allow-unsafe-code.png


Build a wrapper

You can place your JavaScript code file either in the wwwroot folder or in the same directory as the corresponding component. For standard JavaScript, import it in the index.html and create a C# wrapper. When working with JavaScript modules, you have the option to either create a C# wrapper that is imported only when needed, or import it when the application starts. Importing the module only when needed is recommended for optimal performance.

Build a C# wrapper for a global standard JavaScript

Assuming you have the following standard JavaScript that you want to import into your Blazor project:

wwwroot/js/GlobalClassicJavaScript.js:

function HelloBlazorSchool()
{
    alert(`Hello Blazor School from Global standard JS.`);
}
  1. In the index.html file, import the standard JavaScript file:
<body>
    ...
    <script src="js/GlobalClassicJavaScript.js"></script>
</body>
  1. Create a C# wrapper class.
[SupportedOSPlatform("browser")]
public partial class ClassicJavaScript
{
    [JSImport("globalThis.HelloBlazorSchool")]
    public static partial void HelloBlazorSchool();
}

In the above example, the first parameter of JSImport attribute is the function name that you want to map. In this case, it's HelloBlazorSchool. Note that the globalThis object in the JavaScript code is a special object created by Blazor, and it's a reference to the window object.

Build a C# wrapper for a JavaScript module

Assuming you have the following JavaScript Module that you want to import into your Blazor project:

wwwroot/js/JavaScriptModule.js:

export function HelloBlazorSchool()
{
    alert("Hello Blazor School!");
}
  1. Create a C# class:
[SupportedOSPlatform("browser")]
public partial class JavaScriptModule
{
    public static async Task ImportModuleAsync() => await JSHost.ImportAsync("MyModule", "/js/JavaScriptModule.js");
}
  1. Call the ImportModuleAsync method to load the JavaScript module. This method can be called multiple times without sending a request if the file is already loaded. There are two options for when to call this method:

Eager loading: Load the JavaScript module in Program.cs. This will slow down your website for the first load, but subsequent loads will be faster.

builder.Services...

if (OperatingSystem.IsBrowser())
{
    await JavaScriptModule.ImportModuleAsync();
}

Lazy loading: Load the JavaScript module in a Razor Component, during the OnInit or OnAfterRender phase. This will not slow down your website for the first load.

@code { 
   protected override async Task OnInitializedAsync()
    {
        await JavaScriptModule.ImportModuleAsync();
    }
}
@code { 
   protected override Task OnAfterRenderAsync(bool firstRender)
    {
        await JavaScriptModule.ImportModuleAsync();
    }
}

Build a C# wrapper for a colocated JavaScript module

To create a C# wrapper for a colocated JavaScript module using InteropServices.JavaScript, you need to follow these steps:

  1. Place the JavaScript module in the same folder as the component.

colocate-javascript-module.png

  1. Create a partial class for the component with the following code:
[SupportedOSPlatform("browser")]
public partial class CallJavaScriptFunction
{
    public static async Task ImportModuleAsync() => await JSHost.ImportAsync("Collocated Module", "../Pages/InteropServices/CallJavaScriptFunction.razor.js");
}
  1. Import the JavaScript module during the OnInit or OnAfterRender phase:
@code { 
   protected override async Task OnInitializedAsync()
    {
        await ImportModuleAsync();
    }
}
@code { 
   protected override Task OnAfterRenderAsync(bool firstRender)
    {
        await ImportModuleAsync();
    }
}

By following these steps, you can easily create a C# wrapper for a colocated JavaScript module and use it in your Blazor project.


Import a JavaScript function

To call a JavaScript function from a C# method, you must first import the function using the JSImport attribute. Let's assume you want to call the following JavaScript function:

export function HelloBlazorSchool()
{
    alert("Hello Blazor School!");
}

To map this function in your C# wrapper, you need to declare a mapping method as follows:

[JSImport("HelloBlazorSchool", "MyModule")]
public static partial void HelloBlazorSchool();

Since this is a static method, you can use it directly from the C# wrapper class.


Export a C# method

Not only can you easily access a JavaScript function from a C# method, but it is also possible to access a C# method from a JavaScript function. To export a C# method to the JavaScript environment, you can use the JSExport attribute.

namespace JavaScriptInteraction.InteropServicesModules; // Notice the namespace of this class

[SupportedOSPlatform("browser")]
public partial class JavaScriptModule
{
    [JSExport]
    public static void HelloBlazorSchool()
    {
        Console.WriteLine("Hello Blazor School"); 
    }
}

Then you can call this C# method using the following JavaScript code:

async function CallAutoMarshalingPrimitiveParameterStaticCSharpMethodAsync()
{
    let rootAssembly = await globalThis.getDotnetRuntime(0).getAssemblyExports("JavaScriptInteraction.dll");
    rootAssembly.JavaScriptInteraction.InteropServicesModules
        .JavaScriptModule.HelloBlazorSchool();
}

The JavaScriptInteraction.dll is the name of your project. You can find it in the Properties of your project:

how-to-see-assembly-name.png

From the rootAssembly object, using the namespace, class name, and the method name to call the exported C# method.

Alternatively, you can pass the C# method as the parameter and then invoke it from the JavaScript code. For example, you want to call the method AddNumber:

public class InteropServiceExampleClass
{
    public int AddNumber(int param1, int param2) => param1 + param2;
}

Export a C# method with the respective parameters and return data of the AddNumber method.

[JSImport("FunctionWithMethodParameter", "MyModule")]
public static partial string FunctionWithReferenceParameter([JSMarshalAs<JSType.Function<JSType.Number, JSType.Number, JSType.Number>>] Func<int, int, int> csharpMethod);

You can call the AddNumber method from the JavaScript function as follows:

export function FunctionWithMethodParameter(csharpMethod)
{
    let result = csharpMethod(1, 2);
    alert(`Using C# method to calculate 1 + 2. Result: ${result}`);
}

Then using C# to pass the method to the JavaScript function:

<button type="button" @onclick="CallCSharpMethodByReference">Call a C# Method by Reference</button>

@code {
    public void CallCSharpMethodByReference()
    {
        var service = new InteropServiceExampleClass();

        JavaScriptModule.FunctionWithReferenceParameter(service.AddNumber);
    }
}

Marshalling data

When it comes to passing data from a JavaScript function to a C# method or vice versa, there are two main approaches: returning a value from a function or passing data as parameters to a C# method.

Auto marshalling returned data

Consider the JavaScript function below:

export function FunctionWithReturnedObject()
{
    let exampleObject = {
        exampleString: "Blazor School",
        exampleInt: 9000,
        exampleDate: new Date()
    };

    return exampleObject;
}

Based on the table at Understanding the marshalling process section. A JavaScript Object can be marshalled to a JSObject in C# type. You can map the above JavaScript function as follows:

[JSImport("FunctionWithReturnedObject", "MyModule")]
public static partial JSObject AutoMarshallingReturnedValue();

Marshalling returned data manually

If you need to manually marshal returned data from a JavaScript function to a C# method, you can use the following approach. Consider the JavaScript function below:

export function FunctionWithReturnedPrimaryData()
{
    return 2023;
}

A Number in JavaScript can be marshalled into different C# data types, such as int, short, long, byte, etc. You can control how a Number in JavaScript can be marshalled to C# code as follows:

[JSImport("FunctionWithReturnedPrimaryData", "MyModule")]
[return: JSMarshalAs<JSType.Number>]
public static partial long ManuallyMarshalReturnValue();

The [return: JSMarshalAs<JSType.Number>] attribute indicates that we are expecting a Number to be returned from the mapped JavaScript function, and the return type of the method, long, is the expected data type in C#.

Auto marshalling parameters

Consider the JavaScript function below:

export function FunctionWithAutoMarshallingParameters(stringData, numberData, booleanData)
{
    alert(`Received object data: string ${stringData}, number ${numberData}, boolean ${booleanData}`);
}

Then you can map the JavaScript function as follows:

[JSImport("FunctionWithAutoMarshallingParameters", "MyModule")]
public static partial string AutoMarshallingFunctionWithPrimitiveParameters(string param1, int param2, bool param3);

JavaScript parameters will use the var type for those parameters.

Marshalling parameters manually

When you don't want to convert a var to a specific type such as String, BigInt, Date in the JavaScript code, you can specific the JavaScript type in the C# code as follows:

[JSImport("FunctionWithPrimitiveParameters", "MyModule")]
public static partial string ManualMarshalToFunctionWithPrimitiveParameters([JSMarshalAs<JSType.String>] string param1, [JSMarshalAs<JSType.BigInt>] long param2, [JSMarshalAs<JSType.Date>] DateTime param3);
BLAZOR SCHOOL
Designed and built with care by our dedicated team, with contributions from a supportive community. We strive to provide the best learning experience for our users.
Docs licensed CC-BY-SA-4.0
Copyright © 2021-2024 Blazor School
An unhandled error has occurred. Reload 🗙