Skip to content

API proposal for new user-facing JS interop methods #62454

Open
@oroztocil

Description

@oroztocil

The API change proposal in the JS interop area is split into two issues. This issue deals with "user-facing" interface methods intended for application developers. The other API changes are described in #62455.

Background and Motivation

Previously, users were able to invoke JavaScript functions from .NET code using the InvokeAsync method from the IJSRuntime and IJSObjectReference interfaces (or the Invoke method declared on the synchronous InProcess interface variants). To perform any other JavaScript operation, they had to wrap it into a plain JavaScript function, deploy that function with their application, and invoke it via InvokeAsync. To reduce the need for such boilerplate code, we propose adding methods to the interop API to enable performing common operations directly.

Proposed API

The proposal introduces a number of new methods across multiple types. Logically speaking, however, all of these represent one of these operations:

  1. GetValueAsync<TValue> / GetValue<TValue> - Returns the value of the JS property specified by the identifierargument.
  2. SetValueAsync / SetValue - Sets the value of the JS property specified by the identifierargument.
  3. InvokeNewAsync / InvokeNew - Invokes the JS function specified by the identifierargument with the new keyword. Returns reference to the result as IJSObjectReference.

The new methods will share all relevant behaviors with the existing InvokeAsync / Invoke methods. In particular with regards to:

  • Resolving the target of the operation based on the identifier which has to contain a valid path of property names concatenated with dots (a.b.c). Interop methods invoked on the IJSRuntime instance use the window object implicitly as the root scope to search in. Methods invoked on a IJSObjectReference instance use the referenced JS object as the root scope.
  • Serialization and deserialization of arguments and return values, including special handling of IJSObjectReference instances which are resolved into the actual JS objects on the JS side.

For each operation we propose adding the equivalent set of overloads (including extension methods) as for the existing InvokeAsync / Invoke API in order to provide consistent and predictable experience. These overloads include variants with and without custom CancellationToken, with custom timeout setting, with arguments passed as a regular array and as a params array.

Methods invoked on a JS runtime instance

namespace Microsoft.JSInterop
{
    public interface IJSRuntime
    {
        ValueTask<TValue> GetValueAsync<TValue>(string identifier);
        ValueTask<TValue> GetValueAsync<TValue>(string identifier, CancellationToken cancellationToken);
        ValueTask SetValueAsync<TValue>(string identifier, TValue value);
        ValueTask SetValueAsync<TValue>(string identifier, TValue value, CancellationToken cancellationToken);
        ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, object?[]? args);
        ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, CancellationToken cancellationToken, object?[]? args);
    }

    public abstract class JSRuntime
    {
        public ValueTask<TValue> GetValueAsync<TValue>(string identifier);
        public ValueTask<TValue> GetValueAsync<TValue>(string identifier, CancellationToken cancellationToken);
        public ValueTask SetValueAsync<TValue>(string identifier, TValue value);
        public ValueTask SetValueAsync<TValue>(string identifier, TValue value, CancellationToken cancellationToken);
        public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, object?[]? args);
        public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, CancellationToken cancellationToken, object?[]? args);
    }

    public static class JSRuntimeExtensions
    {
        public ValueTask<TValue> GetValueAsync<TValue>(string identifier, TimeSpan timeout);
        public ValueTask SetValueAsync<TValue>(string identifier, TValue value, TimeSpan timeout);
        public static ValueTask<IJSObjectReference> InvokeNewAsync(this IJSRuntime jsRuntime, string identifier, params object?[]? args);
        public static ValueTask<IJSObjectReference> InvokeNewAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, object?[]? args);
        public static ValueTask<IJSObjectReference> InvokeNewAsync(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, object?[]? args);
    }

    public interface IJSInProcessRuntime
    {
        TValue GetValue<TValue>(string identifier);
        void SetValue<TValue>(string identifier, TValue value);
        IJSInProcessObjectReference InvokeNew(string identifier, params object?[]? args);
    }

    public abstract class JSInProcessRuntime
    {
        public TValue GetValue<TValue>(string identifier);
        public void SetValue<TValue>(string identifier, TValue value);
        public IJSInProcessObjectReference InvokeNew(string identifier, params object?[]? args);
    }
}

Methods invoked on a JS object reference instance

namespace Microsoft.JSInterop
{
    public interface IJSObjectReference
    {
        ValueTask<TValue> GetValueAsync<TValue>(string identifier);
        ValueTask<TValue> GetValueAsync<TValue>(string identifier, CancellationToken cancellationToken);
        ValueTask SetValueAsync<TValue>(string identifier, TValue value);
        ValueTask SetValueAsync<TValue>(string identifier, TValue value, CancellationToken cancellationToken);
        ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, object?[]? args);
        ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, CancellationToken cancellationToken, object?[]? args);
    }

    public static class JSObjectReferenceExtensions
    {
        ValueTask<TValue> GetValueAsync<TValue>(string identifier, TimeSpan timeout);
        ValueTask SetValueAsync<TValue>(string identifier, TValue value, TimeSpan timeout);
        public static ValueTask<IJSObjectReference> InvokeNewAsync(this IJSObjectReference jsObjectReference, string identifier, params object?[]? args);
        public static ValueTask<IJSObjectReference> InvokeNewAsync(this IJSObjectReference jsObjectReference, string identifier, CancellationToken cancellationToken, object?[]? args);
        public static ValueTask<IJSObjectReference> InvokeNewAsync(this IJSObjectReference jsObjectReference, string identifier, TimeSpan timeout, object?[]? args);
    }

    public interface IJSInProcessObjectReference
    {
        TValue GetValue<TValue>(string identifier);
        void SetValue<TValue>(string identifier, TValue value);
        IJSInProcessObjectReference InvokeNew(string identifier, object?[]? args);
    }
}

namespace Microsoft.JSInterop.Implementation
{
    public class JSObjectReference
    {
        public ValueTask<TValue> GetValueAsync<TValue>(string identifier);
        public ValueTask<TValue> GetValueAsync<TValue>(string identifier, CancellationToken cancellationToken);
        public ValueTask SetValueAsync<TValue>(string identifier, TValue value);
        public ValueTask SetValueAsync<TValue>(string identifier, TValue value, CancellationToken cancellationToken);
        public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, object?[]? args);
        public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, CancellationToken cancellationToken, object?[]? args);
    }

    public class JSInProcessObjectReference
    {
        public TValue GetValue<TValue>(string identifier);
        public void SetValue<TValue>(string identifier, TValue value);
        public IJSInProcessObjectReference InvokeNew(string identifier, object?[]? args);
    }
}

Usage Examples

The asynchronous variants of the new methods are used similarly as the existing InvokeAsync method.

@inject IJSRuntime JSRuntime

string title = await JSRuntime.GetValueAsync<string>("document.title");

await JSRuntime.SetValueAsync("document.title", "Hello there");

IJSObjectReference chartRef = await InvokeNewAsync("Chart", chartParams);

var someChartProperty = await chartRef.GetValueAsync<int>("somePropName");

Synchronous variants can be used (where available) in the same way:

@inject IJSRuntime JSRuntime

var runtime = (IJSInProcessRuntime)JSRuntime.

string title = runtime.GetValue<string>("document.title");

runtime.SetValue("document.title", "Hello there");

IJSInProcessObjectReference chartRef = InvokeNew("Chart", chartParams);

var someChartProperty = chartRef.GetValue<int>("somePropName");

Alternative Designs

We considered supporting the additional operations with only the existing InvokeAsync method and selecting its behvaior according to what JS entity is found based on the identifier. However, this approach has obvious UX issues (clarity, predictability). There is also no way to differentiate, in general, between "normal" and "constructor" functions in JavaScript. Therefore, user intent is necessary to determine if a function should be invoked with or without the new keyword. Having a dedicated GetValue and SetValue method also enables working with JS functions as values (references) instead of invoking them.

Risks

The added interface methods have default implementations (which throw NotImplementedException) to avoid breaking builds of their implementors (which there are likely not many apart from Blazor itself).

A possible criticism of the feature is that streamlining interop in this manner might motivate misuse of inefficient interop calls.

Metadata

Metadata

Assignees

Labels

api-ready-for-reviewAPI is ready for formal API review - https://github.com/dotnet/apireviewsarea-blazorIncludes: Blazor, Razor Components

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions