Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Components/test/E2ETest/Tests/InteropTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,9 @@
var testClassRef = await JSRuntime.InvokeNewAsync("jsInteropTests.TestClass", "abraka");
await testClassRef.SetValueAsync("getTextLength", funcRef);
ReturnValues["changeFunctionViaObjectReferenceAsync"] = (await testClassRef.InvokeAsync<int>("getTextLength")).ToString();

var funcDelegate = funcRef.AsFunction<int>();
ReturnValues["invokeDelegateFromAsAsyncFunction"] = (await funcDelegate()).ToString();
}

private void FunctionReferenceTests()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?).`);
Expand Down Expand Up @@ -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.`);
Expand Down
123 changes: 123 additions & 0 deletions src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,127 @@ public static ValueTask<IJSObjectReference> InvokeNewAsync(this IJSObjectReferen

return jsObjectReference.InvokeNewAsync(identifier, cancellationToken, args);
}

/// <summary>
/// Wraps the interop invocation of the JavaScript function referenced by <paramref name="jsObjectReference"/> as a .NET delegate.
/// </summary>
/// <param name="jsObjectReference">The JavaScript object reference that represents the function to be invoked.</param>
/// <returns>A delegate that can be used to invoke the JavaScript function.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="jsObjectReference"/> is null.</exception>
public static Func<ValueTask> AsVoidFunction(this IJSObjectReference jsObjectReference)
Copy link
Member

@pavelsavara pavelsavara Apr 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is guess ValueTask (vs Task) is here for consistency ?

Copy link
Member Author

@oroztocil oroztocil May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because the other Invoke* methods return ValueTask. The JSRuntime method which implements the actual call also returns ValueTask (although it creates it from a regular Task, for some reason).

{
ArgumentNullException.ThrowIfNull(jsObjectReference);

return async () => await jsObjectReference.InvokeVoidAsync(string.Empty);
}

/// <summary>
/// Wraps the interop invocation of the JavaScript function referenced by <paramref name="jsObjectReference"/> as a .NET delegate.
/// </summary>
/// <typeparam name="T1">The JSON-serializable type of the first argument.</typeparam>
/// <typeparam name="TResult">The JSON-serializable return type.</typeparam>
/// <param name="jsObjectReference">The JavaScript object reference that represents the function to be invoked.</param>
/// <returns>A delegate that can be used to invoke the JavaScript function.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="jsObjectReference"/> is null.</exception>
public static Func<T1, ValueTask> AsVoidFunction<T1, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(this IJSObjectReference jsObjectReference)
{
ArgumentNullException.ThrowIfNull(jsObjectReference);

return async (T1 arg1) => await jsObjectReference.InvokeVoidAsync(string.Empty, [arg1]);
}

/// <summary>
/// Wraps the interop invocation of the JavaScript function referenced by <paramref name="jsObjectReference"/> as a .NET delegate.
/// </summary>
/// <typeparam name="T1">The JSON-serializable type of the first argument.</typeparam>
/// <typeparam name="T2">The JSON-serializable type of the second argument.</typeparam>
/// <typeparam name="TResult">The JSON-serializable return type.</typeparam>
/// <param name="jsObjectReference">The JavaScript object reference that represents the function to be invoked.</param>
/// <returns>A delegate that can be used to invoke the JavaScript function.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="jsObjectReference"/> is null.</exception>
public static Func<T1, T2, ValueTask> AsVoidFunction<T1, T2, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(this IJSObjectReference jsObjectReference)
{
ArgumentNullException.ThrowIfNull(jsObjectReference);

return async (T1 arg1, T2 arg2) => await jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2]);
}

/// <summary>
/// Wraps the interop invocation of the JavaScript function referenced by <paramref name="jsObjectReference"/> as a .NET delegate.
/// </summary>
/// <typeparam name="T1">The JSON-serializable type of the first argument.</typeparam>
/// <typeparam name="T2">The JSON-serializable type of the second argument.</typeparam>
/// <typeparam name="T3">The JSON-serializable type of the third argument.</typeparam>
/// <typeparam name="TResult">The JSON-serializable return type.</typeparam>
/// <param name="jsObjectReference">The JavaScript object reference that represents the function to be invoked.</param>
/// <returns>A delegate that can be used to invoke the JavaScript function.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="jsObjectReference"/> is null.</exception>
public static Func<T1, T2, T3, ValueTask> AsVoidFunction<T1, T2, T3, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(this IJSObjectReference jsObjectReference)
{
ArgumentNullException.ThrowIfNull(jsObjectReference);

return async (T1 arg1, T2 arg2, T3 arg3) => await jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3]);
}

/// <summary>
/// Wraps the interop invocation of the JavaScript function referenced by <paramref name="jsObjectReference"/> as a .NET delegate.
/// </summary>
/// <typeparam name="TResult">The JSON-serializable return type.</typeparam>
/// <param name="jsObjectReference">The JavaScript object reference that represents the function to be invoked.</param>
/// <returns>A delegate that can be used to invoke the JavaScript function.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="jsObjectReference"/> is null.</exception>
public static Func<ValueTask<TResult>> AsFunction<[DynamicallyAccessedMembers(JsonSerialized)] TResult>(this IJSObjectReference jsObjectReference)
{
ArgumentNullException.ThrowIfNull(jsObjectReference);

return async () => await jsObjectReference.InvokeAsync<TResult>(string.Empty);
}

/// <summary>
/// Wraps the interop invocation of the JavaScript function referenced by <paramref name="jsObjectReference"/> as a .NET delegate.
/// </summary>
/// <typeparam name="T1">The JSON-serializable type of the first argument.</typeparam>
/// <typeparam name="TResult">The JSON-serializable return type.</typeparam>
/// <param name="jsObjectReference">The JavaScript object reference that represents the function to be invoked.</param>
/// <returns>A delegate that can be used to invoke the JavaScript function.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="jsObjectReference"/> is null.</exception>
public static Func<T1, ValueTask<TResult>> AsFunction<T1, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(this IJSObjectReference jsObjectReference)
{
ArgumentNullException.ThrowIfNull(jsObjectReference);

return async (T1 arg1) => await jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1]);
}

/// <summary>
/// Wraps the interop invocation of the JavaScript function referenced by <paramref name="jsObjectReference"/> as a .NET delegate.
/// </summary>
/// <typeparam name="T1">The JSON-serializable type of the first argument.</typeparam>
/// <typeparam name="T2">The JSON-serializable type of the second argument.</typeparam>
/// <typeparam name="TResult">The JSON-serializable return type.</typeparam>
/// <param name="jsObjectReference">The JavaScript object reference that represents the function to be invoked.</param>
/// <returns>A delegate that can be used to invoke the JavaScript function.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="jsObjectReference"/> is null.</exception>
public static Func<T1, T2, ValueTask<TResult>> AsFunction<T1, T2, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(this IJSObjectReference jsObjectReference)
{
ArgumentNullException.ThrowIfNull(jsObjectReference);

return async (T1 arg1, T2 arg2) => await jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2]);
}

/// <summary>
/// Wraps the interop invocation of the JavaScript function referenced by <paramref name="jsObjectReference"/> as a .NET delegate.
/// </summary>
/// <typeparam name="T1">The JSON-serializable type of the first argument.</typeparam>
/// <typeparam name="T2">The JSON-serializable type of the second argument.</typeparam>
/// <typeparam name="T3">The JSON-serializable type of the third argument.</typeparam>
/// <typeparam name="TResult">The JSON-serializable return type.</typeparam>
/// <param name="jsObjectReference">The JavaScript object reference that represents the function to be invoked.</param>
/// <returns>A delegate that can be used to invoke the JavaScript function.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="jsObjectReference"/> is null.</exception>
public static Func<T1, T2, T3, ValueTask<TResult>> AsFunction<T1, T2, T3, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(this IJSObjectReference jsObjectReference)
{
ArgumentNullException.ThrowIfNull(jsObjectReference);

return async (T1 arg1, T2 arg2, T3 arg3) => await jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2, arg3]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.IJSObjectReference!>
Microsoft.JSInterop.JSRuntime.SetValueAsync<TValue>(string! identifier, TValue value) -> System.Threading.Tasks.ValueTask
Microsoft.JSInterop.JSRuntime.SetValueAsync<TValue>(string! identifier, TValue value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask
static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction<T1, T2, T3, TResult>(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func<T1, T2, T3, System.Threading.Tasks.ValueTask<TResult>>!
static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction<T1, T2, TResult>(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func<T1, T2, System.Threading.Tasks.ValueTask<TResult>>!
static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction<T1, TResult>(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func<T1, System.Threading.Tasks.ValueTask<TResult>>!
static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction<TResult>(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func<System.Threading.Tasks.ValueTask<TResult>>!
static Microsoft.JSInterop.JSObjectReferenceExtensions.AsVoidFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func<System.Threading.Tasks.ValueTask>!
static Microsoft.JSInterop.JSObjectReferenceExtensions.AsVoidFunction<T1, T2, T3, TResult>(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func<T1, T2, T3, System.Threading.Tasks.ValueTask>!
static Microsoft.JSInterop.JSObjectReferenceExtensions.AsVoidFunction<T1, T2, TResult>(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func<T1, T2, System.Threading.Tasks.ValueTask>!
static Microsoft.JSInterop.JSObjectReferenceExtensions.AsVoidFunction<T1, TResult>(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func<T1, System.Threading.Tasks.ValueTask>!
static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>
static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>
static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.TimeSpan timeout, object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<int, int>();
ValueTask<int> 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<JSInvocationInfo> InvokeCalls { get; set; } = [];

protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo)
{
InvokeCalls.Add(invocationInfo);
}
}
}
Loading