Description
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:
GetValueAsync<TValue>
/GetValue<TValue>
- Returns the value of the JS property specified by theidentifier
argument.SetValueAsync
/SetValue
- Sets the value of the JS property specified by theidentifier
argument.InvokeNewAsync
/InvokeNew
- Invokes the JS function specified by theidentifier
argument with thenew
keyword. Returns reference to the result asIJSObjectReference
.
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 theIJSRuntime
instance use thewindow
object implicitly as the root scope to search in. Methods invoked on aIJSObjectReference
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.