Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New JS interop #27083

Merged
merged 35 commits into from
Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2206412
New JS interop
guardrex Sep 23, 2022
bd0c425
Updates
guardrex Sep 26, 2022
af4f6c3
Setup for two classes/two modules
guardrex Sep 26, 2022
25e9650
New section for one module approach
guardrex Sep 26, 2022
a6339d8
Updates
guardrex Sep 26, 2022
a81553f
Rename and collocate the C# classes
guardrex Sep 26, 2022
37f3c82
Updates
guardrex Sep 27, 2022
7397bad
Updates
guardrex Sep 27, 2022
21c7a0c
Updates
guardrex Sep 28, 2022
e1b797a
Updates
guardrex Sep 28, 2022
090159f
Updates
guardrex Sep 28, 2022
590bd65
Update aspnetcore/blazor/javascript-interoperability/import-export-in…
guardrex Sep 28, 2022
d582795
Updates
guardrex Sep 28, 2022
c5154a0
Updates
guardrex Sep 29, 2022
6d5f2fb
Updates
guardrex Sep 29, 2022
afa4377
Updates
guardrex Oct 1, 2022
e100d54
Updates
guardrex Oct 1, 2022
8fa3a09
Updates
guardrex Oct 4, 2022
35ce08a
Updates
guardrex Oct 4, 2022
37fc696
Apply suggestions from code review
guardrex Oct 6, 2022
8778281
Updates
guardrex Oct 11, 2022
d3a1a93
Updates
guardrex Oct 12, 2022
dcb25a0
Updates
guardrex Oct 12, 2022
82bd973
Update aspnetcore/client-side/import-export-interop.md
guardrex Oct 18, 2022
981ccce
Apply suggestions from code review
guardrex Oct 18, 2022
19ba9ba
Updates
guardrex Oct 18, 2022
7d5b15c
Update aspnetcore/blazor/javascript-interoperability/import-export-in…
guardrex Oct 18, 2022
f032b69
Updates
guardrex Oct 18, 2022
7e6b02d
Updates
guardrex Oct 18, 2022
1b89ec8
Apply suggestions from code review
guardrex Oct 20, 2022
10747cd
Updates
guardrex Oct 20, 2022
c499c34
Updates
guardrex Oct 21, 2022
cd7dac8
Updates
guardrex Oct 21, 2022
bae4f5d
Updates
guardrex Oct 21, 2022
d457fe4
Updates
guardrex Oct 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2135,6 +2135,14 @@ In the preceding example:

[!INCLUDE[](~/blazor/includes/js-interop/6.0/size-limits.md)]

## .NET JavaScript `[JSImport]`/`[JSExport]` interop

*This section applies to Blazor WebAssembly apps.*

As an alternative to interacting with JavaScript (JS) in Blazor WebAssembly apps using Blazor's JS interop mechanism based on the <xref:Microsoft.JSInterop.IJSRuntime> interface, a .NET JS `[JSImport]`/`[JSExport]` interop API is available to apps targeting ASP.NET Core 7.0 or later.
guardrex marked this conversation as resolved.
Show resolved Hide resolved

For more information, see <xref:blazor/js-interop/import-export-interop>.

## Additional resources

* <xref:blazor/js-interop/call-javascript-from-dotnet>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2436,79 +2436,6 @@ For information on using a byte array when calling .NET from JavaScript, see <xr

[!INCLUDE[](~/blazor/includes/js-interop/6.0/size-limits.md)]

## Unmarshalled JavaScript interop

Blazor WebAssembly components may experience poor performance when .NET objects are serialized for JavaScript (JS) interop and either of the following are true:

* A high volume of .NET objects are rapidly serialized. For example, poor performance may result when JS interop calls are made on the basis of moving an input device, such as spinning a mouse wheel.
* Large .NET objects or many .NET objects must be serialized for JS interop. For example, poor performance may result when JS interop calls require serializing dozens of files.

<xref:Microsoft.JSInterop.IJSUnmarshalledObjectReference> represents a reference to an JS object whose functions can be invoked without the overhead of serializing .NET data.

In the following example:

* A [struct](/dotnet/csharp/language-reference/builtin-types/struct) containing a string and an integer is passed unserialized to JS.
* JS functions process the data and return either a boolean or string to the caller.
* A JS string isn't directly convertible into a .NET `string` object. The `unmarshalledFunctionReturnString` function calls `BINDING.js_string_to_mono_string` to manage the conversion of a JS string.

> [!NOTE]
> The following examples aren't typical use cases for this scenario because the [struct](/dotnet/csharp/language-reference/builtin-types/struct) passed to JS doesn't result in poor component performance. The example uses a small object merely to demonstrate the concepts for passing unserialized .NET data.

```javascript
<script>
window.returnObjectReference = () => {
return {
unmarshalledFunctionReturnBoolean: function (fields) {
const name = Blazor.platform.readStringField(fields, 0);
const year = Blazor.platform.readInt32Field(fields, 8);

return name === "Brigadier Alistair Gordon Lethbridge-Stewart" &&
year === 1968;
},
unmarshalledFunctionReturnString: function (fields) {
const name = Blazor.platform.readStringField(fields, 0);
const year = Blazor.platform.readInt32Field(fields, 8);

return BINDING.js_string_to_mono_string(`Hello, ${name} (${year})!`);
}
};
}
</script>
```

[!INCLUDE[](~/blazor/includes/js-location.md)]

> [!WARNING]
> The `js_string_to_mono_string` function name, behavior, and existence is subject to change in a future release of .NET. For example:
>
> * The function is likely to be renamed.
> * The function itself might be removed in favor of automatic conversion of strings by the framework.

`Pages/CallJsExample10.razor`:

:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/call-js-from-dotnet/CallJsExample10.razor":::

If an <xref:Microsoft.JSInterop.IJSUnmarshalledObjectReference> instance isn't disposed in C# code, it can be disposed in JS. The following `dispose` function disposes the object reference when called from JS:

```javascript
window.exampleJSObjectReferenceNotDisposedInCSharp = () => {
return {
dispose: function () {
DotNet.disposeJSObjectReference(this);
},

...
};
}
```

Array types can be converted from JS objects into .NET objects using `js_typed_array_to_array`, but the JS array must be a typed array. Arrays from JS can be read in C# code as a .NET object array (`object[]`).

Other data types, such as string arrays, can be converted but require creating a new Mono array object (`mono_obj_array_new`) and setting its value (`mono_obj_array_set`).

> [!WARNING]
> JS functions provided by the Blazor framework, such as `js_typed_array_to_array`, `mono_obj_array_new`, and `mono_obj_array_set`, are subject to name changes, behavioral changes, or removal in future releases of .NET.

## Stream from .NET to JavaScript

Blazor supports streaming data directly from .NET to JavaScript. Streams are created using a <xref:Microsoft.JSInterop.DotNetStreamReference>.
Expand Down Expand Up @@ -2652,6 +2579,22 @@ longRunningFn: 3
longRunningFn aborted!
```

## .NET JavaScript `[JSImport]`/`[JSExport]` interop

*This section applies to Blazor WebAssembly apps.*

As an alternative to interacting with JavaScript (JS) in Blazor WebAssembly apps using Blazor's JS interop mechanism based on the <xref:Microsoft.JSInterop.IJSRuntime> interface, a .NET JS `[JSImport]`/`[JSExport]` interop API is available to apps targeting ASP.NET Core 7.0 or later.
guardrex marked this conversation as resolved.
Show resolved Hide resolved

For more information, see <xref:blazor/js-interop/import-export-interop>.

## Unmarshalled JavaScript interop

*This section applies to Blazor WebAssembly apps.*

Unmarshalled interop is obsolete and should be replaced with .NET JavaScript `[JSImport]`/`[JSExport]` interop.
guardrex marked this conversation as resolved.
Show resolved Hide resolved

For more information, see <xref:blazor/js-interop/import-export-interop>.

## Additional resources

* <xref:blazor/js-interop/call-dotnet-from-javascript>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
---
title: .NET JavaScript `[JSImport]`/`[JSExport]` interop with ASP.NET Core Blazor WebAssembly
author: guardrex
description: Learn how to interact with JavaScript in Blazor WebAssembly apps using .NET JavaScript `[JSImport]`/`[JSExport]` interop.
monikerRange: '= aspnetcore-7.0'
ms.author: riande
ms.custom: mvc
ms.date: 09/23/2022
uid: blazor/js-interop/import-export-interop
---
# .NET JavaScript `[JSImport]`/`[JSExport]` interop with ASP.NET Core Blazor

This article explains how to interact with JavaScript (JS) in Blazor WebAssembly apps using .NET JS `[JSImport]`/`[JSExport]` interop API released with .NET 7.

Blazor provides its own JS interop mechanism based on the <xref:Microsoft.JSInterop.IJSRuntime> interface, which is uniformly supported across Blazor hosting models and described in the following articles:

* <xref:blazor/js-interop/call-javascript-from-dotnet>
* <xref:blazor/js-interop/call-dotnet-from-javascript>

<xref:Microsoft.JSInterop.IJSRuntime> enables library authors to build JS interop libraries that can be shared across the Blazor ecosystem and remains the recommend approach for JS interop in Blazor. However, this article describes an alternative JS interop approach available for the first time with the release of .NET 7. The approaches described in this article should be used to replace the obsolete unmarshalled JS interop API of prior Blazor framework releases.

## Obsolete JavaScript interop API

Unmarshalled JS interop using <xref:Microsoft.JSInterop.IJSUnmarshalledRuntime> API is obsolete in ASP.NET Core 7.0. Follow the guidance in this article to replace the obsolete API.

## Prerequisites

[!INCLUDE[](~/includes/7.0-SDK.md)]

## Call JavaScript from .NET

This section explains how to call JS functions from .NET.

> [!NOTE]
> Add the <xref:Microsoft.Build.Tasks.Csc.AllowUnsafeBlocks> property to the app's project file. Setting this property to `true` permits the code generator in the Roslyn compiler to use pointers and isn't a security concern.
guardrex marked this conversation as resolved.
Show resolved Hide resolved

To import a JS function to call it from C#, use the `[JSImport]` attribute on a C# method signature that matches the JS function's signature. The first parameter to the `[JSImport]` attribute is the name of the JS function to import, and the second parameter is the name of the [JS module](xref:blazor/js-interop/index#javascript-isolation-in-javascript-modules).

In the following example, `getMessage` is a JS function that returns a `string` for a module named `Interop`. The C# method signature matches: No parameters are passed to the JS function, and the JS function returns a `string`. The JS function is called in C# using standard syntax, `Interop.GetWelcomeMessage()`.

`Interop.cs`:

```csharp
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;

[SupportedOSPlatform("browser")]
public partial class Interop
{
[JSImport("getMessage", "Interop")]
internal static partial string GetWelcomeMessage();
}
```

In the imported method signature, you can use .NET types for parameters and return values, which are marshalled automatically by the runtime. Use `JSMarshalAsAttribute<T>` to control how the imported method parameters are marshalled. For example, you might choose to marshal a `long` as <xref:System.Runtime.InteropServices.JavaScript.JSType.Number?displayProperty=nameWithType> or <xref:System.Runtime.InteropServices.JavaScript.JSType.BigInt?displayProperty=nameWithType>. You can pass <xref:System.Action>/<xref:System.Func%601> callbacks as parameters, which are marshalled as callable JS functions. You can pass both JS and managed object references, and they are marshaled as proxy objects, keeping the object alive across the boundary until the proxy is garbage collected. You can also import and export asynchronous methods with a <xref:System.Threading.Tasks.Task> result, which are marshaled as [JS promises](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise). Most of the marshalled types work in both directions, as parameters and as return values, on both imported and exported methods. Exported methods are covered in the [Call .NET from JavaScript](#call-net-from-javascript) section.

The module name in the `[JSImport]` attribute and the call to load the module in the component, which is covered next with `JSHost.ImportAsync` syntax, should match. The module name must also be unique in the app. When authoring a library for deployment in a NuGet package, we recommend using the NuGet package namespace as a prefix in module names. In the following example, the module name reflects the `Contoso.InteropServices.JavaScript` package:

```csharp
[JSImport("getMessage", "Contoso.InteropServices.JavaScript.Interop")]
```

If the JS function doesn't directly interact with the rendered Document Object Model (DOM), import the module in [`OnInitializedAsync`](xref:blazor/components/lifecycle#component-initialization-oninitializedasync). Call the imported JS function with the .NET interop method.

In the following `CallJavaScript` component:

* The `Interop` module is imported asychronously from the [collocated JS file](xref:blazor/js-interop/index#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component).
* The imported `getMessage` JS function is called with `Interop.GetWelcomeMessage()`.
* The returned welcome string is displayed in the UI via the `message` field.

`Pages/CallJavaScript.razor`:

```razor
@page "/call-javascript"
@using System.Runtime.InteropServices.JavaScript

<h1>.NET JS <code>[JSImport]</code>/<code>[JSExport]</code> interop (Call JS)</h1>

@(message is not null ? message : string.Empty)

@code {
private string? message;

protected override async Task OnInitializedAsync()
{
if (OperatingSystem.IsBrowser())
{
await JSHost.ImportAsync("Interop", "../Pages/CallJavaScript.razor.js");

message = Interop.GetWelcomeMessage();
}
}
}
```

> [!NOTE]
> The conditional check for <xref:System.OperatingSystem.IsBrowser%2A?displayProperty=nameWithType> in the preceding example ensures that the code is only called in Blazor WebAssembly apps running on the client. This is important for library code deployed as NuGet packages that might be referenced by developers of Blazor Server apps, where the preceding code can't execute. If you know for sure that the code is only implemented in Blazor WebAssembly apps, you can remove the check for <xref:System.OperatingSystem.IsBrowser%2A?displayProperty=nameWithType>.

If the JS module interacts directly with the component's rendered UI, import the module in [`OnAfterRenderAsync`](xref:blazor/components/lifecycle#after-component-render-onafterrenderasync) and call the imported JS function:

```csharp
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (OperatingSystem.IsBrowser() && firstRender)
{
await JSHost.ImportAsync("Interop", "../Pages/CallJavaScript.razor.js");

Interop.DoSomething();
}
}
```

If the code holds a reference to the <xref:System.Runtime.InteropServices.JavaScript.JSObject> returned by `JSHost.ImportAsync`, be sure to call <xref:System.Runtime.InteropServices.JavaScript.JSObject.Dispose%2A?displayProperty=nameWithType> when the component is disposed:
guardrex marked this conversation as resolved.
Show resolved Hide resolved

```razor
...
@implements IDisposable

...

@code {
private JSObject? module;

...

module = await JSHost.ImportAsync("Interop", "../Pages/CallJavaScript.razor.js");

...

public void Dispose() => module?.Dispose();
}
```

Export scripts from a standard [JavaScript ES6 module](xref:blazor/js-interop/index#javascript-isolation-in-javascript-modules) collocated with a component, which is covered in the <xref:blazor/js-interop/index#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component> article or placed with other JavaScript static assets.

In the following example, a JS function named `getMessage` is exported from a collocated JS file that returns a welcome message string, "Hello from Blazor!" in Portuguese:

`Pages/CallJavaScript.razor.js`:

guardrex marked this conversation as resolved.
Show resolved Hide resolved
```javascript
export function getMessage() {
return 'Olá do Blazor!';
}
```

## Call .NET from JavaScript

This section explains how to call .NET methods from JS.

> [!NOTE]
> Add the <xref:Microsoft.Build.Tasks.Csc.AllowUnsafeBlocks> property to the app's project file. Setting this property to `true` permits the code generator in the Roslyn compiler to use pointers and isn't a security concern.

To export a .NET method so that it can be called from JS, use the `[JSExport]` attribute.

In the following example:

* `DotNetInteropCall` is a .NET method with the `[JSExport]` attribute that returns a welcome message string, "Hello from Blazor!" in Portuguese.
* `SetWelcomeMessage` calls a JS function named `setmessage`. The JS function calls into .NET to receive the welcome string from `DotNetInteropCall` and displays the welcome string in the UI.
guardrex marked this conversation as resolved.
Show resolved Hide resolved

`Interop.cs`:

```csharp
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;

[SupportedOSPlatform("browser")]
public partial class Interop
{
[JSExport]
internal static string DotNetInteropCall()
guardrex marked this conversation as resolved.
Show resolved Hide resolved
{
return "Olá do Blazor!";
}

[JSImport("setMessage", "Interop2")]
internal static partial void SetWelcomeMessage();
}
```

The following `CallDotNet` component calls JS that directly interacts with the DOM to render the welcome message:

* The `Interop2` [JS module](xref:blazor/js-interop/index#javascript-isolation-in-javascript-modules) is imported asychronously from the collocated JS file for this component.
* The imported `setMessage` JS function is called with `Interop.SetWelcomeMessage()`.
guardrex marked this conversation as resolved.
Show resolved Hide resolved
* The returned welcome string is displayed by `setMessage` in the UI via the `message` field.

> [!IMPORTANT]
> In this section's example, JS interop is used to mutate a DOM element *purely for demonstration purposes* after the component is rendered in [`OnAfterRenderAsync`](xref:blazor/components/lifecycle#after-component-render-onafterrenderasync). Typically, you should only mutate the DOM with JS when the object doesn't interact with Blazor. The approach shown in this section is similar to cases where a third-party JS library is used in a Razor component, where the component interacts with the JS library via JS interop, the third-party JS library interacts with part of the DOM, and Blazor isn't involved directly with the DOM updates to that part of the DOM. For more information, see <xref:blazor/js-interop/index#interaction-with-the-document-object-model-dom>.

```razor
@page "/call-dotnet"
@using System.Runtime.InteropServices.JavaScript

<h1>.NET JS <code>[JSImport]</code>/<code>[JSExport]</code> (Call .NET)</h1>

<p>
<span id="result">.NET method not executed yet</span>
</p>

@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (OperatingSystem.IsBrowser() && firstRender)
{
await JSHost.ImportAsync("Interop2", "../Pages/CallDotNet.razor.js");

Interop.SetWelcomeMessage();
}
}
}
```

> [!NOTE]
> As demonstrated in the [Call JavaScript from .NET](#call-javascript-from-net) section, call <xref:System.Runtime.InteropServices.JavaScript.JSObject.Dispose%2A?displayProperty=nameWithType> in a `Dispose` method if the component holds a reference to the <xref:System.Runtime.InteropServices.JavaScript.JSObject> returned by `JSHost.ImportAsync`.

> [!NOTE]
> The conditional check for <xref:System.OperatingSystem.IsBrowser%2A?displayProperty=nameWithType> in the preceding example ensures that the code is only called in Blazor WebAssembly apps running on the client. This is important for library code deployed as NuGet packages that might be referenced by developers of Blazor Server apps, where the preceding code can't execute. If you know for sure that the code is only implemented in Blazor WebAssembly apps, you can remove the check for <xref:System.OperatingSystem.IsBrowser%2A?displayProperty=nameWithType>.

In the following example, a JS function named `setMessage` is exported from a collocated JS file.

The `setMessage` method:

* Calls `globalThis.getDotnetRuntime(0)` to expose the WebAssembly .NET runtime instance for calling exported .NET methods.
* Obtains the app assembly's JS exports. The name of the app's assembly in the following example is `BlazorSample`.
* Calls the `Interop.DotNetInteropCall()` method from the exports (`exports`). The returned value, which is the welcome string, is assigned to the preceding `<span>`'s inner text.

```javascript
export async function setMessage() {
const { getAssemblyExports } = await globalThis.getDotnetRuntime(0);
var exports = await getAssemblyExports("BlazorSample.dll");
guardrex marked this conversation as resolved.
Show resolved Hide resolved

document.getElementById("result").innerText =
exports.Interop.DotNetInteropCall();
guardrex marked this conversation as resolved.
Show resolved Hide resolved
}
```

## Additional example

For an additional example of the JS interop techniques described in this article, see the *Blazor WASM demo of new JSInterop* sample app:

* [Reference source (`pavelsavara/blazor-wasm-hands-pose` GitHub repository)](https://github.com/pavelsavara/blazor-wasm-hands-pose)
* [Live demonstration](https://pavelsavara.github.io/blazor-wasm-hands-pose/)

> [!NOTE]
> The [`pavelsavara/blazor-wasm-hands-pose` GitHub repository](https://github.com/SteveSandersonMS/BlazorOnGitHubPages) isn't owned, maintained, or supported by the .NET foundation or Microsoft.
guardrex marked this conversation as resolved.
Show resolved Hide resolved
>
> The *Blazor WASM demo of new JSInterop* sample app uses a public JS library from [MediaPipe](https://www.mediapipe.dev/), which isn't owned, maintained, or supported by the .NET foundation or Microsoft.
Loading