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
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
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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|x86.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -7934,6 +7948,7 @@ Global
{B06040BC-DA28-4923-8CAC-20EB517D471B} = {22D7D74B-565D-4047-97B4-F149B1A13350}
{55CACC1F-FE96-47C8-8073-91F4CAA55C75} = {2A91479A-4ABE-4BB7-9A5E-CA3B9CCFC69E}
{7509AA1E-3093-4BEE-984F-E11579E98A11} = {7CB09412-C9B0-47E8-A8C3-311AA4CFDE04}
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E} = {16898702-3E33-41C1-B8D8-4CE3F1D46BD9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
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));
}
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)
{
@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
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

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.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);
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
const platform = Environment.setPlatform(monoPlatform);
window['Blazor'].platform = platform;
@ -84,6 +87,28 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
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;
if (shouldAutoStart()) {
boot().catch(error => {

View File

@ -16,12 +16,7 @@ namespace WebAssembly.JSInterop
// in driver.c in the Mono distribution
/// 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)]
public static extern string InvokeJSMarshalled(out string exception, ref long asyncHandle, string functionIdentifier, string argsJson);
[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);
public static extern TRes InvokeJS<T0, T1, T2, TRes>(out string exception, ref JSCallInfo callInfo, [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
{
/// <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 result = InternalCalls.InvokeJSMarshalled(out var exception, ref noAsyncHandle, identifier, argsJson);
var callInfo = new JSCallInfo
{
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
? throw new JSException(exception)
: result;
}
/// <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)
@ -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
// 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);
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args);
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args, JSCallResultType.Default, 0);
}
/// <inheritdoc />
@ -57,7 +75,14 @@ namespace Microsoft.JSInterop.WebAssembly
/// <inheritdoc />
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
? throw new JSException(exception)
: 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]",
["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}]",
["roundTripJSObjectReferenceAsync"] = @"""successful""",
["invokeDisposedJSObjectReferenceExceptionAsync"] = @"""JS object instance with ID",
["AsyncThrowSyncException"] = @"""System.InvalidOperationException: Threw a sync exception!",
["AsyncThrowAsyncException"] = @"""System.InvalidOperationException: Threw an async exception!",
["SyncExceptionFromAsyncMethod"] = "Function threw a sync exception!",
["AsyncExceptionFromAsyncMethod"] = "Function threw an async exception!",
["JSObjectReferenceInvokeNonFunctionException"] = "The value 'nonFunction' is not a function.",
["resultReturnDotNetObjectByRefAsync"] = "1001",
["instanceMethodThisTypeNameAsync"] = @"""JavaScriptInterop""",
["instanceMethodStringValueUpperAsync"] = @"""MY STRING""",
@ -69,6 +72,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
["testDtoAsync"] = "Same",
["returnPrimitiveAsync"] = "123",
["returnArrayAsync"] = "first,second",
["jsObjectReference.identity"] = "Invoked from JSObjectReference",
["jsObjectReference.nested.add"] = "5",
["addViaJSObjectReference"] = "5",
["jsObjectReferenceModule"] = "Returned from module!",
["syncGenericInstanceMethod"] = @"""Initial value""",
["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]",
["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}]",
["roundTripJSObjectReference"] = @"""successful""",
["invokeDisposedJSObjectReferenceException"] = @"""JS object instance with ID",
["ThrowException"] = @"""System.InvalidOperationException: Threw an exception!",
["ExceptionFromSyncMethod"] = "Function threw an exception!",
["resultReturnDotNetObjectByRefSync"] = "1000",
@ -100,6 +109,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
["instanceMethodStringValueUpper"] = @"""MY STRING""",
["instanceMethodIncomingByRef"] = "123",
["instanceMethodOutgoingByRef"] = "1234",
["jsInProcessObjectReference.identity"] = "Invoked from JSInProcessObjectReference",
["stringValueUpperSync"] = "MY STRING",
["testDtoNonSerializedValueSync"] = "99999",
["testDtoSync"] = "Same",

View File

@ -46,6 +46,8 @@
<p id="@nameof(SyncExceptionFromAsyncMethod)">@SyncExceptionFromAsyncMethod?.Message</p>
<h2>@nameof(AsyncExceptionFromAsyncMethod)</h2>
<p id="@nameof(AsyncExceptionFromAsyncMethod)">@AsyncExceptionFromAsyncMethod?.Message</p>
<h2>@nameof(JSObjectReferenceInvokeNonFunctionException)</h2>
<p id="@nameof(JSObjectReferenceInvokeNonFunctionException)">@JSObjectReferenceInvokeNonFunctionException?.Message</p>
</div>
@if (DoneWithInterop)
{
@ -59,6 +61,7 @@
public JSException ExceptionFromSyncMethod { get; set; }
public JSException SyncExceptionFromAsyncMethod { 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> 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());
}
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;
DoneWithInterop = true;
}
@ -163,6 +188,14 @@
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 string StringValue { get; set; }

View File

@ -414,6 +414,47 @@ namespace BasicTestApp.InteropTest
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]
public InstanceMethodOutput InstanceMethod(InstanceMethodInput input)
{

View File

@ -30,6 +30,17 @@ async function invokeDotNetInteropMethodsAsync(shouldSupportSyncInterop, dotNetO
var returnDotNetObjectByRefResult = DotNet.invokeMethod(assemblyName, 'ReturnDotNetObjectByRef');
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', {
stringValue: 'My string',
dtoByRef: dotNetObjectByRef
@ -66,6 +77,17 @@ async function invokeDotNetInteropMethodsAsync(shouldSupportSyncInterop, dotNetO
const returnDotNetObjectByRefAsync = await DotNet.invokeMethodAsync(assemblyName, 'ReturnDotNetObjectByRefAsync');
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', {
stringValue: 'My string',
dtoByRef: dotNetObjectByRef
@ -167,6 +189,8 @@ window.jsInteropTests = {
asyncFunctionThrowsAsyncException: asyncFunctionThrowsAsyncException,
returnPrimitive: returnPrimitive,
returnPrimitiveAsync: returnPrimitiveAsync,
returnJSObjectReference: returnJSObjectReference,
addViaJSObjectReference: addViaJSObjectReference,
receiveDotNetObjectByRef: receiveDotNetObjectByRef,
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() {
throw new Error('Function threw an exception!');
}
@ -258,4 +303,4 @@ function receiveDotNetObjectByRefAsync(incomingData) {
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);
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 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 nextJsObjectId = 1; // Start at 1 because zero is reserved for "window"
let dotNetDispatcher: DotNetCallDispatcher | null = null;
@ -55,6 +114,58 @@ export module DotNet {
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 {
const dispatcher = getRequiredDispatcher();
if (dispatcher.invokeDotNetFromJS) {
@ -114,6 +225,14 @@ export module DotNet {
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.
*/
@ -158,19 +277,31 @@ export module DotNet {
* Finds the JavaScript function matching the specified identifier.
*
* @param identifier Identifies the globally-reachable function to be returned.
* @param targetInstanceId The instance ID of the target JS object.
* @returns A Function instance.
*/
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.
*
* @param identifier Identifies the globally-reachable function to invoke.
* @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.
*/
invokeJSFromDotNet: (identifier: string, argsJson: string) => {
const result = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson));
invokeJSFromDotNet: (identifier: string, argsJson: string, resultType: JSCallResultType, targetInstanceId: number) => {
const returnValue = findJSFunction(identifier, targetInstanceId).apply(null, parseJsonWithRevivers(argsJson));
const result = createJSCallResult(returnValue, resultType);
return result === null || result === undefined
? null
: 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 identifier Identifies the globally-reachable function to invoke.
* @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
// synchronous exceptions the same as async ones
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);
});
@ -196,7 +329,7 @@ export module DotNet {
// On completion, dispatch result back to .NET
// Not using "await" because it codegens a lot of boilerplate
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)]))
);
}
@ -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 {
if (error instanceof Error) {
return `${error.message}\n${error.stack}`;
@ -233,33 +355,20 @@ export module DotNet {
}
}
function findJSFunction(identifier: string): Function {
if (Object.prototype.hasOwnProperty.call(cachedJSFunctions, identifier)) {
return cachedJSFunctions[identifier];
}
function findJSFunction(identifier: string, targetInstanceId: number): Function {
let targetInstance = cachedJSObjectsById[targetInstanceId];
let result: any = window;
let resultIdentifier = 'window';
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;
if (targetInstance) {
return targetInstance.findFunction(identifier);
} 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 {
constructor(private _id: number) {
}
@ -292,6 +401,33 @@ export module DotNet {
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) {
return value instanceof DotNetObject ? value.serializeAsArg() : value;
}

View File

@ -8,7 +8,8 @@
"lib": ["es2015", "dom", "es2015.promise"],
"strict": true,
"declaration": true,
"outDir": "dist"
"outDir": "dist",
"module": "ESNext",
},
"include": [
"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.Text.Json;
using Microsoft.JSInterop.Infrastructure;
namespace Microsoft.JSInterop
{
@ -12,16 +13,23 @@ namespace Microsoft.JSInterop
public abstract class JSInProcessRuntime : JSRuntime, IJSInProcessRuntime
{
/// <summary>
/// Invokes the specified JavaScript function synchronously.
/// Initializes a new instance of <see cref="JSInProcessRuntime"/>.
/// </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)
protected JSInProcessRuntime()
{
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)
{
return default;
@ -30,12 +38,34 @@ namespace Microsoft.JSInterop
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>
/// 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>
/// <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 =
{
new DotNetObjectReferenceJsonConverterFactory(this),
new JSObjectReferenceJsonConverter<JSObjectReference>(id => new JSObjectReference(this, id)),
}
};
}
@ -51,6 +52,17 @@ namespace Microsoft.JSInterop
/// </summary>
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>
/// Invokes the specified JavaScript function asynchronously.
/// <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="args">JSON-serializable arguments.</param>
/// <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)
{
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);
}
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object?[]? args)
=> InvokeAsync<TValue>(0, identifier, args);
/// <summary>
/// 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="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.
/// (<see cref="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, 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 tcs = new TaskCompletionSource<TValue>(TaskContinuationOptions.RunContinuationsAsynchronously);
@ -112,7 +134,9 @@ namespace Microsoft.JSInterop
var argsJson = args?.Any() == true ?
JsonSerializer.Serialize(args, JsonSerializerOptions) :
null;
BeginInvokeJS(taskId, identifier, argsJson);
var resultType = ResultTypeFromGeneric<TValue>();
BeginInvokeJS(taskId, identifier, argsJson, resultType, targetInstanceId);
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="identifier">The identifier for the function to invoke.</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>
/// 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 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;
LastInvocationIdentifier = identifier;
@ -894,7 +894,7 @@ namespace Microsoft.JSInterop.Infrastructure
_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;
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; }
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 });
return NextResultJson;
@ -111,7 +111,7 @@ namespace Microsoft.JSInterop
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");
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
{

View File

@ -8,7 +8,7 @@ namespace Microsoft.JSInterop
{
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();
}