diff --git a/aspnetcore/blazor/javascript-interoperability/call-dotnet-from-javascript.md b/aspnetcore/blazor/javascript-interoperability/call-dotnet-from-javascript.md index 1478aa83a9f0..db253ac93ed2 100644 --- a/aspnetcore/blazor/javascript-interoperability/call-dotnet-from-javascript.md +++ b/aspnetcore/blazor/javascript-interoperability/call-dotnet-from-javascript.md @@ -2135,6 +2135,14 @@ In the preceding example: [!INCLUDE[](~/blazor/includes/js-interop/6.0/size-limits.md)] +## 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 interface, a JS `[JSImport]`/`[JSExport]` interop API is available to apps targeting .NET 7 or later. + +For more information, see . + ## Additional resources * diff --git a/aspnetcore/blazor/javascript-interoperability/call-javascript-from-dotnet.md b/aspnetcore/blazor/javascript-interoperability/call-javascript-from-dotnet.md index a97c5251cb21..d7d57244c81f 100644 --- a/aspnetcore/blazor/javascript-interoperability/call-javascript-from-dotnet.md +++ b/aspnetcore/blazor/javascript-interoperability/call-javascript-from-dotnet.md @@ -2436,79 +2436,6 @@ For information on using a byte array when calling .NET from JavaScript, see 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 - -``` - -[!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 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 . @@ -2652,6 +2579,22 @@ longRunningFn: 3 longRunningFn aborted! ``` +## 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 interface, a JS `[JSImport]`/`[JSExport]` interop API is available to apps targeting .NET 7 or later. + +For more information, see . + +## Unmarshalled JavaScript interop + +*This section applies to Blazor WebAssembly apps.* + +Unmarshalled interop using the interface is obsolete and should be replaced with JavaScript `[JSImport]`/`[JSExport]` interop. + +For more information, see . + ## Additional resources * diff --git a/aspnetcore/blazor/javascript-interoperability/import-export-interop.md b/aspnetcore/blazor/javascript-interoperability/import-export-interop.md new file mode 100644 index 000000000000..16ed5d83321a --- /dev/null +++ b/aspnetcore/blazor/javascript-interoperability/import-export-interop.md @@ -0,0 +1,374 @@ +--- +title: JavaScript `[JSImport]`/`[JSExport]` interop with ASP.NET Core Blazor WebAssembly +author: guardrex +description: Learn how to interact with JavaScript in Blazor WebAssembly apps using JavaScript `[JSImport]`/`[JSExport]` interop. +monikerRange: '= aspnetcore-7.0' +ms.author: riande +ms.custom: mvc +ms.date: 10/21/2022 +uid: blazor/js-interop/import-export-interop +--- +# JavaScript `[JSImport]`/`[JSExport]` interop with ASP.NET Core Blazor + +This article explains how to interact with JavaScript (JS) in Blazor WebAssembly apps using JavaScript (JS) `[JSImport]`/`[JSExport]` interop API released with .NET 7. + +Blazor provides its own JS interop mechanism based on the interface, which is uniformly supported across Blazor hosting models and described in the following articles: + +* +* + + enables library authors to build JS interop libraries that can be shared across the Blazor ecosystem and remains the recommended approach for JS interop in Blazor. + +This article describes an alternative JS interop approach specific to WebAssembly-based apps available for the first time with the release of .NET 7. These approaches are appropriate when you only expect to run on client-side WebAssembly and not in the other Blazor hosting models. Library authors can use these approaches to optimize JS interop by checking at runtime if the app is running on WebAssembly in a browser (). The approaches described in this article should be used to replace the obsolete unmarshalled JS interop API when migrating to .NET 7. + + + +## Obsolete JavaScript interop API + +Unmarshalled JS interop using 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)] + +## Namespace + +The JS interop API described in this article is controlled by attributes in the namespace. + +## Enable unsafe blocks + +Enable the property in app's project file, which permits the code generator in the Roslyn compiler to use pointers for JS interop: + +```xml + + true + +``` + +> [!WARNING] +> The JS interop API requires enabling . Be careful when implementing your own unsafe code in .NET apps, which can introduce security and stability risks. For more information, see [Unsafe code, pointer types, and function pointers](/dotnet/csharp/language-reference/unsafe-code). + +## Call JavaScript from .NET + +This section explains how to call JS functions from .NET. + +In the following `CallJavaScript1` component: + +* The `CallJavaScript1` module is imported asynchronously from the [collocated JS file](xref:blazor/js-interop/index#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component) with `JSHost.ImportAsync`. +* The imported `getMessage` JS function is called by `GetWelcomeMessage`. +* The returned welcome message string is displayed in the UI via the `message` field. + +`Pages/CallJavaScript1.razor`: + +```razor +@page "/call-javascript-1" +@using System.Runtime.InteropServices.JavaScript + +

+ JS [JSImport]/[JSExport] Interop + (Call JS Example 1) +

+ +@(message is not null ? message : string.Empty) + +@code { + private string? message; + + protected override async Task OnInitializedAsync() + { + await JSHost.ImportAsync("CallJavaScript1", + "../Pages/CallJavaScript1.razor.js"); + + message = GetWelcomeMessage(); + } +} +``` + +> [!NOTE] +> Code can include a conditional check for to ensure that the JS interop is only called in Blazor WebAssembly apps running on the client in a browser. This is important for libraries/NuGet packages that target Blazor hosting models that aren't based on WebAssembly, such as Blazor Server and Blazor Hybrid, which can't execute the code provided by this JS interop API. + +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 `CallJavaScript1`. 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 by `GetWelcomeMessage` in C# code. + +`Pages/CallJavaScript1.razor.cs`: + +```csharp +using System.Runtime.InteropServices.JavaScript; +using System.Runtime.Versioning; + +namespace BlazorSample.Pages; + +[SupportedOSPlatform("browser")] +public partial class CallJavaScript1 +{ + [JSImport("getMessage", "CallJavaScript1")] + internal static partial string GetWelcomeMessage(); +} +``` + +The app's namespace for the preceding `CallJavaScript1` partial class is `BlazorSample`. The component's namespace is `BlazorSample.Pages`. If using the preceding component in a local test app, update the namespace to match the app. For example, the namespace is `ContosoApp.Pages` if the app's namespace is `ContosoApp`. For more information, see . + +In the imported method signature, you can use .NET types for parameters and return values, which are marshalled automatically by the runtime. Use `JSMarshalAsAttribute` to control how the imported method parameters are marshalled. For example, you might choose to marshal a `long` as or . You can pass / 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 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, which are covered in the [Call .NET from JavaScript](#call-net-from-javascript) section later in this article. + +The module name in the `[JSImport]` attribute and the call to load the module in the component with `JSHost.ImportAsync` must match and 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 and a folder of user message interop classes (`UserMessages`): + +```csharp +[JSImport("getMessage", + "Contoso.InteropServices.JavaScript.UserMessages.CallJavaScript1")] +``` + +Export scripts from a standard [JavaScript ES6 module](xref:blazor/js-interop/index#javascript-isolation-in-javascript-modules) either [collocated with a component](xref:blazor/js-interop/index#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component) or placed with other JavaScript static assets in a JS file (for example, `wwwroot/js/{FILENAME}.js`, where JS static assets are maintained in a folder named `js` in the app's `wwwroot` folder and the `{FILENAME}` placeholder is the filename). + +In the following example, a JS function named `getMessage` is exported from a collocated JS file that returns a welcome message, "Hello from Blazor!" in Portuguese: + +`Pages/CallJavaScript1.razor.js`: + +```javascript +export function getMessage() { + return 'Olá do Blazor!'; +} +``` + +## Call .NET from JavaScript + +This section explains how to call .NET methods from JS. + +The following `CallDotNet1` component calls JS that directly interacts with the DOM to render the welcome message string: + +* The `CallDotNet` [JS module](xref:blazor/js-interop/index#javascript-isolation-in-javascript-modules) is imported asynchronously from the collocated JS file for this component. +* The imported `setMessage` JS function is called by `SetWelcomeMessage`. +* The returned welcome message 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 [`OnAfterRender`](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 . + +`Pages/CallDotNet1.razor`: + +```razor +@page "/call-dotnet-1" +@using System.Runtime.InteropServices.JavaScript + +

+ JS [JSImport]/[JSExport] Interop + (Call .NET Example 1) +

+ +

+ .NET method not executed yet +

+ +@code { + protected override async Task OnInitializedAsync() + { + await JSHost.ImportAsync("CallDotNet1", + "../Pages/CallDotNet1.razor.js"); + } + + protected override void OnAfterRender(bool firstRender) + { + SetWelcomeMessage(); + } +} +``` + +To export a .NET method so that it can be called from JS, use the `[JSExport]` attribute. + +In the following example: + +* `SetWelcomeMessage` calls a JS function named `setMessage`. The JS function calls into .NET to receive the welcome message from `GetMessageFromDotnet` and displays the message in the UI. +* `GetMessageFromDotnet` is a .NET method with the `[JSExport]` attribute that returns a welcome message, "Hello from Blazor!" in Portuguese. + +`Pages/CallDotNet1.razor.cs`: + +```csharp +using System.Runtime.InteropServices.JavaScript; +using System.Runtime.Versioning; + +namespace BlazorSample.Pages; + +[SupportedOSPlatform("browser")] +public partial class CallDotNet1 +{ + [JSImport("setMessage", "CallDotNet1")] + internal static partial void SetWelcomeMessage(); + + [JSExport] + internal static string GetMessageFromDotnet() + { + return "Olá do Blazor!"; + } +} +``` + +The app's namespace for the preceding `CallDotNet1` partial class is `BlazorSample`. The component's namespace is `BlazorSample.Pages`. If using the preceding component in a local test app, update the app's namespace to match the app. For example, the component namespace is `ContosoApp.Pages` if the app's namespace is `ContosoApp`. For more information, see . + +In the following example, a JS function named `setMessage` is imported 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 `BlazorSample.Pages.CallDotNet1.GetMessageFromDotnet` method from the exports (`exports`). The returned value, which is the welcome message, is assigned to the `CallDotNet1` component's `` text. The app's namespace is `BlazorSample`, and the `CallDotNet1` component's namespace is `BlazorSample.Pages`. + +`Pages/CallDotNet1.razor.js`: + +```javascript +export async function setMessage() { + const { getAssemblyExports } = await globalThis.getDotnetRuntime(0); + var exports = await getAssemblyExports("BlazorSample.dll"); + + document.getElementById("result").innerText = + exports.BlazorSample.Pages.CallDotNet1.GetMessageFromDotnet(); +} +``` + +> [!NOTE] +> Calling `getAssemblyExports` to obtain the exports can occur in a [JavaScript initializer](xref:blazor/js-interop/index#javascript-initializers) for availability across the app. + +## Multiple module import calls + +After a JS module is loaded, the module's JS functions are available to the app's components and classes as long as the app is running in the browser window or tab without the user manually reloading the app. `JSHost.ImportAsync` can be called multiple times on the same module without a significant performance penalty when: + +* The user visits a component that calls `JSHost.ImportAsync` to import a module, navigates away from the component, and then returns to the component where `JSHost.ImportAsync` is called again for the same module import. +* The same module is used by different components and loaded by `JSHost.ImportAsync` in each of the components. + +## Use of a single JavaScript module across components + +*Before following the guidance in this section, read the [Call JavaScript from .NET](#call-javascript-from-net) and [Call .NET from JavaScript](#call-net-from-javascript) sections of this article, which provide general guidance on `[JSImport]`/`[JSExport]` interop.* + +The example in this section shows how to use JS interop from a shared JS module in a Blazor WebAssembly app. The guidance in this section isn't applicable to Razor class libraries (RCLs). + +The following components, classes, C# methods, and JS functions are used: + +* `Interop` class (`Interop.cs`): Sets up import and export JS interop with the `[JSImport]` and `[JSExport]` attributes for a module named `Interop`. + * `GetWelcomeMessage`: .NET method that calls the imported `getMessage` JS function. + * `SetWelcomeMessage`: .NET method that calls the imported `setMessage` JS function. + * `GetMessageFromDotnet`: An exported C# method that returns a welcome message string when called from JS. +* `wwwroot/js/interop.js` file: Contains the JS functions. + * `getMessage`: Returns a welcome message when called by C# code in a component. + * `setMessage`: Calls the `GetMessageFromDotnet` C# method and assigns the returned welcome message to a DOM `` element. +* `Program.cs` calls `JSHost.ImportAsync` to load the module from `wwwroot/js/interop.js`. +* `CallJavaScript2` component (`Pages/CallJavaScript2.razor`): Calls `GetWelcomeMessage` and displays the returned welcome message in the component's UI. +* `CallDotNet2` component (`Pages/CallDotNet2.razor`): Calls `SetWelcomeMessage`. + +`Interop.cs`: + +```csharp +using System.Runtime.InteropServices.JavaScript; +using System.Runtime.Versioning; + +namespace BlazorSample.JavaScriptInterop; + +[SupportedOSPlatform("browser")] +public partial class Interop +{ + [JSImport("getMessage", "Interop")] + internal static partial string GetWelcomeMessage(); + + [JSImport("setMessage", "Interop")] + internal static partial void SetWelcomeMessage(); + + [JSExport] + internal static string GetMessageFromDotnet() + { + return "Olá do Blazor!"; + } +} +``` + +In the preceding example, the app's namespace is `BlazorSample`, and the full namespace for C# interop classes is `BlazorSample.JavaScriptInterop`. + +`wwwroot/js/interop.js`: + +```javascript +export function getMessage() { + return 'Olá do Blazor!'; +} + +export async function setMessage() { + const { getAssemblyExports } = await globalThis.getDotnetRuntime(0); + var exports = await getAssemblyExports("BlazorSample.dll"); + + document.getElementById("result").innerText = + exports.BlazorSample.JavaScriptInterop.Interop.GetMessageFromDotnet(); +} +``` + +Make the `System.Runtime.InteropServices.JavaScript` namespace available at the top of the `Program.cs` file: + +```csharp +using System.Runtime.InteropServices.JavaScript; +``` + +Load the module in `Program.cs` before is called: + +```csharp +await JSHost.ImportAsync("Interop", "../js/interop.js"); +``` + +`Pages/CallJavaScript2.razor`: + +```razor +@page "/call-javascript-2" +@using BlazorSample.JavaScriptInterop + +

+ JS [JSImport]/[JSExport] Interop + (Call JS Example 2) +

+ +@(message is not null ? message : string.Empty) + +@code { + private string? message; + + protected override void OnInitializedAsync() + { + message = Interop.GetWelcomeMessage(); + } +} +``` + +`Pages/CallDotNet2.razor`: + +```razor +@page "/call-dotnet-2" +@using BlazorSample.JavaScriptInterop + +

+ JS [JSImport]/[JSExport] Interop + (Call .NET Example 2) +

+ +

+ .NET method not executed +

+ +@code { + protected override void OnAfterRender(bool firstRender) + { + Interop.SetWelcomeMessage(); + } +} +``` + +> [!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 [`OnAfterRender`](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 . + + diff --git a/aspnetcore/blazor/javascript-interoperability/index.md b/aspnetcore/blazor/javascript-interoperability/index.md index ba889c47e3e2..85a6ebcbe066 100644 --- a/aspnetcore/blazor/javascript-interoperability/index.md +++ b/aspnetcore/blazor/javascript-interoperability/index.md @@ -636,7 +636,6 @@ The `{webassembly|server}` placeholder in the preceding markup is either `webass For more information on Blazor startup, see . - ## Cached JavaScript files JavaScript (JS) files and other static assets aren't generally cached on clients during development in the [`Development` environment](xref:fundamentals/index#environments). During development, static asset requests include the [`Cache-Control` header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control) with a value of [`no-cache`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control#cacheability) or [`max-age`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control#expiration) with a value of zero (`0`). @@ -667,6 +666,11 @@ Further JS interop guidance is provided in the following articles: * * +> [!NOTE] +> JavaScript `[JSImport]`/`[JSExport]` interop API is available for Blazor WebAssembly apps in ASP.NET Core 7.0 or later. +> +> For more information, see . + ## Interaction with the Document Object Model (DOM) Only mutate the Document Object Model (DOM) with JavaScript (JS) when the object doesn't interact with Blazor. Blazor maintains representations of the DOM and interacts directly with DOM objects. If an element rendered by Blazor is modified externally using JS directly or via JS Interop, the DOM may no longer match Blazor's internal representation, which can result in undefined behavior. Undefined behavior may merely interfere with the presentation of elements or their functions but may also introduce security risks to the app or server. @@ -707,8 +711,6 @@ JSON serializer support for and Blazor supports optimized byte array JS interop that avoids encoding/decoding byte arrays into Base64. The app can apply custom serialization and pass the resulting bytes. For more information, see . -Blazor supports unmarshalled JS interop when a high volume of .NET objects are rapidly serialized or when large .NET objects or many .NET objects must be serialized. For more information, see . - ## JavaScript initializers [!INCLUDE[](~/blazor/includes/js-initializers.md)] diff --git a/aspnetcore/blazor/performance.md b/aspnetcore/blazor/performance.md index e2031c8f7869..680df00ac62e 100644 --- a/aspnetcore/blazor/performance.md +++ b/aspnetcore/blazor/performance.md @@ -2772,32 +2772,11 @@ For Blazor WebAssembly apps, rolling individual JS interop calls into a single c [!INCLUDE[](~/blazor/includes/js-interop/synchronous-js-interop-call-dotnet.md)] -### Consider the use of unmarshalled calls - -*This section only applies to Blazor WebAssembly apps.* - -When running on Blazor WebAssembly, it's possible to make unmarshalled calls from .NET to JavaScript. These are synchronous calls that don't perform JSON serialization of arguments or return values. All aspects of memory management and translations between .NET and JavaScript representations are left up to the developer. - -> [!WARNING] -> While using has the least overhead of the JS interop approaches, the JavaScript APIs required to interact with these APIs are currently undocumented and subject to breaking changes in future releases. - -```javascript -function jsInteropCall() { - return BINDING.js_to_mono_obj("Hello world"); -} -``` +### Use JavaScript `[JSImport]`/`[JSExport]` interop -```razor -@inject IJSRuntime JS +JavaScript `[JSImport]`/`[JSExport]` interop for Blazor WebAssembly apps offers improved performance and stability over the JS interop API in framework releases prior to ASP.NET Core 7.0. -@code { - protected override void OnInitialized() - { - var unmarshalledJs = (IJSUnmarshalledRuntime)JS; - var value = unmarshalledJs.InvokeUnmarshalled("jsInteropCall"); - } -} -``` +For more information, see . ## Ahead-of-time (AOT) compilation diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index 9b32b880380b..67ad92bda195 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -457,6 +457,8 @@ items: uid: blazor/js-interop/call-javascript-from-dotnet - name: Call .NET from JavaScript uid: blazor/js-interop/call-dotnet-from-javascript + - name: JS [JSImport]/[JSExport] interop + uid: blazor/js-interop/import-export-interop - name: Call a web API uid: blazor/call-web-api - name: Images