Change JSInterop to avoid using async locals (dotnet/extensions#2163)
* Remove the use of async local JSRuntime
* Update DotNetDispatcher to accept a JSRuntime instance rather than use a ambient value.
* Modify DotNetObjectReference to start tracking it's value during serialization.
\n\nCommit migrated from ae9878bb99
This commit is contained in:
parent
c717230b13
commit
ef83e3359d
|
|
@ -55,7 +55,7 @@ module DotNet {
|
||||||
return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args);
|
return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
function invokePossibleInstanceMethod<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): T {
|
function invokePossibleInstanceMethod<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): T {
|
||||||
const dispatcher = getRequiredDispatcher();
|
const dispatcher = getRequiredDispatcher();
|
||||||
if (dispatcher.invokeDotNetFromJS) {
|
if (dispatcher.invokeDotNetFromJS) {
|
||||||
const argsJson = JSON.stringify(args, argReplacer);
|
const argsJson = JSON.stringify(args, argReplacer);
|
||||||
|
|
@ -66,7 +66,7 @@ module DotNet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function invokePossibleInstanceMethodAsync<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, ...args: any[]): Promise<T> {
|
function invokePossibleInstanceMethodAsync<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): Promise<T> {
|
||||||
if (assemblyName && dotNetObjectId) {
|
if (assemblyName && dotNetObjectId) {
|
||||||
throw new Error(`For instance method calls, assemblyName should be null. Received '${assemblyName}'.`) ;
|
throw new Error(`For instance method calls, assemblyName should be null. Received '${assemblyName}'.`) ;
|
||||||
}
|
}
|
||||||
|
|
@ -273,7 +273,7 @@ module DotNet {
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public dispose() {
|
||||||
const promise = invokePossibleInstanceMethodAsync<any>(null, '__Dispose', this._id);
|
const promise = invokePossibleInstanceMethodAsync<any>(null, '__Dispose', this._id, null);
|
||||||
promise.catch(error => console.error(error));
|
promise.catch(error => console.error(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,11 @@ namespace Microsoft.JSInterop
|
||||||
{
|
{
|
||||||
protected JSRuntime() { }
|
protected JSRuntime() { }
|
||||||
protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
|
protected internal System.Text.Json.JsonSerializerOptions JsonSerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||||
protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson);
|
protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson);
|
||||||
protected internal abstract void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId);
|
protected internal abstract void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId);
|
||||||
public System.Threading.Tasks.ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args) { throw null; }
|
public System.Threading.Tasks.ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args) { throw null; }
|
||||||
public System.Threading.Tasks.ValueTask<TValue> InvokeAsync<TValue>(string identifier, System.Threading.CancellationToken cancellationToken, object[] args) { throw null; }
|
public System.Threading.Tasks.ValueTask<TValue> InvokeAsync<TValue>(string identifier, System.Threading.CancellationToken cancellationToken, object[] args) { throw null; }
|
||||||
public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { }
|
|
||||||
}
|
}
|
||||||
public static partial class JSRuntimeExtensions
|
public static partial class JSRuntimeExtensions
|
||||||
{
|
{
|
||||||
|
|
@ -72,8 +72,8 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
{
|
{
|
||||||
public static partial class DotNetDispatcher
|
public static partial class DotNetDispatcher
|
||||||
{
|
{
|
||||||
public static void BeginInvokeDotNet(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { }
|
public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { }
|
||||||
public static void EndInvokeJS(string arguments) { }
|
public static void EndInvokeJS(Microsoft.JSInterop.JSRuntime jsRuntime, string arguments) { }
|
||||||
public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; }
|
public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,11 @@ namespace Microsoft.JSInterop
|
||||||
{
|
{
|
||||||
protected JSRuntime() { }
|
protected JSRuntime() { }
|
||||||
protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
|
protected internal System.Text.Json.JsonSerializerOptions JsonSerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||||
protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson);
|
protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson);
|
||||||
protected internal abstract void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId);
|
protected internal abstract void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId);
|
||||||
public System.Threading.Tasks.ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args) { throw null; }
|
public System.Threading.Tasks.ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args) { throw null; }
|
||||||
public System.Threading.Tasks.ValueTask<TValue> InvokeAsync<TValue>(string identifier, System.Threading.CancellationToken cancellationToken, object[] args) { throw null; }
|
public System.Threading.Tasks.ValueTask<TValue> InvokeAsync<TValue>(string identifier, System.Threading.CancellationToken cancellationToken, object[] args) { throw null; }
|
||||||
public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { }
|
|
||||||
}
|
}
|
||||||
public static partial class JSRuntimeExtensions
|
public static partial class JSRuntimeExtensions
|
||||||
{
|
{
|
||||||
|
|
@ -72,8 +72,8 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
{
|
{
|
||||||
public static partial class DotNetDispatcher
|
public static partial class DotNetDispatcher
|
||||||
{
|
{
|
||||||
public static void BeginInvokeDotNet(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { }
|
public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { }
|
||||||
public static void EndInvokeJS(string arguments) { }
|
public static void EndInvokeJS(Microsoft.JSInterop.JSRuntime jsRuntime, string arguments) { }
|
||||||
public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; }
|
public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Microsoft.JSInterop.Infrastructure;
|
|
||||||
|
|
||||||
namespace Microsoft.JSInterop
|
namespace Microsoft.JSInterop
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -17,7 +15,7 @@ namespace Microsoft.JSInterop
|
||||||
/// <returns>An instance of <see cref="DotNetObjectReference{TValue}" />.</returns>
|
/// <returns>An instance of <see cref="DotNetObjectReference{TValue}" />.</returns>
|
||||||
public static DotNetObjectReference<TValue> Create<TValue>(TValue value) where TValue : class
|
public static DotNetObjectReference<TValue> Create<TValue>(TValue value) where TValue : class
|
||||||
{
|
{
|
||||||
return new DotNetObjectReference<TValue>(DotNetObjectReferenceManager.Current, value);
|
return new DotNetObjectReference<TValue>(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Text.Json.Serialization;
|
using System.Diagnostics;
|
||||||
using Microsoft.JSInterop.Infrastructure;
|
using Microsoft.JSInterop.Infrastructure;
|
||||||
|
|
||||||
namespace Microsoft.JSInterop
|
namespace Microsoft.JSInterop
|
||||||
|
|
@ -14,22 +14,18 @@ namespace Microsoft.JSInterop
|
||||||
/// To avoid leaking memory, the reference must later be disposed by JS code or by .NET code.
|
/// To avoid leaking memory, the reference must later be disposed by JS code or by .NET code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TValue">The type of the value to wrap.</typeparam>
|
/// <typeparam name="TValue">The type of the value to wrap.</typeparam>
|
||||||
[JsonConverter(typeof(DotNetObjectReferenceJsonConverterFactory))]
|
|
||||||
public sealed class DotNetObjectReference<TValue> : IDotNetObjectReference, IDisposable where TValue : class
|
public sealed class DotNetObjectReference<TValue> : IDotNetObjectReference, IDisposable where TValue : class
|
||||||
{
|
{
|
||||||
private readonly DotNetObjectReferenceManager _referenceManager;
|
|
||||||
private readonly TValue _value;
|
private readonly TValue _value;
|
||||||
private readonly long _objectId;
|
private long _objectId;
|
||||||
|
private JSRuntime _jsRuntime;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="DotNetObjectReference{TValue}" />.
|
/// Initializes a new instance of <see cref="DotNetObjectReference{TValue}" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="referenceManager"></param>
|
|
||||||
/// <param name="value">The value to pass by reference.</param>
|
/// <param name="value">The value to pass by reference.</param>
|
||||||
internal DotNetObjectReference(DotNetObjectReferenceManager referenceManager, TValue value)
|
internal DotNetObjectReference(TValue value)
|
||||||
{
|
{
|
||||||
_referenceManager = referenceManager;
|
|
||||||
_objectId = _referenceManager.TrackObject(this);
|
|
||||||
_value = value;
|
_value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,8 +46,30 @@ namespace Microsoft.JSInterop
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
|
Debug.Assert(_objectId != 0, "Accessing ObjectId without tracking is always incorrect.");
|
||||||
|
|
||||||
return _objectId;
|
return _objectId;
|
||||||
}
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
_objectId = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal JSRuntime JSRuntime
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
return _jsRuntime;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
_jsRuntime = value;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object IDotNetObjectReference.Value => Value;
|
object IDotNetObjectReference.Value => Value;
|
||||||
|
|
@ -68,11 +86,15 @@ namespace Microsoft.JSInterop
|
||||||
if (!Disposed)
|
if (!Disposed)
|
||||||
{
|
{
|
||||||
Disposed = true;
|
Disposed = true;
|
||||||
_referenceManager.ReleaseDotNetObject(_objectId);
|
|
||||||
|
if (_jsRuntime != null)
|
||||||
|
{
|
||||||
|
_jsRuntime.ReleaseObjectReference(_objectId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ThrowIfDisposed()
|
internal void ThrowIfDisposed()
|
||||||
{
|
{
|
||||||
if (Disposed)
|
if (Disposed)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,13 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Receives a call from JS to .NET, locating and invoking the specified method.
|
/// Receives a call from JS to .NET, locating and invoking the specified method.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="jsRuntime">The <see cref="JSRuntime"/>.</param>
|
||||||
/// <param name="assemblyName">The assembly containing the method to be invoked.</param>
|
/// <param name="assemblyName">The assembly containing the method to be invoked.</param>
|
||||||
/// <param name="methodIdentifier">The identifier of the method to be invoked. The method must be annotated with a <see cref="JSInvokableAttribute"/> matching this identifier string.</param>
|
/// <param name="methodIdentifier">The identifier of the method to be invoked. The method must be annotated with a <see cref="JSInvokableAttribute"/> matching this identifier string.</param>
|
||||||
/// <param name="dotNetObjectId">For instance method calls, identifies the target object.</param>
|
/// <param name="dotNetObjectId">For instance method calls, identifies the target object.</param>
|
||||||
/// <param name="argsJson">A JSON representation of the parameters.</param>
|
/// <param name="argsJson">A JSON representation of the parameters.</param>
|
||||||
/// <returns>A JSON representation of the return value, or null.</returns>
|
/// <returns>A JSON representation of the return value, or null.</returns>
|
||||||
public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
|
public static string Invoke(JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
|
||||||
{
|
{
|
||||||
// This method doesn't need [JSInvokable] because the platform is responsible for having
|
// This method doesn't need [JSInvokable] because the platform is responsible for having
|
||||||
// some way to dispatch calls here. The logic inside here is the thing that checks whether
|
// some way to dispatch calls here. The logic inside here is the thing that checks whether
|
||||||
|
|
@ -42,41 +43,38 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
IDotNetObjectReference targetInstance = default;
|
IDotNetObjectReference targetInstance = default;
|
||||||
if (dotNetObjectId != default)
|
if (dotNetObjectId != default)
|
||||||
{
|
{
|
||||||
targetInstance = DotNetObjectReferenceManager.Current.FindDotNetObject(dotNetObjectId);
|
targetInstance = jsRuntime.GetObjectReference(dotNetObjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson);
|
var syncResult = InvokeSynchronously(jsRuntime, assemblyName, methodIdentifier, targetInstance, argsJson);
|
||||||
if (syncResult == null)
|
if (syncResult == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonSerializer.Serialize(syncResult, JsonSerializerOptionsProvider.Options);
|
return JsonSerializer.Serialize(syncResult, jsRuntime.JsonSerializerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Receives a call from JS to .NET, locating and invoking the specified method asynchronously.
|
/// Receives a call from JS to .NET, locating and invoking the specified method asynchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="jsRuntime">The <see cref="JSRuntime"/>.</param>
|
||||||
/// <param name="callId">A value identifying the asynchronous call that should be passed back with the result, or null if no result notification is required.</param>
|
/// <param name="callId">A value identifying the asynchronous call that should be passed back with the result, or null if no result notification is required.</param>
|
||||||
/// <param name="assemblyName">The assembly containing the method to be invoked.</param>
|
/// <param name="assemblyName">The assembly containing the method to be invoked.</param>
|
||||||
/// <param name="methodIdentifier">The identifier of the method to be invoked. The method must be annotated with a <see cref="JSInvokableAttribute"/> matching this identifier string.</param>
|
/// <param name="methodIdentifier">The identifier of the method to be invoked. The method must be annotated with a <see cref="JSInvokableAttribute"/> matching this identifier string.</param>
|
||||||
/// <param name="dotNetObjectId">For instance method calls, identifies the target object.</param>
|
/// <param name="dotNetObjectId">For instance method calls, identifies the target object.</param>
|
||||||
/// <param name="argsJson">A JSON representation of the parameters.</param>
|
/// <param name="argsJson">A JSON representation of the parameters.</param>
|
||||||
/// <returns>A JSON representation of the return value, or null.</returns>
|
/// <returns>A JSON representation of the return value, or null.</returns>
|
||||||
public static void BeginInvokeDotNet(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
|
public static void BeginInvokeDotNet(JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
|
||||||
{
|
{
|
||||||
// This method doesn't need [JSInvokable] because the platform is responsible for having
|
// This method doesn't need [JSInvokable] because the platform is responsible for having
|
||||||
// some way to dispatch calls here. The logic inside here is the thing that checks whether
|
// some way to dispatch calls here. The logic inside here is the thing that checks whether
|
||||||
// the targeted method has [JSInvokable]. It is not itself subject to that restriction,
|
// the targeted method has [JSInvokable]. It is not itself subject to that restriction,
|
||||||
// because there would be nobody to police that. This method *is* the police.
|
// because there would be nobody to police that. This method *is* the police.
|
||||||
|
|
||||||
// DotNetDispatcher only works with JSRuntimeBase instances.
|
|
||||||
// If the developer wants to use a totally custom IJSRuntime, then their JS-side
|
|
||||||
// code has to implement its own way of returning async results.
|
|
||||||
var jsRuntimeBaseInstance = (JSRuntime)JSRuntime.Current;
|
|
||||||
|
|
||||||
// Using ExceptionDispatchInfo here throughout because we want to always preserve
|
// Using ExceptionDispatchInfo here throughout because we want to always preserve
|
||||||
// original stack traces.
|
// original stack traces.
|
||||||
|
|
||||||
object syncResult = null;
|
object syncResult = null;
|
||||||
ExceptionDispatchInfo syncException = null;
|
ExceptionDispatchInfo syncException = null;
|
||||||
IDotNetObjectReference targetInstance = null;
|
IDotNetObjectReference targetInstance = null;
|
||||||
|
|
@ -85,10 +83,10 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
{
|
{
|
||||||
if (dotNetObjectId != default)
|
if (dotNetObjectId != default)
|
||||||
{
|
{
|
||||||
targetInstance = DotNetObjectReferenceManager.Current.FindDotNetObject(dotNetObjectId);
|
targetInstance = jsRuntime.GetObjectReference(dotNetObjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson);
|
syncResult = InvokeSynchronously(jsRuntime, assemblyName, methodIdentifier, targetInstance, argsJson);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -103,7 +101,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
else if (syncException != null)
|
else if (syncException != null)
|
||||||
{
|
{
|
||||||
// Threw synchronously, let's respond.
|
// Threw synchronously, let's respond.
|
||||||
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier, dotNetObjectId);
|
jsRuntime.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier, dotNetObjectId);
|
||||||
}
|
}
|
||||||
else if (syncResult is Task task)
|
else if (syncResult is Task task)
|
||||||
{
|
{
|
||||||
|
|
@ -115,20 +113,20 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
{
|
{
|
||||||
var exception = t.Exception.GetBaseException();
|
var exception = t.Exception.GetBaseException();
|
||||||
|
|
||||||
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception), assemblyName, methodIdentifier, dotNetObjectId);
|
jsRuntime.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception), assemblyName, methodIdentifier, dotNetObjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = TaskGenericsUtil.GetTaskResult(task);
|
var result = TaskGenericsUtil.GetTaskResult(task);
|
||||||
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier, dotNetObjectId);
|
jsRuntime.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier, dotNetObjectId);
|
||||||
}, TaskScheduler.Current);
|
}, TaskScheduler.Current);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier, dotNetObjectId);
|
jsRuntime.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier, dotNetObjectId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object InvokeSynchronously(string assemblyName, string methodIdentifier, IDotNetObjectReference objectReference, string argsJson)
|
private static object InvokeSynchronously(JSRuntime jsRuntime, string assemblyName, string methodIdentifier, IDotNetObjectReference objectReference, string argsJson)
|
||||||
{
|
{
|
||||||
AssemblyKey assemblyKey;
|
AssemblyKey assemblyKey;
|
||||||
if (objectReference is null)
|
if (objectReference is null)
|
||||||
|
|
@ -154,7 +152,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
|
|
||||||
var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, methodIdentifier);
|
var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, methodIdentifier);
|
||||||
|
|
||||||
var suppliedArgs = ParseArguments(methodIdentifier, argsJson, parameterTypes);
|
var suppliedArgs = ParseArguments(jsRuntime, methodIdentifier, argsJson, parameterTypes);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -173,7 +171,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static object[] ParseArguments(string methodIdentifier, string arguments, Type[] parameterTypes)
|
internal static object[] ParseArguments(JSRuntime jsRuntime, string methodIdentifier, string arguments, Type[] parameterTypes)
|
||||||
{
|
{
|
||||||
if (parameterTypes.Length == 0)
|
if (parameterTypes.Length == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -198,7 +196,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
throw new InvalidOperationException($"In call to '{methodIdentifier}', parameter of type '{parameterType.Name}' at index {(index + 1)} must be declared as type 'DotNetObjectRef<{parameterType.Name}>' to receive the incoming value.");
|
throw new InvalidOperationException($"In call to '{methodIdentifier}', parameter of type '{parameterType.Name}' at index {(index + 1)} must be declared as type 'DotNetObjectRef<{parameterType.Name}>' to receive the incoming value.");
|
||||||
}
|
}
|
||||||
|
|
||||||
suppliedArgs[index] = JsonSerializer.Deserialize(ref reader, parameterType, JsonSerializerOptionsProvider.Options);
|
suppliedArgs[index] = JsonSerializer.Deserialize(ref reader, parameterType, jsRuntime.JsonSerializerOptions);
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,18 +245,13 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
/// method is responsible for handling any possible exception generated from the arguments
|
/// method is responsible for handling any possible exception generated from the arguments
|
||||||
/// passed in as parameters.
|
/// passed in as parameters.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
/// <param name="jsRuntime">The <see cref="JSRuntime"/>.</param>
|
||||||
/// <param name="arguments">The serialized arguments for the callback completion.</param>
|
/// <param name="arguments">The serialized arguments for the callback completion.</param>
|
||||||
/// <exception cref="Exception">
|
/// <exception cref="Exception">
|
||||||
/// This method can throw any exception either from the argument received or as a result
|
/// This method can throw any exception either from the argument received or as a result
|
||||||
/// of executing any callback synchronously upon completion.
|
/// of executing any callback synchronously upon completion.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
public static void EndInvokeJS(string arguments)
|
public static void EndInvokeJS(JSRuntime jsRuntime, string arguments)
|
||||||
{
|
|
||||||
var jsRuntimeBase = (JSRuntime)JSRuntime.Current;
|
|
||||||
ParseEndInvokeArguments(jsRuntimeBase, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void ParseEndInvokeArguments(JSRuntime jsRuntimeBase, string arguments)
|
|
||||||
{
|
{
|
||||||
var utf8JsonBytes = Encoding.UTF8.GetBytes(arguments);
|
var utf8JsonBytes = Encoding.UTF8.GetBytes(arguments);
|
||||||
|
|
||||||
|
|
@ -281,7 +274,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
var success = reader.GetBoolean();
|
var success = reader.GetBoolean();
|
||||||
|
|
||||||
reader.Read();
|
reader.Read();
|
||||||
jsRuntimeBase.EndInvokeJS(taskId, success, ref reader);
|
jsRuntime.EndInvokeJS(taskId, success, ref reader);
|
||||||
|
|
||||||
if (!reader.Read() || reader.TokenType != JsonTokenType.EndArray)
|
if (!reader.Read() || reader.TokenType != JsonTokenType.EndArray)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,15 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
{
|
{
|
||||||
internal sealed class DotNetObjectReferenceJsonConverter<TValue> : JsonConverter<DotNetObjectReference<TValue>> where TValue : class
|
internal sealed class DotNetObjectReferenceJsonConverter<TValue> : JsonConverter<DotNetObjectReference<TValue>> where TValue : class
|
||||||
{
|
{
|
||||||
|
public DotNetObjectReferenceJsonConverter(JSRuntime jsRuntime)
|
||||||
|
{
|
||||||
|
JSRuntime = jsRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
private static JsonEncodedText DotNetObjectRefKey => DotNetDispatcher.DotNetObjectRefKey;
|
private static JsonEncodedText DotNetObjectRefKey => DotNetDispatcher.DotNetObjectRefKey;
|
||||||
|
|
||||||
|
public JSRuntime JSRuntime { get; }
|
||||||
|
|
||||||
public override DotNetObjectReference<TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override DotNetObjectReference<TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
long dotNetObjectId = 0;
|
long dotNetObjectId = 0;
|
||||||
|
|
@ -40,14 +47,16 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
throw new JsonException($"Required property {DotNetObjectRefKey} not found.");
|
throw new JsonException($"Required property {DotNetObjectRefKey} not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var referenceManager = DotNetObjectReferenceManager.Current;
|
var value = (DotNetObjectReference<TValue>)JSRuntime.GetObjectReference(dotNetObjectId);
|
||||||
return (DotNetObjectReference<TValue>)referenceManager.FindDotNetObject(dotNetObjectId);
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, DotNetObjectReference<TValue> value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, DotNetObjectReference<TValue> value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
|
var objectId = JSRuntime.TrackObjectReference<TValue>(value);
|
||||||
|
|
||||||
writer.WriteStartObject();
|
writer.WriteStartObject();
|
||||||
writer.WriteNumber(DotNetObjectRefKey, value.ObjectId);
|
writer.WriteNumber(DotNetObjectRefKey, objectId);
|
||||||
writer.WriteEndObject();
|
writer.WriteEndObject();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,13 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
{
|
{
|
||||||
internal sealed class DotNetObjectReferenceJsonConverterFactory : JsonConverterFactory
|
internal sealed class DotNetObjectReferenceJsonConverterFactory : JsonConverterFactory
|
||||||
{
|
{
|
||||||
|
public DotNetObjectReferenceJsonConverterFactory(JSRuntime jsRuntime)
|
||||||
|
{
|
||||||
|
JSRuntime = jsRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JSRuntime JSRuntime { get; }
|
||||||
|
|
||||||
public override bool CanConvert(Type typeToConvert)
|
public override bool CanConvert(Type typeToConvert)
|
||||||
{
|
{
|
||||||
return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(DotNetObjectReference<>);
|
return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(DotNetObjectReference<>);
|
||||||
|
|
@ -20,7 +27,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
var instanceType = typeToConvert.GetGenericArguments()[0];
|
var instanceType = typeToConvert.GetGenericArguments()[0];
|
||||||
var converterType = typeof(DotNetObjectReferenceJsonConverter<>).MakeGenericType(instanceType);
|
var converterType = typeof(DotNetObjectReferenceJsonConverter<>).MakeGenericType(instanceType);
|
||||||
|
|
||||||
return (JsonConverter)Activator.CreateInstance(converterType);
|
return (JsonConverter)Activator.CreateInstance(converterType, JSRuntime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Microsoft.JSInterop.Infrastructure
|
|
||||||
{
|
|
||||||
internal class DotNetObjectReferenceManager
|
|
||||||
{
|
|
||||||
private long _nextId = 0; // 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1
|
|
||||||
private readonly ConcurrentDictionary<long, IDotNetObjectReference> _trackedRefsById = new ConcurrentDictionary<long, IDotNetObjectReference>();
|
|
||||||
|
|
||||||
public static DotNetObjectReferenceManager Current
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!(JSRuntime.Current is JSRuntime jsRuntime))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("JSRuntime must be set up correctly and must be an instance of JSRuntimeBase to use DotNetObjectReference.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsRuntime.ObjectRefManager;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public long TrackObject(IDotNetObjectReference dotNetObjectRef)
|
|
||||||
{
|
|
||||||
var dotNetObjectId = Interlocked.Increment(ref _nextId);
|
|
||||||
_trackedRefsById[dotNetObjectId] = dotNetObjectRef;
|
|
||||||
|
|
||||||
return dotNetObjectId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDotNetObjectReference FindDotNetObject(long dotNetObjectId)
|
|
||||||
{
|
|
||||||
return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef)
|
|
||||||
? dotNetObjectRef
|
|
||||||
: throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the DotNetObjectRef instance was already disposed.", nameof(dotNetObjectId));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stops tracking the specified .NET object reference.
|
|
||||||
/// This may be invoked either by disposing a DotNetObjectRef in .NET code, or via JS interop by calling "dispose" on the corresponding instance in JavaScript code
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dotNetObjectId">The ID of the <see cref="DotNetObjectReference{TValue}"/>.</param>
|
|
||||||
public void ReleaseDotNetObject(long dotNetObjectId) => _trackedRefsById.TryRemove(dotNetObjectId, out _);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -19,13 +19,13 @@ namespace Microsoft.JSInterop
|
||||||
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
|
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
|
||||||
public TValue Invoke<TValue>(string identifier, params object[] args)
|
public TValue Invoke<TValue>(string identifier, params object[] args)
|
||||||
{
|
{
|
||||||
var resultJson = InvokeJS(identifier, JsonSerializer.Serialize(args, JsonSerializerOptionsProvider.Options));
|
var resultJson = InvokeJS(identifier, JsonSerializer.Serialize(args, JsonSerializerOptions));
|
||||||
if (resultJson is null)
|
if (resultJson is null)
|
||||||
{
|
{
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<TValue>(resultJson, JsonSerializerOptionsProvider.Options);
|
return JsonSerializer.Deserialize<TValue>(resultJson, JsonSerializerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
@ -16,36 +17,40 @@ namespace Microsoft.JSInterop
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract partial class JSRuntime : IJSRuntime
|
public abstract partial class JSRuntime : IJSRuntime
|
||||||
{
|
{
|
||||||
private static readonly AsyncLocal<IJSRuntime> _currentJSRuntime = new AsyncLocal<IJSRuntime>();
|
private long _nextObjectReferenceId = 0; // 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1
|
||||||
|
|
||||||
internal static IJSRuntime Current => _currentJSRuntime.Value;
|
|
||||||
|
|
||||||
private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed"
|
private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed"
|
||||||
private readonly ConcurrentDictionary<long, object> _pendingTasks
|
private readonly ConcurrentDictionary<long, object> _pendingTasks = new ConcurrentDictionary<long, object>();
|
||||||
= new ConcurrentDictionary<long, object>();
|
private readonly ConcurrentDictionary<long, IDotNetObjectReference> _trackedRefsById = new ConcurrentDictionary<long, IDotNetObjectReference>();
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<long, CancellationTokenRegistration> _cancellationRegistrations =
|
private readonly ConcurrentDictionary<long, CancellationTokenRegistration> _cancellationRegistrations =
|
||||||
new ConcurrentDictionary<long, CancellationTokenRegistration>();
|
new ConcurrentDictionary<long, CancellationTokenRegistration>();
|
||||||
|
|
||||||
internal DotNetObjectReferenceManager ObjectRefManager { get; } = new DotNetObjectReferenceManager();
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="JSRuntime"/>.
|
||||||
|
/// </summary>
|
||||||
|
protected JSRuntime()
|
||||||
|
{
|
||||||
|
JsonSerializerOptions = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
MaxDepth = 32,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
new DotNetObjectReferenceJsonConverterFactory(this),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="System.Text.Json.JsonSerializerOptions"/> used to serialize and deserialize interop payloads.
|
||||||
|
/// </summary>
|
||||||
|
protected internal JsonSerializerOptions JsonSerializerOptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the default timeout for asynchronous JavaScript calls.
|
/// Gets or sets the default timeout for asynchronous JavaScript calls.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected TimeSpan? DefaultAsyncTimeout { get; set; }
|
protected TimeSpan? DefaultAsyncTimeout { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the current JS runtime to the supplied instance.
|
|
||||||
///
|
|
||||||
/// This is intended for framework use. Developers should not normally need to call this method.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="instance">The new current <see cref="IJSRuntime"/>.</param>
|
|
||||||
public static void SetCurrentJSRuntime(IJSRuntime instance)
|
|
||||||
{
|
|
||||||
_currentJSRuntime.Value = instance
|
|
||||||
?? throw new ArgumentNullException(nameof(instance));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invokes the specified JavaScript function asynchronously.
|
/// Invokes the specified JavaScript function asynchronously.
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -103,7 +108,7 @@ namespace Microsoft.JSInterop
|
||||||
}
|
}
|
||||||
|
|
||||||
var argsJson = args?.Any() == true ?
|
var argsJson = args?.Any() == true ?
|
||||||
JsonSerializer.Serialize(args, JsonSerializerOptionsProvider.Options) :
|
JsonSerializer.Serialize(args, JsonSerializerOptions) :
|
||||||
null;
|
null;
|
||||||
BeginInvokeJS(taskId, identifier, argsJson);
|
BeginInvokeJS(taskId, identifier, argsJson);
|
||||||
|
|
||||||
|
|
@ -176,7 +181,7 @@ namespace Microsoft.JSInterop
|
||||||
{
|
{
|
||||||
var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);
|
var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);
|
||||||
|
|
||||||
var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptionsProvider.Options);
|
var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions);
|
||||||
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result);
|
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -191,5 +196,48 @@ namespace Microsoft.JSInterop
|
||||||
TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(message, exception));
|
TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(message, exception));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal long TrackObjectReference<TValue>(DotNetObjectReference<TValue> dotNetObjectReference) where TValue : class
|
||||||
|
{
|
||||||
|
if (dotNetObjectReference == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(dotNetObjectReference));
|
||||||
|
}
|
||||||
|
|
||||||
|
dotNetObjectReference.ThrowIfDisposed();
|
||||||
|
|
||||||
|
var jsRuntime = dotNetObjectReference.JSRuntime;
|
||||||
|
if (jsRuntime is null)
|
||||||
|
{
|
||||||
|
var dotNetObjectId = Interlocked.Increment(ref _nextObjectReferenceId);
|
||||||
|
|
||||||
|
dotNetObjectReference.JSRuntime = this;
|
||||||
|
dotNetObjectReference.ObjectId = dotNetObjectId;
|
||||||
|
|
||||||
|
_trackedRefsById[dotNetObjectId] = dotNetObjectReference;
|
||||||
|
}
|
||||||
|
else if (!ReferenceEquals(this, jsRuntime))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{dotNetObjectReference.GetType().Name} is already being tracked by a different instance of {nameof(JSRuntime)}." +
|
||||||
|
$" A common cause is caching an instance of {nameof(DotNetObjectReference<TValue>)} globally. Consider creating instances of {nameof(DotNetObjectReference<TValue>)} at the JSInterop callsite.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Assert(dotNetObjectReference.ObjectId != 0);
|
||||||
|
return dotNetObjectReference.ObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal IDotNetObjectReference GetObjectReference(long dotNetObjectId)
|
||||||
|
{
|
||||||
|
return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef)
|
||||||
|
? dotNetObjectRef
|
||||||
|
: throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the DotNetObjectReference instance was already disposed.", nameof(dotNetObjectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops tracking the specified .NET object reference.
|
||||||
|
/// This may be invoked either by disposing a DotNetObjectRef in .NET code, or via JS interop by calling "dispose" on the corresponding instance in JavaScript code
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dotNetObjectId">The ID of the <see cref="DotNetObjectReference{TValue}"/>.</param>
|
||||||
|
internal void ReleaseObjectReference(long dotNetObjectId) => _trackedRefsById.TryRemove(dotNetObjectId, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Microsoft.JSInterop
|
|
||||||
{
|
|
||||||
internal static class JsonSerializerOptionsProvider
|
|
||||||
{
|
|
||||||
public static readonly JsonSerializerOptions Options = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
MaxDepth = 32,
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
PropertyNameCaseInsensitive = true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,34 +2,102 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using static Microsoft.JSInterop.TestJSRuntime;
|
|
||||||
|
|
||||||
namespace Microsoft.JSInterop
|
namespace Microsoft.JSInterop
|
||||||
{
|
{
|
||||||
public class DotNetObjectReferenceTest
|
public class DotNetObjectReferenceTest
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CanAccessValue() => WithJSRuntime(_ =>
|
public void CanAccessValue()
|
||||||
{
|
{
|
||||||
var obj = new object();
|
var obj = new object();
|
||||||
Assert.Same(obj, DotNetObjectReference.Create(obj).Value);
|
Assert.Same(obj, DotNetObjectReference.Create(obj).Value);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task NotifiesAssociatedJsRuntimeOfDisposal() => WithJSRuntime(jsRuntime =>
|
public void TrackObjectReference_AssignsObjectId()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var objRef = DotNetObjectReference.Create(new object());
|
var objRef = DotNetObjectReference.Create(new object());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
var objectId = jsRuntime.TrackObjectReference(objRef);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Assert.Equal(objectId, objRef.ObjectId);
|
||||||
Assert.Equal(1, objRef.ObjectId);
|
Assert.Equal(1, objRef.ObjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TrackObjectReference_AllowsMultipleCallsUsingTheSameJSRuntime()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
|
var objRef = DotNetObjectReference.Create(new object());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var objectId1 = jsRuntime.TrackObjectReference(objRef);
|
||||||
|
var objectId2 = jsRuntime.TrackObjectReference(objRef);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Assert.Equal(objectId1, objectId2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TrackObjectReference_ThrowsIfDifferentJSRuntimeInstancesAreUsed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var objRef = DotNetObjectReference.Create("Hello world");
|
||||||
|
var expected = $"{objRef.GetType().Name} is already being tracked by a different instance of {nameof(JSRuntime)}. A common cause is caching an instance of {nameof(DotNetObjectReference<string>)}" +
|
||||||
|
$" globally. Consider creating instances of {nameof(DotNetObjectReference<string>)} at the JSInterop callsite.";
|
||||||
|
var jsRuntime1 = new TestJSRuntime();
|
||||||
|
var jsRuntime2 = new TestJSRuntime();
|
||||||
|
jsRuntime1.TrackObjectReference(objRef);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var ex = Assert.Throws<InvalidOperationException>(() => jsRuntime2.TrackObjectReference(objRef));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Dispose_StopsTrackingObject()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var objRef = DotNetObjectReference.Create("Hello world");
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
|
jsRuntime.TrackObjectReference(objRef);
|
||||||
|
var objectId = objRef.ObjectId;
|
||||||
|
var expected = $"There is no tracked object with id '{objectId}'. Perhaps the DotNetObjectReference instance was already disposed.";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Assert.Same(objRef, jsRuntime.GetObjectReference(objectId));
|
||||||
objRef.Dispose();
|
objRef.Dispose();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var ex = Assert.Throws<ArgumentException>(() => jsRuntime.ObjectRefManager.FindDotNetObject(1));
|
Assert.True(objRef.Disposed);
|
||||||
Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
|
Assert.Throws<ArgumentException>(() => jsRuntime.GetObjectReference(objectId));
|
||||||
});
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoubleDispose_Works()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var objRef = DotNetObjectReference.Create("Hello world");
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
|
jsRuntime.TrackObjectReference(objRef);
|
||||||
|
var objectId = objRef.ObjectId;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Assert.Same(objRef, jsRuntime.GetObjectReference(objectId));
|
||||||
|
objRef.Dispose();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
objRef.Dispose();
|
||||||
|
// If we got this far, this did not throw.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
{
|
{
|
||||||
var ex = Assert.Throws<ArgumentException>(() =>
|
var ex = Assert.Throws<ArgumentException>(() =>
|
||||||
{
|
{
|
||||||
DotNetDispatcher.Invoke(" ", "SomeMethod", default, "[]");
|
DotNetDispatcher.Invoke(new TestJSRuntime(), " ", "SomeMethod", default, "[]");
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
|
Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
|
||||||
|
|
@ -32,7 +32,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
{
|
{
|
||||||
var ex = Assert.Throws<ArgumentException>(() =>
|
var ex = Assert.Throws<ArgumentException>(() =>
|
||||||
{
|
{
|
||||||
DotNetDispatcher.Invoke("SomeAssembly", " ", default, "[]");
|
DotNetDispatcher.Invoke(new TestJSRuntime(), "SomeAssembly", " ", default, "[]");
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
|
Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
|
||||||
|
|
@ -45,7 +45,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
var assemblyName = "Some.Fake.Assembly";
|
var assemblyName = "Some.Fake.Assembly";
|
||||||
var ex = Assert.Throws<ArgumentException>(() =>
|
var ex = Assert.Throws<ArgumentException>(() =>
|
||||||
{
|
{
|
||||||
DotNetDispatcher.Invoke(assemblyName, "SomeMethod", default, null);
|
DotNetDispatcher.Invoke(new TestJSRuntime(), assemblyName, "SomeMethod", default, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal($"There is no loaded assembly with the name '{assemblyName}'.", ex.Message);
|
Assert.Equal($"There is no loaded assembly with the name '{assemblyName}'.", ex.Message);
|
||||||
|
|
@ -67,52 +67,56 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
{
|
{
|
||||||
var ex = Assert.Throws<ArgumentException>(() =>
|
var ex = Assert.Throws<ArgumentException>(() =>
|
||||||
{
|
{
|
||||||
DotNetDispatcher.Invoke(thisAssemblyName, methodIdentifier, default, null);
|
DotNetDispatcher.Invoke(new TestJSRuntime(), thisAssemblyName, methodIdentifier, default, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message);
|
Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CanInvokeStaticVoidMethod() => WithJSRuntime(jsRuntime =>
|
public void CanInvokeStaticVoidMethod()
|
||||||
{
|
{
|
||||||
// Arrange/Act
|
// Arrange/Act
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
SomePublicType.DidInvokeMyInvocableStaticVoid = false;
|
SomePublicType.DidInvokeMyInvocableStaticVoid = false;
|
||||||
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticVoid", default, null);
|
var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticVoid", default, null);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Null(resultJson);
|
Assert.Null(resultJson);
|
||||||
Assert.True(SomePublicType.DidInvokeMyInvocableStaticVoid);
|
Assert.True(SomePublicType.DidInvokeMyInvocableStaticVoid);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CanInvokeStaticNonVoidMethod() => WithJSRuntime(jsRuntime =>
|
public void CanInvokeStaticNonVoidMethod()
|
||||||
{
|
{
|
||||||
// Arrange/Act
|
// Arrange/Act
|
||||||
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", default, null);
|
var jsRuntime = new TestJSRuntime();
|
||||||
var result = JsonSerializer.Deserialize<TestDTO>(resultJson, JsonSerializerOptionsProvider.Options);
|
var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticNonVoid", default, null);
|
||||||
|
var result = JsonSerializer.Deserialize<TestDTO>(resultJson, jsRuntime.JsonSerializerOptions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("Test", result.StringVal);
|
Assert.Equal("Test", result.StringVal);
|
||||||
Assert.Equal(123, result.IntVal);
|
Assert.Equal(123, result.IntVal);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CanInvokeStaticNonVoidMethodWithoutCustomIdentifier() => WithJSRuntime(jsRuntime =>
|
public void CanInvokeStaticNonVoidMethodWithoutCustomIdentifier()
|
||||||
{
|
{
|
||||||
// Arrange/Act
|
// Arrange/Act
|
||||||
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null);
|
var jsRuntime = new TestJSRuntime();
|
||||||
var result = JsonSerializer.Deserialize<TestDTO>(resultJson, JsonSerializerOptionsProvider.Options);
|
var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null);
|
||||||
|
var result = JsonSerializer.Deserialize<TestDTO>(resultJson, jsRuntime.JsonSerializerOptions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal);
|
Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal);
|
||||||
Assert.Equal(456, result.IntVal);
|
Assert.Equal(456, result.IntVal);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CanInvokeStaticWithParams() => WithJSRuntime(jsRuntime =>
|
public void CanInvokeStaticWithParams()
|
||||||
{
|
{
|
||||||
// Arrange: Track a .NET object to use as an arg
|
// Arrange: Track a .NET object to use as an arg
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" };
|
var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" };
|
||||||
var objectRef = DotNetObjectReference.Create(arg3);
|
var objectRef = DotNetObjectReference.Create(arg3);
|
||||||
jsRuntime.Invoke<object>("unimportant", objectRef);
|
jsRuntime.Invoke<object>("unimportant", objectRef);
|
||||||
|
|
@ -123,15 +127,15 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
new TestDTO { StringVal = "Another string", IntVal = 456 },
|
new TestDTO { StringVal = "Another string", IntVal = 456 },
|
||||||
new[] { 100, 200 },
|
new[] { 100, 200 },
|
||||||
objectRef
|
objectRef
|
||||||
}, JsonSerializerOptionsProvider.Options);
|
}, jsRuntime.JsonSerializerOptions);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson);
|
var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson);
|
||||||
var result = JsonDocument.Parse(resultJson);
|
var result = JsonDocument.Parse(resultJson);
|
||||||
var root = result.RootElement;
|
var root = result.RootElement;
|
||||||
|
|
||||||
// Assert: First result value marshalled via JSON
|
// Assert: First result value marshalled via JSON
|
||||||
var resultDto1 = JsonSerializer.Deserialize<TestDTO>(root[0].GetRawText(), JsonSerializerOptionsProvider.Options);
|
var resultDto1 = JsonSerializer.Deserialize<TestDTO>(root[0].GetRawText(), jsRuntime.JsonSerializerOptions);
|
||||||
|
|
||||||
Assert.Equal("ANOTHER STRING", resultDto1.StringVal);
|
Assert.Equal("ANOTHER STRING", resultDto1.StringVal);
|
||||||
Assert.Equal(756, resultDto1.IntVal);
|
Assert.Equal(756, resultDto1.IntVal);
|
||||||
|
|
@ -142,15 +146,16 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.IntVal), out _));
|
Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.IntVal), out _));
|
||||||
|
|
||||||
Assert.True(resultDto2Ref.TryGetProperty(DotNetDispatcher.DotNetObjectRefKey.EncodedUtf8Bytes, out var property));
|
Assert.True(resultDto2Ref.TryGetProperty(DotNetDispatcher.DotNetObjectRefKey.EncodedUtf8Bytes, out var property));
|
||||||
var resultDto2 = Assert.IsType<DotNetObjectReference<TestDTO>>(DotNetObjectReferenceManager.Current.FindDotNetObject(property.GetInt64())).Value;
|
var resultDto2 = Assert.IsType<DotNetObjectReference<TestDTO>>(jsRuntime.GetObjectReference(property.GetInt64())).Value;
|
||||||
Assert.Equal("MY STRING", resultDto2.StringVal);
|
Assert.Equal("MY STRING", resultDto2.StringVal);
|
||||||
Assert.Equal(1299, resultDto2.IntVal);
|
Assert.Equal(1299, resultDto2.IntVal);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task InvokingWithIncorrectUseOfDotNetObjectRefThrows() => WithJSRuntime(jsRuntime =>
|
public void InvokingWithIncorrectUseOfDotNetObjectRefThrows()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var method = nameof(SomePublicType.IncorrectDotNetObjectRefUsage);
|
var method = nameof(SomePublicType.IncorrectDotNetObjectRefUsage);
|
||||||
var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" };
|
var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" };
|
||||||
var objectRef = DotNetObjectReference.Create(arg3);
|
var objectRef = DotNetObjectReference.Create(arg3);
|
||||||
|
|
@ -162,67 +167,72 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
new TestDTO { StringVal = "Another string", IntVal = 456 },
|
new TestDTO { StringVal = "Another string", IntVal = 456 },
|
||||||
new[] { 100, 200 },
|
new[] { 100, 200 },
|
||||||
objectRef
|
objectRef
|
||||||
}, JsonSerializerOptionsProvider.Options);
|
}, jsRuntime.JsonSerializerOptions);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||||
DotNetDispatcher.Invoke(thisAssemblyName, method, default, argsJson));
|
DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, method, default, argsJson));
|
||||||
Assert.Equal($"In call to '{method}', parameter of type '{nameof(TestDTO)}' at index 3 must be declared as type 'DotNetObjectRef<TestDTO>' to receive the incoming value.", ex.Message);
|
Assert.Equal($"In call to '{method}', parameter of type '{nameof(TestDTO)}' at index 3 must be declared as type 'DotNetObjectRef<TestDTO>' to receive the incoming value.", ex.Message);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CanInvokeInstanceVoidMethod() => WithJSRuntime(jsRuntime =>
|
public void CanInvokeInstanceVoidMethod()
|
||||||
{
|
{
|
||||||
// Arrange: Track some instance
|
// Arrange: Track some instance
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var targetInstance = new SomePublicType();
|
var targetInstance = new SomePublicType();
|
||||||
var objectRef = DotNetObjectReference.Create(targetInstance);
|
var objectRef = DotNetObjectReference.Create(targetInstance);
|
||||||
jsRuntime.Invoke<object>("unimportant", objectRef);
|
jsRuntime.Invoke<object>("unimportant", objectRef);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null);
|
var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Null(resultJson);
|
Assert.Null(resultJson);
|
||||||
Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid);
|
Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CanInvokeBaseInstanceVoidMethod() => WithJSRuntime(jsRuntime =>
|
public void CanInvokeBaseInstanceVoidMethod()
|
||||||
{
|
{
|
||||||
// Arrange: Track some instance
|
// Arrange: Track some instance
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var targetInstance = new DerivedClass();
|
var targetInstance = new DerivedClass();
|
||||||
var objectRef = DotNetObjectReference.Create(targetInstance);
|
var objectRef = DotNetObjectReference.Create(targetInstance);
|
||||||
jsRuntime.Invoke<object>("unimportant", objectRef);
|
jsRuntime.Invoke<object>("unimportant", objectRef);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var resultJson = DotNetDispatcher.Invoke(null, "BaseClassInvokableInstanceVoid", 1, null);
|
var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "BaseClassInvokableInstanceVoid", 1, null);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Null(resultJson);
|
Assert.Null(resultJson);
|
||||||
Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid);
|
Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task DotNetObjectReferencesCanBeDisposed() => WithJSRuntime(jsRuntime =>
|
public void DotNetObjectReferencesCanBeDisposed()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var targetInstance = new SomePublicType();
|
var targetInstance = new SomePublicType();
|
||||||
var objectRef = DotNetObjectReference.Create(targetInstance);
|
var objectRef = DotNetObjectReference.Create(targetInstance);
|
||||||
|
jsRuntime.Invoke<object>("unimportant", objectRef);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
DotNetDispatcher.BeginInvokeDotNet(null, null, "__Dispose", objectRef.ObjectId, null);
|
DotNetDispatcher.BeginInvokeDotNet(jsRuntime, null, null, "__Dispose", objectRef.ObjectId, null);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(objectRef.Disposed);
|
Assert.True(objectRef.Disposed);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CannotUseDotNetObjectRefAfterDisposal() => WithJSRuntime(jsRuntime =>
|
public void CannotUseDotNetObjectRefAfterDisposal()
|
||||||
{
|
{
|
||||||
// This test addresses the case where the developer calls objectRef.Dispose()
|
// This test addresses the case where the developer calls objectRef.Dispose()
|
||||||
// from .NET code, as opposed to .dispose() from JS code
|
// from .NET code, as opposed to .dispose() from JS code
|
||||||
|
|
||||||
// Arrange: Track some instance, then dispose it
|
// Arrange: Track some instance, then dispose it
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var targetInstance = new SomePublicType();
|
var targetInstance = new SomePublicType();
|
||||||
var objectRef = DotNetObjectReference.Create(targetInstance);
|
var objectRef = DotNetObjectReference.Create(targetInstance);
|
||||||
jsRuntime.Invoke<object>("unimportant", objectRef);
|
jsRuntime.Invoke<object>("unimportant", objectRef);
|
||||||
|
|
@ -230,17 +240,18 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
|
|
||||||
// Act/Assert
|
// Act/Assert
|
||||||
var ex = Assert.Throws<ArgumentException>(
|
var ex = Assert.Throws<ArgumentException>(
|
||||||
() => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null));
|
() => DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null));
|
||||||
Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
|
Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CannotUseDotNetObjectRefAfterReleaseDotNetObject() => WithJSRuntime(jsRuntime =>
|
public void CannotUseDotNetObjectRefAfterReleaseDotNetObject()
|
||||||
{
|
{
|
||||||
// This test addresses the case where the developer calls .dispose()
|
// This test addresses the case where the developer calls .dispose()
|
||||||
// from JS code, as opposed to objectRef.Dispose() from .NET code
|
// from JS code, as opposed to objectRef.Dispose() from .NET code
|
||||||
|
|
||||||
// Arrange: Track some instance, then dispose it
|
// Arrange: Track some instance, then dispose it
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var targetInstance = new SomePublicType();
|
var targetInstance = new SomePublicType();
|
||||||
var objectRef = DotNetObjectReference.Create(targetInstance);
|
var objectRef = DotNetObjectReference.Create(targetInstance);
|
||||||
jsRuntime.Invoke<object>("unimportant", objectRef);
|
jsRuntime.Invoke<object>("unimportant", objectRef);
|
||||||
|
|
@ -248,80 +259,85 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
|
|
||||||
// Act/Assert
|
// Act/Assert
|
||||||
var ex = Assert.Throws<ArgumentException>(
|
var ex = Assert.Throws<ArgumentException>(
|
||||||
() => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null));
|
() => DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null));
|
||||||
Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
|
Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task EndInvoke_WithSuccessValue() => WithJSRuntime(jsRuntime =>
|
public void EndInvoke_WithSuccessValue()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var testDTO = new TestDTO { StringVal = "Hello", IntVal = 4 };
|
var testDTO = new TestDTO { StringVal = "Hello", IntVal = 4 };
|
||||||
var task = jsRuntime.InvokeAsync<TestDTO>("unimportant");
|
var task = jsRuntime.InvokeAsync<TestDTO>("unimportant");
|
||||||
var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, JsonSerializerOptionsProvider.Options);
|
var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, jsRuntime.JsonSerializerOptions);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
DotNetDispatcher.EndInvokeJS(argsJson);
|
DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(task.IsCompletedSuccessfully);
|
Assert.True(task.IsCompletedSuccessfully);
|
||||||
var result = task.Result;
|
var result = task.Result;
|
||||||
Assert.Equal(testDTO.StringVal, result.StringVal);
|
Assert.Equal(testDTO.StringVal, result.StringVal);
|
||||||
Assert.Equal(testDTO.IntVal, result.IntVal);
|
Assert.Equal(testDTO.IntVal, result.IntVal);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task EndInvoke_WithErrorString() => WithJSRuntime(async jsRuntime =>
|
public async Task EndInvoke_WithErrorString()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var expected = "Some error";
|
var expected = "Some error";
|
||||||
var task = jsRuntime.InvokeAsync<TestDTO>("unimportant");
|
var task = jsRuntime.InvokeAsync<TestDTO>("unimportant");
|
||||||
var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, expected }, JsonSerializerOptionsProvider.Options);
|
var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, expected }, jsRuntime.JsonSerializerOptions);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
DotNetDispatcher.EndInvokeJS(argsJson);
|
DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var ex = await Assert.ThrowsAsync<JSException>(async () => await task);
|
var ex = await Assert.ThrowsAsync<JSException>(async () => await task);
|
||||||
Assert.Equal(expected, ex.Message);
|
Assert.Equal(expected, ex.Message);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12357")]
|
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12357")]
|
||||||
public Task EndInvoke_AfterCancel() => WithJSRuntime(jsRuntime =>
|
public void EndInvoke_AfterCancel()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var testDTO = new TestDTO { StringVal = "Hello", IntVal = 4 };
|
var testDTO = new TestDTO { StringVal = "Hello", IntVal = 4 };
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
var task = jsRuntime.InvokeAsync<TestDTO>("unimportant", cts.Token);
|
var task = jsRuntime.InvokeAsync<TestDTO>("unimportant", cts.Token);
|
||||||
var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, JsonSerializerOptionsProvider.Options);
|
var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, jsRuntime.JsonSerializerOptions);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
cts.Cancel();
|
cts.Cancel();
|
||||||
DotNetDispatcher.EndInvokeJS(argsJson);
|
DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(task.IsCanceled);
|
Assert.True(task.IsCanceled);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task EndInvoke_WithNullError() => WithJSRuntime(async jsRuntime =>
|
public async Task EndInvoke_WithNullError()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var task = jsRuntime.InvokeAsync<TestDTO>("unimportant");
|
var task = jsRuntime.InvokeAsync<TestDTO>("unimportant");
|
||||||
var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, null }, JsonSerializerOptionsProvider.Options);
|
var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, null }, jsRuntime.JsonSerializerOptions);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
DotNetDispatcher.EndInvokeJS(argsJson);
|
DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var ex = await Assert.ThrowsAsync<JSException>(async () => await task);
|
var ex = await Assert.ThrowsAsync<JSException>(async () => await task);
|
||||||
Assert.Empty(ex.Message);
|
Assert.Empty(ex.Message);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CanInvokeInstanceMethodWithParams() => WithJSRuntime(jsRuntime =>
|
public void CanInvokeInstanceMethodWithParams()
|
||||||
{
|
{
|
||||||
// Arrange: Track some instance plus another object we'll pass as a param
|
// Arrange: Track some instance plus another object we'll pass as a param
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var targetInstance = new SomePublicType();
|
var targetInstance = new SomePublicType();
|
||||||
var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" };
|
var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" };
|
||||||
jsRuntime.Invoke<object>("unimportant",
|
jsRuntime.Invoke<object>("unimportant",
|
||||||
|
|
@ -330,38 +346,40 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
var argsJson = "[\"myvalue\",{\"__dotNetObject\":2}]";
|
var argsJson = "[\"myvalue\",{\"__dotNetObject\":2}]";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceMethod", 1, argsJson);
|
var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceMethod", 1, argsJson);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson);
|
Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson);
|
||||||
var resultDto = ((DotNetObjectReference<TestDTO>)jsRuntime.ObjectRefManager.FindDotNetObject(3)).Value;
|
var resultDto = ((DotNetObjectReference<TestDTO>)jsRuntime.GetObjectReference(3)).Value;
|
||||||
Assert.Equal(1235, resultDto.IntVal);
|
Assert.Equal(1235, resultDto.IntVal);
|
||||||
Assert.Equal("MY STRING", resultDto.StringVal);
|
Assert.Equal("MY STRING", resultDto.StringVal);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CannotInvokeWithFewerNumberOfParameters() => WithJSRuntime(jsRuntime =>
|
public void CannotInvokeWithFewerNumberOfParameters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var argsJson = JsonSerializer.Serialize(new object[]
|
var argsJson = JsonSerializer.Serialize(new object[]
|
||||||
{
|
{
|
||||||
new TestDTO { StringVal = "Another string", IntVal = 456 },
|
new TestDTO { StringVal = "Another string", IntVal = 456 },
|
||||||
new[] { 100, 200 },
|
new[] { 100, 200 },
|
||||||
}, JsonSerializerOptionsProvider.Options);
|
}, jsRuntime.JsonSerializerOptions);
|
||||||
|
|
||||||
// Act/Assert
|
// Act/Assert
|
||||||
var ex = Assert.Throws<ArgumentException>(() =>
|
var ex = Assert.Throws<ArgumentException>(() =>
|
||||||
{
|
{
|
||||||
DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson);
|
DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson);
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal("The call to 'InvocableStaticWithParams' expects '3' parameters, but received '2'.", ex.Message);
|
Assert.Equal("The call to 'InvocableStaticWithParams' expects '3' parameters, but received '2'.", ex.Message);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CannotInvokeWithMoreParameters() => WithJSRuntime(jsRuntime =>
|
public void CannotInvokeWithMoreParameters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var objectRef = DotNetObjectReference.Create(new TestDTO { IntVal = 4 });
|
var objectRef = DotNetObjectReference.Create(new TestDTO { IntVal = 4 });
|
||||||
var argsJson = JsonSerializer.Serialize(new object[]
|
var argsJson = JsonSerializer.Serialize(new object[]
|
||||||
{
|
{
|
||||||
|
|
@ -369,21 +387,22 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
new[] { 100, 200 },
|
new[] { 100, 200 },
|
||||||
objectRef,
|
objectRef,
|
||||||
7,
|
7,
|
||||||
}, JsonSerializerOptionsProvider.Options);
|
}, jsRuntime.JsonSerializerOptions);
|
||||||
|
|
||||||
// Act/Assert
|
// Act/Assert
|
||||||
var ex = Assert.Throws<JsonException>(() =>
|
var ex = Assert.Throws<JsonException>(() =>
|
||||||
{
|
{
|
||||||
DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson);
|
DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson);
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal("Unexpected JSON token Number. Ensure that the call to `InvocableStaticWithParams' is supplied with exactly '3' parameters.", ex.Message);
|
Assert.Equal("Unexpected JSON token Number. Ensure that the call to `InvocableStaticWithParams' is supplied with exactly '3' parameters.", ex.Message);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CanInvokeAsyncMethod() => WithJSRuntime(async jsRuntime =>
|
public async Task CanInvokeAsyncMethod()
|
||||||
{
|
{
|
||||||
// Arrange: Track some instance plus another object we'll pass as a param
|
// Arrange: Track some instance plus another object we'll pass as a param
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var targetInstance = new SomePublicType();
|
var targetInstance = new SomePublicType();
|
||||||
var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" };
|
var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" };
|
||||||
var arg1Ref = DotNetObjectReference.Create(targetInstance);
|
var arg1Ref = DotNetObjectReference.Create(targetInstance);
|
||||||
|
|
@ -395,12 +414,12 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
{
|
{
|
||||||
new TestDTO { IntVal = 1000, StringVal = "String via JSON" },
|
new TestDTO { IntVal = 1000, StringVal = "String via JSON" },
|
||||||
arg2Ref,
|
arg2Ref,
|
||||||
}, JsonSerializerOptionsProvider.Options);
|
}, jsRuntime.JsonSerializerOptions);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var callId = "123";
|
var callId = "123";
|
||||||
var resultTask = jsRuntime.NextInvocationTask;
|
var resultTask = jsRuntime.NextInvocationTask;
|
||||||
DotNetDispatcher.BeginInvokeDotNet(callId, null, "InvokableAsyncMethod", 1, argsJson);
|
DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, null, "InvokableAsyncMethod", 1, argsJson);
|
||||||
await resultTask;
|
await resultTask;
|
||||||
|
|
||||||
// Assert: Correct completion information
|
// Assert: Correct completion information
|
||||||
|
|
@ -417,17 +436,18 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
var resultDto2 = resultDto2Ref.Value;
|
var resultDto2 = resultDto2Ref.Value;
|
||||||
Assert.Equal("MY STRING", resultDto2.StringVal);
|
Assert.Equal("MY STRING", resultDto2.StringVal);
|
||||||
Assert.Equal(2468, resultDto2.IntVal);
|
Assert.Equal(2468, resultDto2.IntVal);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CanInvokeSyncThrowingMethod() => WithJSRuntime(async jsRuntime =>
|
public async Task CanInvokeSyncThrowingMethod()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var callId = "123";
|
var callId = "123";
|
||||||
var resultTask = jsRuntime.NextInvocationTask;
|
var resultTask = jsRuntime.NextInvocationTask;
|
||||||
DotNetDispatcher.BeginInvokeDotNet(callId, thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, default);
|
DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, default);
|
||||||
|
|
||||||
await resultTask; // This won't throw, it sets properties on the jsRuntime.
|
await resultTask; // This won't throw, it sets properties on the jsRuntime.
|
||||||
|
|
||||||
|
|
@ -439,17 +459,18 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
// https://github.com/aspnet/AspNetCore/issues/8612
|
// https://github.com/aspnet/AspNetCore/issues/8612
|
||||||
var exception = jsRuntime.LastCompletionResult is ExceptionDispatchInfo edi ? edi.SourceException.ToString() : null;
|
var exception = jsRuntime.LastCompletionResult is ExceptionDispatchInfo edi ? edi.SourceException.ToString() : null;
|
||||||
Assert.Contains(nameof(ThrowingClass.ThrowingMethod), exception);
|
Assert.Contains(nameof(ThrowingClass.ThrowingMethod), exception);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task CanInvokeAsyncThrowingMethod() => WithJSRuntime(async jsRuntime =>
|
public async Task CanInvokeAsyncThrowingMethod()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var callId = "123";
|
var callId = "123";
|
||||||
var resultTask = jsRuntime.NextInvocationTask;
|
var resultTask = jsRuntime.NextInvocationTask;
|
||||||
DotNetDispatcher.BeginInvokeDotNet(callId, thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, default);
|
DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, default);
|
||||||
|
|
||||||
await resultTask; // This won't throw, it sets properties on the jsRuntime.
|
await resultTask; // This won't throw, it sets properties on the jsRuntime.
|
||||||
|
|
||||||
|
|
@ -461,15 +482,16 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
// https://github.com/aspnet/AspNetCore/issues/8612
|
// https://github.com/aspnet/AspNetCore/issues/8612
|
||||||
var exception = jsRuntime.LastCompletionResult is ExceptionDispatchInfo edi ? edi.SourceException.ToString() : null;
|
var exception = jsRuntime.LastCompletionResult is ExceptionDispatchInfo edi ? edi.SourceException.ToString() : null;
|
||||||
Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), exception);
|
Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), exception);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task BeginInvoke_ThrowsWithInvalidArgsJson_WithCallId() => WithJSRuntime(async jsRuntime =>
|
public async Task BeginInvoke_ThrowsWithInvalidArgsJson_WithCallId()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var callId = "123";
|
var callId = "123";
|
||||||
var resultTask = jsRuntime.NextInvocationTask;
|
var resultTask = jsRuntime.NextInvocationTask;
|
||||||
DotNetDispatcher.BeginInvokeDotNet(callId, thisAssemblyName, "InvocableStaticWithParams", default, "<xml>not json</xml>");
|
DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, "InvocableStaticWithParams", default, "<xml>not json</xml>");
|
||||||
|
|
||||||
await resultTask; // This won't throw, it sets properties on the jsRuntime.
|
await resultTask; // This won't throw, it sets properties on the jsRuntime.
|
||||||
|
|
||||||
|
|
@ -478,29 +500,30 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
Assert.False(jsRuntime.LastCompletionStatus); // Fails
|
Assert.False(jsRuntime.LastCompletionStatus); // Fails
|
||||||
var result = Assert.IsType<ExceptionDispatchInfo>(jsRuntime.LastCompletionResult);
|
var result = Assert.IsType<ExceptionDispatchInfo>(jsRuntime.LastCompletionResult);
|
||||||
Assert.Contains("JsonReaderException: '<' is an invalid start of a value.", result.SourceException.ToString());
|
Assert.Contains("JsonReaderException: '<' is an invalid start of a value.", result.SourceException.ToString());
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task BeginInvoke_ThrowsWithInvalid_DotNetObjectRef() => WithJSRuntime(jsRuntime =>
|
public void BeginInvoke_ThrowsWithInvalid_DotNetObjectRef()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var callId = "123";
|
var callId = "123";
|
||||||
var resultTask = jsRuntime.NextInvocationTask;
|
var resultTask = jsRuntime.NextInvocationTask;
|
||||||
DotNetDispatcher.BeginInvokeDotNet(callId, null, "InvokableInstanceVoid", 1, null);
|
DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, null, "InvokableInstanceVoid", 1, null);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(callId, jsRuntime.LastCompletionCallId);
|
Assert.Equal(callId, jsRuntime.LastCompletionCallId);
|
||||||
Assert.False(jsRuntime.LastCompletionStatus); // Fails
|
Assert.False(jsRuntime.LastCompletionStatus); // Fails
|
||||||
var result = Assert.IsType<ExceptionDispatchInfo>(jsRuntime.LastCompletionResult);
|
var result = Assert.IsType<ExceptionDispatchInfo>(jsRuntime.LastCompletionResult);
|
||||||
Assert.StartsWith("System.ArgumentException: There is no tracked object with id '1'. Perhaps the DotNetObjectRef instance was already disposed.", result.SourceException.ToString());
|
Assert.StartsWith("System.ArgumentException: There is no tracked object with id '1'. Perhaps the DotNetObjectReference instance was already disposed.", result.SourceException.ToString());
|
||||||
});
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("")]
|
[InlineData("")]
|
||||||
[InlineData("<xml>")]
|
[InlineData("<xml>")]
|
||||||
public void ParseArguments_ThrowsIfJsonIsInvalid(string arguments)
|
public void ParseArguments_ThrowsIfJsonIsInvalid(string arguments)
|
||||||
{
|
{
|
||||||
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(string) }));
|
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -509,7 +532,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
public void ParseArguments_ThrowsIfTheArgsJsonIsNotArray(string arguments)
|
public void ParseArguments_ThrowsIfTheArgsJsonIsNotArray(string arguments)
|
||||||
{
|
{
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(string) }));
|
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -518,7 +541,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
public void ParseArguments_ThrowsIfTheArgsJsonIsInvalidArray(string arguments)
|
public void ParseArguments_ThrowsIfTheArgsJsonIsInvalidArray(string arguments)
|
||||||
{
|
{
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(string) }));
|
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -528,7 +551,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
var arguments = "[\"Hello\", 2]";
|
var arguments = "[\"Hello\", 2]";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(string), typeof(int), });
|
var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string), typeof(int), });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(new object[] { "Hello", 2 }, result);
|
Assert.Equal(new object[] { "Hello", 2 }, result);
|
||||||
|
|
@ -541,7 +564,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
var arguments = "[{\"IntVal\": 7}]";
|
var arguments = "[{\"IntVal\": 7}]";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(TestDTO), });
|
var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(TestDTO), });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var value = Assert.IsType<TestDTO>(Assert.Single(result));
|
var value = Assert.IsType<TestDTO>(Assert.Single(result));
|
||||||
|
|
@ -556,7 +579,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
var arguments = "[4, null]";
|
var arguments = "[4, null]";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(int), typeof(TestDTO), });
|
var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(int), typeof(TestDTO), });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
|
|
@ -573,92 +596,72 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
var arguments = "[4, {\"__dotNetObject\": 7}]";
|
var arguments = "[4, {\"__dotNetObject\": 7}]";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var ex = Assert.Throws<InvalidOperationException>(() => DotNetDispatcher.ParseArguments(method, arguments, new[] { typeof(int), typeof(TestDTO), }));
|
var ex = Assert.Throws<InvalidOperationException>(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), method, arguments, new[] { typeof(int), typeof(TestDTO), }));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal($"In call to '{method}', parameter of type '{nameof(TestDTO)}' at index 2 must be declared as type 'DotNetObjectRef<TestDTO>' to receive the incoming value.", ex.Message);
|
Assert.Equal($"In call to '{method}', parameter of type '{nameof(TestDTO)}' at index 2 must be declared as type 'DotNetObjectRef<TestDTO>' to receive the incoming value.", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseEndInvokeArguments_ThrowsIfJsonIsEmptyString()
|
public void EndInvokeJS_ThrowsIfJsonIsEmptyString()
|
||||||
{
|
{
|
||||||
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseEndInvokeArguments(new TestJSRuntime(), ""));
|
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseEndInvokeArguments_ThrowsIfJsonIsNotArray()
|
public void EndInvokeJS_ThrowsIfJsonIsNotArray()
|
||||||
{
|
{
|
||||||
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseEndInvokeArguments(new TestJSRuntime(), "{\"key\": \"value\"}"));
|
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "{\"key\": \"value\"}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseEndInvokeArguments_ThrowsIfJsonArrayIsInComplete()
|
public void EndInvokeJS_ThrowsIfJsonArrayIsInComplete()
|
||||||
{
|
{
|
||||||
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseEndInvokeArguments(new TestJSRuntime(), "[7, false"));
|
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "[7, false"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseEndInvokeArguments_ThrowsIfJsonArrayHasMoreThan3Arguments()
|
public void EndInvokeJS_ThrowsIfJsonArrayHasMoreThan3Arguments()
|
||||||
{
|
{
|
||||||
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseEndInvokeArguments(new TestJSRuntime(), "[7, false, \"Hello\", 5]"));
|
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "[7, false, \"Hello\", 5]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseEndInvokeArguments_Works()
|
public void EndInvokeJS_Works()
|
||||||
{
|
{
|
||||||
var jsRuntime = new TestJSRuntime();
|
var jsRuntime = new TestJSRuntime();
|
||||||
var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
|
var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
|
||||||
|
|
||||||
DotNetDispatcher.ParseEndInvokeArguments(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, {{\"intVal\": 7}}]");
|
DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, {{\"intVal\": 7}}]");
|
||||||
|
|
||||||
Assert.True(task.IsCompletedSuccessfully);
|
Assert.True(task.IsCompletedSuccessfully);
|
||||||
Assert.Equal(7, task.Result.IntVal);
|
Assert.Equal(7, task.Result.IntVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseEndInvokeArguments_WithArrayValue()
|
public void EndInvokeJS_WithArrayValue()
|
||||||
{
|
{
|
||||||
var jsRuntime = new TestJSRuntime();
|
var jsRuntime = new TestJSRuntime();
|
||||||
var task = jsRuntime.InvokeAsync<int[]>("somemethod");
|
var task = jsRuntime.InvokeAsync<int[]>("somemethod");
|
||||||
|
|
||||||
DotNetDispatcher.ParseEndInvokeArguments(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, [1, 2, 3]]");
|
DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, [1, 2, 3]]");
|
||||||
|
|
||||||
Assert.True(task.IsCompletedSuccessfully);
|
Assert.True(task.IsCompletedSuccessfully);
|
||||||
Assert.Equal(new[] { 1, 2, 3 }, task.Result);
|
Assert.Equal(new[] { 1, 2, 3 }, task.Result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseEndInvokeArguments_WithNullValue()
|
public void EndInvokeJS_WithNullValue()
|
||||||
{
|
{
|
||||||
var jsRuntime = new TestJSRuntime();
|
var jsRuntime = new TestJSRuntime();
|
||||||
var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
|
var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
|
||||||
|
|
||||||
DotNetDispatcher.ParseEndInvokeArguments(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, null]");
|
DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, null]");
|
||||||
|
|
||||||
Assert.True(task.IsCompletedSuccessfully);
|
Assert.True(task.IsCompletedSuccessfully);
|
||||||
Assert.Null(task.Result);
|
Assert.Null(task.Result);
|
||||||
}
|
}
|
||||||
|
|
||||||
Task WithJSRuntime(Action<TestJSRuntime> testCode)
|
|
||||||
{
|
|
||||||
return WithJSRuntime(jsRuntime =>
|
|
||||||
{
|
|
||||||
testCode(jsRuntime);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task WithJSRuntime(Func<TestJSRuntime, Task> testCode)
|
|
||||||
{
|
|
||||||
// Since the tests rely on the asynclocal JSRuntime.Current, ensure we
|
|
||||||
// are on a distinct async context with a non-null JSRuntime.Current
|
|
||||||
await Task.Yield();
|
|
||||||
|
|
||||||
var runtime = new TestJSRuntime();
|
|
||||||
JSRuntime.SetCurrentJSRuntime(runtime);
|
|
||||||
await testCode(runtime);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class SomeInteralType
|
internal class SomeInteralType
|
||||||
{
|
{
|
||||||
[JSInvokable("MethodOnInternalType")] public void MyMethod() { }
|
[JSInvokable("MethodOnInternalType")] public void MyMethod() { }
|
||||||
|
|
|
||||||
|
|
@ -2,91 +2,93 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using static Microsoft.JSInterop.TestJSRuntime;
|
|
||||||
|
|
||||||
namespace Microsoft.JSInterop.Infrastructure
|
namespace Microsoft.JSInterop.Infrastructure
|
||||||
{
|
{
|
||||||
public class DotNetObjectReferenceJsonConverterTest
|
public class DotNetObjectReferenceJsonConverterTest
|
||||||
{
|
{
|
||||||
|
private readonly JSRuntime JSRuntime = new TestJSRuntime();
|
||||||
|
private JsonSerializerOptions JsonSerializerOptions => JSRuntime.JsonSerializerOptions;
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task Read_Throws_IfJsonIsMissingDotNetObjectProperty() => WithJSRuntime(_ =>
|
public void Read_Throws_IfJsonIsMissingDotNetObjectProperty()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var dotNetObjectRef = DotNetObjectReference.Create(new TestModel());
|
var dotNetObjectRef = DotNetObjectReference.Create(new TestModel());
|
||||||
|
|
||||||
var json = "{}";
|
var json = "{}";
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<DotNetObjectReference<TestModel>>(json));
|
var ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<DotNetObjectReference<TestModel>>(json, JsonSerializerOptions));
|
||||||
Assert.Equal("Required property __dotNetObject not found.", ex.Message);
|
Assert.Equal("Required property __dotNetObject not found.", ex.Message);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task Read_Throws_IfJsonContainsUnknownContent() => WithJSRuntime(_ =>
|
public void Read_Throws_IfJsonContainsUnknownContent()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var jsRuntime = new TestJSRuntime();
|
||||||
var dotNetObjectRef = DotNetObjectReference.Create(new TestModel());
|
var dotNetObjectRef = DotNetObjectReference.Create(new TestModel());
|
||||||
|
|
||||||
var json = "{\"foo\":2}";
|
var json = "{\"foo\":2}";
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<DotNetObjectReference<TestModel>>(json));
|
var ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<DotNetObjectReference<TestModel>>(json, JsonSerializerOptions));
|
||||||
Assert.Equal("Unexcepted JSON property foo.", ex.Message);
|
Assert.Equal("Unexcepted JSON property foo.", ex.Message);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task Read_Throws_IfJsonIsIncomplete() => WithJSRuntime(_ =>
|
public void Read_Throws_IfJsonIsIncomplete()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var input = new TestModel();
|
var input = new TestModel();
|
||||||
var dotNetObjectRef = DotNetObjectReference.Create(input);
|
var dotNetObjectRef = DotNetObjectReference.Create(input);
|
||||||
var objectId = dotNetObjectRef.ObjectId;
|
var objectId = JSRuntime.TrackObjectReference(dotNetObjectRef);
|
||||||
|
|
||||||
var json = $"{{\"__dotNetObject\":{objectId}";
|
var json = $"{{\"__dotNetObject\":{objectId}";
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = Record.Exception(() => JsonSerializer.Deserialize<DotNetObjectReference<TestModel>>(json));
|
var ex = Record.Exception(() => JsonSerializer.Deserialize<DotNetObjectReference<TestModel>>(json, JsonSerializerOptions));
|
||||||
Assert.IsAssignableFrom<JsonException>(ex);
|
Assert.IsAssignableFrom<JsonException>(ex);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task Read_Throws_IfDotNetObjectIdAppearsMultipleTimes() => WithJSRuntime(_ =>
|
public void Read_Throws_IfDotNetObjectIdAppearsMultipleTimes()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var input = new TestModel();
|
var input = new TestModel();
|
||||||
var dotNetObjectRef = DotNetObjectReference.Create(input);
|
var dotNetObjectRef = DotNetObjectReference.Create(input);
|
||||||
var objectId = dotNetObjectRef.ObjectId;
|
var objectId = JSRuntime.TrackObjectReference(dotNetObjectRef);
|
||||||
|
|
||||||
var json = $"{{\"__dotNetObject\":{objectId},\"__dotNetObject\":{objectId}}}";
|
var json = $"{{\"__dotNetObject\":{objectId},\"__dotNetObject\":{objectId}}}";
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = Record.Exception(() => JsonSerializer.Deserialize<DotNetObjectReference<TestModel>>(json));
|
var ex = Record.Exception(() => JsonSerializer.Deserialize<DotNetObjectReference<TestModel>>(json, JsonSerializerOptions));
|
||||||
Assert.IsAssignableFrom<JsonException>(ex);
|
Assert.IsAssignableFrom<JsonException>(ex);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task Read_ReadsJson() => WithJSRuntime(_ =>
|
public void Read_ReadsJson()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var input = new TestModel();
|
var input = new TestModel();
|
||||||
var dotNetObjectRef = DotNetObjectReference.Create(input);
|
var dotNetObjectRef = DotNetObjectReference.Create(input);
|
||||||
var objectId = dotNetObjectRef.ObjectId;
|
var objectId = JSRuntime.TrackObjectReference(dotNetObjectRef);
|
||||||
|
|
||||||
var json = $"{{\"__dotNetObject\":{objectId}}}";
|
var json = $"{{\"__dotNetObject\":{objectId}}}";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var deserialized = JsonSerializer.Deserialize<DotNetObjectReference<TestModel>>(json);
|
var deserialized = JsonSerializer.Deserialize<DotNetObjectReference<TestModel>>(json, JsonSerializerOptions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(input, deserialized.Value);
|
Assert.Same(input, deserialized.Value);
|
||||||
Assert.Equal(objectId, deserialized.ObjectId);
|
Assert.Equal(objectId, deserialized.ObjectId);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task Read_ReturnsTheCorrectInstance() => WithJSRuntime(_ =>
|
public void Read_ReturnsTheCorrectInstance()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
// Track a few instances and verify that the deserialized value returns the correct value.
|
// Track a few instances and verify that the deserialized value returns the correct value.
|
||||||
|
|
@ -95,23 +97,23 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
var ref1 = DotNetObjectReference.Create(instance1);
|
var ref1 = DotNetObjectReference.Create(instance1);
|
||||||
var ref2 = DotNetObjectReference.Create(instance2);
|
var ref2 = DotNetObjectReference.Create(instance2);
|
||||||
|
|
||||||
var json = $"[{{\"__dotNetObject\":{ref2.ObjectId}}},{{\"__dotNetObject\":{ref1.ObjectId}}}]";
|
var json = $"[{{\"__dotNetObject\":{JSRuntime.TrackObjectReference(ref1)}}},{{\"__dotNetObject\":{JSRuntime.TrackObjectReference(ref2)}}}]";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var deserialized = JsonSerializer.Deserialize<DotNetObjectReference<TestModel>[]>(json);
|
var deserialized = JsonSerializer.Deserialize<DotNetObjectReference<TestModel>[]>(json, JsonSerializerOptions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(instance2, deserialized[0].Value);
|
Assert.Same(instance1, deserialized[0].Value);
|
||||||
Assert.Same(instance1, deserialized[1].Value);
|
Assert.Same(instance2, deserialized[1].Value);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task Read_ReadsJson_WithFormatting() => WithJSRuntime(_ =>
|
public void Read_ReadsJson_WithFormatting()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var input = new TestModel();
|
var input = new TestModel();
|
||||||
var dotNetObjectRef = DotNetObjectReference.Create(input);
|
var dotNetObjectRef = DotNetObjectReference.Create(input);
|
||||||
var objectId = dotNetObjectRef.ObjectId;
|
var objectId = JSRuntime.TrackObjectReference(dotNetObjectRef);
|
||||||
|
|
||||||
var json =
|
var json =
|
||||||
@$"{{
|
@$"{{
|
||||||
|
|
@ -119,27 +121,27 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
}}";
|
}}";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var deserialized = JsonSerializer.Deserialize<DotNetObjectReference<TestModel>>(json);
|
var deserialized = JsonSerializer.Deserialize<DotNetObjectReference<TestModel>>(json, JsonSerializerOptions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(input, deserialized.Value);
|
Assert.Same(input, deserialized.Value);
|
||||||
Assert.Equal(objectId, deserialized.ObjectId);
|
Assert.Equal(objectId, deserialized.ObjectId);
|
||||||
});
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task WriteJsonTwice_KeepsObjectId() => WithJSRuntime(_ =>
|
public void WriteJsonTwice_KeepsObjectId()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var dotNetObjectRef = DotNetObjectReference.Create(new TestModel());
|
var dotNetObjectRef = DotNetObjectReference.Create(new TestModel());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var json1 = JsonSerializer.Serialize(dotNetObjectRef);
|
var json1 = JsonSerializer.Serialize(dotNetObjectRef, JsonSerializerOptions);
|
||||||
var json2 = JsonSerializer.Serialize(dotNetObjectRef);
|
var json2 = JsonSerializer.Serialize(dotNetObjectRef, JsonSerializerOptions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal($"{{\"__dotNetObject\":{dotNetObjectRef.ObjectId}}}", json1);
|
Assert.Equal($"{{\"__dotNetObject\":{dotNetObjectRef.ObjectId}}}", json1);
|
||||||
Assert.Equal(json1, json2);
|
Assert.Equal(json1, json2);
|
||||||
});
|
}
|
||||||
|
|
||||||
private class TestModel
|
private class TestModel
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ namespace Microsoft.JSInterop
|
||||||
{
|
{
|
||||||
NextResultJson = "{\"intValue\":123,\"stringValue\":\"Hello\"}"
|
NextResultJson = "{\"intValue\":123,\"stringValue\":\"Hello\"}"
|
||||||
};
|
};
|
||||||
JSRuntime.SetCurrentJSRuntime(runtime);
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var syncResult = runtime.Invoke<TestDTO>("test identifier 1", "arg1", 123, true);
|
var syncResult = runtime.Invoke<TestDTO>("test identifier 1", "arg1", 123, true);
|
||||||
|
|
@ -36,7 +35,6 @@ namespace Microsoft.JSInterop
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var runtime = new TestJSInProcessRuntime { NextResultJson = null };
|
var runtime = new TestJSInProcessRuntime { NextResultJson = null };
|
||||||
JSRuntime.SetCurrentJSRuntime(runtime);
|
|
||||||
var obj1 = new object();
|
var obj1 = new object();
|
||||||
var obj2 = new object();
|
var obj2 = new object();
|
||||||
var obj3 = new object();
|
var obj3 = new object();
|
||||||
|
|
@ -60,9 +58,9 @@ namespace Microsoft.JSInterop
|
||||||
Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":2},\"obj3\":{\"__dotNetObject\":3}}]", call.ArgsJson);
|
Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":2},\"obj3\":{\"__dotNetObject\":3}}]", call.ArgsJson);
|
||||||
|
|
||||||
// Assert: Objects were tracked
|
// Assert: Objects were tracked
|
||||||
Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1).Value);
|
Assert.Same(obj1, runtime.GetObjectReference(1).Value);
|
||||||
Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(2).Value);
|
Assert.Same(obj2, runtime.GetObjectReference(2).Value);
|
||||||
Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(3).Value);
|
Assert.Same(obj3, runtime.GetObjectReference(3).Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -73,7 +71,6 @@ namespace Microsoft.JSInterop
|
||||||
{
|
{
|
||||||
NextResultJson = "[{\"__dotNetObject\":2},{\"__dotNetObject\":1}]"
|
NextResultJson = "[{\"__dotNetObject\":2},{\"__dotNetObject\":1}]"
|
||||||
};
|
};
|
||||||
JSRuntime.SetCurrentJSRuntime(runtime);
|
|
||||||
var obj1 = new object();
|
var obj1 = new object();
|
||||||
var obj2 = new object();
|
var obj2 = new object();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,23 +14,6 @@ namespace Microsoft.JSInterop
|
||||||
{
|
{
|
||||||
public class JSRuntimeTest
|
public class JSRuntimeTest
|
||||||
{
|
{
|
||||||
#region this will be removed eventually
|
|
||||||
[Fact]
|
|
||||||
public async Task CanHaveDistinctJSRuntimeInstancesInEachAsyncContext()
|
|
||||||
{
|
|
||||||
var tasks = Enumerable.Range(0, 20).Select(async _ =>
|
|
||||||
{
|
|
||||||
var jsRuntime = new TestJSRuntime();
|
|
||||||
JSRuntime.SetCurrentJSRuntime(jsRuntime);
|
|
||||||
await Task.Delay(50).ConfigureAwait(false);
|
|
||||||
Assert.Same(jsRuntime, JSRuntime.Current);
|
|
||||||
});
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
|
||||||
Assert.Null(JSRuntime.Current);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DispatchesAsyncCallsWithDistinctAsyncHandles()
|
public void DispatchesAsyncCallsWithDistinctAsyncHandles()
|
||||||
{
|
{
|
||||||
|
|
@ -274,7 +257,6 @@ namespace Microsoft.JSInterop
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var runtime = new TestJSRuntime();
|
var runtime = new TestJSRuntime();
|
||||||
JSRuntime.SetCurrentJSRuntime(runtime);
|
|
||||||
var obj1 = new object();
|
var obj1 = new object();
|
||||||
var obj2 = new object();
|
var obj2 = new object();
|
||||||
var obj3 = new object();
|
var obj3 = new object();
|
||||||
|
|
@ -296,15 +278,15 @@ namespace Microsoft.JSInterop
|
||||||
// Assert: Serialized as expected
|
// Assert: Serialized as expected
|
||||||
var call = runtime.BeginInvokeCalls.Single();
|
var call = runtime.BeginInvokeCalls.Single();
|
||||||
Assert.Equal("test identifier", call.Identifier);
|
Assert.Equal("test identifier", call.Identifier);
|
||||||
Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":3},\"obj3\":{\"__dotNetObject\":4},\"obj1SameRef\":{\"__dotNetObject\":1},\"obj1DifferentRef\":{\"__dotNetObject\":2}}]", call.ArgsJson);
|
Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":2},\"obj3\":{\"__dotNetObject\":3},\"obj1SameRef\":{\"__dotNetObject\":1},\"obj1DifferentRef\":{\"__dotNetObject\":4}}]", call.ArgsJson);
|
||||||
|
|
||||||
// Assert: Objects were tracked
|
// Assert: Objects were tracked
|
||||||
Assert.Same(obj1Ref, runtime.ObjectRefManager.FindDotNetObject(1));
|
Assert.Same(obj1Ref, runtime.GetObjectReference(1));
|
||||||
Assert.Same(obj1, obj1Ref.Value);
|
Assert.Same(obj1, obj1Ref.Value);
|
||||||
Assert.NotSame(obj1Ref, runtime.ObjectRefManager.FindDotNetObject(2));
|
Assert.NotSame(obj1Ref, runtime.GetObjectReference(2));
|
||||||
Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(2).Value);
|
Assert.Same(obj2, runtime.GetObjectReference(2).Value);
|
||||||
Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(3).Value);
|
Assert.Same(obj3, runtime.GetObjectReference(3).Value);
|
||||||
Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(4).Value);
|
Assert.Same(obj1, runtime.GetObjectReference(4).Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.JSInterop
|
namespace Microsoft.JSInterop
|
||||||
{
|
{
|
||||||
|
|
@ -17,16 +16,5 @@ namespace Microsoft.JSInterop
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task WithJSRuntime(Action<JSRuntime> testCode)
|
|
||||||
{
|
|
||||||
// Since the tests rely on the asynclocal JSRuntime.Current, ensure we
|
|
||||||
// are on a distinct async context with a non-null JSRuntime.Current
|
|
||||||
await Task.Yield();
|
|
||||||
|
|
||||||
var runtime = new TestJSRuntime();
|
|
||||||
JSRuntime.SetCurrentJSRuntime(runtime);
|
|
||||||
testCode(runtime);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ namespace Mono.WebAssembly.Interop
|
||||||
public MonoWebAssemblyJSRuntime() { }
|
public MonoWebAssemblyJSRuntime() { }
|
||||||
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) { }
|
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) { }
|
||||||
protected override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) { }
|
protected override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) { }
|
||||||
|
protected static void Initialize(Mono.WebAssembly.Interop.MonoWebAssemblyJSRuntime jsRuntime) { }
|
||||||
protected override string InvokeJS(string identifier, string argsJson) { throw null; }
|
protected override string InvokeJS(string identifier, string argsJson) { throw null; }
|
||||||
public TRes InvokeUnmarshalled<TRes>(string identifier) { throw null; }
|
public TRes InvokeUnmarshalled<TRes>(string identifier) { throw null; }
|
||||||
public TRes InvokeUnmarshalled<T0, TRes>(string identifier, T0 arg0) { throw null; }
|
public TRes InvokeUnmarshalled<T0, TRes>(string identifier, T0 arg0) { throw null; }
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,25 @@ namespace Mono.WebAssembly.Interop
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MonoWebAssemblyJSRuntime : JSInProcessRuntime
|
public class MonoWebAssemblyJSRuntime : JSInProcessRuntime
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="MonoWebAssemblyJSRuntime"/> used to perform operations using <see cref="DotNetDispatcher"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static MonoWebAssemblyJSRuntime Instance { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the <see cref="MonoWebAssemblyJSRuntime"/> to be used to perform operations using <see cref="DotNetDispatcher"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsRuntime">The <see cref="MonoWebAssemblyJSRuntime"/> instance.</param>
|
||||||
|
protected static void Initialize(MonoWebAssemblyJSRuntime jsRuntime)
|
||||||
|
{
|
||||||
|
if (Instance != null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("MonoWebAssemblyJSRuntime has already been initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Instance = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime));
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override string InvokeJS(string identifier, string argsJson)
|
protected override string InvokeJS(string identifier, string argsJson)
|
||||||
{
|
{
|
||||||
|
|
@ -34,11 +53,11 @@ namespace Mono.WebAssembly.Interop
|
||||||
|
|
||||||
// Invoked via Mono's JS interop mechanism (invoke_method)
|
// Invoked via Mono's JS interop mechanism (invoke_method)
|
||||||
private static string InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson)
|
private static string InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson)
|
||||||
=> DotNetDispatcher.Invoke(assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson);
|
=> DotNetDispatcher.Invoke(Instance, assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson);
|
||||||
|
|
||||||
// Invoked via Mono's JS interop mechanism (invoke_method)
|
// Invoked via Mono's JS interop mechanism (invoke_method)
|
||||||
private static void EndInvokeJS(string argsJson)
|
private static void EndInvokeJS(string argsJson)
|
||||||
=> DotNetDispatcher.EndInvokeJS(argsJson);
|
=> DotNetDispatcher.EndInvokeJS(Instance, argsJson);
|
||||||
|
|
||||||
// Invoked via Mono's JS interop mechanism (invoke_method)
|
// Invoked via Mono's JS interop mechanism (invoke_method)
|
||||||
private static void BeginInvokeDotNet(string callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson)
|
private static void BeginInvokeDotNet(string callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson)
|
||||||
|
|
@ -59,7 +78,7 @@ namespace Mono.WebAssembly.Interop
|
||||||
assemblyName = assemblyNameOrDotNetObjectId;
|
assemblyName = assemblyNameOrDotNetObjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
DotNetDispatcher.BeginInvokeDotNet(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
|
DotNetDispatcher.BeginInvokeDotNet(Instance, callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void EndInvokeDotNet(
|
protected override void EndInvokeDotNet(
|
||||||
|
|
@ -84,7 +103,7 @@ namespace Mono.WebAssembly.Interop
|
||||||
|
|
||||||
// We pass 0 as the async handle because we don't want the JS-side code to
|
// We pass 0 as the async handle because we don't want the JS-side code to
|
||||||
// send back any notification (we're just providing a result for an existing async call)
|
// send back any notification (we're just providing a result for an existing async call)
|
||||||
var args = JsonSerializer.Serialize(new[] { callId, success, resultOrError }, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
var args = JsonSerializer.Serialize(new[] { callId, success, resultOrError }, JsonSerializerOptions);
|
||||||
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args);
|
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue