diff --git a/package-lock.json b/package-lock.json index 00c0c746cf83..c9f11b07ccbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18388,14 +18388,21 @@ }, "src/JSInterop/Microsoft.JSInterop.JS/src": { "name": "@microsoft/dotnet-js-interop", - "version": "9.0.0-dev", + "version": "10.0.0-dev", "license": "MIT", "devDependencies": { + "@babel/core": "^7.23.6", + "@babel/preset-env": "^7.23.6", + "@babel/preset-typescript": "^7.26.0", "@typescript-eslint/eslint-plugin": "^6.15.0", "@typescript-eslint/parser": "^6.15.0", + "babel-jest": "^29.7.0", "eslint": "^8.56.0", "eslint-plugin-jsdoc": "^46.9.1", "eslint-plugin-prefer-arrow": "^1.2.3", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jest-junit": "^16.0.0", "rimraf": "^5.0.5", "typescript": "^5.3.3" } @@ -18675,6 +18682,22 @@ "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, + "src/JSInterop/Microsoft.JSInterop.JS/src/node_modules/jest-junit": { + "version": "16.0.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/jest-junit/-/jest-junit-16.0.0.tgz", + "integrity": "sha1-2DjoxWHPn91+tU9jAgd37uQTZ4U=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, "src/JSInterop/Microsoft.JSInterop.JS/src/node_modules/minimatch": { "version": "9.0.3", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/minimatch/-/minimatch-9.0.3.tgz", diff --git a/src/Components/Endpoints/src/DependencyInjection/UnsupportedJavaScriptRuntime.cs b/src/Components/Endpoints/src/DependencyInjection/UnsupportedJavaScriptRuntime.cs index 3c7af336b23a..d5a261a725f0 100644 --- a/src/Components/Endpoints/src/DependencyInjection/UnsupportedJavaScriptRuntime.cs +++ b/src/Components/Endpoints/src/DependencyInjection/UnsupportedJavaScriptRuntime.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.JSInterop; +using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components.Endpoints; @@ -9,9 +11,27 @@ internal sealed class UnsupportedJavaScriptRuntime : IJSRuntime { private const string Message = "JavaScript interop calls cannot be issued during server-side static rendering, because the page has not yet loaded in the browser. Statically-rendered components must wrap any JavaScript interop calls in conditional logic to ensure those interop calls are not attempted during static rendering."; - public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object?[]? args) + public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args) => throw new InvalidOperationException(Message); - ValueTask IJSRuntime.InvokeAsync(string identifier, object?[]? args) + ValueTask IJSRuntime.InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args) + => throw new InvalidOperationException(Message); + + public ValueTask InvokeNewAsync(string identifier, object?[]? args) + => throw new InvalidOperationException(Message); + + public ValueTask InvokeNewAsync(string identifier, CancellationToken cancellationToken, object?[]? args) + => throw new InvalidOperationException(Message); + + public ValueTask GetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier) + => throw new InvalidOperationException(Message); + + public ValueTask GetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken) + => throw new InvalidOperationException(Message); + + public ValueTask SetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value) + => throw new InvalidOperationException(Message); + + public ValueTask SetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value, CancellationToken cancellationToken) => throw new InvalidOperationException(Message); } diff --git a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs index 4ced7924b291..c9346439f1b0 100644 --- a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs +++ b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs @@ -105,6 +105,21 @@ protected override void SendByteArray(int id, byte[] data) } protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId) + { + var invocationInfo = new JSInvocationInfo + { + AsyncHandle = asyncHandle, + TargetInstanceId = targetInstanceId, + Identifier = identifier, + CallType = JSCallType.FunctionCall, + ResultType = resultType, + ArgsJson = argsJson, + }; + + BeginInvokeJS(invocationInfo); + } + + protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo) { if (_clientProxy is null) { @@ -123,9 +138,16 @@ protected override void BeginInvokeJS(long asyncHandle, string identifier, strin } } - Log.BeginInvokeJS(_logger, asyncHandle, identifier); + Log.BeginInvokeJS(_logger, invocationInfo.AsyncHandle, invocationInfo.Identifier); - _clientProxy.SendAsync("JS.BeginInvokeJS", asyncHandle, identifier, argsJson, (int)resultType, targetInstanceId); + _clientProxy.SendAsync( + "JS.BeginInvokeJS", + invocationInfo.AsyncHandle, + invocationInfo.Identifier, + invocationInfo.ArgsJson, + (int)invocationInfo.ResultType, + invocationInfo.TargetInstanceId, + (int)invocationInfo.CallType); } protected override void ReceiveByteArray(int id, byte[] data) diff --git a/src/Components/Server/test/ProtectedBrowserStorageTest.cs b/src/Components/Server/test/ProtectedBrowserStorageTest.cs index 89da1b351874..e42734f2bac1 100644 --- a/src/Components/Server/test/ProtectedBrowserStorageTest.cs +++ b/src/Components/Server/test/ProtectedBrowserStorageTest.cs @@ -352,19 +352,54 @@ private static string ProtectionPrefix(string purpose) class TestJSRuntime : IJSRuntime { - public List<(string Identifier, object[] Args)> Invocations { get; } - = new List<(string Identifier, object[] Args)>(); + public List<(string Identifier, object[] Args, JSCallType CallType)> Invocations { get; } = []; public object NextInvocationResult { get; set; } public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args) { - Invocations.Add((identifier, args)); + Invocations.Add((identifier, args, JSCallType.FunctionCall)); return (ValueTask)NextInvocationResult; } public ValueTask InvokeAsync(string identifier, object[] args) => InvokeAsync(identifier, cancellationToken: CancellationToken.None, args: args); + + public ValueTask InvokeNewAsync(string identifier, object[] args) + { + Invocations.Add((identifier, args, JSCallType.NewCall)); + return (ValueTask)NextInvocationResult; + } + + public ValueTask InvokeNewAsync(string identifier, CancellationToken cancellationToken, object[] args) + { + Invocations.Add((identifier, args, JSCallType.NewCall)); + return (ValueTask)NextInvocationResult; + } + + public ValueTask GetValueAsync(string identifier) + { + Invocations.Add((identifier, [], JSCallType.GetValue)); + return (ValueTask)NextInvocationResult; + } + + public ValueTask GetValueAsync(string identifier, CancellationToken cancellationToken) + { + Invocations.Add((identifier, [], JSCallType.GetValue)); + return (ValueTask)NextInvocationResult; + } + + public ValueTask SetValueAsync(string identifier, TValue value) + { + Invocations.Add((identifier, [value], JSCallType.SetValue)); + return ValueTask.CompletedTask; + } + + public ValueTask SetValueAsync(string identifier, TValue value, CancellationToken cancellationToken) + { + Invocations.Add((identifier, [value], JSCallType.SetValue)); + return ValueTask.CompletedTask; + } } class TestProtectedBrowserStorage : ProtectedBrowserStorage diff --git a/src/Components/Web.JS/src/Boot.WebAssembly.Common.ts b/src/Components/Web.JS/src/Boot.WebAssembly.Common.ts index dd11e0958fdd..a5bb02d5b80d 100644 --- a/src/Components/Web.JS/src/Boot.WebAssembly.Common.ts +++ b/src/Components/Web.JS/src/Boot.WebAssembly.Common.ts @@ -14,6 +14,7 @@ import { addDispatchEventMiddleware } from './Rendering/WebRendererInteropMethod import { WebAssemblyComponentDescriptor, WebAssemblyServerOptions, discoverWebAssemblyPersistedState } from './Services/ComponentDescriptorDiscovery'; import { receiveDotNetDataStream } from './StreamingInterop'; import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher'; +import { DotNet } from '@microsoft/dotnet-js-interop'; import { MonoConfig } from '@microsoft/dotnet-runtime'; import { RootComponentManager } from './Services/RootComponentManager'; import { WebRendererId } from './Rendering/WebRendererId'; @@ -263,12 +264,12 @@ async function scheduleAfterStarted(operations: string): Promise { Blazor._internal.updateRootComponents(operations); } -function invokeJSJson(identifier: string, targetInstanceId: number, resultType: number, argsJson: string, asyncHandle: number): string | null { +function invokeJSJson(identifier: string, targetInstanceId: number, resultType: number, argsJson: string, asyncHandle: number, callType: number): string | null { if (asyncHandle !== 0) { - dispatcher.beginInvokeJSFromDotNet(asyncHandle, identifier, argsJson, resultType, targetInstanceId); + dispatcher.beginInvokeJSFromDotNet(asyncHandle, identifier, argsJson, resultType, targetInstanceId, callType); return null; } else { - return dispatcher.invokeJSFromDotNet(identifier, argsJson, resultType, targetInstanceId); + return dispatcher.invokeJSFromDotNet(identifier, argsJson, resultType, targetInstanceId, callType); } } diff --git a/src/Components/Web.JS/src/GlobalExports.ts b/src/Components/Web.JS/src/GlobalExports.ts index 71d9b052c177..261ed2c2ce5d 100644 --- a/src/Components/Web.JS/src/GlobalExports.ts +++ b/src/Components/Web.JS/src/GlobalExports.ts @@ -52,7 +52,7 @@ export interface IBlazor { forceCloseConnection?: () => Promise; InputFile?: typeof InputFile; NavigationLock: typeof NavigationLock; - invokeJSJson?: (identifier: string, targetInstanceId: number, resultType: number, argsJson: string, asyncHandle: number) => string | null; + invokeJSJson?: (identifier: string, targetInstanceId: number, resultType: number, argsJson: string, asyncHandle: number, callType: number) => string | null; endInvokeDotNetFromJS?: (callId: string, success: boolean, resultJsonOrErrorMessage: string) => void; receiveByteArray?: (id: number, data: Uint8Array) => void; getPersistedState?: () => string; diff --git a/src/Components/Web/src/Internal/IInternalWebJSInProcessRuntime.cs b/src/Components/Web/src/Internal/IInternalWebJSInProcessRuntime.cs index dc3383f45ced..86d91a3fb2dd 100644 --- a/src/Components/Web/src/Internal/IInternalWebJSInProcessRuntime.cs +++ b/src/Components/Web/src/Internal/IInternalWebJSInProcessRuntime.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using Microsoft.JSInterop; +using Microsoft.JSInterop.Infrastructure; namespace Microsoft.AspNetCore.Components.Web.Internal; @@ -17,4 +18,9 @@ public interface IInternalWebJSInProcessRuntime /// For internal framework use only. /// string InvokeJS(string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson, JSCallResultType resultType, long targetInstanceId); + + /// + /// For internal framework use only. + /// + string InvokeJS(in JSInvocationInfo invocationInfo); } diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 397955cb832c..99365e10804e 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -1,2 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Components.Web.Internal.IInternalWebJSInProcessRuntime.InvokeJS(in Microsoft.JSInterop.Infrastructure.JSInvocationInfo invocationInfo) -> string! virtual Microsoft.AspNetCore.Components.Routing.NavLink.ShouldMatch(string! uriAbsolute) -> bool \ No newline at end of file diff --git a/src/Components/WebAssembly/JSInterop/src/InternalCalls.cs b/src/Components/WebAssembly/JSInterop/src/InternalCalls.cs index 0b35b9509204..4ad543a146ce 100644 --- a/src/Components/WebAssembly/JSInterop/src/InternalCalls.cs +++ b/src/Components/WebAssembly/JSInterop/src/InternalCalls.cs @@ -23,7 +23,8 @@ public static partial string InvokeJSJson( [JSMarshalAs] long targetInstanceId, int resultType, string argsJson, - [JSMarshalAs] long asyncHandle); + [JSMarshalAs] long asyncHandle, + int callType); [JSImport("Blazor._internal.endInvokeDotNetFromJS", "blazor-internal")] public static partial void EndInvokeDotNetFromJS( diff --git a/src/Components/WebAssembly/JSInterop/src/PublicAPI.Unshipped.txt b/src/Components/WebAssembly/JSInterop/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..b92576acb036 100644 --- a/src/Components/WebAssembly/JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/Components/WebAssembly/JSInterop/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +override Microsoft.JSInterop.WebAssembly.WebAssemblyJSRuntime.BeginInvokeJS(in Microsoft.JSInterop.Infrastructure.JSInvocationInfo invocationInfo) -> void +override Microsoft.JSInterop.WebAssembly.WebAssemblyJSRuntime.InvokeJS(in Microsoft.JSInterop.Infrastructure.JSInvocationInfo invocationInfo) -> string! diff --git a/src/Components/WebAssembly/JSInterop/src/WebAssemblyJSRuntime.cs b/src/Components/WebAssembly/JSInterop/src/WebAssemblyJSRuntime.cs index 21f62b89f369..827d36dc8ac6 100644 --- a/src/Components/WebAssembly/JSInterop/src/WebAssemblyJSRuntime.cs +++ b/src/Components/WebAssembly/JSInterop/src/WebAssemblyJSRuntime.cs @@ -24,10 +24,30 @@ protected WebAssemblyJSRuntime() /// protected override string InvokeJS(string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson, JSCallResultType resultType, long targetInstanceId) + { + return InternalCalls.InvokeJSJson( + identifier, + targetInstanceId, + (int)resultType, + argsJson ?? "[]", + 0, + (int)JSCallType.FunctionCall + ); + } + + /// + protected override string InvokeJS(in JSInvocationInfo invocationInfo) { try { - return InternalCalls.InvokeJSJson(identifier, targetInstanceId, (int)resultType, argsJson ?? "[]", 0); + return InternalCalls.InvokeJSJson( + invocationInfo.Identifier, + invocationInfo.TargetInstanceId, + (int)invocationInfo.ResultType, + invocationInfo.ArgsJson, + invocationInfo.AsyncHandle, + (int)invocationInfo.CallType + ); } catch (Exception ex) { @@ -38,7 +58,27 @@ protected override string InvokeJS(string identifier, [StringSyntax(StringSyntax /// protected override void BeginInvokeJS(long asyncHandle, string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson, JSCallResultType resultType, long targetInstanceId) { - InternalCalls.InvokeJSJson(identifier, targetInstanceId, (int)resultType, argsJson ?? "[]", asyncHandle); + InternalCalls.InvokeJSJson( + identifier, + targetInstanceId, + (int)resultType, + argsJson ?? "[]", + asyncHandle, + (int)JSCallType.FunctionCall + ); + } + + /// + protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo) + { + InternalCalls.InvokeJSJson( + invocationInfo.Identifier, + invocationInfo.TargetInstanceId, + (int)invocationInfo.ResultType, + invocationInfo.ArgsJson, + invocationInfo.AsyncHandle, + (int)invocationInfo.CallType + ); } /// diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/test/RemoteAuthenticationServiceTests.cs b/src/Components/WebAssembly/WebAssembly.Authentication/test/RemoteAuthenticationServiceTests.cs index 79f3bb98b680..9477611fafde 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/test/RemoteAuthenticationServiceTests.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/test/RemoteAuthenticationServiceTests.cs @@ -543,6 +543,24 @@ public ValueTask InvokeAsync(string identifier, CancellationToke return new ValueTask((TValue)GetInvocationResult(identifier)); } + public ValueTask GetValueAsync(string identifier) + => throw new NotImplementedException(); + + public ValueTask GetValueAsync(string identifier, CancellationToken cancellationToken) + => throw new NotImplementedException(); + + public ValueTask InvokeNewAsync(string identifier, object[] args) + => throw new NotImplementedException(); + + public ValueTask InvokeNewAsync(string identifier, CancellationToken cancellationToken, object[] args) + => throw new NotImplementedException(); + + public ValueTask SetValueAsync(string identifier, TValue value) + => throw new NotImplementedException(); + + public ValueTask SetValueAsync(string identifier, TValue value, CancellationToken cancellationToken) + => throw new NotImplementedException(); + private object GetInvocationResult(string identifier) { switch (identifier) diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/test/RemoteAuthenticatorCoreTests.cs b/src/Components/WebAssembly/WebAssembly.Authentication/test/RemoteAuthenticatorCoreTests.cs index 68ce23ac5ffe..20ac527d6247 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/test/RemoteAuthenticatorCoreTests.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/test/RemoteAuthenticatorCoreTests.cs @@ -777,6 +777,7 @@ public override ValueTask SetSignOutState() private class TestJsRuntime : IJSRuntime { public (string identifier, object[] args) LastInvocation { get; set; } + public ValueTask InvokeAsync(string identifier, object[] args) { LastInvocation = (identifier, args); @@ -788,6 +789,24 @@ public ValueTask InvokeAsync(string identifier, CancellationToke LastInvocation = (identifier, args); return default; } + + public ValueTask InvokeNewAsync(string identifier, object[] args) + => throw new NotImplementedException(); + + public ValueTask InvokeNewAsync(string identifier, CancellationToken cancellationToken, object[] args) + => throw new NotImplementedException(); + + public ValueTask GetValueAsync(string identifier) + => throw new NotImplementedException(); + + public ValueTask GetValueAsync(string identifier, CancellationToken cancellationToken) + => throw new NotImplementedException(); + + public ValueTask SetValueAsync(string identifier, TValue value) + => throw new NotImplementedException(); + + public ValueTask SetValueAsync(string identifier, TValue value, CancellationToken cancellationToken) + => throw new NotImplementedException(); } public class TestRemoteAuthenticatorView : RemoteAuthenticatorViewCore diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs index 57850a705ebb..b033a3fd5849 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs @@ -168,4 +168,7 @@ protected override Task TransmitStreamAsync(long streamId, DotNetStreamReference string IInternalWebJSInProcessRuntime.InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId) => InvokeJS(identifier, argsJson, resultType, targetInstanceId); + + string IInternalWebJSInProcessRuntime.InvokeJS(in JSInvocationInfo invocationInfo) + => InvokeJS(invocationInfo); } diff --git a/src/Components/WebAssembly/WebAssembly/test/PullFromJSDataStreamTest.cs b/src/Components/WebAssembly/WebAssembly/test/PullFromJSDataStreamTest.cs index 59eafab522be..c84f9c5b1798 100644 --- a/src/Components/WebAssembly/WebAssembly/test/PullFromJSDataStreamTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/PullFromJSDataStreamTest.cs @@ -118,7 +118,7 @@ public TestJSRuntime(byte[] data = default) _data = data; } - public virtual ValueTask InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, CancellationToken cancellationToken, object[] args) + public virtual ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args) { Assert.Equal("Blazor._internal.getJSDataStreamChunk", identifier); if (typeof(TValue) != typeof(byte[])) @@ -130,10 +130,26 @@ public TestJSRuntime(byte[] data = default) return ValueTask.FromResult((TValue)(object)_data.Skip((int)offset).Take(bytesToRead).ToArray()); } - public async ValueTask InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, object[] args) - { - return await InvokeAsync(identifier, CancellationToken.None, args); - } + public async ValueTask InvokeAsync(string identifier, object[] args) + => await InvokeAsync(identifier, CancellationToken.None, args); + + public ValueTask InvokeNewAsync(string identifier, object[] args) + => throw new NotImplementedException(); + + public ValueTask InvokeNewAsync(string identifier, CancellationToken cancellationToken, object[] args) + => throw new NotImplementedException(); + + public ValueTask GetValueAsync(string identifier) + => throw new NotImplementedException(); + + public ValueTask GetValueAsync(string identifier, CancellationToken cancellationToken) + => throw new NotImplementedException(); + + public ValueTask SetValueAsync(string identifier, TValue value) + => throw new NotImplementedException(); + + public ValueTask SetValueAsync(string identifier, TValue value, CancellationToken cancellationToken) + => throw new NotImplementedException(); } class TestJSRuntime_ProvidesInsufficientData : TestJSRuntime @@ -142,7 +158,7 @@ public TestJSRuntime_ProvidesInsufficientData(byte[] data) : base(data) { } - public override ValueTask InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, CancellationToken cancellationToken, object[] args) + public override ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args) { var offset = (long)args[1]; var bytesToRead = (int)args[2]; @@ -156,7 +172,7 @@ public TestJSRuntime_ProvidesExcessData(byte[] data) : base(data) { } - public override ValueTask InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, CancellationToken cancellationToken, object[] args) + public override ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args) { var offset = (long)args[1]; var bytesToRead = (int)args[2]; diff --git a/src/Components/WebView/WebView/src/IpcSender.cs b/src/Components/WebView/WebView/src/IpcSender.cs index 60edbd12a8c0..0df2f54f5fc8 100644 --- a/src/Components/WebView/WebView/src/IpcSender.cs +++ b/src/Components/WebView/WebView/src/IpcSender.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.ExceptionServices; using Microsoft.AspNetCore.Components.RenderTree; -using Microsoft.JSInterop; +using Microsoft.JSInterop.Infrastructure; namespace Microsoft.AspNetCore.Components.WebView; @@ -49,9 +49,17 @@ public void AttachToDocument(int componentId, string selector) DispatchMessageWithErrorHandling(IpcCommon.Serialize(IpcCommon.OutgoingMessageType.AttachToDocument, componentId, selector)); } - public void BeginInvokeJS(long taskId, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId) + public void BeginInvokeJS(in JSInvocationInfo invocationInfo) { - DispatchMessageWithErrorHandling(IpcCommon.Serialize(IpcCommon.OutgoingMessageType.BeginInvokeJS, taskId, identifier, argsJson, resultType, targetInstanceId)); + DispatchMessageWithErrorHandling(IpcCommon.Serialize( + IpcCommon.OutgoingMessageType.BeginInvokeJS, + invocationInfo.AsyncHandle, + invocationInfo.Identifier, + invocationInfo.ArgsJson, + invocationInfo.ResultType, + invocationInfo.TargetInstanceId, + invocationInfo.CallType + )); } public void EndInvokeDotNet(string callId, bool success, string invocationResultOrError) diff --git a/src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs b/src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs index 6633bbb4bc5b..7295575068cf 100644 --- a/src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs +++ b/src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs @@ -29,13 +29,28 @@ public void AttachToWebView(IpcSender ipcSender) public JsonSerializerOptions ReadJsonSerializerOptions() => JsonSerializerOptions; protected override void BeginInvokeJS(long taskId, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId) + { + var invocationInfo = new JSInvocationInfo + { + AsyncHandle = taskId, + Identifier = identifier, + ArgsJson = argsJson, + CallType = JSCallType.FunctionCall, + ResultType = resultType, + TargetInstanceId = targetInstanceId, + }; + + BeginInvokeJS(invocationInfo); + } + + protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo) { if (_ipcSender is null) { throw new InvalidOperationException("Cannot invoke JavaScript outside of a WebView context."); } - _ipcSender.BeginInvokeJS(taskId, identifier, argsJson, resultType, targetInstanceId); + _ipcSender.BeginInvokeJS(invocationInfo); } protected override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) diff --git a/src/Components/WebView/WebView/test/Infrastructure/AssertHelpers.cs b/src/Components/WebView/WebView/test/Infrastructure/AssertHelpers.cs index 1a1338d13b28..3a625353eeee 100644 --- a/src/Components/WebView/WebView/test/Infrastructure/AssertHelpers.cs +++ b/src/Components/WebView/WebView/test/Infrastructure/AssertHelpers.cs @@ -1,7 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; +using System.Text.Json; using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.JSInterop.Infrastructure; namespace Microsoft.AspNetCore.Components.WebView; @@ -11,7 +14,7 @@ internal static void IsAttachWebRendererInteropMessage(string message) { Assert.True(IpcCommon.TryDeserializeOutgoing(message, out var messageType, out var args)); Assert.Equal(IpcCommon.OutgoingMessageType.BeginInvokeJS, messageType); - Assert.Equal(5, args.Count); + Assert.Equal(6, args.Count); Assert.Equal("Blazor._internal.attachWebRendererInterop", args[1].GetString()); } diff --git a/src/Components/test/E2ETest/Tests/InteropTest.cs b/src/Components/test/E2ETest/Tests/InteropTest.cs index f85e4036f398..bb0e371cb902 100644 --- a/src/Components/test/E2ETest/Tests/InteropTest.cs +++ b/src/Components/test/E2ETest/Tests/InteropTest.cs @@ -28,7 +28,7 @@ protected override void InitializeAsyncCore() } [Fact] - public void CanInvokeDotNetMethods() + public void CanInvokeInteropMethods() { // Arrange var expectedAsyncValues = new Dictionary @@ -92,6 +92,21 @@ public void CanInvokeDotNetMethods() ["invokeAsyncThrowsUndefinedJSObjectReference"] = "Success", ["invokeAsyncThrowsNullJSObjectReference"] = "Success", ["disposeJSObjectReferenceAsync"] = "Success", + // GetValue tests + ["getValueFromDataPropertyAsync"] = "10", + ["getValueFromGetterAsync"] = "20", + ["getValueFromSetterAsync"] = "Success", + ["getValueFromUndefinedPropertyAsync"] = "Success", + // SetValueTests + ["setValueToDataPropertyAsync"] = "30", + ["setValueToSetterAsync"] = "40", + ["setValueToUndefinedPropertyAsync"] = "50", + ["setValueToGetterAsync"] = "Success", + // InvokeNew tests + ["invokeNewWithClassConstructorAsync"] = "Success", + ["invokeNewWithClassConstructorAsync.dataProperty"] = "abraka", + ["invokeNewWithClassConstructorAsync.function"] = "6", + ["invokeNewWithNonConstructorAsync"] = "Success", }; var expectedSyncValues = new Dictionary @@ -141,6 +156,21 @@ public void CanInvokeDotNetMethods() ["genericInstanceMethod"] = @"""Updated value 2""", ["requestDotNetStreamReference"] = @"""Success""", ["requestDotNetStreamWrapperReference"] = @"""Success""", + // GetValue tests + ["getValueFromDataProperty"] = "10", + ["getValueFromGetter"] = "20", + ["getValueFromSetter"] = "Success", + ["getValueFromUndefinedProperty"] = "Success", + // SetValue tests + ["setValueToDataProperty"] = "30", + ["setValueToSetter"] = "40", + ["setValueToUndefinedProperty"] = "50", + ["setValueToGetter"] = "Success", + // InvokeNew tests + ["invokeNewWithClassConstructor"] = "Success", + ["invokeNewWithClassConstructor.dataProperty"] = "abraka", + ["invokeNewWithClassConstructor.function"] = "6", + ["invokeNewWithNonConstructor"] = "Success", }; // Include the sync assertions only when running under WebAssembly diff --git a/src/Components/test/testassets/BasicTestApp/DotNetToJSInterop.razor b/src/Components/test/testassets/BasicTestApp/DotNetToJSInterop.razor new file mode 100644 index 000000000000..54641d49b69c --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/DotNetToJSInterop.razor @@ -0,0 +1,175 @@ +@using Microsoft.JSInterop +@inject IJSRuntime JSRuntime + + + +

DotNetToJSInterop

+ +
+ +
+ +
+ Message: + +
+ +
+ Title: + + +
+ +
+ + @CurrentTitle +
+ +
+ @TestObjectDisplay
+ + +
+ +
+ + + @InstanceMessage +
+ +
+ + + @ErrorMessage +
+ +@code { + private string Message { get; set; } + private string CurrentTitle { get; set; } + private string NewTitle { get; set; } + private string TestObjectDisplay { get; set; } + private string InstanceMessage { get; set; } + private string ErrorMessage { get; set; } + + private async Task LogMessageWithInvoke(string message) + { + await JSRuntime.InvokeVoidAsync("logMessage", message); + } + + private async Task SetDocumentTitleWithInvoke(string title) + { + await JSRuntime.InvokeVoidAsync("setDocumentTitle", title); + } + + private async Task SetDocumentTitleWithSetValue(string title) + { + await JSRuntime.SetValueAsync("document.title", title); + } + + private async Task GetDocumentTitleWithGetValue() + { + CurrentTitle = await JSRuntime.GetValueAsync("document.title"); + } + + private async Task GetObjectModelWithInvoke() + { + var model = await JSRuntime.InvokeAsync("getTestObject"); + TestObjectDisplay = $"State loaded from model with Invoke: {model.Num} | {model.Text} | {model.GetOnlyProperty}"; + } + + private async Task GetObjectPropertiesWithGetValue() + { + var objectRef = await JSRuntime.InvokeAsync("getTestObject"); + var numValue = await objectRef.GetValueAsync("num"); + var textValue = await objectRef.GetValueAsync("text"); + var getOnlyProperty = await objectRef.GetValueAsync("getOnlyProperty"); + TestObjectDisplay = $"State loaded from properties with GetValue: {numValue} | {textValue} | {getOnlyProperty}"; + } + + private async Task CreateInstanceByConstructorFunction() + { + var dogRef = await JSRuntime.InvokeNewAsync("Dog", ["Igor"]); + InstanceMessage = await dogRef.InvokeAsync("bark"); + } + + private async Task CreateInstanceByClassConstructor() + { + var catRef = await JSRuntime.InvokeNewAsync("Cat", ["Whiskers"]); + InstanceMessage = await catRef.InvokeAsync("meow"); + } + + private async Task GetInvalid(MouseEventArgs args) + { + var value = await JSRuntime.GetValueAsync("testObject.setOnlyProperty"); + } + + private async Task SetInvalid(MouseEventArgs args) + { + await JSRuntime.SetValueAsync("testObject.getOnlyProperty", 123); + } + + class TestObjectModel + { + public int Num { get; set; } + public string Text { get; set; } + public int GetOnlyProperty { get; set; } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index 3ce2eb27dc95..1b6be21e915f 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -67,6 +67,7 @@ + diff --git a/src/Components/test/testassets/BasicTestApp/InteropComponent.razor b/src/Components/test/testassets/BasicTestApp/InteropComponent.razor index 6310b0e745be..1009732d9e87 100644 --- a/src/Components/test/testassets/BasicTestApp/InteropComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/InteropComponent.razor @@ -280,6 +280,27 @@ var dotNetStreamReferenceWrapper = DotNetStreamReferenceInterop.GetDotNetStreamWrapperReference(); ReturnValues["dotNetToJSReceiveDotNetStreamWrapperReferenceAsync"] = await JSRuntime.InvokeAsync("jsInteropTests.receiveDotNetStreamWrapperReference", dotNetStreamReferenceWrapper); + await GetValueAsyncTests(); + + if (shouldSupportSyncInterop) + { + GetValueSyncTests(); + } + + await SetValueAsyncTests(); + + if (shouldSupportSyncInterop) + { + SetValueTests(); + } + + await InvokeNewAsyncTests(); + + if (shouldSupportSyncInterop) + { + InvokeNewTests(); + } + Invocations = invocations; DoneWithInterop = true; } @@ -410,6 +431,176 @@ } } + private async Task GetValueAsyncTests() + { + ReturnValues["getValueFromDataPropertyAsync"] = (await JSRuntime.GetValueAsync("jsInteropTests.testObject.num")).ToString(); + ReturnValues["getValueFromGetterAsync"] = (await JSRuntime.GetValueAsync("jsInteropTests.testObject.getOnlyProperty")).ToString(); + + try + { + var _ = await JSRuntime.GetValueAsync("jsInteropTests.testObject.setOnlyProperty"); + } + catch (JSException) + { + ReturnValues["getValueFromSetterAsync"] = "Success"; + } + catch (Exception ex) + { + ReturnValues["getValueFromSetterAsync"] = $"Failure: {ex.Message}"; + } + + try + { + var _ = await JSRuntime.GetValueAsync("jsInteropTests.testObject.undefinedProperty"); + } + catch (JSException) + { + ReturnValues["getValueFromUndefinedPropertyAsync"] = "Success"; + } + catch (Exception ex) + { + ReturnValues["getValueFromUndefinedPropertyAsync"] = $"Failure: {ex.Message}"; + } + } + + private void GetValueSyncTests() + { + var inProcRuntime = ((IJSInProcessRuntime)JSRuntime); + + ReturnValues["getValueFromDataProperty"] = inProcRuntime.GetValue("jsInteropTests.testObject.num").ToString(); + ReturnValues["getValueFromGetter"] = inProcRuntime.GetValue("jsInteropTests.testObject.getOnlyProperty").ToString(); + + try + { + var _ = inProcRuntime.GetValue("jsInteropTests.testObject.setOnlyProperty"); + } + catch (JSException) + { + ReturnValues["getValueFromSetter"] = "Success"; + } + catch (Exception ex) + { + ReturnValues["getValueFromSetter"] = $"Failure: {ex.Message}"; + } + + try + { + var _ = inProcRuntime.GetValue("jsInteropTests.testObject.undefinedProperty"); + } + catch (JSException) + { + ReturnValues["getValueFromUndefinedProperty"] = "Success"; + } + catch (Exception ex) + { + ReturnValues["getValueFromUndefinedProperty"] = $"Failure: {ex.Message}"; + } + } + + private async Task SetValueAsyncTests() + { + ReturnValues["getValueFromGetterAsync"] = (await JSRuntime.GetValueAsync("jsInteropTests.testObject.getOnlyProperty")).ToString(); + + await JSRuntime.SetValueAsync("jsInteropTests.testObject.num", 30); + ReturnValues["setValueToDataPropertyAsync"] = (await JSRuntime.GetValueAsync("jsInteropTests.testObject.num")).ToString(); + + await JSRuntime.SetValueAsync("jsInteropTests.testObject.setOnlyProperty", 40); + ReturnValues["setValueToSetterAsync"] = (await JSRuntime.GetValueAsync("jsInteropTests.testObject.num")).ToString(); + + await JSRuntime.SetValueAsync("jsInteropTests.testObject.newProperty", 50); + ReturnValues["setValueToUndefinedPropertyAsync"] = (await JSRuntime.GetValueAsync("jsInteropTests.testObject.newProperty")).ToString(); + + try + { + await JSRuntime.SetValueAsync("jsInteropTests.testObject.getOnlyProperty", 50); + } + catch (JSException) + { + ReturnValues["setValueToGetterAsync"] = "Success"; + } + catch (Exception ex) + { + ReturnValues["setValueToGetterAsync"] = $"Failure: {ex.Message}"; + } + } + + private void SetValueTests() + { + var inProcRuntime = ((IJSInProcessRuntime)JSRuntime); + + ReturnValues["getValueFromGetter"] = inProcRuntime.GetValue("jsInteropTests.testObject.getOnlyProperty").ToString(); + + inProcRuntime.SetValue("jsInteropTests.testObject.num", 30); + ReturnValues["setValueToDataProperty"] = inProcRuntime.GetValue("jsInteropTests.testObject.num").ToString(); + + inProcRuntime.SetValue("jsInteropTests.testObject.setOnlyProperty", 40); + ReturnValues["setValueToSetter"] = inProcRuntime.GetValue("jsInteropTests.testObject.num").ToString(); + + inProcRuntime.SetValue("jsInteropTests.testObject.newProperty", 50); + ReturnValues["setValueToUndefinedProperty"] = inProcRuntime.GetValue("jsInteropTests.testObject.newProperty").ToString(); + + try + { + inProcRuntime.SetValue("jsInteropTests.testObject.getOnlyProperty", 60); + } + catch (JSException) + { + ReturnValues["setValueToGetter"] = "Success"; + } + catch (Exception ex) + { + ReturnValues["setValueToGetter"] = $"Failure: {ex.Message}"; + } + } + + private async Task InvokeNewAsyncTests() + { + var testClassRef = await JSRuntime.InvokeNewAsync("jsInteropTests.TestClass", "abraka"); + + ReturnValues["invokeNewWithClassConstructorAsync"] = testClassRef is IJSObjectReference ? "Success" : "Failure"; + ReturnValues["invokeNewWithClassConstructorAsync.dataProperty"] = await testClassRef.GetValueAsync("text"); + ReturnValues["invokeNewWithClassConstructorAsync.function"] = (await testClassRef.InvokeAsync("getTextLength")).ToString(); + + try + { + var nonConstructorRef = await JSRuntime.InvokeNewAsync("jsInteropTests.nonConstructorFunction"); + ReturnValues["invokeNewWithNonConstructorAsync"] = nonConstructorRef is null ? "Failure: null" : "Failure: not null"; + } + catch (JSException) + { + ReturnValues["invokeNewWithNonConstructorAsync"] = "Success"; + } + catch (Exception ex) + { + ReturnValues["invokeNewWithNonConstructorAsync"] = $"Failure: {ex.Message}"; + } + } + + private void InvokeNewTests() + { + var inProcRuntime = ((IJSInProcessRuntime)JSRuntime); + + var testClassRef = inProcRuntime.InvokeNew("jsInteropTests.TestClass", "abraka"); + + ReturnValues["invokeNewWithClassConstructor"] = testClassRef is IJSInProcessObjectReference ? "Success" : "Failure"; + ReturnValues["invokeNewWithClassConstructor.dataProperty"] = testClassRef.GetValue("text"); + ReturnValues["invokeNewWithClassConstructor.function"] = testClassRef.Invoke("getTextLength").ToString(); + + try + { + var nonConstructorRef = inProcRuntime.InvokeNew("jsInteropTests.nonConstructorFunction"); + ReturnValues["invokeNewWithNonConstructor"] = nonConstructorRef is null ? "Failure: null" : "Failure: not null"; + } + catch (JSException) + { + ReturnValues["invokeNewWithNonConstructor"] = "Success"; + } + catch (Exception ex) + { + ReturnValues["invokeNewWithNonConstructor"] = $"Failure: {ex.Message}"; + } + } + public class PassDotNetObjectByRefArgs { public string StringValue { get; set; } diff --git a/src/Components/test/testassets/BasicTestApp/wwwroot/js/jsinteroptests.js b/src/Components/test/testassets/BasicTestApp/wwwroot/js/jsinteroptests.js index dac70d67e6ec..2a22736a58e2 100644 --- a/src/Components/test/testassets/BasicTestApp/wwwroot/js/jsinteroptests.js +++ b/src/Components/test/testassets/BasicTestApp/wwwroot/js/jsinteroptests.js @@ -217,6 +217,26 @@ function createArgumentList(argumentNumber, dotNetObjectByRef) { return array; } +class TestClass { + constructor(text) { + this.text = text; + } + + getTextLength() { + return this.text.length; + } +} + +const testObject = { + num: 10, + get getOnlyProperty() { + return 20; + }, + set setOnlyProperty(value) { + this.num = value; + } +} + window.jsInteropTests = { invokeDotNetInteropMethodsAsync: invokeDotNetInteropMethodsAsync, collectInteropResults: collectInteropResults, @@ -233,6 +253,9 @@ window.jsInteropTests = { receiveDotNetObjectByRefAsync: receiveDotNetObjectByRefAsync, receiveDotNetStreamReference: receiveDotNetStreamReference, receiveDotNetStreamWrapperReference: receiveDotNetStreamWrapperReference, + TestClass: TestClass, + nonConstructorFunction: () => { return 42; }, + testObject: testObject, }; function returnUndefined() { diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/babel.config.js b/src/JSInterop/Microsoft.JSInterop.JS/src/babel.config.js new file mode 100644 index 000000000000..8bd00d8d7eb0 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/babel.config.js @@ -0,0 +1,4 @@ +export const presets = [ + ['@babel/preset-env', { targets: { node: 'current' } }], + '@babel/preset-typescript', +]; \ No newline at end of file diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/jest.config.mjs b/src/JSInterop/Microsoft.JSInterop.JS/src/jest.config.mjs new file mode 100644 index 000000000000..757fa59aa88c --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/jest.config.mjs @@ -0,0 +1,33 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ + +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const ROOT_DIR = path.resolve(__dirname, "..", "..", "..", ".."); + +/** @type {import('jest').Config} */ + +const config = { + roots: ["/src", "/test"], + testMatch: ["**/*.test.(ts|js)"], + moduleFileExtensions: ["js", "ts"], + transform: { + "^.+\\.(js|ts)$": "babel-jest", + }, + moduleDirectories: ["node_modules", "src"], + testEnvironment: "jsdom", + reporters: [ + "default", + [ + path.resolve(ROOT_DIR, "node_modules", "jest-junit", "index.js"), + { "outputDirectory": path.resolve(ROOT_DIR, "artifacts", "log"), "outputName": `${process.platform}` + ".jsinterop.junit.xml" } + ] + ], +}; + +export default config; \ No newline at end of file diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/package.json b/src/JSInterop/Microsoft.JSInterop.JS/src/package.json index 76a5ad8e4cb9..4a91339cf24f 100644 --- a/src/JSInterop/Microsoft.JSInterop.JS/src/package.json +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/package.json @@ -1,12 +1,15 @@ { "name": "@microsoft/dotnet-js-interop", - "version": "9.0.0-dev", + "version": "10.0.0-dev", "description": "Provides abstractions and features for interop between .NET and JavaScript code.", "main": "dist/src/Microsoft.JSInterop.js", "types": "dist/src/Microsoft.JSInterop.d.ts", "type": "module", "scripts": { "clean": "rimraf ./dist", + "test": "jest", + "test:watch": "jest --watch", + "test:debug": "node --nolazy --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --colors --verbose", "build": "npm run clean && npm run build:esm", "build:lint": "eslint -c .eslintrc.json --ext .ts ./src", "build:esm": "tsc --project ./tsconfig.json", @@ -26,12 +29,19 @@ "dist/**" ], "devDependencies": { + "@babel/core": "^7.23.6", + "@babel/preset-env": "^7.23.6", + "@babel/preset-typescript": "^7.26.0", "@typescript-eslint/eslint-plugin": "^6.15.0", "@typescript-eslint/parser": "^6.15.0", + "babel-jest": "^29.7.0", "eslint": "^8.56.0", "eslint-plugin-jsdoc": "^46.9.1", "eslint-plugin-prefer-arrow": "^1.2.3", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jest-junit": "^16.0.0", "rimraf": "^5.0.5", "typescript": "^5.3.3" } -} +} \ No newline at end of file diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts index dafbf13a9687..909207da68e0 100644 --- a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts @@ -19,47 +19,56 @@ export module DotNet { let defaultCallDispatcher: CallDispatcher | null | undefined; // Provides access to the "current" call dispatcher without having to flow it through nested function calls. - let currentCallDispatcher : CallDispatcher | undefined; + let currentCallDispatcher: CallDispatcher | undefined; + + type InvocationHandlerCache = Map; class JSObject { - _cachedFunctions: Map; + // We cache resolved members. Note that this means we can return stale values if the object has been modified since then. + _cachedHandlers: InvocationHandlerCache;; // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(private _jsObject: any) { - this._cachedFunctions = new Map(); + this._cachedHandlers = new Map(); } - public findFunction(identifier: string) { - const cachedFunction = this._cachedFunctions.get(identifier); + public resolveInvocationHandler(identifier: string, callType: JSCallType): Function { + const cachedFunction = this._cachedHandlers.get(identifier)?.[callType]; if (cachedFunction) { return cachedFunction; } - let result: any = this._jsObject; - let lastSegmentValue: any; - - identifier.split(".").forEach(segment => { - if (segment in result) { - lastSegmentValue = result; - result = result[segment]; - } else { - throw new Error(`Could not find '${identifier}' ('${segment}' was undefined).`); - } - }); - - if (result instanceof Function) { - result = result.bind(lastSegmentValue); - this._cachedFunctions.set(identifier, result); - return result; - } + const [parent, memberName] = findObjectMember(this._jsObject, identifier); + const func = wrapJSCallAsFunction(parent, memberName, callType, identifier); + this.addHandlerToCache(identifier, func, callType); - throw new Error(`The value '${identifier}' is not a function.`); + return func; } public getWrappedObject() { return this._jsObject; } + + private addHandlerToCache(identifier: string, func: Function, callType: JSCallType) { + const cachedIdentifier = this._cachedHandlers.get(identifier); + + if (cachedIdentifier) { + cachedIdentifier[callType] = func; + } else { + this._cachedHandlers.set(identifier, { [callType]: func }); + } + } + } + + /** + * Represents the type of operation that should be performed in JS. + */ + export enum JSCallType { + FunctionCall = 1, + NewCall = 2, + GetValue = 3, + SetValue = 4 } const windowJSObjectId = 0; @@ -67,7 +76,8 @@ export module DotNet { [windowJSObjectId]: new JSObject(window) }; - cachedJSObjectsById[windowJSObjectId]._cachedFunctions.set("import", (url: any) => { + const windowObject = cachedJSObjectsById[windowJSObjectId]; + windowObject._cachedHandlers.set("import", { [JSCallType.FunctionCall]: (url: any) => { // In most cases developers will want to resolve dynamic imports relative to the base HREF. // However since we're the one calling the import keyword, they would be resolved relative to // this framework bundle URL. Fix this by providing an absolute URL. @@ -76,7 +86,7 @@ export module DotNet { } return import(/* webpackIgnore: true */ url); - }); + }}); let nextJsObjectId = 1; // Start at 1 because zero is reserved for "window" @@ -310,9 +320,10 @@ export module DotNet { * @param argsJson JSON representation of arguments to be passed to the function. * @param resultType The type of result expected from the JS interop call. * @param targetInstanceId The instance ID of the target JS object. + * @param callType The type of operation that should be performed in JS. * @returns JSON representation of the invocation result. */ - invokeJSFromDotNet(identifier: string, argsJson: string, resultType: JSCallResultType, targetInstanceId: number): string | null; + invokeJSFromDotNet(identifier: string, argsJson: string, resultType: JSCallResultType, targetInstanceId: number, callType: JSCallType): string | null; /** * Invokes the specified synchronous or asynchronous JavaScript function. @@ -322,8 +333,9 @@ export module DotNet { * @param argsJson JSON representation of arguments to be passed to the function. * @param resultType The type of result expected from the JS interop call. * @param targetInstanceId The ID of the target JS object instance. + * @param callType The type of operation that should be performed in JS. */ - beginInvokeJSFromDotNet(asyncHandle: number, identifier: string, argsJson: string | null, resultType: JSCallResultType, targetInstanceId: number): void; + beginInvokeJSFromDotNet(asyncHandle: number, identifier: string, argsJson: string | null, resultType: JSCallResultType, targetInstanceId: number, callType: JSCallType): Promise; /** * Receives notification that an async call from JS to .NET has completed. @@ -387,10 +399,8 @@ export module DotNet { return this._dotNetCallDispatcher; } - invokeJSFromDotNet(identifier: string, argsJson: string, resultType: JSCallResultType, targetInstanceId: number): string | null { - const args = parseJsonWithRevivers(this, argsJson); - const jsFunction = findJSFunction(identifier, targetInstanceId); - const returnValue = jsFunction(...(args || [])); + invokeJSFromDotNet(identifier: string, argsJson: string, resultType: JSCallResultType, targetInstanceId: number, callType: JSCallType): string | null { + const returnValue = this.processJSCall(targetInstanceId, identifier, callType, argsJson); const result = createJSCallResult(returnValue, resultType); return result === null || result === undefined @@ -398,37 +408,40 @@ export module DotNet { : stringifyArgs(this, result); } - beginInvokeJSFromDotNet(asyncHandle: number, identifier: string, argsJson: string | null, resultType: JSCallResultType, targetInstanceId: number): void { - // Coerce synchronous functions into async ones, plus treat - // synchronous exceptions the same as async ones - const promise = new Promise(resolve => { - const args = parseJsonWithRevivers(this, argsJson); - const jsFunction = findJSFunction(identifier, targetInstanceId); - const synchronousResultOrPromise = jsFunction(...(args || [])); - resolve(synchronousResultOrPromise); - }); - - // We only listen for a result if the caller wants to be notified about it - if (asyncHandle) { - // On completion, dispatch result back to .NET - // Not using "await" because it codegens a lot of boilerplate - promise. - then(result => stringifyArgs(this, [ - asyncHandle, - true, - createJSCallResult(result, resultType) - ])). - then( - result => this._dotNetCallDispatcher.endInvokeJSFromDotNet(asyncHandle, true, result), - error => this._dotNetCallDispatcher.endInvokeJSFromDotNet(asyncHandle, false, JSON.stringify([ - asyncHandle, - false, - formatError(error) - ])) - ); + async beginInvokeJSFromDotNet(asyncHandle: number, identifier: string, argsJson: string | null, resultType: JSCallResultType, targetInstanceId: number, callType: JSCallType): Promise { + try { + const valueOrPromise = this.processJSCall(targetInstanceId, identifier, callType, argsJson); + + // We only await the result if the caller wants to be notified about it + if (asyncHandle) { + const result = await valueOrPromise; + const serializedResult = stringifyArgs(this, [ + asyncHandle, + true, + createJSCallResult(result, resultType) + ]); + // On success, dispatch result back to .NET + this._dotNetCallDispatcher.endInvokeJSFromDotNet(asyncHandle, true, serializedResult); + } + } catch(error: any) { + if (asyncHandle) { + const serializedError = JSON.stringify([ + asyncHandle, + false, + formatError(error) + ]); + // On failure, dispatch error back to .NET + this._dotNetCallDispatcher.endInvokeJSFromDotNet(asyncHandle, false, serializedError); + } } } + processJSCall(targetInstanceId: number, identifier: string, callType: JSCallType, argsJson: string | null) { + const args = parseJsonWithRevivers(this, argsJson) ?? []; + const func = findJSFunction(identifier, targetInstanceId, callType); + return func(...args); + } + endInvokeDotNetFromJS(asyncCallId: string, success: boolean, resultJsonOrExceptionMessage: string): void { const resultOrError = success ? parseJsonWithRevivers(this, resultJsonOrExceptionMessage) @@ -445,14 +458,14 @@ export module DotNet { } invokeDotNetMethod(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): T | null { - if (this._dotNetCallDispatcher.invokeDotNetFromJS) { - const argsJson = stringifyArgs(this, args); - const resultJson = this._dotNetCallDispatcher.invokeDotNetFromJS(assemblyName, methodIdentifier, dotNetObjectId, argsJson); - return resultJson ? parseJsonWithRevivers(this, resultJson) : null; - } + if (this._dotNetCallDispatcher.invokeDotNetFromJS) { + const argsJson = stringifyArgs(this, args); + const resultJson = this._dotNetCallDispatcher.invokeDotNetFromJS(assemblyName, methodIdentifier, dotNetObjectId, argsJson); + return resultJson ? parseJsonWithRevivers(this, resultJson) : null; + } - throw new Error("The current dispatcher does not support synchronous calls from JS to .NET. Use invokeDotNetMethodAsync instead."); - } + throw new Error("The current dispatcher does not support synchronous calls from JS to .NET. Use invokeDotNetMethodAsync instead."); + } invokeDotNetMethodAsync(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): Promise { if (assemblyName && dotNetObjectId) { @@ -545,11 +558,11 @@ export module DotNet { return error ? error.toString() : "null"; } - export function findJSFunction(identifier: string, targetInstanceId: number): Function { + export function findJSFunction(identifier: string, targetInstanceId: number, callType?: JSCallType): Function { const targetInstance = cachedJSObjectsById[targetInstanceId]; if (targetInstance) { - return targetInstance.findFunction(identifier); + return targetInstance.resolveInvocationHandler(identifier, callType ?? JSCallType.FunctionCall); } throw new Error(`JS object instance with ID ${targetInstanceId} does not exist (has it been disposed?).`); @@ -559,6 +572,121 @@ export module DotNet { delete cachedJSObjectsById[id]; } + /** Traverses the object hierarchy to find an object member specified by the identifier. + * + * @param obj Root object to search in. + * @param identifier Complete identifier of the member to find, e.g. "document.location.href". + * @returns A tuple containing the immediate parent of the member and the member name. + */ + export function findObjectMember(obj: any, identifier: string): [any, string] { + const keys = identifier.split("."); + let current = obj; + + // First, we iterate over all but the last key. We throw error for missing intermediate keys. + // Error handling in case of undefined last key depends on the type of operation. + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]; + + if (current && typeof current === 'object' && key in current) { + current = current[key]; + } else { + throw new Error(`Could not find '${identifier}' ('${key}' was undefined).`); + } + } + + return [current, keys[keys.length - 1]]; + } + + /** Takes an object member and a call type and returns a function that performs the operation specified by the call type on the member. + * + * @param parent Immediate parent of the accessed object member. + * @param memberName Name (key) of the accessed member. + * @param callType The type of the operation to perform on the member. + * @param identifier The full member identifier. Only used for error messages. + * @returns A function that performs the operation on the member. + */ + function wrapJSCallAsFunction(parent: any, memberName: string, callType: JSCallType, identifier: string): Function { + switch (callType) { + case JSCallType.FunctionCall: + const func = parent[memberName]; + if (func instanceof Function) { + return func.bind(parent); + } else { + throw new Error(`The value '${identifier}' is not a function.`); + } + case JSCallType.NewCall: + const ctor = parent[memberName]; + if (ctor instanceof Function) { + const bound = ctor.bind(parent); + return (...args: any[]) => new bound(...args); + } else { + throw new Error(`The value '${identifier}' is not a function.`); + } + case JSCallType.GetValue: + if (!isReadableProperty(parent, memberName)) { + throw new Error(`The property '${identifier}' is not defined or is not readable.`); + } + return () => parent[memberName]; + case JSCallType.SetValue: + if (!isWritableProperty(parent, memberName)) { + throw new Error(`The property '${identifier}' is not writable.`); + } + return (...args: any[]) => parent[memberName] = args[0]; + } + } + + function isReadableProperty(obj: any, propName: string) { + // Return false for missing property. + if (!(propName in obj)) { + return false; + } + + // If the property is present we examine its descriptor, potentially needing to walk up the prototype chain. + while (obj !== undefined) { + const descriptor = Object.getOwnPropertyDescriptor(obj, propName); + + if (descriptor) { + // Return true for data property + if (descriptor.hasOwnProperty('value')) { + return true + } + + // Return true for accessor property with defined getter. + return descriptor.hasOwnProperty('get') && typeof descriptor.get === 'function'; + } + + obj = Object.getPrototypeOf(obj); + } + + return false; + } + + function isWritableProperty(obj: any, propName: string) { + // Return true for missing property if the property can be added. + if (!(propName in obj)) { + return Object.isExtensible(obj); + } + + // If the property is present we examine its descriptor, potentially needing to walk up the prototype chain. + while (obj !== undefined) { + const descriptor = Object.getOwnPropertyDescriptor(obj, propName); + + if (descriptor) { + // Return true for writable data property. + if (descriptor.hasOwnProperty('value') && descriptor.writable) { + return true; + } + + // Return true for accessor property with defined setter. + return descriptor.hasOwnProperty('set') && typeof descriptor.set === 'function'; + } + + obj = Object.getPrototypeOf(obj); + } + + return false; + } + export class DotNetObject { // eslint-disable-next-line no-empty-function constructor(private readonly _id: number, private readonly _callDispatcher: CallDispatcher) { diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/test/CallDispatcher.test.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/test/CallDispatcher.test.ts new file mode 100644 index 000000000000..38ee0cc07996 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/test/CallDispatcher.test.ts @@ -0,0 +1,398 @@ +import { expect } from "@jest/globals"; +import { DotNet } from "../src/Microsoft.JSInterop"; + +const jsObjectId = "__jsObjectId"; +let lastAsyncResult: null | { callId: number, succeeded: boolean, resultOrError: any } = null; +const dotNetCallDispatcher: DotNet.DotNetCallDispatcher = { + beginInvokeDotNetFromJS: function (callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): void { }, + sendByteArray: function (id: number, data: Uint8Array): void { }, + endInvokeJSFromDotNet: function (callId: number, succeeded: boolean, resultOrError: any): void { + lastAsyncResult = { callId, succeeded, resultOrError }; + }, +}; +const dispatcher: DotNet.ICallDispatcher = DotNet.attachDispatcher(dotNetCallDispatcher); +const getObjectReferenceId = (obj: any) => DotNet.createJSObjectReference(obj)[jsObjectId]; + +describe("CallDispatcher", () => { + test("FunctionCall: Function with no arguments is invoked and returns value", () => { + const testFunc = () => 1; + const objectId = getObjectReferenceId({ testFunc }); + + const result = dispatcher.invokeJSFromDotNet( + "testFunc", + "[]", + DotNet.JSCallResultType.Default, + objectId, + DotNet.JSCallType.FunctionCall + ); + expect(result).toBe("1"); + }); + + test("FunctionCall: Function with arguments is invoked and returns value", () => { + const testFunc = (a: number, b: number) => a + b; + const objectId = getObjectReferenceId({ testFunc }); + + const result = dispatcher.invokeJSFromDotNet( + "testFunc", + JSON.stringify([1, 2]), + DotNet.JSCallResultType.Default, + objectId, + DotNet.JSCallType.FunctionCall + ); + + expect(result).toBe("3"); + }); + + test("FunctionCall: Non-function value is invoked and throws", () => { + const objectId = getObjectReferenceId({ x: 1 }); + + expect(() => dispatcher.invokeJSFromDotNet( + "x", + "[]", + DotNet.JSCallResultType.Default, + objectId, + DotNet.JSCallType.FunctionCall + )).toThrowError("The value 'x' is not a function."); + }); + + test("FunctionCall: Function is invoked via async interop and returns value", () => { + const testFunc = (a: number, b: number) => a + b; + const objectId = getObjectReferenceId({ testFunc }); + + const promise = dispatcher.beginInvokeJSFromDotNet( + 1, + "testFunc", + JSON.stringify([1, 2]), + DotNet.JSCallResultType.Default, + objectId, + DotNet.JSCallType.FunctionCall + ); + + promise?.then(() => { + expect(lastAsyncResult).toStrictEqual({ callId: 1, succeeded: true, resultOrError: "[1,true,3]" }); + }); + }); + + test("FunctionCall: Non-function value is invoked via async interop and throws", () => { + const objectId = getObjectReferenceId({ x: 1 }); + + const promise = dispatcher.beginInvokeJSFromDotNet( + 1, + "x", + "[]", + DotNet.JSCallResultType.Default, + objectId, + DotNet.JSCallType.FunctionCall + ); + + promise?.then(() => { + expect(lastAsyncResult?.succeeded).toBe(false); + expect(lastAsyncResult?.resultOrError).toMatch("The value 'x' is not a function."); + }); + }); + + test("FunctionCall: should handle functions that throw errors", () => { + const testFunc = () => { throw new Error("Test error"); }; + const objectId = getObjectReferenceId({ testFunc }); + + expect(() => dispatcher.invokeJSFromDotNet( + "testFunc", + "[]", + DotNet.JSCallResultType.Default, + objectId, + DotNet.JSCallType.FunctionCall + )).toThrowError("Test error"); + }); + + test("NewCall: Constructor function is invoked and returns reference to new object", () => { + window["testCtor"] = function () { this.a = 10; }; + + const result = dispatcher.invokeJSFromDotNet( + "testCtor", + "[]", + DotNet.JSCallResultType.JSObjectReference, + 0, + DotNet.JSCallType.NewCall + ); + + expect(result).toMatch("__jsObjectId"); + }); + + test("NewCall: Class constructor is invoked and returns reference to the new instance", () => { + const TestClass = class { + a: number; + constructor() { this.a = 10; } + }; + const objectId = getObjectReferenceId({ TestClass }); + + const result = dispatcher.invokeJSFromDotNet( + "TestClass", + "[]", + DotNet.JSCallResultType.JSObjectReference, + objectId, + DotNet.JSCallType.NewCall + ); + + expect(result).toMatch("__jsObjectId"); + }); + + test("GetValue: Simple property value is retrieved", () => { + const testObject = { a: 10 }; + const objectId = getObjectReferenceId(testObject); + + const result = dispatcher.invokeJSFromDotNet( + "a", + "[]", + DotNet.JSCallResultType.Default, + objectId, + DotNet.JSCallType.GetValue + ); + + expect(result).toBe("10"); + }); + + test("GetValue: Nested property value is retrieved", () => { + const testObject = { a: { b: 20 } }; + const objectId = getObjectReferenceId(testObject); + + const result = dispatcher.invokeJSFromDotNet( + "a.b", + "[]", + DotNet.JSCallResultType.Default, + objectId, + DotNet.JSCallType.GetValue + ); + + expect(result).toBe("20"); + }); + + test("GetValue: Property defined on prototype is retrieved", () => { + const grandParentPrototype = { a: 30 }; + const parentPrototype = Object.create(grandParentPrototype); + const testObject = Object.create(parentPrototype); + const objectId = getObjectReferenceId(testObject); + + const result = dispatcher.invokeJSFromDotNet( + "a", + "[]", + DotNet.JSCallResultType.Default, + objectId, + DotNet.JSCallType.GetValue + ); + + expect(result).toBe("30"); + }); + + test("GetValue: Reading undefined property throws", () => { + const testObject = { a: 10 }; + const objectId = getObjectReferenceId(testObject); + + expect(() => dispatcher.invokeJSFromDotNet( + "b", + "[]", + DotNet.JSCallResultType.Default, + objectId, + DotNet.JSCallType.GetValue + )).toThrowError("The property 'b' is not defined or is not readable."); + }); + + test("GetValue: Object reference is retrieved", () => { + const testObject = { a: { b: 20 } }; + const objectId = getObjectReferenceId(testObject); + + const result = dispatcher.invokeJSFromDotNet( + "a", + "[]", + DotNet.JSCallResultType.JSObjectReference, + objectId, + DotNet.JSCallType.GetValue + ); + + expect(result).toMatch("__jsObjectId"); + }); + + test("GetValue: Reading from setter-only property throws", () => { + const testObject = { set a(_: any) { } }; + const objectId = getObjectReferenceId(testObject); + + expect(() => dispatcher.invokeJSFromDotNet( + "a", + "[]", + DotNet.JSCallResultType.Default, + objectId, + DotNet.JSCallType.GetValue + )).toThrowError("The property 'a' is not defined or is not readable"); + }); + + test("SetValue: Simple property is updated", () => { + const testObject = { a: 10 }; + const objectId = getObjectReferenceId(testObject); + + dispatcher.invokeJSFromDotNet( + "a", + JSON.stringify([20]), + DotNet.JSCallResultType.JSVoidResult, + objectId, + DotNet.JSCallType.SetValue + ); + + expect(testObject.a).toBe(20); + }); + + test("SetValue: Nested property is updated", () => { + const testObject = { a: { b: 10 } }; + const objectId = getObjectReferenceId(testObject); + + dispatcher.invokeJSFromDotNet( + "a.b", + JSON.stringify([20]), + DotNet.JSCallResultType.JSVoidResult, + objectId, + DotNet.JSCallType.SetValue + ); + + expect(testObject.a.b).toBe(20); + }); + + test("SetValue: Undefined property can be set", () => { + const testObject = { a: 10 }; + const objectId = getObjectReferenceId(testObject); + + dispatcher.invokeJSFromDotNet( + "b", + JSON.stringify([30]), + DotNet.JSCallResultType.JSVoidResult, + objectId, + DotNet.JSCallType.SetValue + ); + + expect((testObject as any).b).toBe(30); + }); + + test("SetValue: Writing to getter-only property throws", () => { + const testObject = { get a() { return 10; } }; + const objectId = getObjectReferenceId(testObject); + + expect(() => dispatcher.invokeJSFromDotNet( + "a", + JSON.stringify([20]), + DotNet.JSCallResultType.JSVoidResult, + objectId, + DotNet.JSCallType.SetValue + )).toThrowError("The property 'a' is not writable."); + }); + + test("SetValue: Writing to non-writable data property throws", () => { + const testObject = Object.create({}, { a: { value: 10, writable: false } }); + const objectId = getObjectReferenceId(testObject); + + expect(() => dispatcher.invokeJSFromDotNet( + "a", + JSON.stringify([20]), + DotNet.JSCallResultType.JSVoidResult, + objectId, + DotNet.JSCallType.SetValue + )).toThrowError("The property 'a' is not writable."); + }); + + test("SetValue + GetValue: Updated primitive value is read", () => { + const testObject = { a: 10 }; + const objectId = getObjectReferenceId(testObject); + + dispatcher.invokeJSFromDotNet( + "a", + JSON.stringify([20]), + DotNet.JSCallResultType.JSVoidResult, + objectId, + DotNet.JSCallType.SetValue + ); + + const result = dispatcher.invokeJSFromDotNet( + "a", + "[]", + DotNet.JSCallResultType.Default, + objectId, + DotNet.JSCallType.GetValue + ); + + expect(result).toBe("20"); + }); + + test("SetValue + GetValue: Updated object value is read", () => { + const objA = {}; + const objARef = DotNet.createJSObjectReference(objA); + const objB = { x: 30 }; + const objBRef = DotNet.createJSObjectReference(objB); + + dispatcher.invokeJSFromDotNet( + "b", + JSON.stringify([objBRef]), + DotNet.JSCallResultType.JSVoidResult, + objARef[jsObjectId], + DotNet.JSCallType.SetValue + ); + + const result = dispatcher.invokeJSFromDotNet( + "b.x", + "[]", + DotNet.JSCallResultType.Default, + objARef[jsObjectId], + DotNet.JSCallType.GetValue + ); + + expect(result).toBe("30"); + }); + + test("NewCall + GetValue: Class constructor is invoked and the new instance value is retrieved", () => { + const TestClass = class { + a: number; + constructor() { this.a = 20; } + }; + const objectId = getObjectReferenceId({ TestClass }); + + const result = dispatcher.invokeJSFromDotNet( + "TestClass", + "[]", + DotNet.JSCallResultType.JSObjectReference, + objectId, + DotNet.JSCallType.NewCall + ); + const newObjectId = JSON.parse(result ?? "")[jsObjectId]; + + const result2 = dispatcher.invokeJSFromDotNet( + "a", + "[]", + DotNet.JSCallResultType.Default, + newObjectId, + DotNet.JSCallType.GetValue + ); + + expect(result2).toBe("20"); + }); + + test("NewCall + FunctionCall: Class constructor is invoked and method is invoked on the new instance", () => { + const TestClass = class { + f() { return 30; } + }; + const objectId = getObjectReferenceId({ TestClass }); + + const result = dispatcher.invokeJSFromDotNet( + "TestClass", + "[]", + DotNet.JSCallResultType.JSObjectReference, + objectId, + DotNet.JSCallType.NewCall + ); + const newObjectId = JSON.parse(result ?? "")[jsObjectId]; + + const result2 = dispatcher.invokeJSFromDotNet( + "f", + "[]", + DotNet.JSCallResultType.Default, + newObjectId, + DotNet.JSCallType.FunctionCall + ); + + expect(result2).toBe("30"); + }); +}); \ No newline at end of file diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/test/findObjectMember.test.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/test/findObjectMember.test.ts new file mode 100644 index 000000000000..a905936d59d4 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/test/findObjectMember.test.ts @@ -0,0 +1,28 @@ +import { expect } from "@jest/globals"; +import { DotNet } from "../src/Microsoft.JSInterop"; + +describe("findObjectMember", () => { + const testObject = { + a: { + b: { + c: 42, + d: function () { return "hello"; }, + e: class { constructor() { } } + } + } + }; + + test("Resolves nested member", () => { + const result = DotNet.findObjectMember(testObject, "a.b.c"); + expect(result).toEqual([testObject.a.b, "c"]); + }); + + test("Resolves undefined last-level member", () => { + const result = DotNet.findObjectMember(testObject, "a.f"); + expect(result).toEqual([testObject.a, "f"]); + }); + + test("Throws for undefined intermediate member", () => { + expect(() => DotNet.findObjectMember(testObject, "a.f.g")).toThrow("Could not find 'a.f.g' ('f' was undefined)."); + }); +}); diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessObjectReference.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessObjectReference.cs index d7e352659199..39f376a11953 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessObjectReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessObjectReference.cs @@ -20,4 +20,31 @@ public interface IJSInProcessObjectReference : IJSObjectReference, IDisposable /// An instance of obtained by JSON-deserializing the return value. [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] TValue Invoke<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, params object?[]? args); + + /// + /// Invokes the specified JavaScript constructor function synchronously. The function is invoked with the new operator. + /// + /// An identifier for the constructor function to invoke. For example, the value "someScope.SomeClass" will invoke the constructor window.someScope.SomeClass. + /// JSON-serializable arguments. + /// An instance that represents the created JS object. + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + IJSInProcessObjectReference InvokeNew(string identifier, object?[]? args); + + /// + /// Reads the value of the specified JavaScript property synchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the property to read. For example, the value "someScope.someProp" will read the value of the property window.someScope.someProp. + /// An instance of obtained by JSON-deserializing the return value. + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + TValue GetValue<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier); + + /// + /// Updates the value of the specified JavaScript property synchronously. If the property is not defined on the target object, it will be created. + /// + /// JSON-serializable argument type. + /// An identifier for the property to set. For example, the value "someScope.someProp" will update the property window.someScope.someProp. + /// JSON-serializable value. + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + void SetValue<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs index 82a49f14acc2..ed1ea479c8d1 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs @@ -20,4 +20,31 @@ public interface IJSInProcessRuntime : IJSRuntime /// An instance of obtained by JSON-deserializing the return value. [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] TResult Invoke<[DynamicallyAccessedMembers(JsonSerialized)] TResult>(string identifier, params object?[]? args); + + /// + /// Invokes the specified JavaScript constructor function synchronously. The function is invoked with the new operator. + /// + /// An identifier for the constructor function to invoke. For example, the value "someScope.SomeClass" will invoke the constructor window.someScope.SomeClass. + /// JSON-serializable arguments. + /// An instance that represents the created JS object. + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + IJSInProcessObjectReference InvokeNew(string identifier, params object?[]? args); + + /// + /// Reads the value of the specified JavaScript property synchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the property to read. For example, the value "someScope.someProp" will read the value of the property window.someScope.someProp. + /// An instance of obtained by JSON-deserializing the return value. + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + TValue GetValue<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier); + + /// + /// Updates the value of the specified JavaScript property synchronously. If the property is not defined on the target object, it will be created. + /// + /// JSON-serializable argument type. + /// An identifier for the property to set. For example, the value "someScope.someProp" will update the property window.someScope.someProp. + /// JSON-serializable value. + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + void SetValue<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSObjectReference.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSObjectReference.cs index 55a99827849d..30df4c46d232 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSObjectReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSObjectReference.cs @@ -36,4 +36,66 @@ public interface IJSObjectReference : IAsyncDisposable /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args); + + /// + /// Invokes the specified JavaScript constructor function asynchronously. The function is invoked with the new operator. + /// + /// An identifier for the constructor function to invoke. For example, the value "someScope.SomeClass" will invoke the constructor someScope.SomeClass on the target instance. + /// JSON-serializable arguments. + /// An instance that represents the created JS object. + ValueTask InvokeNewAsync(string identifier, object?[]? args); + + /// + /// Invokes the specified JavaScript constructor function asynchronously. The function is invoked with the new operator. + /// + /// An identifier for the constructor function to invoke. For example, the value "someScope.SomeClass" will invoke the constructor someScope.SomeClass on the target instance. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// JSON-serializable arguments. + /// An instance that represents the created JS object. + ValueTask InvokeNewAsync(string identifier, CancellationToken cancellationToken, object?[]? args); + + /// + /// Reads the value of the specified JavaScript property asynchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the property to read. For example, the value "someScope.someProp" will read the value of the property someScope.someProp on the target instance. + /// An instance of obtained by JSON-deserializing the return value. + ValueTask GetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier); + + /// + /// Reads the value of the specified JavaScript property asynchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the property to read. For example, the value "someScope.someProp" will read the value of the property window.someScope.someProp. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// An instance of obtained by JSON-deserializing the return value. + ValueTask GetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken); + + /// + /// Updates the value of the specified JavaScript property asynchronously. If the property is not defined on the target object, it will be created. + /// + /// JSON-serializable argument type. + /// An identifier for the property to set. For example, the value "someScope.someProp" will update the property someScope.someProp on the target instance. + /// JSON-serializable value. + /// A that represents the asynchronous invocation operation. + ValueTask SetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value); + + /// + /// Updates the value of the specified JavaScript property asynchronously. If the property is not defined on the target object, it will be created. + /// + /// JSON-serializable argument type. + /// An identifier for the property to set. For example, the value "someScope.someProp" will update the property someScope.someProp on the target instance. + /// JSON-serializable value. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// A that represents the asynchronous invocation operation. + ValueTask SetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value, CancellationToken cancellationToken); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs index 0313bf613470..0050c357c49c 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs @@ -36,4 +36,66 @@ public interface IJSRuntime /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args); + + /// + /// Invokes the specified JavaScript constructor function asynchronously. The function is invoked with the new operator. + /// + /// An identifier for the constructor function to invoke. For example, the value "someScope.SomeClass" will invoke the constructor window.someScope.SomeClass. + /// JSON-serializable arguments. + /// An instance that represents the created JS object. + ValueTask InvokeNewAsync(string identifier, object?[]? args); + + /// + /// Invokes the specified JavaScript constructor function asynchronously. The function is invoked with the new operator. + /// + /// An identifier for the constructor function to invoke. For example, the value "someScope.SomeClass" will invoke the constructor window.someScope.SomeClass. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// JSON-serializable arguments. + /// An instance that represents the created JS object. + ValueTask InvokeNewAsync(string identifier, CancellationToken cancellationToken, object?[]? args); + + /// + /// Reads the value of the specified JavaScript property asynchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the property to read. For example, the value "someScope.someProp" will read the value of the property window.someScope.someProp. + /// An instance of obtained by JSON-deserializing the return value. + ValueTask GetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier); + + /// + /// Reads the value of the specified JavaScript property asynchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the property to read. For example, the value "someScope.someProp" will read the value of the property window.someScope.someProp. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// An instance of obtained by JSON-deserializing the return value. + ValueTask GetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken); + + /// + /// Updates the value of the specified JavaScript property asynchronously. If the property is not defined on the target object, it will be created. + /// + /// JSON-serializable argument type. + /// An identifier for the property to set. For example, the value "someScope.someProp" will update the property window.someScope.someProp. + /// JSON-serializable value. + /// A that represents the asynchronous invocation operation. + ValueTask SetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value); + + /// + /// Updates the value of the specified JavaScript property asynchronously. If the property is not defined on the target object, it will be created. + /// + /// JSON-serializable argument type. + /// An identifier for the property to set. For example, the value "someScope.someProp" will update the property window.someScope.someProp. + /// JSON-serializable value. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// A that represents the asynchronous invocation operation. + ValueTask SetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value, CancellationToken cancellationToken); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSInProcessObjectReference.cs b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSInProcessObjectReference.cs index 46e4af57b4d7..abe9472251cc 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSInProcessObjectReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSInProcessObjectReference.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices.JavaScript; +using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.JSInterop.Implementation; @@ -30,7 +31,34 @@ protected internal JSInProcessObjectReference(JSInProcessRuntime jsRuntime, long { ThrowIfDisposed(); - return _jsRuntime.Invoke(identifier, Id, args); + return _jsRuntime.Invoke(identifier, Id, JSCallType.FunctionCall, args); + } + + /// + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + public IJSInProcessObjectReference InvokeNew(string identifier, object?[]? args) + { + ThrowIfDisposed(); + + return _jsRuntime.Invoke(identifier, Id, JSCallType.NewCall, args); + } + + /// + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + public TValue GetValue<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier) + { + ThrowIfDisposed(); + + return _jsRuntime.Invoke(identifier, Id, JSCallType.GetValue); + } + + /// + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + public void SetValue<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value) + { + ThrowIfDisposed(); + + _jsRuntime.Invoke(identifier, Id, JSCallType.SetValue, value); } /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSObjectReference.cs b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSObjectReference.cs index 8661dd4b5ad8..f1479cf50730 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSObjectReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSObjectReference.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.JSInterop.Implementation; @@ -37,7 +38,7 @@ protected internal JSObjectReference(JSRuntime jsRuntime, long id) { ThrowIfDisposed(); - return _jsRuntime.InvokeAsync(Id, identifier, args); + return _jsRuntime.InvokeAsync(Id, identifier, JSCallType.FunctionCall, args); } /// @@ -45,7 +46,55 @@ protected internal JSObjectReference(JSRuntime jsRuntime, long id) { ThrowIfDisposed(); - return _jsRuntime.InvokeAsync(Id, identifier, cancellationToken, args); + return _jsRuntime.InvokeAsync(Id, identifier, JSCallType.FunctionCall, cancellationToken, args); + } + + /// + public ValueTask InvokeNewAsync(string identifier, object?[]? args) + { + ThrowIfDisposed(); + + return _jsRuntime.InvokeAsync(Id, identifier, JSCallType.NewCall, args); + } + + /// + public ValueTask InvokeNewAsync(string identifier, CancellationToken cancellationToken, object?[]? args) + { + ThrowIfDisposed(); + + return _jsRuntime.InvokeAsync(Id, identifier, JSCallType.NewCall, cancellationToken, args); + } + + /// + public ValueTask GetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier) + { + ThrowIfDisposed(); + + return _jsRuntime.InvokeAsync(Id, identifier, JSCallType.GetValue, null); + } + + /// + public ValueTask GetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + + return _jsRuntime.InvokeAsync(Id, identifier, JSCallType.GetValue, null); + } + + /// + public async ValueTask SetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value) + { + ThrowIfDisposed(); + + await _jsRuntime.InvokeAsync(Id, identifier, JSCallType.SetValue, [value]); + } + + /// + public async ValueTask SetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + + await _jsRuntime.InvokeAsync(Id, identifier, JSCallType.SetValue, [value]); } /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSCallType.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSCallType.cs new file mode 100644 index 000000000000..a217a800f159 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSCallType.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.JSInterop.Infrastructure; + +/// +/// Describes type of operation invoked in JavaScript via interop. +/// +public enum JSCallType : int +{ + /// + /// Represents a regular function invocation. + /// + FunctionCall = 1, + + /// + /// Represents a constructor function invocation with the new operator. + /// + NewCall = 2, + + /// + /// Represents reading a property value. + /// + GetValue = 3, + + /// + /// Represents updating or defining a property value. + /// + SetValue = 4, +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSInvocationInfo.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSInvocationInfo.cs new file mode 100644 index 000000000000..209530e2b2c8 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSInvocationInfo.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.JSInterop.Infrastructure; + +/// +/// Configuration of an interop call from .NET to JavaScript. +/// +public readonly struct JSInvocationInfo +{ + private readonly string? _argsJson; + + /// + /// The identifier for the interop call, or zero if no async callback is required. + /// + public required long AsyncHandle { get; init; } + + /// + /// The instance ID of the target JS object. + /// + public required long TargetInstanceId { get; init; } + + /// + /// The identifier of the function to invoke or property to access. + /// + public required string Identifier { get; init; } + + /// + /// The type of operation that should be performed in JS. + /// + public required JSCallType CallType { get; init; } + + /// + /// The type of result expected from the invocation. + /// + public required JSCallResultType ResultType { get; init; } + + /// + /// A JSON representation of the arguments. + /// + [StringSyntax(StringSyntaxAttribute.Json)] + public required string ArgsJson + { + get => _argsJson ?? "[]"; + init => _argsJson = value; + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs index 4092349c3882..5c8a73b29cd4 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.JSInterop; @@ -12,14 +13,50 @@ namespace Microsoft.JSInterop; /// public abstract class JSInProcessRuntime : JSRuntime, IJSInProcessRuntime { + internal const long SyncCallIndicator = 0; + + /// + /// Invokes the specified JavaScript function synchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + public TValue Invoke<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, params object?[]? args) + => Invoke(identifier, WindowObjectId, JSCallType.FunctionCall, args); + + /// + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + public IJSInProcessObjectReference InvokeNew(string identifier, params object?[]? args) + => Invoke(identifier, WindowObjectId, JSCallType.NewCall, args); + + /// + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + public TValue GetValue<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier) + => Invoke(identifier, WindowObjectId, JSCallType.GetValue); + + /// + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + public void SetValue<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value) + => Invoke(identifier, WindowObjectId, JSCallType.SetValue, value); + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] - internal TValue Invoke<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, long targetInstanceId, params object?[]? args) + internal TValue Invoke<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, long targetInstanceId, JSCallType callType, params object?[]? args) { - var resultJson = InvokeJS( - identifier, - JsonSerializer.Serialize(args, JsonSerializerOptions), - JSCallResultTypeHelper.FromGeneric(), - targetInstanceId); + var argsJson = args is not null && args.Length != 0 ? JsonSerializer.Serialize(args, JsonSerializerOptions) : "[]"; + var resultType = JSCallResultTypeHelper.FromGeneric(); + var invocationInfo = new JSInvocationInfo + { + AsyncHandle = SyncCallIndicator, + TargetInstanceId = targetInstanceId, + Identifier = identifier, + CallType = callType, + ResultType = resultType, + ArgsJson = argsJson, + }; + + var resultJson = InvokeJS(invocationInfo); // While the result of deserialization could be null, we're making a // quality of life decision and letting users explicitly determine if they expect @@ -34,17 +71,6 @@ public abstract class JSInProcessRuntime : JSRuntime, IJSInProcessRuntime return result; } - /// - /// Invokes the specified JavaScript function synchronously. - /// - /// The JSON-serializable return type. - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. - [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] - public TValue Invoke<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, params object?[]? args) - => Invoke(identifier, 0, args); - /// /// Performs a synchronous function invocation. /// @@ -52,7 +78,7 @@ public abstract class JSInProcessRuntime : JSRuntime, IJSInProcessRuntime /// A JSON representation of the arguments. /// A JSON representation of the result. protected virtual string? InvokeJS(string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson) - => InvokeJS(identifier, argsJson, JSCallResultType.Default, 0); + => InvokeJS(identifier, argsJson, JSCallResultType.Default, WindowObjectId); /// /// Performs a synchronous function invocation. @@ -63,4 +89,11 @@ public abstract class JSInProcessRuntime : JSRuntime, IJSInProcessRuntime /// The instance ID of the target JS object. /// A JSON representation of the result. protected abstract string? InvokeJS(string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson, JSCallResultType resultType, long targetInstanceId); + + /// + /// Performs a synchronous function invocation. + /// + /// Configuration of the interop call. + /// A JSON representation of the result. + protected abstract string? InvokeJS(in JSInvocationInfo invocationInfo); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs index 01066efb3dc5..4ddcc358ef2f 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs @@ -117,4 +117,54 @@ public static async ValueTask InvokeVoidAsync(this IJSObjectReference jsObjectRe await jsObjectReference.InvokeAsync(identifier, cancellationToken, args); } + + /// + /// Invokes the specified JavaScript constructor function asynchronously. The function is invoked with the new operator. + /// + /// The . + /// An identifier for the constructor function to invoke. For example, the value "someScope.SomeClass" will invoke the constructor someScope.SomeClass. + /// JSON-serializable arguments. + /// An instance that represents the created JS object. + public static ValueTask InvokeNewAsync(this IJSObjectReference jsObjectReference, string identifier, params object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return jsObjectReference.InvokeNewAsync(identifier, args); + } + + /// + /// Invokes the specified JavaScript constructor function asynchronously. The function is invoked with the new operator. + /// + /// The . + /// An identifier for the constructor function to invoke. For example, the value "someScope.SomeClass" will invoke the constructor someScope.SomeClass. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// JSON-serializable arguments. + /// An instance that represents the created JS object. + public static ValueTask InvokeNewAsync(this IJSObjectReference jsObjectReference, string identifier, CancellationToken cancellationToken, object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return jsObjectReference.InvokeNewAsync(identifier, cancellationToken, args); + } + + /// + /// Invokes the specified JavaScript constructor function asynchronously. The function is invoked with the new operator. + /// + /// The . + /// An identifier for the constructor function to invoke. For example, the value "someScope.SomeClass" will invoke the constructor someScope.SomeClass. + /// The duration after which to cancel the async operation. Overrides default timeouts (). + /// JSON-serializable arguments. + /// An instance that represents the created JS object. + public static ValueTask InvokeNewAsync(this IJSObjectReference jsObjectReference, string identifier, TimeSpan timeout, object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); + var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; + + return jsObjectReference.InvokeNewAsync(identifier, cancellationToken, args); + } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index 3cbd024d2755..a970c7900970 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -15,6 +15,8 @@ namespace Microsoft.JSInterop; /// public abstract partial class JSRuntime : IJSRuntime, IDisposable { + internal const long WindowObjectId = 0; + private long _nextObjectReferenceId; // Initial value of 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1 private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed" private readonly ConcurrentDictionary _pendingTasks = new(); @@ -66,7 +68,7 @@ protected JSRuntime() /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args) - => InvokeAsync(0, identifier, args); + => InvokeAsync(WindowObjectId, identifier, JSCallType.FunctionCall, args); /// /// Invokes the specified JavaScript function asynchronously. @@ -80,24 +82,49 @@ protected JSRuntime() /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args) - => InvokeAsync(0, identifier, cancellationToken, args); + => InvokeAsync(WindowObjectId, identifier, JSCallType.FunctionCall, cancellationToken, args); + + /// + public ValueTask InvokeNewAsync(string identifier, object?[]? args) + => InvokeAsync(WindowObjectId, identifier, JSCallType.NewCall, args); + + /// + public ValueTask InvokeNewAsync(string identifier, CancellationToken cancellationToken, object?[]? args) + => InvokeAsync(WindowObjectId, identifier, JSCallType.NewCall, cancellationToken, args); + + /// + public ValueTask GetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier) + => InvokeAsync(WindowObjectId, identifier, JSCallType.GetValue, null); + + /// + public ValueTask GetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken) + => InvokeAsync(WindowObjectId, identifier, JSCallType.GetValue, cancellationToken, null); + + /// + public async ValueTask SetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value) + => await InvokeAsync(WindowObjectId, identifier, JSCallType.SetValue, [value]); - internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, object?[]? args) + /// + public async ValueTask SetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value, CancellationToken cancellationToken) + => await InvokeAsync(WindowObjectId, identifier, JSCallType.SetValue, cancellationToken, [value]); + + internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, JSCallType callType, object?[]? args) { if (DefaultAsyncTimeout.HasValue) { using var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value); // We need to await here due to the using - return await InvokeAsync(targetInstanceId, identifier, cts.Token, args); + return await InvokeAsync(targetInstanceId, identifier, callType, cts.Token, args); } - return await InvokeAsync(targetInstanceId, identifier, CancellationToken.None, args); + return await InvokeAsync(targetInstanceId, identifier, callType, CancellationToken.None, args); } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect application code is configured to ensure JS interop arguments are linker friendly.")] internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>( long targetInstanceId, string identifier, + JSCallType callType, CancellationToken cancellationToken, object?[]? args) { @@ -123,12 +150,19 @@ protected JSRuntime() return new ValueTask(tcs.Task); } - var argsJson = args is not null && args.Length != 0 ? - JsonSerializer.Serialize(args, JsonSerializerOptions) : - null; + var argsJson = args is not null && args.Length != 0 ? JsonSerializer.Serialize(args, JsonSerializerOptions) : "[]"; var resultType = JSCallResultTypeHelper.FromGeneric(); + var invocationInfo = new JSInvocationInfo + { + AsyncHandle = taskId, + TargetInstanceId = targetInstanceId, + Identifier = identifier, + CallType = callType, + ResultType = resultType, + ArgsJson = argsJson, + }; - BeginInvokeJS(taskId, identifier, argsJson, resultType, targetInstanceId); + BeginInvokeJS(invocationInfo); return new ValueTask(tcs.Task); } @@ -155,7 +189,7 @@ private void CleanupTasksAndRegistrations(long taskId) /// The identifier for the function to invoke. /// A JSON representation of the arguments. protected virtual void BeginInvokeJS(long taskId, string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson) - => BeginInvokeJS(taskId, identifier, argsJson, JSCallResultType.Default, 0); + => BeginInvokeJS(taskId, identifier, argsJson, JSCallResultType.Default, WindowObjectId); /// /// Begins an asynchronous function invocation. @@ -167,6 +201,12 @@ protected virtual void BeginInvokeJS(long taskId, string identifier, [StringSynt /// The instance ID of the target JS object. protected abstract void BeginInvokeJS(long taskId, string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson, JSCallResultType resultType, long targetInstanceId); + /// + /// Begins an asynchronous function invocation. + /// + /// Configuration of the interop call from .NET to JavaScript. + protected abstract void BeginInvokeJS(in JSInvocationInfo invocationInfo); + /// /// Completes an async JS interop call from JavaScript to .NET /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs index 5a4202ed15da..aa05aa681cad 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs @@ -117,4 +117,54 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string await jsRuntime.InvokeAsync(identifier, cancellationToken, args); } + + /// + /// Invokes the specified JavaScript constructor function asynchronously. The function is invoked with the new operator. + /// + /// The . + /// An identifier for the constructor function to invoke. For example, the value "someScope.SomeClass" will invoke the constructor window.someScope.SomeClass. + /// JSON-serializable arguments. + /// An instance that represents the created JS object. + public static ValueTask InvokeNewAsync(this IJSRuntime jsRuntime, string identifier, params object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + return jsRuntime.InvokeNewAsync(identifier, args); + } + + /// + /// Invokes the specified JavaScript constructor function asynchronously. The function is invoked with the new operator. + /// + /// The . + /// An identifier for the constructor function to invoke. For example, the value "someScope.SomeClass" will invoke the constructor window.someScope.SomeClass. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// JSON-serializable arguments. + /// An instance that represents the created JS object. + public static ValueTask InvokeNewAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + return jsRuntime.InvokeNewAsync(identifier, cancellationToken, args); + } + + /// + /// Invokes the specified JavaScript constructor function asynchronously. The function is invoked with the new operator. + /// + /// The . + /// An identifier for the constructor function to invoke. For example, the value "someScope.SomeClass" will invoke the constructor window.someScope.SomeClass. + /// The duration after which to cancel the async operation. Overrides default timeouts (). + /// JSON-serializable arguments. + /// An instance that represents the created JS object. + public static ValueTask InvokeNewAsync(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); + var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; + + return jsRuntime.InvokeNewAsync(identifier, cancellationToken, args); + } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..3270d74b01a0 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -1 +1,64 @@ #nullable enable +abstract Microsoft.JSInterop.JSInProcessRuntime.InvokeJS(in Microsoft.JSInterop.Infrastructure.JSInvocationInfo invocationInfo) -> string? +abstract Microsoft.JSInterop.JSRuntime.BeginInvokeJS(in Microsoft.JSInterop.Infrastructure.JSInvocationInfo invocationInfo) -> void +Microsoft.JSInterop.IJSInProcessObjectReference.GetValue(string! identifier) -> TValue +Microsoft.JSInterop.IJSInProcessObjectReference.InvokeNew(string! identifier, object?[]? args) -> Microsoft.JSInterop.IJSInProcessObjectReference! +Microsoft.JSInterop.IJSInProcessObjectReference.SetValue(string! identifier, TValue value) -> void +Microsoft.JSInterop.IJSInProcessRuntime.GetValue(string! identifier) -> TValue +Microsoft.JSInterop.IJSInProcessRuntime.InvokeNew(string! identifier, params object?[]? args) -> Microsoft.JSInterop.IJSInProcessObjectReference! +Microsoft.JSInterop.IJSInProcessRuntime.SetValue(string! identifier, TValue value) -> void +Microsoft.JSInterop.IJSObjectReference.GetValueAsync(string! identifier) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSObjectReference.GetValueAsync(string! identifier, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSObjectReference.InvokeNewAsync(string! identifier, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSObjectReference.InvokeNewAsync(string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSObjectReference.SetValueAsync(string! identifier, TValue value) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSObjectReference.SetValueAsync(string! identifier, TValue value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSRuntime.GetValueAsync(string! identifier) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSRuntime.GetValueAsync(string! identifier, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSRuntime.InvokeNewAsync(string! identifier, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSRuntime.InvokeNewAsync(string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSRuntime.SetValueAsync(string! identifier, TValue value) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSRuntime.SetValueAsync(string! identifier, TValue value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.Implementation.JSInProcessObjectReference.GetValue(string! identifier) -> TValue +Microsoft.JSInterop.Implementation.JSInProcessObjectReference.InvokeNew(string! identifier, object?[]? args) -> Microsoft.JSInterop.IJSInProcessObjectReference! +Microsoft.JSInterop.Implementation.JSInProcessObjectReference.SetValue(string! identifier, TValue value) -> void +Microsoft.JSInterop.Implementation.JSObjectReference.GetValueAsync(string! identifier) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.Implementation.JSObjectReference.GetValueAsync(string! identifier, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.Implementation.JSObjectReference.InvokeNewAsync(string! identifier, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.Implementation.JSObjectReference.InvokeNewAsync(string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.Implementation.JSObjectReference.SetValueAsync(string! identifier, TValue value) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.Implementation.JSObjectReference.SetValueAsync(string! identifier, TValue value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.Infrastructure.JSCallType +Microsoft.JSInterop.Infrastructure.JSCallType.FunctionCall = 1 -> Microsoft.JSInterop.Infrastructure.JSCallType +Microsoft.JSInterop.Infrastructure.JSCallType.GetValue = 3 -> Microsoft.JSInterop.Infrastructure.JSCallType +Microsoft.JSInterop.Infrastructure.JSCallType.NewCall = 2 -> Microsoft.JSInterop.Infrastructure.JSCallType +Microsoft.JSInterop.Infrastructure.JSCallType.SetValue = 4 -> Microsoft.JSInterop.Infrastructure.JSCallType +Microsoft.JSInterop.Infrastructure.JSInvocationInfo +Microsoft.JSInterop.Infrastructure.JSInvocationInfo.ArgsJson.get -> string! +Microsoft.JSInterop.Infrastructure.JSInvocationInfo.ArgsJson.init -> void +Microsoft.JSInterop.Infrastructure.JSInvocationInfo.AsyncHandle.get -> long +Microsoft.JSInterop.Infrastructure.JSInvocationInfo.AsyncHandle.init -> void +Microsoft.JSInterop.Infrastructure.JSInvocationInfo.CallType.get -> Microsoft.JSInterop.Infrastructure.JSCallType +Microsoft.JSInterop.Infrastructure.JSInvocationInfo.CallType.init -> void +Microsoft.JSInterop.Infrastructure.JSInvocationInfo.Identifier.get -> string! +Microsoft.JSInterop.Infrastructure.JSInvocationInfo.Identifier.init -> void +Microsoft.JSInterop.Infrastructure.JSInvocationInfo.JSInvocationInfo() -> void +Microsoft.JSInterop.Infrastructure.JSInvocationInfo.ResultType.get -> Microsoft.JSInterop.JSCallResultType +Microsoft.JSInterop.Infrastructure.JSInvocationInfo.ResultType.init -> void +Microsoft.JSInterop.Infrastructure.JSInvocationInfo.TargetInstanceId.get -> long +Microsoft.JSInterop.Infrastructure.JSInvocationInfo.TargetInstanceId.init -> void +Microsoft.JSInterop.JSInProcessRuntime.GetValue(string! identifier) -> TValue +Microsoft.JSInterop.JSInProcessRuntime.InvokeNew(string! identifier, params object?[]? args) -> Microsoft.JSInterop.IJSInProcessObjectReference! +Microsoft.JSInterop.JSInProcessRuntime.SetValue(string! identifier, TValue value) -> void +Microsoft.JSInterop.JSRuntime.GetValueAsync(string! identifier) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.JSRuntime.GetValueAsync(string! identifier, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.JSRuntime.SetValueAsync(string! identifier, TValue value) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.JSRuntime.SetValueAsync(string! identifier, TValue value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.TimeSpan timeout, object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.TimeSpan timeout, object?[]? args) -> System.Threading.Tasks.ValueTask diff --git a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs index 6987c9282940..35233afd497d 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using Microsoft.AspNetCore.InternalTesting; @@ -1079,25 +1080,35 @@ public class TestJSRuntime : JSInProcessRuntime public string LastCompletionCallId { get; private set; } public DotNetInvocationResult LastCompletionResult { get; private set; } - protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId) + protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo) { - LastInvocationAsyncHandle = asyncHandle; - LastInvocationIdentifier = identifier; - LastInvocationArgsJson = argsJson; + LastInvocationAsyncHandle = invocationInfo.AsyncHandle; + LastInvocationIdentifier = invocationInfo.Identifier; + LastInvocationArgsJson = invocationInfo.ArgsJson; _nextInvocationTcs.SetResult(); _nextInvocationTcs = new TaskCompletionSource(); } - protected override string InvokeJS(string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId) + protected override void BeginInvokeJS(long taskId, string identifier, [StringSyntax("Json")] string argsJson, JSCallResultType resultType, long targetInstanceId) + { + throw new NotImplementedException(); + } + + protected override string InvokeJS(in JSInvocationInfo invocationInfo) { LastInvocationAsyncHandle = default; - LastInvocationIdentifier = identifier; - LastInvocationArgsJson = argsJson; + LastInvocationIdentifier = invocationInfo.Identifier; + LastInvocationArgsJson = invocationInfo.ArgsJson; _nextInvocationTcs.SetResult(); _nextInvocationTcs = new TaskCompletionSource(); return null; } + protected override string InvokeJS(string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId) + { + throw new NotImplementedException(); + } + protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) { LastCompletionCallId = invocationInfo.CallId; diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs index 5ad8aaa94159..f879c6ff528e 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.JSInterop.Infrastructure; namespace Microsoft.JSInterop; @@ -91,23 +92,25 @@ class TestDTO class TestJSInProcessRuntime : JSInProcessRuntime { - public List InvokeCalls { get; set; } = new List(); + public List InvokeCalls { get; set; } = []; public string? NextResultJson { get; set; } protected override string? InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId) { - InvokeCalls.Add(new InvokeArgs { Identifier = identifier, ArgsJson = argsJson }); - return NextResultJson; + throw new NotImplementedException(); } - public class InvokeArgs + protected override string? InvokeJS(in JSInvocationInfo invocationInfo) { - public string? Identifier { get; set; } - public string? ArgsJson { get; set; } + InvokeCalls.Add(invocationInfo); + return NextResultJson; } - protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId) + protected override void BeginInvokeJS(long taskId, string identifier, [StringSyntax("Json")] string? argsJson, JSCallResultType resultType, long targetInstanceId) + => throw new NotImplementedException("This test only covers sync calls"); + + protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo) => throw new NotImplementedException("This test only covers sync calls"); protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceTest.cs index 909f3f0ddb28..2864c7fcc327 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.JSInterop.Implementation; using Microsoft.JSInterop.Infrastructure; @@ -69,11 +70,16 @@ class TestJSRuntime : JSRuntime { public int BeginInvokeJSInvocationCount { get; private set; } - protected override void BeginInvokeJS(long taskId, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId) + protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo) { BeginInvokeJSInvocationCount++; } + protected override void BeginInvokeJS(long taskId, string identifier, [StringSyntax("Json")] string? argsJson, JSCallResultType resultType, long targetInstanceId) + { + throw new NotImplementedException(); + } + protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) { } @@ -83,17 +89,27 @@ class TestJSInProcessRuntime : JSInProcessRuntime { public int InvokeJSInvocationCount { get; private set; } - protected override void BeginInvokeJS(long taskId, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId) + protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo) { } - protected override string? InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId) + protected override void BeginInvokeJS(long taskId, string identifier, [StringSyntax("Json")] string? argsJson, JSCallResultType resultType, long targetInstanceId) + { + throw new NotImplementedException(); + } + + protected override string? InvokeJS(in JSInvocationInfo invocationInfo) { InvokeJSInvocationCount++; return null; } + protected override string? InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId) + { + throw new NotImplementedException(); + } + protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) { } diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs index 96f3f5265ec4..65ce56f65ac9 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs @@ -445,8 +445,8 @@ private class TestPoco class TestJSRuntime : JSRuntime { - public List BeginInvokeCalls = new List(); - public List EndInvokeDotNetCalls = new List(); + public List BeginInvokeCalls = []; + public List EndInvokeDotNetCalls = []; public TimeSpan? DefaultTimeout { @@ -456,13 +456,6 @@ public TimeSpan? DefaultTimeout } } - public class BeginInvokeAsyncArgs - { - public long AsyncHandle { get; set; } - public string? Identifier { get; set; } - public string? ArgsJson { get; set; } - } - public class EndInvokeDotNetArgs { public string? CallId { get; set; } @@ -484,12 +477,12 @@ protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocation protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId) { - BeginInvokeCalls.Add(new BeginInvokeAsyncArgs - { - AsyncHandle = asyncHandle, - Identifier = identifier, - ArgsJson = argsJson, - }); + throw new NotImplementedException(); + } + + protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo) + { + BeginInvokeCalls.Add(invocationInfo); } protected internal override Task TransmitStreamAsync(long streamId, DotNetStreamReference dotNetStreamReference) diff --git a/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs index accd66db1934..ffa182667c3b 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs @@ -1,13 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.JSInterop.Infrastructure; namespace Microsoft.JSInterop; internal class TestJSRuntime : JSRuntime { - protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId) + protected override void BeginInvokeJS(long taskId, string identifier, [StringSyntax("Json")] string? argsJson, JSCallResultType resultType, long targetInstanceId) + { + throw new NotImplementedException(); + } + + protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo) { throw new NotImplementedException(); }