JSObjectReference (#25028)

This commit is contained in:
Mackinnon Buck 2020-08-24 21:02:19 -07:00 committed by GitHub
parent 0050ece118
commit 8a2f29bb53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 920 additions and 91 deletions

View File

@ -1507,6 +1507,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagno
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.HealthChecks.Tests", "src\HealthChecks\HealthChecks\test\Microsoft.Extensions.Diagnostics.HealthChecks.Tests.csproj", "{7509AA1E-3093-4BEE-984F-E11579E98A11}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.HealthChecks.Tests", "src\HealthChecks\HealthChecks\test\Microsoft.Extensions.Diagnostics.HealthChecks.Tests.csproj", "{7509AA1E-3093-4BEE-984F-E11579E98A11}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.Tests", "src\JSInterop\Microsoft.JSInterop\test\Microsoft.JSInterop.Tests.csproj", "{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -7179,6 +7181,18 @@ Global
{7509AA1E-3093-4BEE-984F-E11579E98A11}.Release|x64.Build.0 = Release|Any CPU {7509AA1E-3093-4BEE-984F-E11579E98A11}.Release|x64.Build.0 = Release|Any CPU
{7509AA1E-3093-4BEE-984F-E11579E98A11}.Release|x86.ActiveCfg = Release|Any CPU {7509AA1E-3093-4BEE-984F-E11579E98A11}.Release|x86.ActiveCfg = Release|Any CPU
{7509AA1E-3093-4BEE-984F-E11579E98A11}.Release|x86.Build.0 = Release|Any CPU {7509AA1E-3093-4BEE-984F-E11579E98A11}.Release|x86.Build.0 = Release|Any CPU
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Debug|x64.ActiveCfg = Debug|Any CPU
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Debug|x64.Build.0 = Debug|Any CPU
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Debug|x86.ActiveCfg = Debug|Any CPU
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Debug|x86.Build.0 = Debug|Any CPU
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|Any CPU.Build.0 = Release|Any CPU
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|x64.ActiveCfg = Release|Any CPU
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|x64.Build.0 = Release|Any CPU
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|x86.ActiveCfg = Release|Any CPU
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -7934,6 +7948,7 @@ Global
{B06040BC-DA28-4923-8CAC-20EB517D471B} = {22D7D74B-565D-4047-97B4-F149B1A13350} {B06040BC-DA28-4923-8CAC-20EB517D471B} = {22D7D74B-565D-4047-97B4-F149B1A13350}
{55CACC1F-FE96-47C8-8073-91F4CAA55C75} = {2A91479A-4ABE-4BB7-9A5E-CA3B9CCFC69E} {55CACC1F-FE96-47C8-8073-91F4CAA55C75} = {2A91479A-4ABE-4BB7-9A5E-CA3B9CCFC69E}
{7509AA1E-3093-4BEE-984F-E11579E98A11} = {7CB09412-C9B0-47E8-A8C3-311AA4CFDE04} {7509AA1E-3093-4BEE-984F-E11579E98A11} = {7CB09412-C9B0-47E8-A8C3-311AA4CFDE04}
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E} = {16898702-3E33-41C1-B8D8-4CE3F1D46BD9}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

View File

@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
JsonSerializer.Serialize(new[] { callId, success, resultOrError }, JsonSerializerOptions)); JsonSerializer.Serialize(new[] { callId, success, resultOrError }, JsonSerializerOptions));
} }
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
{ {
if (_clientProxy is null) if (_clientProxy is null)
{ {
@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
Log.BeginInvokeJS(_logger, asyncHandle, identifier); Log.BeginInvokeJS(_logger, asyncHandle, identifier);
_clientProxy.SendAsync("JS.BeginInvokeJS", asyncHandle, identifier, argsJson); _clientProxy.SendAsync("JS.BeginInvokeJS", asyncHandle, identifier, argsJson, (int)resultType, targetInstanceId);
} }
public static class Log public static class Log

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -97,7 +97,7 @@ async function initializeConnection(options: CircuitStartOptions, logger: Logger
connection.on('JS.AttachComponent', (componentId, selector) => attachRootComponentToLogicalElement(0, circuit.resolveElement(selector), componentId)); connection.on('JS.AttachComponent', (componentId, selector) => attachRootComponentToLogicalElement(0, circuit.resolveElement(selector), componentId));
connection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet); connection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet);
connection.on('JS.EndInvokeDotNet', (args: string) => DotNet.jsCallDispatcher.endInvokeDotNetFromJS(...(JSON.parse(args) as [string, boolean, unknown]))); connection.on('JS.EndInvokeDotNet', (args: string) => DotNet.jsCallDispatcher.endInvokeDotNetFromJS(...(DotNet.parseJsonWithRevivers(args) as [string, boolean, unknown])));
const renderQueue = RenderQueue.getOrCreate(logger); const renderQueue = RenderQueue.getOrCreate(logger);
connection.on('JS.RenderBatch', (batchId: number, batchData: Uint8Array) => { connection.on('JS.RenderBatch', (batchId: number, batchData: Uint8Array) => {

View File

@ -32,6 +32,9 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
} }
}); });
// Configure JS interop
window['Blazor']._internal.invokeJSFromDotNet = invokeJSFromDotNet;
// Configure environment for execution under Mono WebAssembly with shared-memory rendering // Configure environment for execution under Mono WebAssembly with shared-memory rendering
const platform = Environment.setPlatform(monoPlatform); const platform = Environment.setPlatform(monoPlatform);
window['Blazor'].platform = platform; window['Blazor'].platform = platform;
@ -84,6 +87,28 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
platform.callEntryPoint(resourceLoader.bootConfig.entryAssembly); platform.callEntryPoint(resourceLoader.bootConfig.entryAssembly);
} }
function invokeJSFromDotNet(callInfo: Pointer, arg0: any, arg1: any, arg2: any): any {
const functionIdentifier = monoPlatform.readStringField(callInfo, 0)!;
const resultType = monoPlatform.readInt32Field(callInfo, 4);
const marshalledCallArgsJson = monoPlatform.readStringField(callInfo, 8);
const targetInstanceId = monoPlatform.readUint64Field(callInfo, 20);
if (marshalledCallArgsJson !== null) {
const marshalledCallAsyncHandle = monoPlatform.readUint64Field(callInfo, 12);
if (marshalledCallAsyncHandle !== 0) {
DotNet.jsCallDispatcher.beginInvokeJSFromDotNet(marshalledCallAsyncHandle, functionIdentifier, marshalledCallArgsJson, resultType, targetInstanceId);
return 0;
} else {
const resultJson = DotNet.jsCallDispatcher.invokeJSFromDotNet(functionIdentifier, marshalledCallArgsJson, resultType, targetInstanceId)!;
return resultJson === null ? 0 : BINDING.js_string_to_mono_string(resultJson);
}
} else {
const func = DotNet.jsCallDispatcher.findJSFunction(functionIdentifier, targetInstanceId);
return func.call(null, arg0, arg1, arg2);
}
}
window['Blazor'].start = boot; window['Blazor'].start = boot;
if (shouldAutoStart()) { if (shouldAutoStart()) {
boot().catch(error => { boot().catch(error => {

View File

@ -16,12 +16,7 @@ namespace WebAssembly.JSInterop
// in driver.c in the Mono distribution // in driver.c in the Mono distribution
/// See: https://github.com/mono/mono/blob/90574987940959fe386008a850982ea18236a533/sdks/wasm/src/driver.c#L318-L319 /// See: https://github.com/mono/mono/blob/90574987940959fe386008a850982ea18236a533/sdks/wasm/src/driver.c#L318-L319
// We're passing asyncHandle by ref not because we want it to be writable, but so it gets
// passed as a pointer (4 bytes). We can pass 4-byte values, but not 8-byte ones.
[MethodImpl(MethodImplOptions.InternalCall)] [MethodImpl(MethodImplOptions.InternalCall)]
public static extern string InvokeJSMarshalled(out string exception, ref long asyncHandle, string functionIdentifier, string argsJson); public static extern TRes InvokeJS<T0, T1, T2, TRes>(out string exception, ref JSCallInfo callInfo, [AllowNull] T0 arg0, [AllowNull] T1 arg1, [AllowNull] T2 arg2);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern TRes InvokeJSUnmarshalled<T0, T1, T2, TRes>(out string exception, string functionIdentifier, [AllowNull] T0 arg0, [AllowNull] T1 arg1, [AllowNull] T2 arg2);
} }
} }

View File

@ -0,0 +1,27 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Runtime.InteropServices;
using Microsoft.JSInterop;
namespace WebAssembly.JSInterop
{
[StructLayout(LayoutKind.Explicit, Pack = 4)]
internal struct JSCallInfo
{
[FieldOffset(0)]
public string FunctionIdentifier;
[FieldOffset(4)]
public JSCallResultType ResultType;
[FieldOffset(8)]
public string MarshalledCallArgsJson;
[FieldOffset(12)]
public long MarshalledCallAsyncHandle;
[FieldOffset(20)]
public long TargetInstanceId;
}
}

View File

@ -14,19 +14,37 @@ namespace Microsoft.JSInterop.WebAssembly
public abstract class WebAssemblyJSRuntime : JSInProcessRuntime, IJSUnmarshalledRuntime public abstract class WebAssemblyJSRuntime : JSInProcessRuntime, IJSUnmarshalledRuntime
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override string InvokeJS(string identifier, string argsJson) protected override string InvokeJS(string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
{ {
var noAsyncHandle = default(long); var callInfo = new JSCallInfo
var result = InternalCalls.InvokeJSMarshalled(out var exception, ref noAsyncHandle, identifier, argsJson); {
FunctionIdentifier = identifier,
TargetInstanceId = targetInstanceId,
ResultType = resultType,
MarshalledCallArgsJson = argsJson ?? "[]",
MarshalledCallAsyncHandle = default
};
var result = InternalCalls.InvokeJS<object, object, object, string>(out var exception, ref callInfo, null, null, null);
return exception != null return exception != null
? throw new JSException(exception) ? throw new JSException(exception)
: result; : result;
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
{ {
InternalCalls.InvokeJSMarshalled(out _, ref asyncHandle, identifier, argsJson); var callInfo = new JSCallInfo
{
FunctionIdentifier = identifier,
TargetInstanceId = targetInstanceId,
ResultType = resultType,
MarshalledCallArgsJson = argsJson ?? "[]",
MarshalledCallAsyncHandle = asyncHandle
};
InternalCalls.InvokeJS<object, object, object, string>(out _, ref callInfo, null, null, null);
} }
protected override void EndInvokeDotNet(DotNetInvocationInfo callInfo, in DotNetInvocationResult dispatchResult) protected override void EndInvokeDotNet(DotNetInvocationInfo callInfo, in DotNetInvocationResult dispatchResult)
@ -39,7 +57,7 @@ namespace Microsoft.JSInterop.WebAssembly
// We pass 0 as the async handle because we don't want the JS-side code to // We pass 0 as the async handle because we don't want the JS-side code to
// send back any notification (we're just providing a result for an existing async call) // send back any notification (we're just providing a result for an existing async call)
var args = JsonSerializer.Serialize(new[] { callInfo.CallId, dispatchResult.Success, resultOrError }, JsonSerializerOptions); var args = JsonSerializer.Serialize(new[] { callInfo.CallId, dispatchResult.Success, resultOrError }, JsonSerializerOptions);
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args); BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args, JSCallResultType.Default, 0);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -57,7 +75,14 @@ namespace Microsoft.JSInterop.WebAssembly
/// <inheritdoc /> /// <inheritdoc />
TResult IJSUnmarshalledRuntime.InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2) TResult IJSUnmarshalledRuntime.InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2)
{ {
var result = InternalCalls.InvokeJSUnmarshalled<T0, T1, T2, TResult>(out var exception, identifier, arg0, arg1, arg2); var callInfo = new JSCallInfo
{
FunctionIdentifier = identifier,
ResultType = ResultTypeFromGeneric<TResult>()
};
var result = InternalCalls.InvokeJS<T0, T1, T2, TResult>(out var exception, ref callInfo, arg0, arg1, arg2);
return exception != null return exception != null
? throw new JSException(exception) ? throw new JSException(exception)
: result; : result;

View File

@ -55,10 +55,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
["result7Async"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,123,24,48,6.25]", ["result7Async"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,123,24,48,6.25]",
["result8Async"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]", ["result8Async"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
["result9Async"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]", ["result9Async"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
["roundTripJSObjectReferenceAsync"] = @"""successful""",
["invokeDisposedJSObjectReferenceExceptionAsync"] = @"""JS object instance with ID",
["AsyncThrowSyncException"] = @"""System.InvalidOperationException: Threw a sync exception!", ["AsyncThrowSyncException"] = @"""System.InvalidOperationException: Threw a sync exception!",
["AsyncThrowAsyncException"] = @"""System.InvalidOperationException: Threw an async exception!", ["AsyncThrowAsyncException"] = @"""System.InvalidOperationException: Threw an async exception!",
["SyncExceptionFromAsyncMethod"] = "Function threw a sync exception!", ["SyncExceptionFromAsyncMethod"] = "Function threw a sync exception!",
["AsyncExceptionFromAsyncMethod"] = "Function threw an async exception!", ["AsyncExceptionFromAsyncMethod"] = "Function threw an async exception!",
["JSObjectReferenceInvokeNonFunctionException"] = "The value 'nonFunction' is not a function.",
["resultReturnDotNetObjectByRefAsync"] = "1001", ["resultReturnDotNetObjectByRefAsync"] = "1001",
["instanceMethodThisTypeNameAsync"] = @"""JavaScriptInterop""", ["instanceMethodThisTypeNameAsync"] = @"""JavaScriptInterop""",
["instanceMethodStringValueUpperAsync"] = @"""MY STRING""", ["instanceMethodStringValueUpperAsync"] = @"""MY STRING""",
@ -69,6 +72,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
["testDtoAsync"] = "Same", ["testDtoAsync"] = "Same",
["returnPrimitiveAsync"] = "123", ["returnPrimitiveAsync"] = "123",
["returnArrayAsync"] = "first,second", ["returnArrayAsync"] = "first,second",
["jsObjectReference.identity"] = "Invoked from JSObjectReference",
["jsObjectReference.nested.add"] = "5",
["addViaJSObjectReference"] = "5",
["jsObjectReferenceModule"] = "Returned from module!",
["syncGenericInstanceMethod"] = @"""Initial value""", ["syncGenericInstanceMethod"] = @"""Initial value""",
["asyncGenericInstanceMethod"] = @"""Updated value 1""", ["asyncGenericInstanceMethod"] = @"""Updated value 1""",
}; };
@ -93,6 +100,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
["result7"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,123,24,48,6.25]", ["result7"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,123,24,48,6.25]",
["result8"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]", ["result8"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
["result9"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]", ["result9"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
["roundTripJSObjectReference"] = @"""successful""",
["invokeDisposedJSObjectReferenceException"] = @"""JS object instance with ID",
["ThrowException"] = @"""System.InvalidOperationException: Threw an exception!", ["ThrowException"] = @"""System.InvalidOperationException: Threw an exception!",
["ExceptionFromSyncMethod"] = "Function threw an exception!", ["ExceptionFromSyncMethod"] = "Function threw an exception!",
["resultReturnDotNetObjectByRefSync"] = "1000", ["resultReturnDotNetObjectByRefSync"] = "1000",
@ -100,6 +109,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
["instanceMethodStringValueUpper"] = @"""MY STRING""", ["instanceMethodStringValueUpper"] = @"""MY STRING""",
["instanceMethodIncomingByRef"] = "123", ["instanceMethodIncomingByRef"] = "123",
["instanceMethodOutgoingByRef"] = "1234", ["instanceMethodOutgoingByRef"] = "1234",
["jsInProcessObjectReference.identity"] = "Invoked from JSInProcessObjectReference",
["stringValueUpperSync"] = "MY STRING", ["stringValueUpperSync"] = "MY STRING",
["testDtoNonSerializedValueSync"] = "99999", ["testDtoNonSerializedValueSync"] = "99999",
["testDtoSync"] = "Same", ["testDtoSync"] = "Same",

View File

@ -46,6 +46,8 @@
<p id="@nameof(SyncExceptionFromAsyncMethod)">@SyncExceptionFromAsyncMethod?.Message</p> <p id="@nameof(SyncExceptionFromAsyncMethod)">@SyncExceptionFromAsyncMethod?.Message</p>
<h2>@nameof(AsyncExceptionFromAsyncMethod)</h2> <h2>@nameof(AsyncExceptionFromAsyncMethod)</h2>
<p id="@nameof(AsyncExceptionFromAsyncMethod)">@AsyncExceptionFromAsyncMethod?.Message</p> <p id="@nameof(AsyncExceptionFromAsyncMethod)">@AsyncExceptionFromAsyncMethod?.Message</p>
<h2>@nameof(JSObjectReferenceInvokeNonFunctionException)</h2>
<p id="@nameof(JSObjectReferenceInvokeNonFunctionException)">@JSObjectReferenceInvokeNonFunctionException?.Message</p>
</div> </div>
@if (DoneWithInterop) @if (DoneWithInterop)
{ {
@ -59,6 +61,7 @@
public JSException ExceptionFromSyncMethod { get; set; } public JSException ExceptionFromSyncMethod { get; set; }
public JSException SyncExceptionFromAsyncMethod { get; set; } public JSException SyncExceptionFromAsyncMethod { get; set; }
public JSException AsyncExceptionFromAsyncMethod { get; set; } public JSException AsyncExceptionFromAsyncMethod { get; set; }
public JSException JSObjectReferenceInvokeNonFunctionException { get; set; }
public IDictionary<string, object> ReceiveDotNetObjectByRefResult { get; set; } = new Dictionary<string, object>(); public IDictionary<string, object> ReceiveDotNetObjectByRefResult { get; set; } = new Dictionary<string, object>();
public IDictionary<string, object> ReceiveDotNetObjectByRefAsyncResult { get; set; } = new Dictionary<string, object>(); public IDictionary<string, object> ReceiveDotNetObjectByRefAsyncResult { get; set; } = new Dictionary<string, object>();
@ -134,6 +137,28 @@
ReturnValues["returnArray"] = string.Join(",", ((IJSInProcessRuntime)JSRuntime).Invoke<Segment[]>("returnArray").Select(x => x.Source).ToArray()); ReturnValues["returnArray"] = string.Join(",", ((IJSInProcessRuntime)JSRuntime).Invoke<Segment[]>("returnArray").Select(x => x.Source).ToArray());
} }
var jsObjectReference = await JSRuntime.InvokeAsync<JSObjectReference>("returnJSObjectReference");
ReturnValues["jsObjectReference.identity"] = await jsObjectReference.InvokeAsync<string>("identity", "Invoked from JSObjectReference");
ReturnValues["jsObjectReference.nested.add"] = (await jsObjectReference.InvokeAsync<int>("nested.add", 2, 3)).ToString();
ReturnValues["addViaJSObjectReference"] = (await JSRuntime.InvokeAsync<int>("addViaJSObjectReference", jsObjectReference, 2, 3)).ToString();
try
{
await jsObjectReference.InvokeAsync<object>("nonFunction");
}
catch (JSException e)
{
JSObjectReferenceInvokeNonFunctionException = e;
}
var module = await JSRuntime.InvokeAsync<JSObjectReference>("import", "./js/testmodule.js");
ReturnValues["jsObjectReferenceModule"] = await module.InvokeAsync<string>("identity", "Returned from module!");
if (shouldSupportSyncInterop)
{
InvokeInProcessJSInterop();
}
Invocations = invocations; Invocations = invocations;
DoneWithInterop = true; DoneWithInterop = true;
} }
@ -163,6 +188,14 @@
ReceiveDotNetObjectByRefResult["testDto"] = result.TestDto.Value == passDotNetObjectByRef ? "Same" : "Different"; ReceiveDotNetObjectByRefResult["testDto"] = result.TestDto.Value == passDotNetObjectByRef ? "Same" : "Different";
} }
public void InvokeInProcessJSInterop()
{
var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);
var jsInProcObjectReference = inProcRuntime.Invoke<JSInProcessObjectReference>("returnJSObjectReference");
ReturnValues["jsInProcessObjectReference.identity"] = jsInProcObjectReference.Invoke<string>("identity", "Invoked from JSInProcessObjectReference");
}
public class PassDotNetObjectByRefArgs public class PassDotNetObjectByRefArgs
{ {
public string StringValue { get; set; } public string StringValue { get; set; }

View File

@ -414,6 +414,47 @@ namespace BasicTestApp.InteropTest
return objectByRef.Value.GetNonSerializedValue(); return objectByRef.Value.GetNonSerializedValue();
} }
[JSInvokable]
public static JSObjectReference RoundTripJSObjectReference(JSObjectReference jsObjectReference)
{
return jsObjectReference;
}
[JSInvokable]
public static async Task<JSObjectReference> RoundTripJSObjectReferenceAsync(JSObjectReference jSObjectReference)
{
await Task.Yield();
return jSObjectReference;
}
[JSInvokable]
public static string InvokeDisposedJSObjectReferenceException(JSInProcessObjectReference jsObjectReference)
{
try
{
jsObjectReference.Invoke<object>("noop");
return "No exception thrown";
}
catch (JSException e)
{
return e.Message;
}
}
[JSInvokable]
public static async Task<string> InvokeDisposedJSObjectReferenceExceptionAsync(JSObjectReference jsObjectReference)
{
try
{
await jsObjectReference.InvokeVoidAsync("noop");
return "No exception thrown";
}
catch (JSException e)
{
return e.Message;
}
}
[JSInvokable] [JSInvokable]
public InstanceMethodOutput InstanceMethod(InstanceMethodInput input) public InstanceMethodOutput InstanceMethod(InstanceMethodInput input)
{ {

View File

@ -30,6 +30,17 @@ async function invokeDotNetInteropMethodsAsync(shouldSupportSyncInterop, dotNetO
var returnDotNetObjectByRefResult = DotNet.invokeMethod(assemblyName, 'ReturnDotNetObjectByRef'); var returnDotNetObjectByRefResult = DotNet.invokeMethod(assemblyName, 'ReturnDotNetObjectByRef');
results['resultReturnDotNetObjectByRefSync'] = DotNet.invokeMethod(assemblyName, 'ExtractNonSerializedValue', returnDotNetObjectByRefResult['Some sync instance']); results['resultReturnDotNetObjectByRefSync'] = DotNet.invokeMethod(assemblyName, 'ExtractNonSerializedValue', returnDotNetObjectByRefResult['Some sync instance']);
var jsObjectReference = DotNet.createJSObjectReference({
prop: 'successful',
noop: function () { }
});
var returnedObject = DotNet.invokeMethod(assemblyName, 'RoundTripJSObjectReference', jsObjectReference);
results['roundTripJSObjectReference'] = returnedObject && returnedObject.prop;
DotNet.disposeJSObjectReference(jsObjectReference);
results['invokeDisposedJSObjectReferenceException'] = DotNet.invokeMethod(assemblyName, 'InvokeDisposedJSObjectReferenceException', jsObjectReference);
var instanceMethodResult = instanceMethodsTarget.invokeMethod('InstanceMethod', { var instanceMethodResult = instanceMethodsTarget.invokeMethod('InstanceMethod', {
stringValue: 'My string', stringValue: 'My string',
dtoByRef: dotNetObjectByRef dtoByRef: dotNetObjectByRef
@ -66,6 +77,17 @@ async function invokeDotNetInteropMethodsAsync(shouldSupportSyncInterop, dotNetO
const returnDotNetObjectByRefAsync = await DotNet.invokeMethodAsync(assemblyName, 'ReturnDotNetObjectByRefAsync'); const returnDotNetObjectByRefAsync = await DotNet.invokeMethodAsync(assemblyName, 'ReturnDotNetObjectByRefAsync');
results['resultReturnDotNetObjectByRefAsync'] = await DotNet.invokeMethodAsync(assemblyName, 'ExtractNonSerializedValue', returnDotNetObjectByRefAsync['Some async instance']); results['resultReturnDotNetObjectByRefAsync'] = await DotNet.invokeMethodAsync(assemblyName, 'ExtractNonSerializedValue', returnDotNetObjectByRefAsync['Some async instance']);
var jsObjectReference = DotNet.createJSObjectReference({
prop: 'successful',
noop: function () { }
});
var returnedObject = await DotNet.invokeMethodAsync(assemblyName, 'RoundTripJSObjectReferenceAsync', jsObjectReference);
results['roundTripJSObjectReferenceAsync'] = returnedObject && returnedObject.prop;
DotNet.disposeJSObjectReference(jsObjectReference);
results['invokeDisposedJSObjectReferenceExceptionAsync'] = await DotNet.invokeMethodAsync(assemblyName, 'InvokeDisposedJSObjectReferenceExceptionAsync', jsObjectReference);
const instanceMethodAsync = await instanceMethodsTarget.invokeMethodAsync('InstanceMethodAsync', { const instanceMethodAsync = await instanceMethodsTarget.invokeMethodAsync('InstanceMethodAsync', {
stringValue: 'My string', stringValue: 'My string',
dtoByRef: dotNetObjectByRef dtoByRef: dotNetObjectByRef
@ -167,6 +189,8 @@ window.jsInteropTests = {
asyncFunctionThrowsAsyncException: asyncFunctionThrowsAsyncException, asyncFunctionThrowsAsyncException: asyncFunctionThrowsAsyncException,
returnPrimitive: returnPrimitive, returnPrimitive: returnPrimitive,
returnPrimitiveAsync: returnPrimitiveAsync, returnPrimitiveAsync: returnPrimitiveAsync,
returnJSObjectReference: returnJSObjectReference,
addViaJSObjectReference: addViaJSObjectReference,
receiveDotNetObjectByRef: receiveDotNetObjectByRef, receiveDotNetObjectByRef: receiveDotNetObjectByRef,
receiveDotNetObjectByRefAsync: receiveDotNetObjectByRefAsync receiveDotNetObjectByRefAsync: receiveDotNetObjectByRefAsync
}; };
@ -195,6 +219,27 @@ function returnArrayAsync() {
}); });
} }
function returnJSObjectReference() {
return {
identity: function (value) {
return value;
},
nonFunction: 123,
nested: {
add: function (a, b) {
return a + b;
}
},
dispose: function () {
DotNet.disposeJSObjectReference(this);
},
};
}
function addViaJSObjectReference(jsObjectReference, a, b) {
return jsObjectReference.nested.add(a, b);
}
function functionThrowsException() { function functionThrowsException() {
throw new Error('Function threw an exception!'); throw new Error('Function threw an exception!');
} }
@ -258,4 +303,4 @@ function receiveDotNetObjectByRefAsync(incomingData) {
testDto: testDto testDto: testDto
}; };
}); });
} }

View File

@ -0,0 +1,3 @@
export function identity(value) {
return value;
}

View File

@ -6,9 +6,68 @@ export module DotNet {
export type JsonReviver = ((key: any, value: any) => any); export type JsonReviver = ((key: any, value: any) => any);
const jsonRevivers: JsonReviver[] = []; const jsonRevivers: JsonReviver[] = [];
class JSObject {
_cachedFunctions: Map<string, Function>;
constructor(private _jsObject: any)
{
this._cachedFunctions = new Map<string, Function>();
}
public findFunction(identifier: string) {
const cachedFunction = this._cachedFunctions.get(identifier);
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;
} else {
throw new Error(`The value '${identifier}' is not a function.`);
}
}
public getWrappedObject() {
return this._jsObject;
}
}
const jsObjectIdKey = "__jsObjectId";
const pendingAsyncCalls: { [id: number]: PendingAsyncCall<any> } = {}; const pendingAsyncCalls: { [id: number]: PendingAsyncCall<any> } = {};
const cachedJSFunctions: { [identifier: string]: Function } = {}; const windowJSObjectId = 0;
const cachedJSObjectsById: { [id: number]: JSObject } = {
[windowJSObjectId]: new JSObject(window),
};
cachedJSObjectsById[windowJSObjectId]._cachedFunctions.set('import', (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.
if (typeof url === 'string' && url.startsWith('./')) {
url = document.baseURI + url.substr(2);
}
return import(/* webpackIgnore: true */ url);
});
let nextAsyncCallId = 1; // Start at 1 because zero signals "no response needed" let nextAsyncCallId = 1; // Start at 1 because zero signals "no response needed"
let nextJsObjectId = 1; // Start at 1 because zero is reserved for "window"
let dotNetDispatcher: DotNetCallDispatcher | null = null; let dotNetDispatcher: DotNetCallDispatcher | null = null;
@ -55,6 +114,58 @@ export module DotNet {
return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args); return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args);
} }
/**
* Creates a JavaScript object reference that can be passed to .NET via interop calls.
*
* @param jsObject The JavaScript Object used to create the JavaScript object reference.
* @returns The JavaScript object reference (this will be the same instance as the given object).
* @throws Error if the given value is not an Object.
*/
export function createJSObjectReference(jsObject: any): any {
if (jsObject && typeof jsObject === 'object') {
cachedJSObjectsById[nextJsObjectId] = new JSObject(jsObject);
const result = {
[jsObjectIdKey]: nextJsObjectId
};
nextJsObjectId++;
return result;
} else {
throw new Error(`Cannot create a JSObjectReference from the value '${jsObject}'.`);
}
}
/**
* Disposes the given JavaScript object reference.
*
* @param jsObjectReference The JavaScript Object reference.
*/
export function disposeJSObjectReference(jsObjectReference: any): void {
const id = jsObjectReference && jsObjectReference[jsObjectIdKey];
if (typeof id === 'number') {
disposeJSObjectReferenceById(id);
}
}
/**
* Parses the given JSON string using revivers to restore args passed from .NET to JS.
*
* @param json The JSON stirng to parse.
*/
export function parseJsonWithRevivers(json: string): any {
return json ? JSON.parse(json, (key, initialValue) => {
// Invoke each reviver in order, passing the output from the previous reviver,
// so that each one gets a chance to transform the value
return jsonRevivers.reduce(
(latestValue, reviver) => reviver(key, latestValue),
initialValue
);
}) : null;
}
function invokePossibleInstanceMethod<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): T { function invokePossibleInstanceMethod<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): T {
const dispatcher = getRequiredDispatcher(); const dispatcher = getRequiredDispatcher();
if (dispatcher.invokeDotNetFromJS) { if (dispatcher.invokeDotNetFromJS) {
@ -114,6 +225,14 @@ export module DotNet {
reject: (reason?: any) => void; reject: (reason?: any) => void;
} }
/**
* Represents the type of result expected from a JS interop call.
*/
export enum JSCallResultType {
Default = 0,
JSObjectReference = 1
}
/** /**
* Represents the ability to dispatch calls from JavaScript to a .NET runtime. * Represents the ability to dispatch calls from JavaScript to a .NET runtime.
*/ */
@ -158,19 +277,31 @@ export module DotNet {
* Finds the JavaScript function matching the specified identifier. * Finds the JavaScript function matching the specified identifier.
* *
* @param identifier Identifies the globally-reachable function to be returned. * @param identifier Identifies the globally-reachable function to be returned.
* @param targetInstanceId The instance ID of the target JS object.
* @returns A Function instance. * @returns A Function instance.
*/ */
findJSFunction, // Note that this is used by the JS interop code inside Mono WebAssembly itself findJSFunction, // Note that this is used by the JS interop code inside Mono WebAssembly itself
/**
* Disposes the JavaScript object reference with the specified object ID.
*
* @param id The ID of the JavaScript object reference.
*/
disposeJSObjectReferenceById,
/** /**
* Invokes the specified synchronous JavaScript function. * Invokes the specified synchronous JavaScript function.
* *
* @param identifier Identifies the globally-reachable function to invoke. * @param identifier Identifies the globally-reachable function to invoke.
* @param argsJson JSON representation of arguments to be passed to the function. * @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.
* @returns JSON representation of the invocation result. * @returns JSON representation of the invocation result.
*/ */
invokeJSFromDotNet: (identifier: string, argsJson: string) => { invokeJSFromDotNet: (identifier: string, argsJson: string, resultType: JSCallResultType, targetInstanceId: number) => {
const result = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson)); const returnValue = findJSFunction(identifier, targetInstanceId).apply(null, parseJsonWithRevivers(argsJson));
const result = createJSCallResult(returnValue, resultType);
return result === null || result === undefined return result === null || result === undefined
? null ? null
: JSON.stringify(result, argReplacer); : JSON.stringify(result, argReplacer);
@ -182,12 +313,14 @@ export module DotNet {
* @param asyncHandle A value identifying the asynchronous operation. This value will be passed back in a later call to endInvokeJSFromDotNet. * @param asyncHandle A value identifying the asynchronous operation. This value will be passed back in a later call to endInvokeJSFromDotNet.
* @param identifier Identifies the globally-reachable function to invoke. * @param identifier Identifies the globally-reachable function to invoke.
* @param argsJson JSON representation of arguments to be passed to the function. * @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.
*/ */
beginInvokeJSFromDotNet: (asyncHandle: number, identifier: string, argsJson: string): void => { beginInvokeJSFromDotNet: (asyncHandle: number, identifier: string, argsJson: string, resultType: JSCallResultType, targetInstanceId: number): void => {
// Coerce synchronous functions into async ones, plus treat // Coerce synchronous functions into async ones, plus treat
// synchronous exceptions the same as async ones // synchronous exceptions the same as async ones
const promise = new Promise<any>(resolve => { const promise = new Promise<any>(resolve => {
const synchronousResultOrPromise = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson)); const synchronousResultOrPromise = findJSFunction(identifier, targetInstanceId).apply(null, parseJsonWithRevivers(argsJson));
resolve(synchronousResultOrPromise); resolve(synchronousResultOrPromise);
}); });
@ -196,7 +329,7 @@ export module DotNet {
// On completion, dispatch result back to .NET // On completion, dispatch result back to .NET
// Not using "await" because it codegens a lot of boilerplate // Not using "await" because it codegens a lot of boilerplate
promise.then( promise.then(
result => getRequiredDispatcher().endInvokeJSFromDotNet(asyncHandle, true, JSON.stringify([asyncHandle, true, result], argReplacer)), result => getRequiredDispatcher().endInvokeJSFromDotNet(asyncHandle, true, JSON.stringify([asyncHandle, true, createJSCallResult(result, resultType)], argReplacer)),
error => getRequiredDispatcher().endInvokeJSFromDotNet(asyncHandle, false, JSON.stringify([asyncHandle, false, formatError(error)])) error => getRequiredDispatcher().endInvokeJSFromDotNet(asyncHandle, false, JSON.stringify([asyncHandle, false, formatError(error)]))
); );
} }
@ -214,17 +347,6 @@ export module DotNet {
} }
} }
function parseJsonWithRevivers(json: string): any {
return json ? JSON.parse(json, (key, initialValue) => {
// Invoke each reviver in order, passing the output from the previous reviver,
// so that each one gets a chance to transform the value
return jsonRevivers.reduce(
(latestValue, reviver) => reviver(key, latestValue),
initialValue
);
}) : null;
}
function formatError(error: any): string { function formatError(error: any): string {
if (error instanceof Error) { if (error instanceof Error) {
return `${error.message}\n${error.stack}`; return `${error.message}\n${error.stack}`;
@ -233,33 +355,20 @@ export module DotNet {
} }
} }
function findJSFunction(identifier: string): Function { function findJSFunction(identifier: string, targetInstanceId: number): Function {
if (Object.prototype.hasOwnProperty.call(cachedJSFunctions, identifier)) { let targetInstance = cachedJSObjectsById[targetInstanceId];
return cachedJSFunctions[identifier];
}
let result: any = window; if (targetInstance) {
let resultIdentifier = 'window'; return targetInstance.findFunction(identifier);
let lastSegmentValue: any;
identifier.split('.').forEach(segment => {
if (segment in result) {
lastSegmentValue = result;
result = result[segment];
resultIdentifier += '.' + segment;
} else {
throw new Error(`Could not find '${segment}' in '${resultIdentifier}'.`);
}
});
if (result instanceof Function) {
result = result.bind(lastSegmentValue);
cachedJSFunctions[identifier] = result;
return result;
} else { } else {
throw new Error(`The value '${resultIdentifier}' is not a function.`); throw new Error(`JS object instance with ID ${targetInstanceId} does not exist (has it been disposed?).`);
} }
} }
function disposeJSObjectReferenceById(id: number) {
delete cachedJSObjectsById[id];
}
class DotNetObject { class DotNetObject {
constructor(private _id: number) { constructor(private _id: number) {
} }
@ -292,6 +401,33 @@ export module DotNet {
return value; return value;
}); });
attachReviver(function reviveJSObjectReference(key: any, value: any) {
if (value && typeof value === 'object' && value.hasOwnProperty(jsObjectIdKey)) {
const id = value[jsObjectIdKey];
const jsObject = cachedJSObjectsById[id];
if (jsObject) {
return jsObject.getWrappedObject();
} else {
throw new Error(`JS object instance with ID ${id} does not exist (has it been disposed?).`);
}
}
// Unrecognized - let another reviver handle it
return value;
});
function createJSCallResult(returnValue: any, resultType: JSCallResultType) {
switch (resultType) {
case JSCallResultType.Default:
return returnValue;
case JSCallResultType.JSObjectReference:
return createJSObjectReference(returnValue);
default:
throw new Error(`Invalid JS call result type '${resultType}'.`);
}
}
function argReplacer(key: string, value: any) { function argReplacer(key: string, value: any) {
return value instanceof DotNetObject ? value.serializeAsArg() : value; return value instanceof DotNetObject ? value.serializeAsArg() : value;
} }

View File

@ -8,7 +8,8 @@
"lib": ["es2015", "dom", "es2015.promise"], "lib": ["es2015", "dom", "es2015.promise"],
"strict": true, "strict": true,
"declaration": true, "declaration": true,
"outDir": "dist" "outDir": "dist",
"module": "ESNext",
}, },
"include": [ "include": [
"src/**/*.ts" "src/**/*.ts"

View File

@ -0,0 +1,59 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.JSInterop.Infrastructure
{
internal sealed class JSObjectReferenceJsonConverter<TJSObjectReference>
: JsonConverter<TJSObjectReference> where TJSObjectReference : JSObjectReference
{
private readonly Func<long, TJSObjectReference> _jsObjectReferenceFactory;
public JSObjectReferenceJsonConverter(Func<long, TJSObjectReference> jsObjectReferenceFactory)
{
_jsObjectReferenceFactory = jsObjectReferenceFactory;
}
public override TJSObjectReference? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
long id = -1;
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
if (id == -1 && reader.ValueTextEquals(JSObjectReference.IdKey.EncodedUtf8Bytes))
{
reader.Read();
id = reader.GetInt64();
}
else
{
throw new JsonException($"Unexcepted JSON property {reader.GetString()}.");
}
}
else
{
throw new JsonException($"Unexcepted JSON token {reader.TokenType}");
}
}
if (id == -1)
{
throw new JsonException($"Required property {JSObjectReference.IdKey} not found.");
}
return _jsObjectReferenceFactory(id);
}
public override void Write(Utf8JsonWriter writer, TJSObjectReference value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteNumber(JSObjectReference.IdKey, value.Id);
writer.WriteEndObject();
}
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.JSInterop
{
/// <summary>
/// Describes the type of result expected from a JS interop call.
/// </summary>
public enum JSCallResultType : int
{
/// <summary>
/// Indicates that the returned value is not treated in a special way.
/// </summary>
Default = 0,
/// <summary>
/// Indicates that the returned value is to be treated as a JS object reference.
/// </summary>
JSObjectReference = 1,
}
}

View File

@ -0,0 +1,35 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.JSInterop
{
/// <summary>
/// Represents a reference to a JavaScript object whose functions can be invoked synchronously.
/// </summary>
public class JSInProcessObjectReference : JSObjectReference
{
private readonly JSInProcessRuntime _jsRuntime;
internal JSInProcessObjectReference(JSInProcessRuntime jsRuntime, long id) : base(jsRuntime, id)
{
_jsRuntime = jsRuntime;
}
/// <summary>
/// Invokes the specified JavaScript function synchronously.
/// </summary>
/// <typeparam name="TValue">The JSON-serializable return type.</typeparam>
/// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>someScope.someFunction</c> on the target instance.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
[return: MaybeNull]
public TValue Invoke<TValue>(string identifier, params object[] args)
{
ThrowIfDisposed();
return _jsRuntime.Invoke<TValue>(identifier, Id, args);
}
}
}

View File

@ -3,6 +3,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json; using System.Text.Json;
using Microsoft.JSInterop.Infrastructure;
namespace Microsoft.JSInterop namespace Microsoft.JSInterop
{ {
@ -12,16 +13,23 @@ namespace Microsoft.JSInterop
public abstract class JSInProcessRuntime : JSRuntime, IJSInProcessRuntime public abstract class JSInProcessRuntime : JSRuntime, IJSInProcessRuntime
{ {
/// <summary> /// <summary>
/// Invokes the specified JavaScript function synchronously. /// Initializes a new instance of <see cref="JSInProcessRuntime"/>.
/// </summary> /// </summary>
/// <typeparam name="TValue">The JSON-serializable return type.</typeparam> protected JSInProcessRuntime()
/// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
[return: MaybeNull]
public TValue Invoke<TValue>(string identifier, params object?[]? args)
{ {
var resultJson = InvokeJS(identifier, JsonSerializer.Serialize(args, JsonSerializerOptions)); JsonSerializerOptions.Converters.Add(new JSObjectReferenceJsonConverter<JSInProcessObjectReference>(
id => new JSInProcessObjectReference(this, id)));
}
[return: MaybeNull]
internal TValue Invoke<TValue>(string identifier, long targetInstanceId, params object?[]? args)
{
var resultJson = InvokeJS(
identifier,
JsonSerializer.Serialize(args, JsonSerializerOptions),
ResultTypeFromGeneric<TValue>(),
targetInstanceId);
if (resultJson is null) if (resultJson is null)
{ {
return default; return default;
@ -30,12 +38,34 @@ namespace Microsoft.JSInterop
return JsonSerializer.Deserialize<TValue>(resultJson, JsonSerializerOptions); return JsonSerializer.Deserialize<TValue>(resultJson, JsonSerializerOptions);
} }
/// <summary>
/// Invokes the specified JavaScript function synchronously.
/// </summary>
/// <typeparam name="TValue">The JSON-serializable return type.</typeparam>
/// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
[return: MaybeNull]
public TValue Invoke<TValue>(string identifier, params object?[]? args)
=> Invoke<TValue>(identifier, 0, args);
/// <summary> /// <summary>
/// Performs a synchronous function invocation. /// Performs a synchronous function invocation.
/// </summary> /// </summary>
/// <param name="identifier">The identifier for the function to invoke.</param> /// <param name="identifier">The identifier for the function to invoke.</param>
/// <param name="argsJson">A JSON representation of the arguments.</param> /// <param name="argsJson">A JSON representation of the arguments.</param>
/// <returns>A JSON representation of the result.</returns> /// <returns>A JSON representation of the result.</returns>
protected abstract string? InvokeJS(string identifier, string? argsJson); protected virtual string? InvokeJS(string identifier, string? argsJson)
=> InvokeJS(identifier, argsJson, JSCallResultType.Default, 0);
/// <summary>
/// Performs a synchronous function invocation.
/// </summary>
/// <param name="identifier">The identifier for the function to invoke.</param>
/// <param name="argsJson">A JSON representation of the arguments.</param>
/// <param name="resultType">The type of result expected from the invocation.</param>
/// <param name="targetInstanceId">The instance ID of the target JS object.</param>
/// <returns>A JSON representation of the result.</returns>
protected abstract string? InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId);
} }
} }

View File

@ -0,0 +1,103 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.JSInterop
{
/// <summary>
/// Represents a reference to a JavaScript object.
/// </summary>
public class JSObjectReference : IAsyncDisposable
{
internal static readonly JsonEncodedText IdKey = JsonEncodedText.Encode("__jsObjectId");
private readonly JSRuntime _jsRuntime;
private bool _disposed;
internal long Id { get; }
internal JSObjectReference(JSRuntime jsRuntime, long id)
{
_jsRuntime = jsRuntime;
Id = id;
}
/// <summary>
/// Invokes the specified JavaScript function asynchronously.
/// </summary>
/// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>someScope.someFunction</c> on the target instance.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>A <see cref="ValueTask"/> that represents the asynchronous invocation operation.</returns>
public async ValueTask InvokeVoidAsync(string identifier, params object[] args)
{
await InvokeAsync<object>(identifier, args);
}
/// <summary>
/// Invokes the specified JavaScript function asynchronously.
/// <para>
/// <see cref="JSRuntime"/> will apply timeouts to this operation based on the value configured in <see cref="JSRuntime.DefaultAsyncTimeout"/>. To dispatch a call with a different, or no timeout,
/// consider using <see cref="InvokeAsync{TValue}(string, CancellationToken, object[])" />.
/// </para>
/// </summary>
/// <typeparam name="TValue">The JSON-serializable return type.</typeparam>
/// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>someScope.someFunction</c> on the target instance.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object[] args)
{
ThrowIfDisposed();
return _jsRuntime.InvokeAsync<TValue>(Id, identifier, args);
}
/// <summary>
/// Invokes the specified JavaScript function asynchronously.
/// </summary>
/// <typeparam name="TValue">The JSON-serializable return type.</typeparam>
/// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>someScope.someFunction</c> on the target instance.</param>
/// <param name="cancellationToken">
/// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts
/// (<see cref="JSRuntime.DefaultAsyncTimeout"/>) from being applied.
/// </param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, params object[] args)
{
ThrowIfDisposed();
return _jsRuntime.InvokeAsync<TValue>(Id, identifier, cancellationToken, args);
}
/// <summary>
/// Disposes the <see cref="JSObjectReference"/>, freeing its resources and disabling it from further use.
/// </summary>
/// <returns>A <see cref="ValueTask"/> representing the completion of the operation.</returns>
public async ValueTask DisposeAsync()
{
if (!_disposed)
{
_disposed = true;
await _jsRuntime.InvokeVoidAsync("DotNet.jsCallDispatcher.disposeJSObjectReferenceById", Id);
}
}
/// <summary>
/// Throws an exception if this instance has been disposed.
/// </summary>
protected void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
}
}
}

View File

@ -37,6 +37,7 @@ namespace Microsoft.JSInterop
Converters = Converters =
{ {
new DotNetObjectReferenceJsonConverterFactory(this), new DotNetObjectReferenceJsonConverterFactory(this),
new JSObjectReferenceJsonConverter<JSObjectReference>(id => new JSObjectReference(this, id)),
} }
}; };
} }
@ -51,6 +52,17 @@ namespace Microsoft.JSInterop
/// </summary> /// </summary>
protected TimeSpan? DefaultAsyncTimeout { get; set; } protected TimeSpan? DefaultAsyncTimeout { get; set; }
/// <summary>
/// Creates a <see cref="JSCallResultType"/> from the given generic type.
/// </summary>
/// <typeparam name="TResult">
/// The type of the result of the relevant JS interop call.
/// </typeparam>
protected static JSCallResultType ResultTypeFromGeneric<TResult>()
=> typeof(TResult) == typeof(JSObjectReference) || typeof(TResult) == typeof(JSInProcessObjectReference) ?
JSCallResultType.JSObjectReference :
JSCallResultType.Default;
/// <summary> /// <summary>
/// Invokes the specified JavaScript function asynchronously. /// Invokes the specified JavaScript function asynchronously.
/// <para> /// <para>
@ -62,17 +74,8 @@ namespace Microsoft.JSInterop
/// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param> /// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param>
/// <param name="args">JSON-serializable arguments.</param> /// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns> /// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
public async ValueTask<TValue> InvokeAsync<TValue>(string identifier, object?[]? args) public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object?[]? args)
{ => InvokeAsync<TValue>(0, identifier, args);
if (DefaultAsyncTimeout.HasValue)
{
using var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value);
// We need to await here due to the using
return await InvokeAsync<TValue>(identifier, cts.Token, args);
}
return await InvokeAsync<TValue>(identifier, CancellationToken.None, args);
}
/// <summary> /// <summary>
/// Invokes the specified JavaScript function asynchronously. /// Invokes the specified JavaScript function asynchronously.
@ -81,11 +84,30 @@ namespace Microsoft.JSInterop
/// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param> /// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param>
/// <param name="cancellationToken"> /// <param name="cancellationToken">
/// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts
/// (<see cref="JSRuntime.DefaultAsyncTimeout"/>) from being applied. /// (<see cref="DefaultAsyncTimeout"/>) from being applied.
/// </param> /// </param>
/// <param name="args">JSON-serializable arguments.</param> /// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns> /// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object?[]? args) public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object?[]? args)
=> InvokeAsync<TValue>(0, identifier, cancellationToken, args);
internal async ValueTask<TValue> InvokeAsync<TValue>(long targetInstanceId, string identifier, object?[]? args)
{
if (DefaultAsyncTimeout.HasValue)
{
using var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value);
// We need to await here due to the using
return await InvokeAsync<TValue>(targetInstanceId, identifier, cts.Token, args);
}
return await InvokeAsync<TValue>(targetInstanceId, identifier, CancellationToken.None, args);
}
internal ValueTask<TValue> InvokeAsync<TValue>(
long targetInstanceId,
string identifier,
CancellationToken cancellationToken,
object?[]? args)
{ {
var taskId = Interlocked.Increment(ref _nextPendingTaskId); var taskId = Interlocked.Increment(ref _nextPendingTaskId);
var tcs = new TaskCompletionSource<TValue>(TaskContinuationOptions.RunContinuationsAsynchronously); var tcs = new TaskCompletionSource<TValue>(TaskContinuationOptions.RunContinuationsAsynchronously);
@ -112,7 +134,9 @@ namespace Microsoft.JSInterop
var argsJson = args?.Any() == true ? var argsJson = args?.Any() == true ?
JsonSerializer.Serialize(args, JsonSerializerOptions) : JsonSerializer.Serialize(args, JsonSerializerOptions) :
null; null;
BeginInvokeJS(taskId, identifier, argsJson); var resultType = ResultTypeFromGeneric<TValue>();
BeginInvokeJS(taskId, identifier, argsJson, resultType, targetInstanceId);
return new ValueTask<TValue>(tcs.Task); return new ValueTask<TValue>(tcs.Task);
} }
@ -138,7 +162,18 @@ namespace Microsoft.JSInterop
/// <param name="taskId">The identifier for the function invocation, or zero if no async callback is required.</param> /// <param name="taskId">The identifier for the function invocation, or zero if no async callback is required.</param>
/// <param name="identifier">The identifier for the function to invoke.</param> /// <param name="identifier">The identifier for the function to invoke.</param>
/// <param name="argsJson">A JSON representation of the arguments.</param> /// <param name="argsJson">A JSON representation of the arguments.</param>
protected abstract void BeginInvokeJS(long taskId, string identifier, string? argsJson); protected virtual void BeginInvokeJS(long taskId, string identifier, string? argsJson)
=> BeginInvokeJS(taskId, identifier, argsJson, JSCallResultType.Default, 0);
/// <summary>
/// Begins an asynchronous function invocation.
/// </summary>
/// <param name="taskId">The identifier for the function invocation, or zero if no async callback is required.</param>
/// <param name="identifier">The identifier for the function to invoke.</param>
/// <param name="argsJson">A JSON representation of the arguments.</param>
/// <param name="resultType">The type of result expected from the invocation.</param>
/// <param name="targetInstanceId">The instance ID of the target JS object.</param>
protected abstract void BeginInvokeJS(long taskId, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId);
/// <summary> /// <summary>
/// Completes an async JS interop call from JavaScript to .NET /// Completes an async JS interop call from JavaScript to .NET

View File

@ -885,7 +885,7 @@ namespace Microsoft.JSInterop.Infrastructure
public string LastCompletionCallId { get; private set; } public string LastCompletionCallId { get; private set; }
public DotNetInvocationResult LastCompletionResult { get; private set; } public DotNetInvocationResult LastCompletionResult { get; private set; }
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
{ {
LastInvocationAsyncHandle = asyncHandle; LastInvocationAsyncHandle = asyncHandle;
LastInvocationIdentifier = identifier; LastInvocationIdentifier = identifier;
@ -894,7 +894,7 @@ namespace Microsoft.JSInterop.Infrastructure
_nextInvocationTcs = new TaskCompletionSource<object>(); _nextInvocationTcs = new TaskCompletionSource<object>();
} }
protected override string InvokeJS(string identifier, string argsJson) protected override string InvokeJS(string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
{ {
LastInvocationAsyncHandle = default; LastInvocationAsyncHandle = default;
LastInvocationIdentifier = identifier; LastInvocationIdentifier = identifier;

View File

@ -0,0 +1,85 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Text.Json;
using Xunit;
namespace Microsoft.JSInterop.Infrastructure
{
public class JSObjectReferenceJsonConverterTest
{
private readonly JSRuntime JSRuntime = new TestJSRuntime();
private JsonSerializerOptions JsonSerializerOptions => JSRuntime.JsonSerializerOptions;
[Fact]
public void Read_Throws_IfJsonIsMissingJSObjectIdProperty()
{
// Arrange
var json = "{}";
// Act & Assert
var ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<JSObjectReference>(json, JsonSerializerOptions));
Assert.Equal("Required property __jsObjectId not found.", ex.Message);
}
[Fact]
public void Read_Throws_IfJsonContainsUnknownContent()
{
// Arrange
var json = "{\"foo\":2}";
// Act & Assert
var ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<JSObjectReference>(json, JsonSerializerOptions));
Assert.Equal("Unexcepted JSON property foo.", ex.Message);
}
[Fact]
public void Read_Throws_IfJsonIsIncomplete()
{
// Arrange
var json = $"{{\"__jsObjectId\":5";
// Act & Assert
var ex = Record.Exception(() => JsonSerializer.Deserialize<JSObjectReference>(json, JsonSerializerOptions));
Assert.IsAssignableFrom<JsonException>(ex);
}
[Fact]
public void Read_Throws_IfJSObjectIdAppearsMultipleTimes()
{
// Arrange
var json = $"{{\"__jsObjectId\":3,\"__jsObjectId\":7}}";
// Act & Assert
var ex = Record.Exception(() => JsonSerializer.Deserialize<JSObjectReference>(json, JsonSerializerOptions));
Assert.IsAssignableFrom<JsonException>(ex);
}
[Fact]
public void Read_ReadsJson()
{
// Arrange
var expectedId = 3;
var json = $"{{\"__jsObjectId\":{expectedId}}}";
// Act
var deserialized = JsonSerializer.Deserialize<JSObjectReference>(json, JsonSerializerOptions)!;
// Assert
Assert.Equal(expectedId, deserialized?.Id);
}
[Fact]
public void Write_WritesValidJson()
{
// Arrange
var jsObjectRef = new JSObjectReference(JSRuntime, 7);
// Act
var json = JsonSerializer.Serialize(jsObjectRef, JsonSerializerOptions);
// Assert
Assert.Equal($"{{\"__jsObjectId\":{jsObjectRef.Id}}}", json);
}
}
}

View File

@ -99,7 +99,7 @@ namespace Microsoft.JSInterop
public string? NextResultJson { get; set; } public string? NextResultJson { get; set; }
protected override string? InvokeJS(string identifier, string? argsJson) protected override string? InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
{ {
InvokeCalls.Add(new InvokeArgs { Identifier = identifier, ArgsJson = argsJson }); InvokeCalls.Add(new InvokeArgs { Identifier = identifier, ArgsJson = argsJson });
return NextResultJson; return NextResultJson;
@ -111,7 +111,7 @@ namespace Microsoft.JSInterop
public string? ArgsJson { get; set; } public string? ArgsJson { get; set; }
} }
protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson) protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
=> throw new NotImplementedException("This test only covers sync calls"); => throw new NotImplementedException("This test only covers sync calls");
protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult)

View File

@ -0,0 +1,105 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.JSInterop.Infrastructure;
using Xunit;
namespace Microsoft.JSInterop.Tests
{
public class JSObjectReferenceTest
{
[Fact]
public void JSObjectReference_InvokeAsync_CallsUnderlyingJSRuntimeInvokeAsync()
{
// Arrange
var jsRuntime = new TestJSRuntime();
var jsObject = new JSObjectReference(jsRuntime, 0);
// Act
_ = jsObject.InvokeAsync<object>("test", "arg1", "arg2");
// Assert
Assert.Equal(1, jsRuntime.BeginInvokeJSInvocationCount);
}
[Fact]
public void JSInProcessObjectReference_Invoke_CallsUnderlyingJSRuntimeInvoke()
{
// Arrange
var jsRuntime = new TestJSInProcessRuntime();
var jsObject = new JSInProcessObjectReference(jsRuntime, 0);
// Act
jsObject.Invoke<object>("test", "arg1", "arg2");
// Assert
Assert.Equal(1, jsRuntime.InvokeJSInvocationCount);
}
[Fact]
public async Task JSObjectReference_Dispose_DisallowsFurtherInteropCalls()
{
// Arrange
var jsRuntime = new TestJSRuntime();
var jsObject = new JSObjectReference(jsRuntime, 0);
// Act
_ = jsObject.DisposeAsync();
// Assert
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await jsObject.InvokeAsync<object>("test", "arg1", "arg2"));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await jsObject.InvokeAsync<object>("test", CancellationToken.None, "arg1", "arg2"));
}
[Fact]
public void JSInProcessObjectReference_Dispose_DisallowsFurtherInteropCalls()
{
// Arrange
var jsRuntime = new TestJSInProcessRuntime();
var jsObject = new JSInProcessObjectReference(jsRuntime, 0);
// Act
_ = jsObject.DisposeAsync();
// Assert
Assert.Throws<ObjectDisposedException>(() => jsObject.Invoke<object>("test", "arg1", "arg2"));
}
class TestJSRuntime : JSRuntime
{
public int BeginInvokeJSInvocationCount { get; private set; }
protected override void BeginInvokeJS(long taskId, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
{
BeginInvokeJSInvocationCount++;
}
protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult)
{
}
}
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 string? InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
{
InvokeJSInvocationCount++;
return null;
}
protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult)
{
}
}
}
}

View File

@ -377,7 +377,7 @@ namespace Microsoft.JSInterop
}); });
} }
protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson) protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
{ {
BeginInvokeCalls.Add(new BeginInvokeAsyncArgs BeginInvokeCalls.Add(new BeginInvokeAsyncArgs
{ {

View File

@ -8,7 +8,7 @@ namespace Microsoft.JSInterop
{ {
internal class TestJSRuntime : JSRuntime internal class TestJSRuntime : JSRuntime
{ {
protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson) protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }