diff --git a/src/Components/test/E2ETest/Tests/InteropTest.cs b/src/Components/test/E2ETest/Tests/InteropTest.cs index b98fb69d72f1..17cefe6d94e3 100644 --- a/src/Components/test/E2ETest/Tests/InteropTest.cs +++ b/src/Components/test/E2ETest/Tests/InteropTest.cs @@ -108,7 +108,8 @@ public void CanInvokeInteropMethods() ["invokeNewWithClassConstructorAsync.function"] = "6", ["invokeNewWithNonConstructorAsync"] = "Success", // Function reference tests - ["changeFunctionViaObjectReferenceAsync"] = "42" + ["changeFunctionViaObjectReferenceAsync"] = "42", + ["invokeDelegateFromAsAsyncFunction"] = "42" }; var expectedSyncValues = new Dictionary diff --git a/src/Components/test/testassets/BasicTestApp/InteropComponent.razor b/src/Components/test/testassets/BasicTestApp/InteropComponent.razor index 92d845d42571..8c5d34a18c01 100644 --- a/src/Components/test/testassets/BasicTestApp/InteropComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/InteropComponent.razor @@ -614,6 +614,9 @@ var testClassRef = await JSRuntime.InvokeNewAsync("jsInteropTests.TestClass", "abraka"); await testClassRef.SetValueAsync("getTextLength", funcRef); ReturnValues["changeFunctionViaObjectReferenceAsync"] = (await testClassRef.InvokeAsync("getTextLength")).ToString(); + + var funcDelegate = funcRef.AsFunction(); + ReturnValues["invokeDelegateFromAsAsyncFunction"] = (await funcDelegate()).ToString(); } private void FunctionReferenceTests() diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts index fdd7a4ed65ec..cc26b2dcd0a2 100644 --- a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts @@ -562,7 +562,17 @@ export module DotNet { const targetInstance = cachedJSObjectsById[targetInstanceId]; if (targetInstance) { - return targetInstance.resolveInvocationHandler(identifier, callType ?? JSCallType.FunctionCall); + if (identifier) { + return targetInstance.resolveInvocationHandler(identifier, callType ?? JSCallType.FunctionCall); + } else { + const wrappedObject = targetInstance.getWrappedObject(); + + if (wrappedObject instanceof Function) { + return wrappedObject; + } else { + throw new Error(`JS object instance with ID ${targetInstanceId} is not a function.`); + } + } } throw new Error(`JS object instance with ID ${targetInstanceId} does not exist (has it been disposed?).`); @@ -626,7 +636,10 @@ export module DotNet { if (!isReadableProperty(parent, memberName)) { throw new Error(`The property '${identifier}' is not defined or is not readable.`); } - return () => parent[memberName]; + + return parent[memberName] instanceof Function + ? () => parent[memberName].bind(parent) + : () => parent[memberName]; case JSCallType.SetValue: if (!isWritableProperty(parent, memberName)) { throw new Error(`The property '${identifier}' is not writable.`); diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs index 4ddcc358ef2f..23a4dd5a7a69 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs @@ -167,4 +167,127 @@ public static ValueTask InvokeNewAsync(this IJSObjectReferen return jsObjectReference.InvokeNewAsync(identifier, cancellationToken, args); } + + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func AsVoidFunction(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async () => await jsObjectReference.InvokeVoidAsync(string.Empty); + } + + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JSON-serializable type of the first argument. + /// The JSON-serializable return type. + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func AsVoidFunction(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async (T1 arg1) => await jsObjectReference.InvokeVoidAsync(string.Empty, [arg1]); + } + + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JSON-serializable type of the first argument. + /// The JSON-serializable type of the second argument. + /// The JSON-serializable return type. + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func AsVoidFunction(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async (T1 arg1, T2 arg2) => await jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2]); + } + + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JSON-serializable type of the first argument. + /// The JSON-serializable type of the second argument. + /// The JSON-serializable type of the third argument. + /// The JSON-serializable return type. + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func AsVoidFunction(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async (T1 arg1, T2 arg2, T3 arg3) => await jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3]); + } + + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JSON-serializable return type. + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func> AsFunction<[DynamicallyAccessedMembers(JsonSerialized)] TResult>(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async () => await jsObjectReference.InvokeAsync(string.Empty); + } + + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JSON-serializable type of the first argument. + /// The JSON-serializable return type. + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func> AsFunction(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async (T1 arg1) => await jsObjectReference.InvokeAsync(string.Empty, [arg1]); + } + + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JSON-serializable type of the first argument. + /// The JSON-serializable type of the second argument. + /// The JSON-serializable return type. + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func> AsFunction(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async (T1 arg1, T2 arg2) => await jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2]); + } + + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JSON-serializable type of the first argument. + /// The JSON-serializable type of the second argument. + /// The JSON-serializable type of the third argument. + /// The JSON-serializable return type. + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func> AsFunction(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async (T1 arg1, T2 arg2, T3 arg3) => await jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3]); + } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index 3270d74b01a0..7a50ffbafe80 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -56,6 +56,14 @@ Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, object?[]? args Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.JSRuntime.SetValueAsync(string! identifier, TValue value) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.JSRuntime.SetValueAsync(string! identifier, TValue value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func>! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func>! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func>! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func>! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsVoidFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsVoidFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsVoidFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsVoidFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func! static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.TimeSpan timeout, object?[]? args) -> System.Threading.Tasks.ValueTask diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs new file mode 100644 index 000000000000..44e22e0ae8e3 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Reflection.PortableExecutable; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.JSInterop.Implementation; +using Microsoft.JSInterop.Infrastructure; + +namespace Microsoft.JSInterop.Tests; + +public class JSObjectReferenceExtensionsTest +{ + [Fact] + public void AsFunction_ReturnsFunc_ThatInvokesInterop() + { + // Arrange + var jsRuntime = new RecordingTestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(42)); + var reader = new Utf8JsonReader(bytes); + + // Act + var func = jsObjectReference.AsFunction(); + ValueTask task = func(1); + + jsRuntime.EndInvokeJS( + jsRuntime.InvokeCalls[0].AsyncHandle, + /* succeeded: */ true, + ref reader); + + // Assert + Assert.True(task.IsCompleted); +#pragma warning disable xUnit1031 // Do not use blocking task operations in test method + Assert.Equal(42, task.Result); +#pragma warning restore xUnit1031 // Do not use blocking task operations in test method + } + + class RecordingTestJSRuntime : TestJSRuntime + { + public List InvokeCalls { get; set; } = []; + + protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo) + { + InvokeCalls.Add(invocationInfo); + } + } +}