Improved interoperability

* Add support for invoking async JavaScript functions from .NET.
* Add support for invoking .NET methods from JavaScript.
* Add support for invoking async .NET methods from JavaScript.
This commit is contained in:
Javier Calvarro Nelson 2018-05-23 14:38:30 -07:00
parent a3a76c2e9a
commit 5cb544ece8
28 changed files with 2172 additions and 48 deletions

View File

@ -1830,15 +1830,6 @@
"xtend": "4.0.1"
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
@ -1872,6 +1863,15 @@
}
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",

View File

@ -1,4 +1,4 @@
import { platform } from './Environment';
import { platform } from './Environment';
import { getAssemblyNameFromUrl } from './Platform/DotNet';
import './Rendering/Renderer';
import './Services/Http';

View File

@ -1,6 +1,7 @@
import { platform } from './Environment'
import { registerFunction } from './Interop/RegisteredFunction';
import { navigateTo } from './Services/UriHelper';
import { invokeDotNetMethod, invokeDotNetMethodAsync } from './Interop/InvokeDotNetMethodWithJsonMarshalling';
if (typeof window !== 'undefined') {
// When the library is loaded in a browser via a <script> element, make the
@ -9,5 +10,7 @@ if (typeof window !== 'undefined') {
platform,
registerFunction,
navigateTo,
invokeDotNetMethod,
invokeDotNetMethodAsync
};
}

View File

@ -1,4 +1,5 @@
import { invokeWithJsonMarshalling } from './InvokeWithJsonMarshalling';
import { invokeWithJsonMarshalling, invokeWithJsonMarshallingAsync } from './InvokeJavaScriptFunctionWithJsonMarshalling';
import { invokePromiseCallback } from './InvokeDotNetMethodWithJsonMarshalling';
import { attachRootComponentToElement, renderBatch } from '../Rendering/Renderer';
/**
@ -8,5 +9,7 @@ import { attachRootComponentToElement, renderBatch } from '../Rendering/Renderer
export const internalRegisteredFunctions = {
attachRootComponentToElement,
invokeWithJsonMarshalling,
invokeWithJsonMarshallingAsync,
invokePromiseCallback,
renderBatch,
};

View File

@ -0,0 +1,190 @@
import { platform } from '../Environment';
import { System_String, Pointer, MethodHandle } from '../Platform/Platform';
import { getRegisteredFunction } from './RegisteredFunction';
import { error } from 'util';
export interface MethodOptions {
type: TypeIdentifier;
method: MethodIdentifier;
}
// Keep in sync with InvocationResult.cs
export interface InvocationResult {
succeeded: boolean;
result?: any;
message?: string;
}
export interface MethodIdentifier {
name: string;
typeArguments?: { [key: string]: TypeIdentifier }
parameterTypes?: TypeIdentifier[];
}
export interface TypeIdentifier {
assembly: string;
name: string;
typeArguments?: { [key: string]: TypeIdentifier };
}
export function invokeDotNetMethod<T>(methodOptions: MethodOptions, ...args: any[]): (T | null) {
return invokeDotNetMethodCore(methodOptions, null, ...args);
}
const registrations = {};
let findDotNetMethodHandle: MethodHandle;
function getFindDotNetMethodHandle() {
if (findDotNetMethodHandle === undefined) {
findDotNetMethodHandle = platform.findMethod(
'Microsoft.AspNetCore.Blazor.Browser',
'Microsoft.AspNetCore.Blazor.Browser.Interop',
'InvokeDotNetFromJavaScript',
'FindDotNetMethod');
}
return findDotNetMethodHandle;
}
function resolveRegistration(methodOptions: MethodOptions) {
const findDotNetMethodHandle = getFindDotNetMethodHandle();
const assemblyEntry = registrations[methodOptions.type.assembly];
const typeEntry = assemblyEntry && assemblyEntry[methodOptions.type.name];
const registration = typeEntry && typeEntry[methodOptions.method.name];
if (registration !== undefined) {
return registration;
} else {
const serializedOptions = platform.toDotNetString(JSON.stringify(methodOptions));
const result = platform.callMethod(findDotNetMethodHandle, null, [serializedOptions]);
const registration = platform.toJavaScriptString(result as System_String);
if (assemblyEntry === undefined) {
const assembly = {};
const type = {};
registrations[methodOptions.type.assembly] = assembly;
assembly[methodOptions.type.name] = type;
type[methodOptions.method.name] = registration;
} else if (typeEntry === undefined) {
const type = {};
assemblyEntry[methodOptions.type.name] = type;
type[methodOptions.method.name] = registration;
} else {
typeEntry[methodOptions.method.name] = registration;
}
return registration;
}
}
let invokeDotNetMethodHandle: MethodHandle;
function getInvokeDotNetMethodHandle() {
if (invokeDotNetMethodHandle === undefined) {
invokeDotNetMethodHandle = platform.findMethod(
'Microsoft.AspNetCore.Blazor.Browser',
'Microsoft.AspNetCore.Blazor.Browser.Interop',
'InvokeDotNetFromJavaScript',
'InvokeDotNetMethod');
}
return invokeDotNetMethodHandle;
}
function invokeDotNetMethodCore<T>(methodOptions: MethodOptions, callbackId: string | null, ...args: any[]): (T | null) {
const invokeDotNetMethodHandle = getInvokeDotNetMethodHandle();
const registration = resolveRegistration(methodOptions);
const packedArgs = packArguments(args);
const serializedCallback = callbackId != null ? platform.toDotNetString(callbackId) : null;
const serializedArgs = platform.toDotNetString(JSON.stringify(packedArgs));
const serializedRegistration = platform.toDotNetString(registration);
const serializedResult = platform.callMethod(invokeDotNetMethodHandle, null, [serializedRegistration, serializedCallback, serializedArgs]);
const result = JSON.parse(platform.toJavaScriptString(serializedResult as System_String));
if (result.succeeded) {
return result.result;
} else {
throw new Error(result.message);
}
}
// We don't have to worry about overflows here. Number.MAX_SAFE_INTEGER in JS is 2^53-1
let globalId = 0;
export function invokeDotNetMethodAsync<T>(methodOptions: MethodOptions, ...args: any[]): Promise<T | null> {
const callbackId = (globalId++).toString();
const result = new Promise<T | null>((resolve, reject) => {
TrackedReference.track(callbackId, (invocationResult: InvocationResult) => {
// We got invoked, so we unregister ourselves.
TrackedReference.untrack(callbackId);
if (invocationResult.succeeded) {
resolve(invocationResult.result);
} else {
reject(new Error(invocationResult.message));
}
});
});
invokeDotNetMethodCore(methodOptions, callbackId, ...args);
return result;
}
export function invokePromiseCallback(id: string, invocationResult: InvocationResult): void {
const callback = TrackedReference.get(id) as Function;
callback.call(null, invocationResult);
}
function packArguments(args: any[]) {
const result = {};
if (args.length == 0) {
return result;
}
if (args.length > 7) {
for (let i = 0; i < 7; i++) {
result[`argument${[i + 1]}`] = args[i];
}
result['argument8'] = packArguments(args.slice(7));
} else {
for (let i = 0; i < args.length; i++) {
result[`argument${[i + 1]}`] = args[i];
}
}
return result;
}
class TrackedReference {
private static references: { [key: string]: any } = {};
public static track(id: string, trackedObject: any): void {
const refs = TrackedReference.references;
if (refs[id] !== undefined) {
throw new Error(`An element with id '${id}' is already being tracked.`);
}
refs[id] = trackedObject;
}
public static untrack(id: string): void {
const refs = TrackedReference.references;
const result = refs[id];
if (result === undefined) {
throw new Error(`An element with id '${id}' is not being being tracked.`);
}
refs[id] = undefined;
}
public static get(id: string): any {
const refs = TrackedReference.references;
const result = refs[id];
if (result === undefined) {
throw new Error(`An element with id '${id}' is not being being tracked.`);
}
return result;
}
}

View File

@ -0,0 +1,66 @@
import { platform } from '../Environment';
import { System_String } from '../Platform/Platform';
import { getRegisteredFunction } from './RegisteredFunction';
import { invokeDotNetMethod, MethodOptions, InvocationResult } from './InvokeDotNetMethodWithJsonMarshalling';
import { getElementByCaptureId } from '../Rendering/ElementReferenceCapture';
import { System } from 'typescript';
import { error } from 'util';
const elementRefKey = '_blazorElementRef'; // Keep in sync with ElementRef.cs
export function invokeWithJsonMarshalling(identifier: System_String, ...argsJson: System_String[]) {
let result: InvocationResult;
const identifierJsString = platform.toJavaScriptString(identifier);
const args = argsJson.map(json => JSON.parse(platform.toJavaScriptString(json), jsonReviver));
try {
result = { succeeded: true, result: invokeWithJsonMarshallingCore(identifierJsString, ...args) };
} catch (e) {
result = { succeeded: false, message: e instanceof Error ? `${e.message}\n${e.stack}` : (e ? e.toString() : null) };
}
const resultJson = JSON.stringify(result);
return platform.toDotNetString(resultJson);
}
function invokeWithJsonMarshallingCore(identifier: string, ...args: any[]) {
const funcInstance = getRegisteredFunction(identifier);
const result = funcInstance.apply(null, args);
if (result !== null && result !== undefined) {
return result;
} else {
return null;
}
}
const invokeDotNetCallback: MethodOptions = {
type: {
assembly: 'Microsoft.AspNetCore.Blazor.Browser',
name: 'Microsoft.AspNetCore.Blazor.Browser.Interop.TaskCallback'
},
method: {
name: 'InvokeTaskCallback'
}
};
export function invokeWithJsonMarshallingAsync<T>(identifier: string, callbackId: string, ...argsJson: string[]) {
const result = invokeWithJsonMarshallingCore(identifier, ...argsJson) as Promise<any>;
result
.then(res => invokeDotNetMethod(invokeDotNetCallback, callbackId, JSON.stringify({ succeeded: true, result: res })))
.catch(reason => invokeDotNetMethod(
invokeDotNetCallback,
callbackId,
JSON.stringify({ succeeded: false, message: (reason && reason.message) || (reason && reason.toString && reason.toString()) })));
return null;
}
function jsonReviver(key: string, value: any): any {
if (value && typeof value === 'object' && value.hasOwnProperty(elementRefKey) && typeof value[elementRefKey] === 'number') {
return getElementByCaptureId(value[elementRefKey]);
}
return value;
}

View File

@ -1,27 +0,0 @@
import { platform } from '../Environment';
import { System_String } from '../Platform/Platform';
import { getRegisteredFunction } from './RegisteredFunction';
import { getElementByCaptureId } from '../Rendering/ElementReferenceCapture';
const elementRefKey = '_blazorElementRef'; // Keep in sync with ElementRef.cs
export function invokeWithJsonMarshalling(identifier: System_String, ...argsJson: System_String[]) {
const identifierJsString = platform.toJavaScriptString(identifier);
const funcInstance = getRegisteredFunction(identifierJsString);
const args = argsJson.map(json => JSON.parse(platform.toJavaScriptString(json), jsonReviver));
const result = funcInstance.apply(null, args);
if (result !== null && result !== undefined) {
const resultJson = JSON.stringify(result);
return platform.toDotNetString(resultJson);
} else {
return null;
}
}
function jsonReviver(key: string, value: any): any {
if (value && typeof value === 'object' && value.hasOwnProperty(elementRefKey) && typeof value[elementRefKey] === 'number') {
return getElementByCaptureId(value[elementRefKey]);
}
return value;
}

View File

@ -1,4 +1,4 @@
import { MethodHandle, System_Object, System_String, System_Array, Pointer, Platform } from '../Platform';
import { MethodHandle, System_Object, System_String, System_Array, Pointer, Platform } from '../Platform';
import { getAssemblyNameFromUrl } from '../DotNet';
import { getRegisteredFunction } from '../../Interop/RegisteredFunction';

View File

@ -1,4 +1,4 @@
export interface Platform {
export interface Platform {
start(loadAssemblyUrls: string[]): Promise<void>;
callEntryPoint(assemblyName: string, entrypointMethod: string, args: (System_Object | null)[]);

View File

@ -1,4 +1,4 @@
const path = require('path');
const path = require('path');
const webpack = require('webpack');
module.exports = {

View File

@ -0,0 +1,260 @@
// 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.Linq;
using System.Reflection;
namespace Microsoft.AspNetCore.Blazor.Browser.Interop
{
internal class ArgumentList
{
private const BindingFlags DeserializeFlags = BindingFlags.Static | BindingFlags.NonPublic;
public static ArgumentList Instance { get; } = new ArgumentList();
private static ConcurrentDictionary<Type, Func<string, ArgumentList>> _deserializers = new ConcurrentDictionary<Type, Func<string, ArgumentList>>();
public static Type GetArgumentClass(Type[] arguments)
{
switch (arguments.Length)
{
case 0:
return typeof(ArgumentList);
case 1:
return typeof(ArgumentList<>).MakeGenericType(arguments);
case 2:
return typeof(ArgumentList<,>).MakeGenericType(arguments);
case 3:
return typeof(ArgumentList<,,>).MakeGenericType(arguments);
case 4:
return typeof(ArgumentList<,,,>).MakeGenericType(arguments);
case 5:
return typeof(ArgumentList<,,,,>).MakeGenericType(arguments);
case 6:
return typeof(ArgumentList<,,,,,>).MakeGenericType(arguments);
case 7:
return typeof(ArgumentList<,,,,,,>).MakeGenericType(arguments);
default:
return GetArgumentsClassCore(arguments, 0);
}
Type GetArgumentsClassCore(Type[] args, int position)
{
var rest = args.Length - position;
switch (rest)
{
case 0:
// We handle this case in the preamble. If there are more than 7 arguments, we pack the
// remaining arguments in nested argument list types, with at least one argument.
throw new InvalidOperationException("We shouldn't get here!");
case 1:
return typeof(ArgumentList<>).MakeGenericType(args.Skip(position).Take(rest).ToArray());
case 2:
return typeof(ArgumentList<,>).MakeGenericType(args.Skip(position).Take(rest).ToArray());
case 3:
return typeof(ArgumentList<,,>).MakeGenericType(args.Skip(position).Take(rest).ToArray());
case 4:
return typeof(ArgumentList<,,,>).MakeGenericType(args.Skip(position).Take(rest).ToArray());
case 5:
return typeof(ArgumentList<,,,,>).MakeGenericType(args.Skip(position).Take(rest).ToArray());
case 6:
return typeof(ArgumentList<,,,,,>).MakeGenericType(args.Skip(position).Take(rest).ToArray());
case 7:
return typeof(ArgumentList<,,,,,,>).MakeGenericType(args.Skip(position).Take(rest).ToArray());
case 8:
// When there are more than 7 arguments, we transparently package more arguments in a nested arguments type.
// {
// argument1: ...,
// argument2: ...,
// argument3: ...,
// argument4: ...,
// argument5: ...,
// argument6: ...,
// argument7: ...,
// argument8: {
// argument1: ..., // Actually argument 8
// }
// }
var typeArguments = args
.Skip(position)
.Take(7)
.Concat(new[] { GetArgumentsClassCore(args, position + 7) }).ToArray();
return typeof(ArgumentList<,,,,,,,>).MakeGenericType(typeArguments);
default:
throw new InvalidOperationException($"Unsupported number of arguments '{arguments.Length}'");
}
}
}
public static Func<string, ArgumentList> GetDeserializer(Type deserializedType)
{
return _deserializers.GetOrAdd(deserializedType, DeserializerFactory);
Func<string, ArgumentList> DeserializerFactory(Type type)
{
switch (deserializedType.GetGenericArguments().Length)
{
case 0:
return JsonDeserialize;
case 1:
return (Func<string, ArgumentList>)deserializedType.GetMethod("JsonDeserialize1", DeserializeFlags)
.CreateDelegate(typeof(Func<string, ArgumentList>));
case 2:
return (Func<string, ArgumentList>)deserializedType.GetMethod("JsonDeserialize2", DeserializeFlags)
.CreateDelegate(typeof(Func<string, ArgumentList>));
case 3:
return (Func<string, ArgumentList>)deserializedType.GetMethod("JsonDeserialize3", DeserializeFlags)
.CreateDelegate(typeof(Func<string, ArgumentList>));
case 4:
return (Func<string, ArgumentList>)deserializedType.GetMethod("JsonDeserialize4", DeserializeFlags)
.CreateDelegate(typeof(Func<string, ArgumentList>));
case 5:
return (Func<string, ArgumentList>)deserializedType.GetMethod("JsonDeserialize5", DeserializeFlags)
.CreateDelegate(typeof(Func<string, ArgumentList>));
case 6:
return (Func<string, ArgumentList>)deserializedType.GetMethod("JsonDeserialize6", DeserializeFlags)
.CreateDelegate(typeof(Func<string, ArgumentList>));
case 7:
return (Func<string, ArgumentList>)deserializedType.GetMethod("JsonDeserialize7", DeserializeFlags)
.CreateDelegate(typeof(Func<string, ArgumentList>));
case 8:
return (Func<string, ArgumentList>)deserializedType.GetMethod("JsonDeserialize8", DeserializeFlags)
.CreateDelegate(typeof(Func<string, ArgumentList>));
default:
throw new InvalidOperationException("Shouldn't have gotten here!");
}
}
}
public static ArgumentList JsonDeserialize(string item) => Instance;
public virtual object[] ToArray() => Array.Empty<object>();
}
internal class ArgumentList<T1> : ArgumentList
{
public T1 Argument1 { get; set; }
internal static ArgumentList<T1> JsonDeserialize1(string item) =>
JsonUtil.Deserialize<ArgumentList<T1>>(item);
public override object[] ToArray() => new object[] { Argument1 };
}
internal class ArgumentList<T1, T2> : ArgumentList
{
public T1 Argument1 { get; set; }
public T2 Argument2 { get; set; }
internal static ArgumentList<T1, T2> JsonDeserialize2(string item) =>
JsonUtil.Deserialize<ArgumentList<T1, T2>>(item);
public override object[] ToArray() => new object[] { Argument1, Argument2 };
}
internal class ArgumentList<T1, T2, T3> : ArgumentList
{
public T1 Argument1 { get; set; }
public T2 Argument2 { get; set; }
public T3 Argument3 { get; set; }
internal static ArgumentList<T1, T2, T3> JsonDeserialize3(string item) =>
JsonUtil.Deserialize<ArgumentList<T1, T2, T3>>(item);
public override object[] ToArray() => new object[] { Argument1, Argument2, Argument3 };
}
internal class ArgumentList<T1, T2, T3, T4> : ArgumentList
{
public T1 Argument1 { get; set; }
public T2 Argument2 { get; set; }
public T3 Argument3 { get; set; }
public T4 Argument4 { get; set; }
internal static ArgumentList<T1, T2, T3, T4> JsonDeserialize4(string item) =>
JsonUtil.Deserialize<ArgumentList<T1, T2, T3, T4>>(item);
public override object[] ToArray() => new object[] { Argument1, Argument2, Argument3, Argument4 };
}
internal class ArgumentList<T1, T2, T3, T4, T5> : ArgumentList
{
public T1 Argument1 { get; set; }
public T2 Argument2 { get; set; }
public T3 Argument3 { get; set; }
public T4 Argument4 { get; set; }
public T5 Argument5 { get; set; }
internal static ArgumentList<T1, T2, T3, T4, T5> JsonDeserialize5(string item) =>
JsonUtil.Deserialize<ArgumentList<T1, T2, T3, T4, T5>>(item);
public override object[] ToArray() => new object[] { Argument1, Argument2, Argument3, Argument4, Argument5 };
}
internal class ArgumentList<T1, T2, T3, T4, T5, T6> : ArgumentList
{
public T1 Argument1 { get; set; }
public T2 Argument2 { get; set; }
public T3 Argument3 { get; set; }
public T4 Argument4 { get; set; }
public T5 Argument5 { get; set; }
public T6 Argument6 { get; set; }
internal static ArgumentList<T1, T2, T3, T4, T5, T6> JsonDeserialize6(string item) =>
JsonUtil.Deserialize<ArgumentList<T1, T2, T3, T4, T5, T6>>(item);
public override object[] ToArray() => new object[] { Argument1, Argument2, Argument3, Argument4, Argument5, Argument6 };
}
internal class ArgumentList<T1, T2, T3, T4, T5, T6, T7> : ArgumentList
{
public T1 Argument1 { get; set; }
public T2 Argument2 { get; set; }
public T3 Argument3 { get; set; }
public T4 Argument4 { get; set; }
public T5 Argument5 { get; set; }
public T6 Argument6 { get; set; }
public T7 Argument7 { get; set; }
internal static ArgumentList<T1, T2, T3, T4, T5, T6, T7> JsonDeserialize7(string item) =>
JsonUtil.Deserialize<ArgumentList<T1, T2, T3, T4, T5, T6, T7>>(item);
public override object[] ToArray() => new object[] { Argument1, Argument2, Argument3, Argument4, Argument5, Argument6, Argument7 };
}
internal class ArgumentList<T1, T2, T3, T4, T5, T6, T7, T8> : ArgumentList
{
public T1 Argument1 { get; set; }
public T2 Argument2 { get; set; }
public T3 Argument3 { get; set; }
public T4 Argument4 { get; set; }
public T5 Argument5 { get; set; }
public T6 Argument6 { get; set; }
public T7 Argument7 { get; set; }
public T8 Argument8 { get; set; }
internal static ArgumentList<T1, T2, T3, T4, T5, T6, T7, T8> JsonDeserialize8(string item) =>
JsonUtil.Deserialize<ArgumentList<T1, T2, T3, T4, T5, T6, T7, T8>>(item);
public override object[] ToArray()
{
if (Argument8 == null)
{
throw new InvalidOperationException("Argument8 can't be null!");
}
if (!(Argument8 is ArgumentList rest))
{
throw new InvalidOperationException("Argument 8 must be an ArgumentList");
}
if (rest.GetType().GetGenericArguments().Length < 1)
{
throw new InvalidOperationException("Argument 8 must contain an inner parameter!");
}
return new object[] { Argument1, Argument2, Argument3, Argument4, Argument5, Argument6, Argument7 }.Concat(rest.ToArray()).ToArray();
}
}
}

View File

@ -0,0 +1,25 @@
// 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;
namespace Microsoft.AspNetCore.Blazor.Browser.Interop
{
internal class InvocationResult<TRes>
{
// Whether the method call succeeded or threw an exception.
public bool Succeeded { get; set; }
// The result of the method call if any.
public TRes Result { get; set; }
// The message from the captured exception in case there was an error.
public string Message { get; set; }
public static string Success(TRes result) =>
JsonUtil.Serialize(new InvocationResult<TRes> { Result = result, Succeeded = true });
public static string Fail(Exception exception) =>
JsonUtil.Serialize(new InvocationResult<object> { Message = exception.Message, Succeeded = false });
}
}

View File

@ -0,0 +1,163 @@
// 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.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Blazor.Browser.Interop
{
internal class InvokeDotNetFromJavaScript
{
private static int NextFunction = 0;
private static readonly ConcurrentDictionary<string, string> ResolvedFunctionRegistrations = new ConcurrentDictionary<string, string>();
private static readonly ConcurrentDictionary<string, object> ResolvedFunctions = new ConcurrentDictionary<string, object>();
private const string InvokePromiseCallback = "invokePromiseCallback";
public static string FindDotNetMethod(string methodOptions)
{
var result = ResolvedFunctionRegistrations.GetOrAdd(methodOptions, opts =>
{
var options = JsonUtil.Deserialize<MethodInvocationOptions>(methodOptions);
var argumentDeserializer = GetOrCreateArgumentDeserializer(options);
var invoker = GetOrCreateInvoker(options, argumentDeserializer);
var invokerRegistration = NextFunction.ToString();
NextFunction++;
if (!ResolvedFunctions.TryAdd(invokerRegistration, invoker))
{
throw new InvalidOperationException($"A function with registration '{invokerRegistration}' was already registered");
}
return invokerRegistration;
});
return result;
}
public static string InvokeDotNetMethod(string registration, string callbackId, string methodArguments)
{
// We invoke the dotnet method and wrap either the result or the exception produced by
// an error into an invocation result type. This invocation result is just a discriminated
// union with either success or failure.
try
{
return InvocationResult<object>.Success(InvokeDotNetMethodCore(registration, callbackId, methodArguments));
}
catch (Exception e)
{
var exception = e;
while (exception.InnerException != null)
{
exception = exception.InnerException;
}
return InvocationResult<object>.Fail(exception);
}
}
private static object InvokeDotNetMethodCore(string registration, string callbackId, string methodArguments)
{
if (!ResolvedFunctions.TryGetValue(registration, out var registeredFunction))
{
throw new InvalidOperationException($"No method exists with registration number '{registration}'.");
}
if (!(registeredFunction is Func<string, object> invoker))
{
throw new InvalidOperationException($"The registered invoker has the wrong signature.");
}
var result = invoker(methodArguments);
if (callbackId != null && !(result is Task))
{
var methodSpec = ResolvedFunctionRegistrations.Single(kvp => kvp.Value == registration);
var options = JsonUtil.Deserialize<MethodInvocationOptions>(methodSpec.Key);
throw new InvalidOperationException($"'{options.Method.Name}' in '{options.Type.Name}' must return a Task.");
}
if (result is Task && callbackId == null)
{
var methodSpec = ResolvedFunctionRegistrations.Single(kvp => kvp.Value == registration);
var options = JsonUtil.Deserialize<MethodInvocationOptions>(methodSpec.Key);
throw new InvalidOperationException($"'{options.Method.Name}' in '{options.Type.Name}' must not return a Task.");
}
if (result is Task taskResult)
{
// For async work, we just setup the callback on the returned task to invoke the appropiate callback in JavaScript.
SetupResultCallback(callbackId, taskResult);
// We just return null here as the proper result will be returned through invoking a JavaScript callback when the
// task completes.
return null;
}
else
{
return result;
}
}
private static void SetupResultCallback(string callbackId, Task taskResult)
{
taskResult.ContinueWith(task =>
{
if (task.Status == TaskStatus.RanToCompletion)
{
if (task.GetType() == typeof(Task))
{
RegisteredFunction.Invoke<bool>(
InvokePromiseCallback,
callbackId,
new InvocationResult<object> { Succeeded = true, Result = null });
}
else
{
var returnValue = TaskResultUtil.GetTaskResult(task);
RegisteredFunction.Invoke<bool>(
InvokePromiseCallback,
callbackId,
new InvocationResult<object> { Succeeded = true, Result = returnValue });
}
}
else
{
Exception exception = task.Exception;
while (exception is AggregateException || exception.InnerException is TargetInvocationException)
{
exception = exception.InnerException;
}
RegisteredFunction.Invoke<bool>(
InvokePromiseCallback,
callbackId,
new InvocationResult<object> { Succeeded = false, Message = exception.Message });
}
});
}
internal static Func<string, object> GetOrCreateInvoker(MethodInvocationOptions options, Func<string, object[]> argumentDeserializer)
{
var method = options.GetMethodOrThrow();
return (string args) => method.Invoke(null, argumentDeserializer(args));
}
private static Func<string, object[]> GetOrCreateArgumentDeserializer(MethodInvocationOptions options)
{
var info = options.GetMethodOrThrow();
var argsClass = ArgumentList.GetArgumentClass(info.GetParameters().Select(p => p.ParameterType).ToArray());
var deserializeMethod = ArgumentList.GetDeserializer(argsClass);
return Deserialize;
object[] Deserialize(string arguments)
{
var argsInstance = deserializeMethod(arguments);
return argsInstance.ToArray();
}
}
}
}

View File

@ -0,0 +1,53 @@
// 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.Generic;
using System.Linq;
using System.Reflection;
namespace Microsoft.AspNetCore.Blazor.Browser.Interop
{
internal class MethodIdentifier
{
public string Name { get; set; }
/// <summary>
/// Required if the method is generic.
/// </summary>
public IDictionary<string, TypeIdentifier> TypeArguments { get; set; }
/// <summary>
/// Required if the method has overloads.
/// </summary>
public TypeIdentifier[] ParameterTypes { get; set; }
internal MethodInfo GetMethodOrThrow(Type type)
{
var result = type.GetMethods(BindingFlags.Static | BindingFlags.Public).Where(m => string.Equals(m.Name, Name, StringComparison.Ordinal)).ToArray();
if (result.Length == 1)
{
// The method doesn't have overloads, we just return the method found by name.
return result[0];
}
result = result.Where(r => r.GetParameters().Length == (ParameterTypes?.Length ?? 0)).ToArray();
if (result.Length == 1)
{
// The method has only a single method with the given number of parameter types.
return result[0];
}
if (result.Length == 0)
{
throw new InvalidOperationException($"Couldn't find a method with name '{Name}' in '{type.FullName}'.");
}
else
{
throw new InvalidOperationException($"Multiple methods with name '{Name}' and '{ParameterTypes.Length}' arguments found.");
}
}
}
}

View File

@ -0,0 +1,21 @@
// 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.Reflection;
namespace Microsoft.AspNetCore.Blazor.Browser.Interop
{
internal class MethodInvocationOptions
{
public TypeIdentifier Type { get; set; }
public MethodIdentifier Method { get; set; }
internal MethodInfo GetMethodOrThrow()
{
var type = Type.GetTypeOrThrow();
var method = Method.GetMethodOrThrow(type);
return method;
}
}
}

View File

@ -1,7 +1,8 @@
// 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.
using System.Linq;
using System;
using System.Threading.Tasks;
using WebAssembly;
namespace Microsoft.AspNetCore.Blazor.Browser.Interop
@ -23,10 +24,74 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Interop
{
// This is a low-perf convenience method that bypasses the need to deal with
// .NET memory and data structures on the JS side
var argsJson = args.Select(JsonUtil.Serialize);
var resultJson = InvokeUnmarshalled<string>("invokeWithJsonMarshalling",
argsJson.Prepend(identifier).ToArray());
return JsonUtil.Deserialize<TRes>(resultJson);
var argsJson = new string[args.Length + 1];
argsJson[0] = identifier;
for (int i = 0; i < args.Length; i++)
{
argsJson[i + 1] = JsonUtil.Serialize(args[i]);
}
var resultJson = InvokeUnmarshalled<string>("invokeWithJsonMarshalling", argsJson);
var result = JsonUtil.Deserialize<InvocationResult<TRes>>(resultJson);
if (result.Succeeded)
{
return result.Result;
}
else
{
throw new JavaScriptException(result.Message);
}
}
/// <summary>
/// Invokes the JavaScript function registered with the specified identifier.
/// Arguments and return values are marshalled via JSON serialization.
/// </summary>
/// <typeparam name="TRes">The .NET type corresponding to the function's return value type. This type must be JSON deserializable.</typeparam>
/// <param name="identifier">The identifier used when registering the target function.</param>
/// <param name="args">The arguments to pass, each of which must be JSON serializable.</param>
/// <returns>The result of the function invocation.</returns>
public static Task<TRes> InvokeAsync<TRes>(string identifier, params object[] args)
{
var tcs = new TaskCompletionSource<TRes>();
var callbackId = Guid.NewGuid().ToString();
var argsJson = new string[args.Length + 2];
argsJson[0] = identifier;
argsJson[1] = callbackId;
for (int i = 0; i < args.Length; i++)
{
argsJson[i + 2] = JsonUtil.Serialize(args[i]);
}
TaskCallbacks.Track(callbackId, new Action<string>(r =>
{
var res = JsonUtil.Deserialize<InvocationResult<TRes>>(r);
TaskCallbacks.Untrack(callbackId);
if (res.Succeeded)
{
tcs.SetResult(res.Result);
}
else
{
tcs.SetException(new JavaScriptException(res.Message));
}
}));
try
{
var result = Invoke<object>("invokeWithJsonMarshallingAsync", argsJson);
}
catch
{
TaskCallbacks.Untrack(callbackId);
throw;
}
return tcs.Task;
}
/// <summary>
@ -102,4 +167,13 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Interop
: result;
}
}
internal class TaskCallback
{
public static void InvokeTaskCallback(string id, string result)
{
var callback = TaskCallbacks.Get(id);
callback(result);
}
}
}

View File

@ -0,0 +1,44 @@
// 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.Generic;
namespace Microsoft.AspNetCore.Blazor.Browser.Interop
{
internal static class TaskCallbacks
{
private static IDictionary<string, Action<string>> References { get; } =
new Dictionary<string, Action<string>>();
public static void Track(string id, Action<string> reference)
{
if (References.ContainsKey(id))
{
throw new InvalidOperationException($"An element with id '{id}' is already being tracked.");
}
References.Add(id, reference);
}
public static void Untrack(string id)
{
if (!References.ContainsKey(id))
{
throw new InvalidOperationException($"An element with id '{id}' is not being tracked.");
}
References.Remove(id);
}
public static Action<string> Get(string id)
{
if (!References.ContainsKey(id))
{
throw new InvalidOperationException($"An element with id '{id}' is not being tracked.");
}
return References[id];
}
}
}

View File

@ -0,0 +1,33 @@
// 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.Linq;
using System.Threading.Tasks;
internal class TaskResultUtil
{
private static ConcurrentDictionary<Type, ITaskResultGetter> _cachedGetters = new ConcurrentDictionary<Type, ITaskResultGetter>();
private interface ITaskResultGetter
{
object GetResult(Task task);
}
private class TaskResultGetter<T> : ITaskResultGetter
{
public object GetResult(Task task) => ((Task<T>)task).Result;
}
public static object GetTaskResult(Task task)
{
var getter = _cachedGetters.GetOrAdd(task.GetType(), taskType =>
{
var resultType = taskType.GetGenericArguments().Single();
return (ITaskResultGetter)Activator.CreateInstance(
typeof(TaskResultGetter<>).MakeGenericType(resultType));
});
return getter.GetResult(task);
}
}

View File

@ -0,0 +1,22 @@
// 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.Generic;
namespace Microsoft.AspNetCore.Blazor.Browser.Interop
{
internal class TypeIdentifier
{
public string Assembly { get; set; }
public string Name { get; set; }
public IDictionary<string, TypeIdentifier> TypeArguments { get; set; }
internal Type GetTypeOrThrow()
{
return Type.GetType($"{Name}, {Assembly}", throwOnError: true);
}
}
}

View File

@ -0,0 +1,429 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Browser.Interop
{
public class JavaScriptInvokeTests
{
public static TheoryData<object> ResolveMethodPropertyData
{
get
{
var result = new TheoryData<object>();
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidParameterless)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithOneParameter)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithTwoParameters)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithThreeParameters)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithFourParameters)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithFiveParameters)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithSixParameters)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithSevenParameters)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithEightParameters)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.ReturnArray)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoOneParameter)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoTwoParameters)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoThreeParameters)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoFourParameters)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoFiveParameters)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoSixParameters)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoSevenParameters)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoEightParameters)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidParameterlessAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithOneParameterAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithTwoParametersAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithThreeParametersAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithFourParametersAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithFiveParametersAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithSixParametersAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithSevenParametersAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.VoidWithEightParametersAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.ReturnArrayAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoOneParameterAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoTwoParametersAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoThreeParametersAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoFourParametersAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoFiveParametersAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoSixParametersAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoSevenParametersAsync)));
result.Add(CreateMethodOptions(nameof(JavaScriptInterop.EchoEightParametersAsync)));
return result;
MethodInvocationOptions CreateMethodOptions(string methodName) =>
new MethodInvocationOptions
{
Type = new TypeIdentifier
{
Assembly = typeof(JavaScriptInterop).Assembly.GetName().Name,
Name = typeof(JavaScriptInterop).FullName
},
Method = new MethodIdentifier
{
Name = methodName
}
};
}
}
[Theory]
[MemberData(nameof(ResolveMethodPropertyData))]
public void ResolveMethod(object optionsObject)
{
var options = optionsObject as MethodInvocationOptions;
var resolvedMethod = options.GetMethodOrThrow();
Assert.NotNull(resolvedMethod);
Assert.Equal(options.Method.Name, resolvedMethod.Name);
}
}
internal class JavaScriptInterop
{
public static IDictionary<string, object[]> Invocations = new Dictionary<string, object[]>();
public static void VoidParameterless()
{
Invocations[nameof(VoidParameterless)] = new object[0];
}
public static void VoidWithOneParameter(ComplexParameter parameter1)
{
Invocations[nameof(VoidWithOneParameter)] = new object[] { parameter1 };
}
public static void VoidWithTwoParameters(
ComplexParameter parameter1,
byte parameter2)
{
Invocations[nameof(VoidWithTwoParameters)] = new object[] { parameter1, parameter2 };
}
public static void VoidWithThreeParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3)
{
Invocations[nameof(VoidWithThreeParameters)] = new object[] { parameter1, parameter2, parameter3 };
}
public static void VoidWithFourParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4)
{
Invocations[nameof(VoidWithFourParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4 };
}
public static void VoidWithFiveParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5)
{
Invocations[nameof(VoidWithFiveParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 };
}
public static void VoidWithSixParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6)
{
Invocations[nameof(VoidWithSixParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 };
}
public static void VoidWithSevenParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7)
{
Invocations[nameof(VoidWithSevenParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7 };
}
public static void VoidWithEightParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7,
Segment parameter8)
{
Invocations[nameof(VoidWithEightParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 };
}
public static decimal[] ReturnArray()
{
return new decimal[] { 0.1M, 0.2M };
}
public static object[] EchoOneParameter(ComplexParameter parameter1)
{
return new object[] { parameter1 };
}
public static object[] EchoTwoParameters(
ComplexParameter parameter1,
byte parameter2)
{
return new object[] { parameter1, parameter2 };
}
public static object[] EchoThreeParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3)
{
return new object[] { parameter1, parameter2, parameter3 };
}
public static object[] EchoFourParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4)
{
return new object[] { parameter1, parameter2, parameter3, parameter4 };
}
public static object[] EchoFiveParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5)
{
return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 };
}
public static object[] EchoSixParameters(ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6)
{
return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 };
}
public static object[] EchoSevenParameters(ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7)
{
return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7 };
}
public static object[] EchoEightParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7,
Segment parameter8)
{
return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 };
}
public static Task VoidParameterlessAsync()
{
Invocations[nameof(VoidParameterlessAsync)] = new object[0];
return Task.CompletedTask;
}
public static Task VoidWithOneParameterAsync(ComplexParameter parameter1)
{
Invocations[nameof(VoidParameterless)] = new object[] { parameter1 };
return Task.CompletedTask;
}
public static Task VoidWithTwoParametersAsync(
ComplexParameter parameter1,
byte parameter2)
{
Invocations[nameof(VoidParameterless)] = new object[] { parameter1, parameter2 };
return Task.CompletedTask;
}
public static Task VoidWithThreeParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3)
{
Invocations[nameof(VoidWithThreeParameters)] = new object[] { parameter1, parameter2, parameter3 };
return Task.CompletedTask;
}
public static Task VoidWithFourParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4)
{
Invocations[nameof(VoidWithFourParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4 };
return Task.CompletedTask;
}
public static Task VoidWithFiveParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5)
{
Invocations[nameof(VoidWithFiveParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 };
return Task.CompletedTask;
}
public static Task VoidWithSixParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6)
{
Invocations[nameof(VoidWithSixParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 };
return Task.CompletedTask;
}
public static Task VoidWithSevenParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7)
{
Invocations[nameof(VoidWithSevenParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7 };
return Task.CompletedTask;
}
public static Task VoidWithEightParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7,
Segment parameter8)
{
Invocations[nameof(VoidWithEightParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 };
return Task.CompletedTask;
}
public static Task<decimal[]> ReturnArrayAsync()
{
return Task.FromResult(new decimal[] { 0.1M, 0.2M });
}
public static Task<object[]> EchoOneParameterAsync(ComplexParameter parameter1)
{
return Task.FromResult(new object[] { parameter1 });
}
public static Task<object[]> EchoTwoParametersAsync(
ComplexParameter parameter1,
byte parameter2)
{
return Task.FromResult(new object[] { parameter1, parameter2 });
}
public static Task<object[]> EchoThreeParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3 });
}
public static Task<object[]> EchoFourParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4 });
}
public static Task<object[]> EchoFiveParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 });
}
public static Task<object[]> EchoSixParametersAsync(ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 });
}
public static Task<object[]> EchoSevenParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7 });
}
public static Task<object[]> EchoEightParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7,
Segment parameter8)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 });
}
}
public struct Segment
{
public string Source { get; set; }
public int Start { get; set; }
public int Length { get; set; }
}
public class ComplexParameter
{
public int Id { get; set; }
public bool IsValid { get; set; }
public Segment Data { get; set; }
}
}

View File

@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using BasicTestApp;
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
{
public class InteropTest : BasicTestAppTestBase
{
public InteropTest(
BrowserFixture browserFixture,
DevHostServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
Navigate(ServerPathBase, noReload: true);
MountTestComponent<InteropComponent>();
}
[Fact]
public void CanInvokeDotNetMethods()
{
// Arrange
var expectedValues = new Dictionary<string, string>
{
["VoidParameterless"] = "[]",
["VoidWithOneParameter"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]",
["VoidWithTwoParameters"] = @"[{""id"":2,""isValid"":true,""data"":{""source"":""Some random text with at least 2 characters"",""start"":2,""length"":2}},2]",
["VoidWithThreeParameters"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,6]",
["VoidWithFourParameters"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,8,16]",
["VoidWithFiveParameters"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,10,20,40]",
["VoidWithSixParameters"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,12,24,48,6.25]",
["VoidWithSevenParameters"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
["VoidWithEightParameters"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,16,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
["VoidParameterlessAsync"] = "[]",
["VoidWithOneParameterAsync"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]",
["VoidWithTwoParametersAsync"] = @"[{""id"":2,""isValid"":true,""data"":{""source"":""Some random text with at least 2 characters"",""start"":2,""length"":2}},2]",
["VoidWithThreeParametersAsync"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,6]",
["VoidWithFourParametersAsync"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,8,16]",
["VoidWithFiveParametersAsync"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,10,20,40]",
["VoidWithSixParametersAsync"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,12,24,48,6.25]",
["VoidWithSevenParametersAsync"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
["VoidWithEightParametersAsync"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,16,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
["result1"] = @"[0.1,0.2]",
["result2"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]",
["result3"] = @"[{""id"":2,""isValid"":true,""data"":{""source"":""Some random text with at least 2 characters"",""start"":2,""length"":2}},2]",
["result4"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,6]",
["result5"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,8,16]",
["result6"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,10,20,40]",
["result7"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,12,24,48,6.25]",
["result8"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
["result9"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,16,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
["result1Async"] = @"[0.1,0.2]",
["result2Async"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]",
["result3Async"] = @"[{""id"":2,""isValid"":true,""data"":{""source"":""Some random text with at least 2 characters"",""start"":2,""length"":2}},2]",
["result4Async"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,6]",
["result5Async"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,8,16]",
["result6Async"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,10,20,40]",
["result7Async"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,12,24,48,6.25]",
["result8Async"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
["result9Async"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,16,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
["ThrowException"] = @"""Threw an exception!""",
["AsyncThrowSyncException"] = @"""Threw a sync exception!""",
["AsyncThrowAsyncException"] = @"""Threw an async exception!""",
["ExceptionFromSyncMethod"] = "Function threw an exception!",
["SyncExceptionFromAsyncMethod"] = "Function threw a sync exception!",
["AsyncExceptionFromAsyncMethod"] = "Function threw an async exception!",
};
var actualValues = new Dictionary<string, string>();
// Act
var interopButton = Browser.FindElement(By.Id("btn-interop"));
interopButton.Click();
var wait = new WebDriverWait(Browser, TimeSpan.FromSeconds(10))
.Until(d => d.FindElement(By.Id("done-with-interop")));
foreach (var expectedValue in expectedValues)
{
var currentValue = Browser.FindElement(By.Id(expectedValue.Key));
actualValues.Add(expectedValue.Key, currentValue.Text);
}
// Assert
foreach (var expectedValue in expectedValues)
{
if (expectedValue.Key.Contains("Exception"))
{
Assert.StartsWith(expectedValue.Value, actualValues[expectedValue.Key]);
}
else
{
Assert.Equal(expectedValue.Value, actualValues[expectedValue.Key]);
}
}
}
}
}

View File

@ -0,0 +1,96 @@
@using Microsoft.AspNetCore.Blazor.Browser.Interop
@using BasicTestApp.InteropTest
@using Microsoft.AspNetCore.Blazor
<button id="btn-interop" onclick="@InvokeInteropAsync">Invoke interop!</button>
<div>
<h1>Invocations</h1>
@foreach (var invocation in Invocations)
{
<h2>@invocation.Key</h2>
<p id="@invocation.Key">@invocation.Value</p>
}
</div>
<div>
<h1>Return values and exceptions thrown from .NET</h1>
@foreach (var returnValue in ReturnValues)
{
<h2>@returnValue.Key</h2>
<p id="@returnValue.Key">@returnValue.Value</p>
}
</div>
<div>
<h1>Exceptions thrown from JavaScript</h1>
<h2>@nameof(ExceptionFromSyncMethod)</h2>
<p id="@nameof(ExceptionFromSyncMethod)">@ExceptionFromSyncMethod?.Message</p>
<h2>@nameof(SyncExceptionFromAsyncMethod)</h2>
<p id="@nameof(SyncExceptionFromAsyncMethod)">@SyncExceptionFromAsyncMethod?.Message</p>
<h2>@nameof(AsyncExceptionFromAsyncMethod)</h2>
<p id="@nameof(AsyncExceptionFromAsyncMethod)">@AsyncExceptionFromAsyncMethod?.Message</p>
</div>
@if (DoneWithInterop)
{
<p id="done-with-interop">Done with interop.</p>
}
@functions {
public IDictionary<string, string> ReturnValues { get; set; } = new Dictionary<string, string>();
public IDictionary<string, string> Invocations { get; set; } = new Dictionary<string, string>();
public JavaScriptException ExceptionFromSyncMethod { get; set; }
public JavaScriptException SyncExceptionFromAsyncMethod { get; set; }
public JavaScriptException AsyncExceptionFromAsyncMethod { get; set; }
public bool DoneWithInterop { get; set; }
public async Task InvokeInteropAsync()
{
Console.WriteLine("Starting interop invocations.");
await RegisteredFunction.InvokeAsync<object>("BasicTestApp.Interop.InvokeDotNetInteropMethodsAsync");
Console.WriteLine("Showing interop invocation results.");
var collectResults = RegisteredFunction.Invoke<Dictionary<string,string>>("BasicTestApp.Interop.CollectResults");
ReturnValues = collectResults.ToDictionary(kvp => kvp.Key,kvp => System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(kvp.Value)));
var invocations = new Dictionary<string, string>();
foreach (var interopResult in JavaScriptInterop.Invocations)
{
var interopResultValue = JsonUtil.Serialize(interopResult.Value);
invocations[interopResult.Key] = interopResultValue;
}
try
{
RegisteredFunction.Invoke<object>("BasicTestApp.Interop.FunctionThrows");
}
catch (JavaScriptException e)
{
ExceptionFromSyncMethod = e;
}
try
{
await RegisteredFunction.InvokeAsync<object>("BasicTestApp.Interop.AsyncFunctionThrowsSyncException");
}
catch (JavaScriptException e)
{
SyncExceptionFromAsyncMethod = e;
}
try
{
await RegisteredFunction.InvokeAsync<object>("BasicTestApp.Interop.AsyncFunctionThrowsAsyncException");
}
catch (JavaScriptException e)
{
AsyncExceptionFromAsyncMethod = e;
}
Invocations = invocations;
DoneWithInterop = true;
}
}

View File

@ -0,0 +1,13 @@
// 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.
namespace BasicTestApp.InteropTest
{
public class ComplexParameter
{
public int Id { get; set; }
public bool IsValid { get; set; }
public Segment Data { get; set; }
}
}

View File

@ -0,0 +1,360 @@
// 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.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace BasicTestApp.InteropTest
{
public class JavaScriptInterop
{
public static IDictionary<string, object[]> Invocations = new Dictionary<string, object[]>();
public static void ThrowException() => throw new InvalidOperationException("Threw an exception!");
public static Task AsyncThrowSyncException() => throw new InvalidOperationException("Threw a sync exception!");
public static Task AsyncThrowAsyncException()
{
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
var timer = new Timer(
state =>
{
tcs.SetException(new InvalidOperationException("Threw an async exception!"));
},
null,
3000,
Timeout.Infinite);
return tcs.Task;
}
public static void VoidParameterless()
{
Invocations[nameof(VoidParameterless)] = new object[0];
}
public static void VoidWithOneParameter(ComplexParameter parameter1)
{
Invocations[nameof(VoidWithOneParameter)] = new object[] { parameter1 };
}
public static void VoidWithTwoParameters(
ComplexParameter parameter1,
byte parameter2)
{
Invocations[nameof(VoidWithTwoParameters)] = new object[] { parameter1, parameter2 };
}
public static void VoidWithThreeParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3)
{
Invocations[nameof(VoidWithThreeParameters)] = new object[] { parameter1, parameter2, parameter3 };
}
public static void VoidWithFourParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4)
{
Invocations[nameof(VoidWithFourParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4 };
}
public static void VoidWithFiveParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5)
{
Invocations[nameof(VoidWithFiveParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 };
}
public static void VoidWithSixParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6)
{
Invocations[nameof(VoidWithSixParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 };
}
public static void VoidWithSevenParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7)
{
Invocations[nameof(VoidWithSevenParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7 };
}
public static void VoidWithEightParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7,
Segment parameter8)
{
Invocations[nameof(VoidWithEightParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 };
}
public static decimal[] ReturnArray()
{
return new decimal[] { 0.1M, 0.2M };
}
public static object[] EchoOneParameter(ComplexParameter parameter1)
{
return new object[] { parameter1 };
}
public static object[] EchoTwoParameters(
ComplexParameter parameter1,
byte parameter2)
{
return new object[] { parameter1, parameter2 };
}
public static object[] EchoThreeParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3)
{
return new object[] { parameter1, parameter2, parameter3 };
}
public static object[] EchoFourParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4)
{
return new object[] { parameter1, parameter2, parameter3, parameter4 };
}
public static object[] EchoFiveParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5)
{
return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 };
}
public static object[] EchoSixParameters(ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6)
{
return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 };
}
public static object[] EchoSevenParameters(ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7)
{
return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7 };
}
public static object[] EchoEightParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7,
Segment parameter8)
{
return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 };
}
public static Task VoidParameterlessAsync()
{
Invocations[nameof(VoidParameterlessAsync)] = new object[0];
return Task.CompletedTask;
}
public static Task VoidWithOneParameterAsync(ComplexParameter parameter1)
{
Invocations[nameof(VoidWithOneParameterAsync)] = new object[] { parameter1 };
return Task.CompletedTask;
}
public static Task VoidWithTwoParametersAsync(
ComplexParameter parameter1,
byte parameter2)
{
Invocations[nameof(VoidWithTwoParametersAsync)] = new object[] { parameter1, parameter2 };
return Task.CompletedTask;
}
public static Task VoidWithThreeParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3)
{
Invocations[nameof(VoidWithThreeParametersAsync)] = new object[] { parameter1, parameter2, parameter3 };
return Task.CompletedTask;
}
public static Task VoidWithFourParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4)
{
Invocations[nameof(VoidWithFourParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4 };
return Task.CompletedTask;
}
public static Task VoidWithFiveParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5)
{
Invocations[nameof(VoidWithFiveParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 };
return Task.CompletedTask;
}
public static Task VoidWithSixParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6)
{
Invocations[nameof(VoidWithSixParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 };
return Task.CompletedTask;
}
public static Task VoidWithSevenParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7)
{
Invocations[nameof(VoidWithSevenParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7 };
return Task.CompletedTask;
}
public static Task VoidWithEightParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7,
Segment parameter8)
{
Invocations[nameof(VoidWithEightParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 };
return Task.CompletedTask;
}
public static Task<decimal[]> ReturnArrayAsync()
{
return Task.FromResult(new decimal[] { 0.1M, 0.2M });
}
public static Task<object[]> EchoOneParameterAsync(ComplexParameter parameter1)
{
return Task.FromResult(new object[] { parameter1 });
}
public static Task<object[]> EchoTwoParametersAsync(
ComplexParameter parameter1,
byte parameter2)
{
return Task.FromResult(new object[] { parameter1, parameter2 });
}
public static Task<object[]> EchoThreeParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3 });
}
public static Task<object[]> EchoFourParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4 });
}
public static Task<object[]> EchoFiveParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 });
}
public static Task<object[]> EchoSixParametersAsync(ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 });
}
public static Task<object[]> EchoSevenParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7 });
}
public static Task<object[]> EchoEightParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7,
Segment parameter8)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 });
}
}
}

View File

@ -0,0 +1,12 @@
// 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.
namespace BasicTestApp.InteropTest
{
public struct Segment
{
public string Source { get; set; }
public int Start { get; set; }
public int Length { get; set; }
}
}

View File

@ -1,4 +1,4 @@
// 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.
using Microsoft.AspNetCore.Blazor.Browser.Http;

View File

@ -10,6 +10,7 @@
Select test:
<select onchange="mountTestComponent(event.target.value)">
<option value="">Choose...</option>
<option value="BasicTestApp.InteropComponent">Interop component</option>
<option value="BasicTestApp.AsyncEventHandlerComponent">Async event handlers</option>
<option value="BasicTestApp.AddRemoveChildComponents">Add/remove child components</option>
<option value="BasicTestApp.CounterComponent">Counter</option>
@ -47,6 +48,8 @@
<app>Loading...</app>
<script type="blazor-boot"></script>
<!-- Used for testing interop scenarios between JS and .NET -->
<script type="text/javascript" src="js/jsinteroptests.js"></script>
<script>
// The client-side .NET code calls this when it is ready to be called from test code
// The Xunit test code polls until it sees the flag is set

View File

@ -0,0 +1,176 @@
// We'll store the results from the tests here
var results = {};
function invokeDotNetInteropMethodsAsync() {
console.log('Invoking void sync methods.');
Blazor.invokeDotNetMethod(createMethodOptions('VoidParameterless'));
Blazor.invokeDotNetMethod(createMethodOptions('VoidWithOneParameter'), ...createArgumentList(1));
Blazor.invokeDotNetMethod(createMethodOptions('VoidWithTwoParameters'), ...createArgumentList(2));
Blazor.invokeDotNetMethod(createMethodOptions('VoidWithThreeParameters'), ...createArgumentList(3));
Blazor.invokeDotNetMethod(createMethodOptions('VoidWithFourParameters'), ...createArgumentList(4));
Blazor.invokeDotNetMethod(createMethodOptions('VoidWithFiveParameters'), ...createArgumentList(5));
Blazor.invokeDotNetMethod(createMethodOptions('VoidWithSixParameters'), ...createArgumentList(6));
Blazor.invokeDotNetMethod(createMethodOptions('VoidWithSevenParameters'), ...createArgumentList(7));
Blazor.invokeDotNetMethod(createMethodOptions('VoidWithEightParameters'), ...createArgumentList(8));
console.log('Invoking returning sync methods.');
results['result1'] = Blazor.invokeDotNetMethod(createMethodOptions('ReturnArray'));
results['result2'] = Blazor.invokeDotNetMethod(createMethodOptions('EchoOneParameter'), ...createArgumentList(1));
results['result3'] = Blazor.invokeDotNetMethod(createMethodOptions('EchoTwoParameters'), ...createArgumentList(2));
results['result4'] = Blazor.invokeDotNetMethod(createMethodOptions('EchoThreeParameters'), ...createArgumentList(3));
results['result5'] = Blazor.invokeDotNetMethod(createMethodOptions('EchoFourParameters'), ...createArgumentList(4));
results['result6'] = Blazor.invokeDotNetMethod(createMethodOptions('EchoFiveParameters'), ...createArgumentList(5));
results['result7'] = Blazor.invokeDotNetMethod(createMethodOptions('EchoSixParameters'), ...createArgumentList(6));
results['result8'] = Blazor.invokeDotNetMethod(createMethodOptions('EchoSevenParameters'), ...createArgumentList(7));
results['result9'] = Blazor.invokeDotNetMethod(createMethodOptions('EchoEightParameters'), ...createArgumentList(8));
console.log('Invoking void async methods.');
return Blazor.invokeDotNetMethodAsync(createMethodOptions('VoidParameterlessAsync'))
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('VoidWithOneParameterAsync'), ...createArgumentList(1)))
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('VoidWithTwoParametersAsync'), ...createArgumentList(2)))
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('VoidWithThreeParametersAsync'), ...createArgumentList(3)))
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('VoidWithFourParametersAsync'), ...createArgumentList(4)))
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('VoidWithFiveParametersAsync'), ...createArgumentList(5)))
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('VoidWithSixParametersAsync'), ...createArgumentList(6)))
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('VoidWithSevenParametersAsync'), ...createArgumentList(7)))
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('VoidWithEightParametersAsync'), ...createArgumentList(8)))
.then(() => {
console.log('Invoking returning async methods.');
return Blazor.invokeDotNetMethodAsync(createMethodOptions('ReturnArrayAsync'))
.then(r => results['result1Async'] = r)
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('EchoOneParameterAsync'), ...createArgumentList(1)))
.then(r => results['result2Async'] = r)
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('EchoTwoParametersAsync'), ...createArgumentList(2)))
.then(r => results['result3Async'] = r)
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('EchoThreeParametersAsync'), ...createArgumentList(3)))
.then(r => results['result4Async'] = r)
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('EchoFourParametersAsync'), ...createArgumentList(4)))
.then(r => results['result5Async'] = r)
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('EchoFiveParametersAsync'), ...createArgumentList(5)))
.then(r => results['result6Async'] = r)
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('EchoSixParametersAsync'), ...createArgumentList(6)))
.then(r => results['result7Async'] = r)
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('EchoSevenParametersAsync'), ...createArgumentList(7)))
.then(r => results['result8Async'] = r)
.then(() => Blazor.invokeDotNetMethodAsync(createMethodOptions('EchoEightParametersAsync'), ...createArgumentList(8)))
.then(r => results['result9Async'] = r);
})
.then(() => {
console.log('Invoking methods that throw exceptions');
try {
Blazor.invokeDotNetMethod(createMethodOptions('ThrowException'))
} catch (e) {
results['ThrowException'] = e.message;
}
try {
Blazor.invokeDotNetMethodAsync(createMethodOptions('AsyncThrowSyncException'));
} catch (e) {
results['AsyncThrowSyncException'] = e.message;
}
return Blazor.invokeDotNetMethodAsync(createMethodOptions('AsyncThrowAsyncException'))
.catch(e => {
results['AsyncThrowAsyncException'] = e.message;
return Promise.resolve();
})
.then(() => console.log('Done invoking interop methods'));
});
}
function createMethodOptions(methodName) {
return {
type: {
assembly: 'BasicTestApp',
name: 'BasicTestApp.InteropTest.JavaScriptInterop'
},
method: {
name: methodName
}
};
}
function createArgumentList(argumentNumber){
const array = new Array(argumentNumber);
if (argumentNumber === 0) {
return [];
}
for (var i = 0; i < argumentNumber; i++) {
switch (i) {
case 0:
array[i] = {
id: argumentNumber,
isValid: argumentNumber % 2 === 0,
data: {
source: `Some random text with at least ${argumentNumber} characters`,
start: argumentNumber,
length: argumentNumber
}
};
break;
case 1:
array[i] = argumentNumber;
break;
case 2:
array[i] = argumentNumber * 2;
break;
case 3:
array[i] = argumentNumber * 4;
break;
case 4:
array[i] = argumentNumber * 8;
break;
case 5:
array[i] = argumentNumber + 0.25;
break;
case 6:
array[i] = Array.apply(null, Array(argumentNumber)).map((v, i) => i + 0.5);
break;
case 7:
array[i] = {
source: `Some random text with at least ${i} characters`,
start: argumentNumber + 1,
length: argumentNumber + 1
}
break;
default:
console.log(i);
throw new Error('Invalid argument count!');
}
}
return array;
}
Blazor.registerFunction('BasicTestApp.Interop.InvokeDotNetInteropMethodsAsync', invokeDotNetInteropMethodsAsync);
Blazor.registerFunction('BasicTestApp.Interop.CollectResults', collectInteropResults);
Blazor.registerFunction('BasicTestApp.Interop.FunctionThrows', functionThrowsException);
Blazor.registerFunction('BasicTestApp.Interop.AsyncFunctionThrowsSyncException', asyncFunctionThrowsSyncException);
Blazor.registerFunction('BasicTestApp.Interop.AsyncFunctionThrowsAsyncException', asyncFunctionThrowsAsyncException);
function functionThrowsException() {
throw new Error('Function threw an exception!');
}
function asyncFunctionThrowsSyncException() {
throw new Error('Function threw a sync exception!');
}
function asyncFunctionThrowsAsyncException() {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Function threw an async exception!')), 3000);
});
}
function collectInteropResults() {
let result = {};
let properties = Object.getOwnPropertyNames(results);
for (let i = 0; i < properties.length; i++) {
let property = properties[i];
result[property] = btoa(JSON.stringify(results[property]));
}
return result;
}