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:
InteropServices.JavaScript
over IJSRuntime
InteropServices.JavaScript
You can download the example code used in this topic on GitHub.
InteropServices.JavaScript
over IJSRuntime
There are three primary advantages of using InteropServices.JavaScript
over IJSRuntime
:
However, InteropServices.JavaScript
cannot be used with unpredictable JavaScript functions.
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); }
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.
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.
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.
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> |
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:
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.
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.`); }
<body> ... <script src="js/GlobalClassicJavaScript.js"></script> </body>
[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.
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!"); }
[SupportedOSPlatform("browser")] public partial class JavaScriptModule { public static async Task ImportModuleAsync() => await JSHost.ImportAsync("MyModule", "/js/JavaScriptModule.js"); }
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(); } }
To create a C# wrapper for a colocated JavaScript module using InteropServices.JavaScript
, you need to follow these steps:
[SupportedOSPlatform("browser")] public partial class CallJavaScriptFunction { public static async Task ImportModuleAsync() => await JSHost.ImportAsync("Collocated Module", "../Pages/InteropServices/CallJavaScriptFunction.razor.js"); }
@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.
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.
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:
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); } }
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.
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();
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#.
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.
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);