Skip to content

Add InvokeNew, GetValue & SetValue to JS interop API #61246

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Apr 9, 2025
Merged
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
df3822f
WIP implementation with JSInvocationInfo
oroztocil Mar 18, 2025
bdea46d
WIP implementation for other platforms, fix tests
oroztocil Mar 18, 2025
8e51e9c
Fix function calls, add JS tests
oroztocil Mar 21, 2025
f227e09
Add JS interop tests
oroztocil Mar 24, 2025
9717446
Add new interop methods to InProcessRuntime
oroztocil Mar 25, 2025
1c525a4
Merge remote-tracking branch 'origin/main' into oroztocil/js-interop-…
oroztocil Mar 25, 2025
e653a00
WIP wasm & webview fixes
oroztocil Mar 25, 2025
4009275
WebView test fixes
oroztocil Mar 26, 2025
5e273e3
Add DotNetToJSInterop component to BasicTestApp
oroztocil Mar 26, 2025
0443be5
Merge remote-tracking branch 'origin/main' into oroztocil/js-interop-…
oroztocil Mar 27, 2025
951de51
WIP add E2E tests for new interop methods
oroztocil Mar 27, 2025
7056120
Add & fix E2E tests for interop
oroztocil Mar 28, 2025
200d736
Merge remote-tracking branch 'origin/main' into oroztocil/js-interop-…
oroztocil Mar 28, 2025
e7294ce
Add test for reading JS property from prototype
oroztocil Mar 28, 2025
42617e4
Remove debug prints and unwanted changes
oroztocil Mar 28, 2025
21caedd
Remove unwanted indentation changes in legacy code
oroztocil Mar 28, 2025
c7f49d6
Fix API, remove uneeded files
oroztocil Mar 31, 2025
93d3006
Add missing extension overloads, clean up code
oroztocil Mar 31, 2025
2508129
Rollback unwanted changes
oroztocil Mar 31, 2025
45260dd
Revert submodule to origin/main state
oroztocil Mar 31, 2025
5270dd8
Merge remote-tracking branch 'origin/main' into oroztocil/js-interop-…
oroztocil Mar 31, 2025
c6934f6
Rollback unwanted changes in submodule
oroztocil Mar 31, 2025
c13223b
Fix Microsoft.JSInterop tests
oroztocil Mar 31, 2025
2f84ea0
Code review fixes
oroztocil Apr 2, 2025
28c8d39
Minor code review changes
oroztocil Apr 3, 2025
1d8495c
Remove JSInvocationInfo from interop bridge communication
oroztocil Apr 4, 2025
b79f224
Refactor resolving JS object members, fix tests
oroztocil Apr 6, 2025
bd0ea2b
Remove unused overloads from PublicAPI.Unshipped.txt
oroztocil Apr 6, 2025
802f96a
Revert unwanted changes, fix style
oroztocil Apr 8, 2025
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
25 changes: 24 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.JSInterop;
using static Microsoft.AspNetCore.Internal.LinkerFlags;

namespace Microsoft.AspNetCore.Components.Endpoints;

internal sealed class UnsupportedJavaScriptRuntime : IJSRuntime
{
private const string Message = "JavaScript interop calls cannot be issued during server-side static rendering, because the page has not yet loaded in the browser. Statically-rendered components must wrap any JavaScript interop calls in conditional logic to ensure those interop calls are not attempted during static rendering.";

public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object?[]? args)
public ValueTask<TValue> InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args)
=> throw new InvalidOperationException(Message);

ValueTask<TValue> IJSRuntime.InvokeAsync<TValue>(string identifier, object?[]? args)
ValueTask<TValue> IJSRuntime.InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args)
=> throw new InvalidOperationException(Message);

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, object?[]? args)
=> throw new InvalidOperationException(Message);

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, CancellationToken cancellationToken, object?[]? args)
=> throw new InvalidOperationException(Message);

public ValueTask<TValue> GetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier)
=> throw new InvalidOperationException(Message);

public ValueTask<TValue> GetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken)
=> throw new InvalidOperationException(Message);

public ValueTask SetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value)
=> throw new InvalidOperationException(Message);

public ValueTask SetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, TValue value, CancellationToken cancellationToken)
=> throw new InvalidOperationException(Message);
}
26 changes: 24 additions & 2 deletions src/Components/Server/src/Circuits/RemoteJSRuntime.cs
Original file line number Diff line number Diff line change
@@ -105,6 +105,21 @@ protected override void SendByteArray(int id, byte[] data)
}

protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
{
var invocationInfo = new JSInvocationInfo
{
AsyncHandle = asyncHandle,
TargetInstanceId = targetInstanceId,
Identifier = identifier,
CallType = JSCallType.FunctionCall,
ResultType = resultType,
ArgsJson = argsJson,
};

BeginInvokeJS(invocationInfo);
}

protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo)
{
if (_clientProxy is null)
{
@@ -123,9 +138,16 @@ protected override void BeginInvokeJS(long asyncHandle, string identifier, strin
}
}

Log.BeginInvokeJS(_logger, asyncHandle, identifier);
Log.BeginInvokeJS(_logger, invocationInfo.AsyncHandle, invocationInfo.Identifier);

_clientProxy.SendAsync("JS.BeginInvokeJS", asyncHandle, identifier, argsJson, (int)resultType, targetInstanceId);
_clientProxy.SendAsync(
"JS.BeginInvokeJS",
invocationInfo.AsyncHandle,
invocationInfo.Identifier,
invocationInfo.ArgsJson,
(int)invocationInfo.ResultType,
invocationInfo.TargetInstanceId,
(int)invocationInfo.CallType);
}

protected override void ReceiveByteArray(int id, byte[] data)
41 changes: 38 additions & 3 deletions src/Components/Server/test/ProtectedBrowserStorageTest.cs
Original file line number Diff line number Diff line change
@@ -352,19 +352,54 @@ private static string ProtectionPrefix(string purpose)

class TestJSRuntime : IJSRuntime
{
public List<(string Identifier, object[] Args)> Invocations { get; }
= new List<(string Identifier, object[] Args)>();
public List<(string Identifier, object[] Args, JSCallType CallType)> Invocations { get; } = [];

public object NextInvocationResult { get; set; }

public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args)
{
Invocations.Add((identifier, args));
Invocations.Add((identifier, args, JSCallType.FunctionCall));
return (ValueTask<TValue>)NextInvocationResult;
}

public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args)
=> InvokeAsync<TValue>(identifier, cancellationToken: CancellationToken.None, args: args);

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, object[] args)
{
Invocations.Add((identifier, args, JSCallType.NewCall));
return (ValueTask<IJSObjectReference>)NextInvocationResult;
}

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, CancellationToken cancellationToken, object[] args)
{
Invocations.Add((identifier, args, JSCallType.NewCall));
return (ValueTask<IJSObjectReference>)NextInvocationResult;
}

public ValueTask<TValue> GetValueAsync<TValue>(string identifier)
{
Invocations.Add((identifier, [], JSCallType.GetValue));
return (ValueTask<TValue>)NextInvocationResult;
}

public ValueTask<TValue> GetValueAsync<TValue>(string identifier, CancellationToken cancellationToken)
{
Invocations.Add((identifier, [], JSCallType.GetValue));
return (ValueTask<TValue>)NextInvocationResult;
}

public ValueTask SetValueAsync<TValue>(string identifier, TValue value)
{
Invocations.Add((identifier, [value], JSCallType.SetValue));
return ValueTask.CompletedTask;
}

public ValueTask SetValueAsync<TValue>(string identifier, TValue value, CancellationToken cancellationToken)
{
Invocations.Add((identifier, [value], JSCallType.SetValue));
return ValueTask.CompletedTask;
}
}

class TestProtectedBrowserStorage : ProtectedBrowserStorage
7 changes: 4 additions & 3 deletions src/Components/Web.JS/src/Boot.WebAssembly.Common.ts
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import { addDispatchEventMiddleware } from './Rendering/WebRendererInteropMethod
import { WebAssemblyComponentDescriptor, WebAssemblyServerOptions, discoverWebAssemblyPersistedState } from './Services/ComponentDescriptorDiscovery';
import { receiveDotNetDataStream } from './StreamingInterop';
import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
import { DotNet } from '@microsoft/dotnet-js-interop';
import { MonoConfig } from '@microsoft/dotnet-runtime';
import { RootComponentManager } from './Services/RootComponentManager';
import { WebRendererId } from './Rendering/WebRendererId';
@@ -263,12 +264,12 @@ async function scheduleAfterStarted(operations: string): Promise<void> {
Blazor._internal.updateRootComponents(operations);
}

function invokeJSJson(identifier: string, targetInstanceId: number, resultType: number, argsJson: string, asyncHandle: number): string | null {
function invokeJSJson(identifier: string, targetInstanceId: number, resultType: number, argsJson: string, asyncHandle: number, callType: number): string | null {
if (asyncHandle !== 0) {
dispatcher.beginInvokeJSFromDotNet(asyncHandle, identifier, argsJson, resultType, targetInstanceId);
dispatcher.beginInvokeJSFromDotNet(asyncHandle, identifier, argsJson, resultType, targetInstanceId, callType);
return null;
} else {
return dispatcher.invokeJSFromDotNet(identifier, argsJson, resultType, targetInstanceId);
return dispatcher.invokeJSFromDotNet(identifier, argsJson, resultType, targetInstanceId, callType);
}
}

2 changes: 1 addition & 1 deletion src/Components/Web.JS/src/GlobalExports.ts
Original file line number Diff line number Diff line change
@@ -52,7 +52,7 @@ export interface IBlazor {
forceCloseConnection?: () => Promise<void>;
InputFile?: typeof InputFile;
NavigationLock: typeof NavigationLock;
invokeJSJson?: (identifier: string, targetInstanceId: number, resultType: number, argsJson: string, asyncHandle: number) => string | null;
invokeJSJson?: (identifier: string, targetInstanceId: number, resultType: number, argsJson: string, asyncHandle: number, callType: number) => string | null;
endInvokeDotNetFromJS?: (callId: string, success: boolean, resultJsonOrErrorMessage: string) => void;
receiveByteArray?: (id: number, data: Uint8Array) => void;
getPersistedState?: () => string;
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Microsoft.JSInterop;
using Microsoft.JSInterop.Infrastructure;

namespace Microsoft.AspNetCore.Components.Web.Internal;

@@ -17,4 +18,9 @@ public interface IInternalWebJSInProcessRuntime
/// For internal framework use only.
/// </summary>
string InvokeJS(string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson, JSCallResultType resultType, long targetInstanceId);

/// <summary>
/// For internal framework use only.
/// </summary>
string InvokeJS(in JSInvocationInfo invocationInfo);
}
1 change: 1 addition & 0 deletions src/Components/Web/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
#nullable enable
Microsoft.AspNetCore.Components.Web.Internal.IInternalWebJSInProcessRuntime.InvokeJS(in Microsoft.JSInterop.Infrastructure.JSInvocationInfo invocationInfo) -> string!
virtual Microsoft.AspNetCore.Components.Routing.NavLink.ShouldMatch(string! uriAbsolute) -> bool
3 changes: 2 additions & 1 deletion src/Components/WebAssembly/JSInterop/src/InternalCalls.cs
Original file line number Diff line number Diff line change
@@ -23,7 +23,8 @@ public static partial string InvokeJSJson(
[JSMarshalAs<JSType.Number>] long targetInstanceId,
int resultType,
string argsJson,
[JSMarshalAs<JSType.Number>] long asyncHandle);
[JSMarshalAs<JSType.Number>] long asyncHandle,
int callType);

[JSImport("Blazor._internal.endInvokeDotNetFromJS", "blazor-internal")]
public static partial void EndInvokeDotNetFromJS(
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
override Microsoft.JSInterop.WebAssembly.WebAssemblyJSRuntime.BeginInvokeJS(in Microsoft.JSInterop.Infrastructure.JSInvocationInfo invocationInfo) -> void
override Microsoft.JSInterop.WebAssembly.WebAssemblyJSRuntime.InvokeJS(in Microsoft.JSInterop.Infrastructure.JSInvocationInfo invocationInfo) -> string!
Original file line number Diff line number Diff line change
@@ -24,10 +24,30 @@ protected WebAssemblyJSRuntime()

/// <inheritdoc />
protected override string InvokeJS(string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson, JSCallResultType resultType, long targetInstanceId)
{
return InternalCalls.InvokeJSJson(
identifier,
targetInstanceId,
(int)resultType,
argsJson ?? "[]",
0,
(int)JSCallType.FunctionCall
);
}

/// <inheritdoc />
protected override string InvokeJS(in JSInvocationInfo invocationInfo)
{
try
{
return InternalCalls.InvokeJSJson(identifier, targetInstanceId, (int)resultType, argsJson ?? "[]", 0);
return InternalCalls.InvokeJSJson(
invocationInfo.Identifier,
invocationInfo.TargetInstanceId,
(int)invocationInfo.ResultType,
invocationInfo.ArgsJson,
invocationInfo.AsyncHandle,
(int)invocationInfo.CallType
);
}
catch (Exception ex)
{
@@ -38,7 +58,27 @@ protected override string InvokeJS(string identifier, [StringSyntax(StringSyntax
/// <inheritdoc />
protected override void BeginInvokeJS(long asyncHandle, string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson, JSCallResultType resultType, long targetInstanceId)
{
InternalCalls.InvokeJSJson(identifier, targetInstanceId, (int)resultType, argsJson ?? "[]", asyncHandle);
InternalCalls.InvokeJSJson(
identifier,
targetInstanceId,
(int)resultType,
argsJson ?? "[]",
asyncHandle,
(int)JSCallType.FunctionCall
);
}

/// <inheritdoc />
protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo)
{
InternalCalls.InvokeJSJson(
invocationInfo.Identifier,
invocationInfo.TargetInstanceId,
(int)invocationInfo.ResultType,
invocationInfo.ArgsJson,
invocationInfo.AsyncHandle,
(int)invocationInfo.CallType
);
}

/// <inheritdoc />
Original file line number Diff line number Diff line change
@@ -543,6 +543,24 @@ public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToke
return new ValueTask<TValue>((TValue)GetInvocationResult(identifier));
}

public ValueTask<TValue> GetValueAsync<TValue>(string identifier)
=> throw new NotImplementedException();

public ValueTask<TValue> GetValueAsync<TValue>(string identifier, CancellationToken cancellationToken)
=> throw new NotImplementedException();

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, object[] args)
=> throw new NotImplementedException();

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, CancellationToken cancellationToken, object[] args)
=> throw new NotImplementedException();

public ValueTask SetValueAsync<TValue>(string identifier, TValue value)
=> throw new NotImplementedException();

public ValueTask SetValueAsync<TValue>(string identifier, TValue value, CancellationToken cancellationToken)
=> throw new NotImplementedException();

private object GetInvocationResult(string identifier)
{
switch (identifier)
Original file line number Diff line number Diff line change
@@ -777,6 +777,7 @@ public override ValueTask SetSignOutState()
private class TestJsRuntime : IJSRuntime
{
public (string identifier, object[] args) LastInvocation { get; set; }

public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args)
{
LastInvocation = (identifier, args);
@@ -788,6 +789,24 @@ public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToke
LastInvocation = (identifier, args);
return default;
}

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, object[] args)
=> throw new NotImplementedException();

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, CancellationToken cancellationToken, object[] args)
=> throw new NotImplementedException();

public ValueTask<TValue> GetValueAsync<TValue>(string identifier)
=> throw new NotImplementedException();

public ValueTask<TValue> GetValueAsync<TValue>(string identifier, CancellationToken cancellationToken)
=> throw new NotImplementedException();

public ValueTask SetValueAsync<TValue>(string identifier, TValue value)
=> throw new NotImplementedException();

public ValueTask SetValueAsync<TValue>(string identifier, TValue value, CancellationToken cancellationToken)
=> throw new NotImplementedException();
}

public class TestRemoteAuthenticatorView : RemoteAuthenticatorViewCore<RemoteAuthenticationState>
Original file line number Diff line number Diff line change
@@ -168,4 +168,7 @@ protected override Task TransmitStreamAsync(long streamId, DotNetStreamReference

string IInternalWebJSInProcessRuntime.InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
=> InvokeJS(identifier, argsJson, resultType, targetInstanceId);

string IInternalWebJSInProcessRuntime.InvokeJS(in JSInvocationInfo invocationInfo)
=> InvokeJS(invocationInfo);
}
Loading
Loading