commit
97bb8897cf
|
|
@ -0,0 +1,27 @@
|
||||||
|
# All Files
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = crlf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = false
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# Solution Files
|
||||||
|
[*.sln]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
# Markdown Files
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
# Web Files
|
||||||
|
[*.{htm,html,js,ts,css,scss,less}]
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{yml,json}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{xml,csproj,config,*proj,targets,props}]
|
||||||
|
indent_size = 2
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio 15
|
||||||
|
VisualStudioVersion = 15.0.27703.2042
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1290437E-A890-419E-A317-D0F7FEE185A5}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B98D4F51-88FB-471C-B56F-752E8EE502E7}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop", "src\Microsoft.JSInterop\Microsoft.JSInterop.csproj", "{CB4CD4A6-9BAA-46D1-944F-CE56DEC2663C}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.Test", "test\Microsoft.JSInterop.Test\Microsoft.JSInterop.Test.csproj", "{7FF8B199-52C0-4DFE-A73B-0C9E18220C0E}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.JSInterop.JS", "src\Microsoft.JSInterop.JS\Microsoft.JSInterop.JS.csproj", "{60BA5AAD-264A-437E-8319-577841C66CC6}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BE4CBB33-5C40-4A07-B6FC-1D7C3AE13024}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
README.md = README.md
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.WebAssembly.Interop", "src\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj", "{10145E99-1B2D-40C5-9595-582BDAF3E024}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{CB4CD4A6-9BAA-46D1-944F-CE56DEC2663C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CB4CD4A6-9BAA-46D1-944F-CE56DEC2663C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CB4CD4A6-9BAA-46D1-944F-CE56DEC2663C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CB4CD4A6-9BAA-46D1-944F-CE56DEC2663C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7FF8B199-52C0-4DFE-A73B-0C9E18220C0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7FF8B199-52C0-4DFE-A73B-0C9E18220C0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7FF8B199-52C0-4DFE-A73B-0C9E18220C0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7FF8B199-52C0-4DFE-A73B-0C9E18220C0E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{60BA5AAD-264A-437E-8319-577841C66CC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{60BA5AAD-264A-437E-8319-577841C66CC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{60BA5AAD-264A-437E-8319-577841C66CC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{60BA5AAD-264A-437E-8319-577841C66CC6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{10145E99-1B2D-40C5-9595-582BDAF3E024}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{10145E99-1B2D-40C5-9595-582BDAF3E024}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{10145E99-1B2D-40C5-9595-582BDAF3E024}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{10145E99-1B2D-40C5-9595-582BDAF3E024}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{CB4CD4A6-9BAA-46D1-944F-CE56DEC2663C} = {1290437E-A890-419E-A317-D0F7FEE185A5}
|
||||||
|
{7FF8B199-52C0-4DFE-A73B-0C9E18220C0E} = {B98D4F51-88FB-471C-B56F-752E8EE502E7}
|
||||||
|
{60BA5AAD-264A-437E-8319-577841C66CC6} = {1290437E-A890-419E-A317-D0F7FEE185A5}
|
||||||
|
{10145E99-1B2D-40C5-9595-582BDAF3E024} = {1290437E-A890-419E-A317-D0F7FEE185A5}
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {7E07ABF2-427A-43FA-A6A4-82B21B96ACAF}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# jsinterop
|
||||||
|
|
||||||
|
This repo is for `Microsoft.JSInterop`, a package that provides abstractions and features for interop between .NET and JavaScript code.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The primary use case is for applications built with Mono WebAssembly or Blazor. It's not expected that developers will typically use these libraries separately from Mono WebAssembly, Blazor, or a similar technology.
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
dist/
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="2.9.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
{
|
||||||
|
"name": "@dotnet/jsinterop",
|
||||||
|
"version": "0.1.1",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"brace-expansion": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"balanced-match": "1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"concat-map": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"fs.realpath": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"glob": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fs.realpath": "1.0.0",
|
||||||
|
"inflight": "1.0.6",
|
||||||
|
"inherits": "2.0.3",
|
||||||
|
"minimatch": "3.0.4",
|
||||||
|
"once": "1.4.0",
|
||||||
|
"path-is-absolute": "1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inflight": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
|
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"once": "1.4.0",
|
||||||
|
"wrappy": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inherits": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||||
|
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"minimatch": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"brace-expansion": "1.1.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"wrappy": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path-is-absolute": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"rimraf": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"glob": "7.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "@dotnet/jsinterop",
|
||||||
|
"version": "0.1.1",
|
||||||
|
"description": "Provides abstractions and features for interop between .NET and JavaScript code.",
|
||||||
|
"main": "dist/Microsoft.JSInterop.js",
|
||||||
|
"types": "dist/Microsoft.JSInterop.d.js",
|
||||||
|
"scripts": {
|
||||||
|
"prepublish": "rimraf dist && dotnet build && echo 'Finished building NPM package \"@dotnet/jsinterop\"'"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**"
|
||||||
|
],
|
||||||
|
"author": "Microsoft",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/dotnet/jsinterop/issues"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/dotnet/jsinterop.git"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"rimraf": "^2.5.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,287 @@
|
||||||
|
// This is a single-file self-contained module to avoid the need for a Webpack build
|
||||||
|
|
||||||
|
module DotNet {
|
||||||
|
(window as any).DotNet = DotNet; // Ensure reachable from anywhere
|
||||||
|
|
||||||
|
export type JsonReviver = ((key: any, value: any) => any);
|
||||||
|
const jsonRevivers: JsonReviver[] = [];
|
||||||
|
|
||||||
|
const pendingAsyncCalls: { [id: number]: PendingAsyncCall<any> } = {};
|
||||||
|
const cachedJSFunctions: { [identifier: string]: Function } = {};
|
||||||
|
let nextAsyncCallId = 1; // Start at 1 because zero signals "no response needed"
|
||||||
|
|
||||||
|
let dotNetDispatcher: DotNetCallDispatcher | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the specified .NET call dispatcher as the current instance so that it will be used
|
||||||
|
* for future invocations.
|
||||||
|
*
|
||||||
|
* @param dispatcher An object that can dispatch calls from JavaScript to a .NET runtime.
|
||||||
|
*/
|
||||||
|
export function attachDispatcher(dispatcher: DotNetCallDispatcher) {
|
||||||
|
dotNetDispatcher = dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a JSON reviver callback that will be used when parsing arguments received from .NET.
|
||||||
|
* @param reviver The reviver to add.
|
||||||
|
*/
|
||||||
|
export function attachReviver(reviver: JsonReviver) {
|
||||||
|
jsonRevivers.push(reviver);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the specified .NET public method synchronously. Not all hosting scenarios support
|
||||||
|
* synchronous invocation, so if possible use invokeMethodAsync instead.
|
||||||
|
*
|
||||||
|
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method.
|
||||||
|
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
|
||||||
|
* @param args Arguments to pass to the method, each of which must be JSON-serializable.
|
||||||
|
* @returns The result of the operation.
|
||||||
|
*/
|
||||||
|
export function invokeMethod<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): T {
|
||||||
|
return invokePossibleInstanceMethod<T>(assemblyName, methodIdentifier, null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the specified .NET public method asynchronously.
|
||||||
|
*
|
||||||
|
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method.
|
||||||
|
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
|
||||||
|
* @param args Arguments to pass to the method, each of which must be JSON-serializable.
|
||||||
|
* @returns A promise representing the result of the operation.
|
||||||
|
*/
|
||||||
|
export function invokeMethodAsync<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise<T> {
|
||||||
|
return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function invokePossibleInstanceMethod<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): T {
|
||||||
|
const dispatcher = getRequiredDispatcher();
|
||||||
|
if (dispatcher.invokeDotNetFromJS) {
|
||||||
|
const argsJson = JSON.stringify(args, argReplacer);
|
||||||
|
const resultJson = dispatcher.invokeDotNetFromJS(assemblyName, methodIdentifier, dotNetObjectId, argsJson);
|
||||||
|
return resultJson ? parseJsonWithRevivers(resultJson) : null;
|
||||||
|
} else {
|
||||||
|
throw new Error('The current dispatcher does not support synchronous calls from JS to .NET. Use invokeMethodAsync instead.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function invokePossibleInstanceMethodAsync<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): Promise<T> {
|
||||||
|
const asyncCallId = nextAsyncCallId++;
|
||||||
|
const resultPromise = new Promise<T>((resolve, reject) => {
|
||||||
|
pendingAsyncCalls[asyncCallId] = { resolve, reject };
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const argsJson = JSON.stringify(args, argReplacer);
|
||||||
|
getRequiredDispatcher().beginInvokeDotNetFromJS(asyncCallId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
|
||||||
|
} catch(ex) {
|
||||||
|
// Synchronous failure
|
||||||
|
completePendingCall(asyncCallId, false, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequiredDispatcher(): DotNetCallDispatcher {
|
||||||
|
if (dotNetDispatcher !== null) {
|
||||||
|
return dotNetDispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No .NET call dispatcher has been set.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function completePendingCall(asyncCallId: number, success: boolean, resultOrError: any) {
|
||||||
|
if (!pendingAsyncCalls.hasOwnProperty(asyncCallId)) {
|
||||||
|
throw new Error(`There is no pending async call with ID ${asyncCallId}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const asyncCall = pendingAsyncCalls[asyncCallId];
|
||||||
|
delete pendingAsyncCalls[asyncCallId];
|
||||||
|
if (success) {
|
||||||
|
asyncCall.resolve(resultOrError);
|
||||||
|
} else {
|
||||||
|
asyncCall.reject(resultOrError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendingAsyncCall<T> {
|
||||||
|
resolve: (value?: T | PromiseLike<T>) => void;
|
||||||
|
reject: (reason?: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the ability to dispatch calls from JavaScript to a .NET runtime.
|
||||||
|
*/
|
||||||
|
export interface DotNetCallDispatcher {
|
||||||
|
/**
|
||||||
|
* Optional. If implemented, invoked by the runtime to perform a synchronous call to a .NET method.
|
||||||
|
*
|
||||||
|
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke. The value may be null when invoking instance methods.
|
||||||
|
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
|
||||||
|
* @param dotNetObjectId If given, the call will be to an instance method on the specified DotNetObject. Pass null or undefined to call static methods.
|
||||||
|
* @param argsJson JSON representation of arguments to pass to the method.
|
||||||
|
* @returns JSON representation of the result of the invocation.
|
||||||
|
*/
|
||||||
|
invokeDotNetFromJS?(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked by the runtime to begin an asynchronous call to a .NET method.
|
||||||
|
*
|
||||||
|
* @param callId A value identifying the asynchronous operation. This value should be passed back in a later call from .NET to JS.
|
||||||
|
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke. The value may be null when invoking instance methods.
|
||||||
|
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
|
||||||
|
* @param dotNetObjectId If given, the call will be to an instance method on the specified DotNetObject. Pass null to call static methods.
|
||||||
|
* @param argsJson JSON representation of arguments to pass to the method.
|
||||||
|
*/
|
||||||
|
beginInvokeDotNetFromJS(callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives incoming calls from .NET and dispatches them to JavaScript.
|
||||||
|
*/
|
||||||
|
export const jsCallDispatcher = {
|
||||||
|
/**
|
||||||
|
* Finds the JavaScript function matching the specified identifier.
|
||||||
|
*
|
||||||
|
* @param identifier Identifies the globally-reachable function to be returned.
|
||||||
|
* @returns A Function instance.
|
||||||
|
*/
|
||||||
|
findJSFunction,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the specified synchronous JavaScript function.
|
||||||
|
*
|
||||||
|
* @param identifier Identifies the globally-reachable function to invoke.
|
||||||
|
* @param argsJson JSON representation of arguments to be passed to the function.
|
||||||
|
* @returns JSON representation of the invocation result.
|
||||||
|
*/
|
||||||
|
invokeJSFromDotNet: (identifier: string, argsJson: string) => {
|
||||||
|
const result = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson));
|
||||||
|
return result === null || result === undefined
|
||||||
|
? null
|
||||||
|
: JSON.stringify(result, argReplacer);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the specified synchronous or asynchronous JavaScript function.
|
||||||
|
*
|
||||||
|
* @param asyncHandle A value identifying the asynchronous operation. This value will be passed back in a later call to endInvokeJSFromDotNet.
|
||||||
|
* @param identifier Identifies the globally-reachable function to invoke.
|
||||||
|
* @param argsJson JSON representation of arguments to be passed to the function.
|
||||||
|
*/
|
||||||
|
beginInvokeJSFromDotNet: (asyncHandle: number, identifier: string, argsJson: string): void => {
|
||||||
|
// Coerce synchronous functions into async ones, plus treat
|
||||||
|
// synchronous exceptions the same as async ones
|
||||||
|
const promise = new Promise<any>(resolve => {
|
||||||
|
const synchronousResultOrPromise = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson));
|
||||||
|
resolve(synchronousResultOrPromise);
|
||||||
|
});
|
||||||
|
|
||||||
|
// We only listen for a result if the caller wants to be notified about it
|
||||||
|
if (asyncHandle) {
|
||||||
|
// On completion, dispatch result back to .NET
|
||||||
|
// Not using "await" because it codegens a lot of boilerplate
|
||||||
|
promise.then(
|
||||||
|
result => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, true, result], argReplacer)),
|
||||||
|
error => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, false, formatError(error)]))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives notification that an async call from JS to .NET has completed.
|
||||||
|
* @param asyncCallId The identifier supplied in an earlier call to beginInvokeDotNetFromJS.
|
||||||
|
* @param success A flag to indicate whether the operation completed successfully.
|
||||||
|
* @param resultOrExceptionMessage Either the operation result or an error message.
|
||||||
|
*/
|
||||||
|
endInvokeDotNetFromJS: (asyncCallId: string, success: boolean, resultOrExceptionMessage: any): void => {
|
||||||
|
const resultOrError = success ? resultOrExceptionMessage : new Error(resultOrExceptionMessage);
|
||||||
|
completePendingCall(parseInt(asyncCallId), success, resultOrError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonWithRevivers(json: string): any {
|
||||||
|
return json ? JSON.parse(json, (key, initialValue) => {
|
||||||
|
// Invoke each reviver in order, passing the output from the previous reviver,
|
||||||
|
// so that each one gets a chance to transform the value
|
||||||
|
return jsonRevivers.reduce(
|
||||||
|
(latestValue, reviver) => reviver(key, latestValue),
|
||||||
|
initialValue
|
||||||
|
);
|
||||||
|
}) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatError(error: any): string {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return `${error.message}\n${error.stack}`;
|
||||||
|
} else {
|
||||||
|
return error ? error.toString() : 'null';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findJSFunction(identifier: string): Function {
|
||||||
|
if (cachedJSFunctions.hasOwnProperty(identifier)) {
|
||||||
|
return cachedJSFunctions[identifier];
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: any = window;
|
||||||
|
let resultIdentifier = 'window';
|
||||||
|
identifier.split('.').forEach(segment => {
|
||||||
|
if (segment in result) {
|
||||||
|
result = result[segment];
|
||||||
|
resultIdentifier += '.' + segment;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Could not find '${segment}' in '${resultIdentifier}'.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result instanceof Function) {
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
throw new Error(`The value '${resultIdentifier}' is not a function.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DotNetObject {
|
||||||
|
constructor(private _id: number) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public invokeMethod<T>(methodIdentifier: string, ...args: any[]): T {
|
||||||
|
return invokePossibleInstanceMethod<T>(null, methodIdentifier, this._id, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public invokeMethodAsync<T>(methodIdentifier: string, ...args: any[]): Promise<T> {
|
||||||
|
return invokePossibleInstanceMethodAsync<T>(null, methodIdentifier, this._id, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
const promise = invokeMethodAsync<any>(
|
||||||
|
'Microsoft.JSInterop',
|
||||||
|
'DotNetDispatcher.ReleaseDotNetObject',
|
||||||
|
this._id);
|
||||||
|
promise.catch(error => console.error(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
public serializeAsArg() {
|
||||||
|
return `__dotNetObject:${this._id}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dotNetObjectValueFormat = /^__dotNetObject\:(\d+)$/;
|
||||||
|
attachReviver(function reviveDotNetObject(key: any, value: any) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const match = value.match(dotNetObjectValueFormat);
|
||||||
|
if (match) {
|
||||||
|
return new DotNetObject(parseInt(match[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unrecognized - let another reviver handle it
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function argReplacer(key: string, value: any) {
|
||||||
|
return value instanceof DotNetObject ? value.serializeAsArg() : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"noEmitOnError": true,
|
||||||
|
"removeComments": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["es2015", "dom", "es2015.promise"],
|
||||||
|
"strict": true,
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"dist/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,299 @@
|
||||||
|
// 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.JSInterop.Internal;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides methods that receive incoming calls from JS to .NET.
|
||||||
|
/// </summary>
|
||||||
|
public static class DotNetDispatcher
|
||||||
|
{
|
||||||
|
private static ConcurrentDictionary<string, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly
|
||||||
|
= new ConcurrentDictionary<string, IReadOnlyDictionary<string, (MethodInfo, Type[])>>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Receives a call from JS to .NET, locating and invoking the specified method.
|
||||||
|
/// </summary>
|
||||||
|
/// <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="dotNetObjectId">For instance method calls, identifies the target object.</param>
|
||||||
|
/// <param name="argsJson">A JSON representation of the parameters.</param>
|
||||||
|
/// <returns>A JSON representation of the return value, or null.</returns>
|
||||||
|
public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
|
||||||
|
{
|
||||||
|
// 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
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// DotNetDispatcher only works with JSRuntimeBase instances.
|
||||||
|
var jsRuntime = (JSRuntimeBase)JSRuntime.Current;
|
||||||
|
|
||||||
|
var targetInstance = (object)null;
|
||||||
|
if (dotNetObjectId != default)
|
||||||
|
{
|
||||||
|
targetInstance = jsRuntime.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson);
|
||||||
|
return syncResult == null ? null : Json.Serialize(syncResult, jsRuntime.ArgSerializerStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Receives a call from JS to .NET, locating and invoking the specified method asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <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="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="argsJson">A JSON representation of the parameters.</param>
|
||||||
|
/// <returns>A JSON representation of the return value, or null.</returns>
|
||||||
|
public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
|
||||||
|
{
|
||||||
|
// 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
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// 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 = (JSRuntimeBase)JSRuntime.Current;
|
||||||
|
|
||||||
|
var targetInstance = dotNetObjectId == default
|
||||||
|
? null
|
||||||
|
: jsRuntimeBaseInstance.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId);
|
||||||
|
|
||||||
|
object syncResult = null;
|
||||||
|
Exception syncException = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
syncException = ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there was no callId, the caller does not want to be notified about the result
|
||||||
|
if (callId != null)
|
||||||
|
{
|
||||||
|
// Invoke and coerce the result to a Task so the caller can use the same async API
|
||||||
|
// for both synchronous and asynchronous methods
|
||||||
|
var task = CoerceToTask(syncResult, syncException);
|
||||||
|
|
||||||
|
task.ContinueWith(completedTask =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = TaskGenericsUtil.GetTaskResult(completedTask);
|
||||||
|
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ex = UnwrapException(ex);
|
||||||
|
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task CoerceToTask(object syncResult, Exception syncException)
|
||||||
|
{
|
||||||
|
if (syncException != null)
|
||||||
|
{
|
||||||
|
return Task.FromException(syncException);
|
||||||
|
}
|
||||||
|
else if (syncResult is Task syncResultTask)
|
||||||
|
{
|
||||||
|
return syncResultTask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Task.FromResult(syncResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object InvokeSynchronously(string assemblyName, string methodIdentifier, object targetInstance, string argsJson)
|
||||||
|
{
|
||||||
|
if (targetInstance != null)
|
||||||
|
{
|
||||||
|
if (assemblyName != null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"For instance method calls, '{nameof(assemblyName)}' should be null. Value received: '{assemblyName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
assemblyName = targetInstance.GetType().Assembly.GetName().Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyName, methodIdentifier);
|
||||||
|
|
||||||
|
// There's no direct way to say we want to deserialize as an array with heterogenous
|
||||||
|
// entry types (e.g., [string, int, bool]), so we need to deserialize in two phases.
|
||||||
|
// First we deserialize as object[], for which SimpleJson will supply JsonObject
|
||||||
|
// instances for nonprimitive values.
|
||||||
|
var suppliedArgs = (object[])null;
|
||||||
|
var suppliedArgsLength = 0;
|
||||||
|
if (argsJson != null)
|
||||||
|
{
|
||||||
|
suppliedArgs = Json.Deserialize<SimpleJson.JsonArray>(argsJson).ToArray<object>();
|
||||||
|
suppliedArgsLength = suppliedArgs.Length;
|
||||||
|
}
|
||||||
|
if (suppliedArgsLength != parameterTypes.Length)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"In call to '{methodIdentifier}', expected {parameterTypes.Length} parameters but received {suppliedArgsLength}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second, convert each supplied value to the type expected by the method
|
||||||
|
var runtime = (JSRuntimeBase)JSRuntime.Current;
|
||||||
|
var serializerStrategy = runtime.ArgSerializerStrategy;
|
||||||
|
for (var i = 0; i < suppliedArgsLength; i++)
|
||||||
|
{
|
||||||
|
if (parameterTypes[i] == typeof(JSAsyncCallResult))
|
||||||
|
{
|
||||||
|
// For JS async call results, we have to defer the deserialization until
|
||||||
|
// later when we know what type it's meant to be deserialized as
|
||||||
|
suppliedArgs[i] = new JSAsyncCallResult(suppliedArgs[i]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
suppliedArgs[i] = serializerStrategy.DeserializeObject(
|
||||||
|
suppliedArgs[i], parameterTypes[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return methodInfo.Invoke(targetInstance, suppliedArgs);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw UnwrapException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Receives notification that a call from .NET to JS has finished, marking the
|
||||||
|
/// associated <see cref="Task"/> as completed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="asyncHandle">The identifier for the function invocation.</param>
|
||||||
|
/// <param name="succeeded">A flag to indicate whether the invocation succeeded.</param>
|
||||||
|
/// <param name="result">If <paramref name="succeeded"/> is <c>true</c>, specifies the invocation result. If <paramref name="succeeded"/> is <c>false</c>, gives the <see cref="Exception"/> corresponding to the invocation failure.</param>
|
||||||
|
[JSInvokable(nameof(DotNetDispatcher) + "." + nameof(EndInvoke))]
|
||||||
|
public static void EndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult result)
|
||||||
|
=> ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, result.ResultOrException);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases the reference to the specified .NET object. This allows the .NET runtime
|
||||||
|
/// to garbage collect that object if there are no other references to it.
|
||||||
|
///
|
||||||
|
/// To avoid leaking memory, the JavaScript side code must call this for every .NET
|
||||||
|
/// object it obtains a reference to. The exception is if that object is used for
|
||||||
|
/// the entire lifetime of a given user's session, in which case it is released
|
||||||
|
/// automatically when the JavaScript runtime is disposed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dotNetObjectId">The identifier previously passed to JavaScript code.</param>
|
||||||
|
[JSInvokable(nameof(DotNetDispatcher) + "." + nameof(ReleaseDotNetObject))]
|
||||||
|
public static void ReleaseDotNetObject(long dotNetObjectId)
|
||||||
|
{
|
||||||
|
// DotNetDispatcher only works with JSRuntimeBase instances.
|
||||||
|
var jsRuntime = (JSRuntimeBase)JSRuntime.Current;
|
||||||
|
jsRuntime.ArgSerializerStrategy.ReleaseDotNetObject(dotNetObjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (MethodInfo, Type[]) GetCachedMethodInfo(string assemblyName, string methodIdentifier)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(assemblyName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(assemblyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(methodIdentifier))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(methodIdentifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
var assemblyMethods = _cachedMethodsByAssembly.GetOrAdd(assemblyName, ScanAssemblyForCallableMethods);
|
||||||
|
if (assemblyMethods.TryGetValue(methodIdentifier, out var result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"The assembly '{assemblyName}' does not contain a public method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")].");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, (MethodInfo, Type[])> ScanAssemblyForCallableMethods(string assemblyName)
|
||||||
|
{
|
||||||
|
// TODO: Consider looking first for assembly-level attributes (i.e., if there are any,
|
||||||
|
// only use those) to avoid scanning, especially for framework assemblies.
|
||||||
|
var result = new Dictionary<string, (MethodInfo, Type[])>();
|
||||||
|
var invokableMethods = GetRequiredLoadedAssembly(assemblyName)
|
||||||
|
.GetExportedTypes()
|
||||||
|
.SelectMany(type => type.GetMethods(
|
||||||
|
BindingFlags.Public |
|
||||||
|
BindingFlags.DeclaredOnly |
|
||||||
|
BindingFlags.Instance |
|
||||||
|
BindingFlags.Static))
|
||||||
|
.Where(method => method.IsDefined(typeof(JSInvokableAttribute), inherit: false));
|
||||||
|
foreach (var method in invokableMethods)
|
||||||
|
{
|
||||||
|
var identifier = method.GetCustomAttribute<JSInvokableAttribute>(false).Identifier ?? method.Name;
|
||||||
|
var parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.Add(identifier, (method, parameterTypes));
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
if (result.ContainsKey(identifier))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"The assembly '{assemblyName}' contains more than one " +
|
||||||
|
$"[JSInvokable] method with identifier '{identifier}'. All [JSInvokable] methods within the same " +
|
||||||
|
$"assembly must have different identifiers. You can pass a custom identifier as a parameter to " +
|
||||||
|
$"the [JSInvokable] attribute.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Assembly GetRequiredLoadedAssembly(string assemblyName)
|
||||||
|
{
|
||||||
|
// We don't want to load assemblies on demand here, because we don't necessarily trust
|
||||||
|
// "assemblyName" to be something the developer intended to load. So only pick from the
|
||||||
|
// set of already-loaded assemblies.
|
||||||
|
// In some edge cases this might force developers to explicitly call something on the
|
||||||
|
// target assembly (from .NET) before they can invoke its allowed methods from JS.
|
||||||
|
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||||
|
return loadedAssemblies.FirstOrDefault(a => a.GetName().Name.Equals(assemblyName, StringComparison.Ordinal))
|
||||||
|
?? throw new ArgumentException($"There is no loaded assembly with the name '{assemblyName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Exception UnwrapException(Exception ex)
|
||||||
|
{
|
||||||
|
while ((ex is AggregateException || ex is TargetInvocationException) && ex.InnerException != null)
|
||||||
|
{
|
||||||
|
ex = ex.InnerException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
// 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.Threading;
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps a JS interop argument, indicating that the value should not be serialized as JSON
|
||||||
|
/// but instead should be passed as a reference.
|
||||||
|
///
|
||||||
|
/// To avoid leaking memory, the reference must later be disposed by JS code or by .NET code.
|
||||||
|
/// </summary>
|
||||||
|
public class DotNetObjectRef : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the object instance represented by this wrapper.
|
||||||
|
/// </summary>
|
||||||
|
public object Value { get; }
|
||||||
|
|
||||||
|
// We track an associated IJSRuntime purely so that this class can be IDisposable
|
||||||
|
// in the normal way. Developers are more likely to use objectRef.Dispose() than
|
||||||
|
// some less familiar API such as JSRuntime.Current.UntrackObjectRef(objectRef).
|
||||||
|
private IJSRuntime _attachedToRuntime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs an instance of <see cref="DotNetObjectRef"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value being wrapped.</param>
|
||||||
|
public DotNetObjectRef(object value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures the <see cref="DotNetObjectRef"/> is associated with the specified <see cref="IJSRuntime"/>.
|
||||||
|
/// Developers do not normally need to invoke this manually, since it is called automatically by
|
||||||
|
/// framework code.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="runtime">The <see cref="IJSRuntime"/>.</param>
|
||||||
|
public void EnsureAttachedToJsRuntime(IJSRuntime runtime)
|
||||||
|
{
|
||||||
|
// The reason we populate _attachedToRuntime here rather than in the constructor
|
||||||
|
// is to ensure developers can't accidentally try to reuse DotNetObjectRef across
|
||||||
|
// different IJSRuntime instances. This method gets called as part of serializing
|
||||||
|
// the DotNetObjectRef during an interop call.
|
||||||
|
|
||||||
|
var existingRuntime = Interlocked.CompareExchange(ref _attachedToRuntime, runtime, null);
|
||||||
|
if (existingRuntime != null && existingRuntime != runtime)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"The {nameof(DotNetObjectRef)} is already associated with a different {nameof(IJSRuntime)}. Do not attempt to re-use {nameof(DotNetObjectRef)} instances with multiple {nameof(IJSRuntime)} instances.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops tracking this object reference, allowing it to be garbage collected
|
||||||
|
/// (if there are no other references to it). Once the instance is disposed, it
|
||||||
|
/// can no longer be used in interop calls from JavaScript code.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_attachedToRuntime?.UntrackObjectRef(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop.Internal
|
||||||
|
{
|
||||||
|
// This is "soft" internal because we're trying to avoid expanding JsonUtil into a sophisticated
|
||||||
|
// API. Developers who want that would be better served by using a different JSON package
|
||||||
|
// instead. Also the perf implications of the ICustomArgSerializer approach aren't ideal
|
||||||
|
// (it forces structs to be boxed, and returning a dictionary means lots more allocations
|
||||||
|
// and boxing of any value-typed properties).
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal. Intended for framework use only.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICustomArgSerializer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Internal. Intended for framework use only.
|
||||||
|
/// </summary>
|
||||||
|
object ToJsonPrimitive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// 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 Microsoft.JSInterop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an instance of a JavaScript runtime to which calls may be dispatched.
|
||||||
|
/// </summary>
|
||||||
|
public interface IJSInProcessRuntime : IJSRuntime
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the specified JavaScript function synchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The JSON-serializable return type.</typeparam>
|
||||||
|
/// <param name="identifier">An identifier for the function to invoke. For example, the value <code>"someScope.someFunction"</code> will invoke the function <code>window.someScope.someFunction</code>.</param>
|
||||||
|
/// <param name="args">JSON-serializable arguments.</param>
|
||||||
|
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns>
|
||||||
|
T Invoke<T>(string identifier, params object[] args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// 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.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an instance of a JavaScript runtime to which calls may be dispatched.
|
||||||
|
/// </summary>
|
||||||
|
public interface IJSRuntime
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the specified JavaScript function asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The JSON-serializable return type.</typeparam>
|
||||||
|
/// <param name="identifier">An identifier for the function to invoke. For example, the value <code>"someScope.someFunction"</code> will invoke the function <code>window.someScope.someFunction</code>.</param>
|
||||||
|
/// <param name="args">JSON-serializable arguments.</param>
|
||||||
|
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns>
|
||||||
|
Task<T> InvokeAsync<T>(string identifier, params object[] args);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops tracking the .NET object represented by the <see cref="DotNetObjectRef"/>.
|
||||||
|
/// This allows it to be garbage collected (if nothing else holds a reference to it)
|
||||||
|
/// and means the JS-side code can no longer invoke methods on the instance or pass
|
||||||
|
/// it as an argument to subsequent calls.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dotNetObjectRef">The reference to stop tracking.</param>
|
||||||
|
/// <remarks>This method is called automatically by <see cref="DotNetObjectRef.Dispose"/>.</remarks>
|
||||||
|
void UntrackObjectRef(DotNetObjectRef dotNetObjectRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
// 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.JSInterop.Internal;
|
||||||
|
using SimpleJson;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop
|
||||||
|
{
|
||||||
|
internal class InteropArgSerializerStrategy : PocoJsonSerializerStrategy
|
||||||
|
{
|
||||||
|
private readonly JSRuntimeBase _jsRuntime;
|
||||||
|
private const string _dotNetObjectPrefix = "__dotNetObject:";
|
||||||
|
private object _storageLock = new object();
|
||||||
|
private long _nextId = 1; // Start at 1, because 0 signals "no object"
|
||||||
|
private Dictionary<long, DotNetObjectRef> _trackedRefsById = new Dictionary<long, DotNetObjectRef>();
|
||||||
|
private Dictionary<DotNetObjectRef, long> _trackedIdsByRef = new Dictionary<DotNetObjectRef, long>();
|
||||||
|
|
||||||
|
public InteropArgSerializerStrategy(JSRuntimeBase jsRuntime)
|
||||||
|
{
|
||||||
|
_jsRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool TrySerializeKnownTypes(object input, out object output)
|
||||||
|
{
|
||||||
|
switch (input)
|
||||||
|
{
|
||||||
|
case DotNetObjectRef marshalByRefValue:
|
||||||
|
EnsureDotNetObjectTracked(marshalByRefValue, out var id);
|
||||||
|
|
||||||
|
// Special value format recognized by the code in Microsoft.JSInterop.js
|
||||||
|
// If we have to make it more clash-resistant, we can do
|
||||||
|
output = _dotNetObjectPrefix + id;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case ICustomArgSerializer customArgSerializer:
|
||||||
|
output = customArgSerializer.ToJsonPrimitive();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return base.TrySerializeKnownTypes(input, out output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object DeserializeObject(object value, Type type)
|
||||||
|
{
|
||||||
|
if (value is string valueString)
|
||||||
|
{
|
||||||
|
if (valueString.StartsWith(_dotNetObjectPrefix))
|
||||||
|
{
|
||||||
|
var dotNetObjectId = long.Parse(valueString.Substring(_dotNetObjectPrefix.Length));
|
||||||
|
return FindDotNetObject(dotNetObjectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.DeserializeObject(value, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object FindDotNetObject(long dotNetObjectId)
|
||||||
|
{
|
||||||
|
lock (_storageLock)
|
||||||
|
{
|
||||||
|
return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef)
|
||||||
|
? dotNetObjectRef.Value
|
||||||
|
: throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the reference was already released.", nameof(dotNetObjectId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops tracking the specified .NET object reference.
|
||||||
|
/// This overload is typically invoked from JS code via JS interop.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dotNetObjectId">The ID of the <see cref="DotNetObjectRef"/>.</param>
|
||||||
|
public void ReleaseDotNetObject(long dotNetObjectId)
|
||||||
|
{
|
||||||
|
lock (_storageLock)
|
||||||
|
{
|
||||||
|
if (_trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef))
|
||||||
|
{
|
||||||
|
_trackedRefsById.Remove(dotNetObjectId);
|
||||||
|
_trackedIdsByRef.Remove(dotNetObjectRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops tracking the specified .NET object reference.
|
||||||
|
/// This overload is typically invoked from .NET code by <see cref="DotNetObjectRef.Dispose"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dotNetObjectRef">The <see cref="DotNetObjectRef"/>.</param>
|
||||||
|
public void ReleaseDotNetObject(DotNetObjectRef dotNetObjectRef)
|
||||||
|
{
|
||||||
|
lock (_storageLock)
|
||||||
|
{
|
||||||
|
if (_trackedIdsByRef.TryGetValue(dotNetObjectRef, out var dotNetObjectId))
|
||||||
|
{
|
||||||
|
_trackedRefsById.Remove(dotNetObjectId);
|
||||||
|
_trackedIdsByRef.Remove(dotNetObjectRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureDotNetObjectTracked(DotNetObjectRef dotNetObjectRef, out long dotNetObjectId)
|
||||||
|
{
|
||||||
|
dotNetObjectRef.EnsureAttachedToJsRuntime(_jsRuntime);
|
||||||
|
|
||||||
|
lock (_storageLock)
|
||||||
|
{
|
||||||
|
// Assign an ID only if it doesn't already have one
|
||||||
|
if (!_trackedIdsByRef.TryGetValue(dotNetObjectRef, out dotNetObjectId))
|
||||||
|
{
|
||||||
|
dotNetObjectId = _nextId++;
|
||||||
|
_trackedRefsById.Add(dotNetObjectId, dotNetObjectRef);
|
||||||
|
_trackedIdsByRef.Add(dotNetObjectRef, dotNetObjectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
// 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 Microsoft.JSInterop.Internal
|
||||||
|
{
|
||||||
|
// This type takes care of a special case in handling the result of an async call from
|
||||||
|
// .NET to JS. The information about what type the result should be exists only on the
|
||||||
|
// corresponding TaskCompletionSource<T>. We don't have that information at the time
|
||||||
|
// that we deserialize the incoming argsJson before calling DotNetDispatcher.EndInvoke.
|
||||||
|
// Declaring the EndInvoke parameter type as JSAsyncCallResult defers the deserialization
|
||||||
|
// until later when we have access to the TaskCompletionSource<T>.
|
||||||
|
//
|
||||||
|
// There's no reason why developers would need anything similar to this in user code,
|
||||||
|
// because this is the mechanism by which we resolve the incoming argsJson to the correct
|
||||||
|
// user types before completing calls.
|
||||||
|
//
|
||||||
|
// It's marked as 'public' only because it has to be for use as an argument on a
|
||||||
|
// [JSInvokable] method.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Intended for framework use only.
|
||||||
|
/// </summary>
|
||||||
|
public class JSAsyncCallResult
|
||||||
|
{
|
||||||
|
internal object ResultOrException { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs an instance of <see cref="JSAsyncCallResult"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resultOrException">The result of the call.</param>
|
||||||
|
internal JSAsyncCallResult(object resultOrException)
|
||||||
|
{
|
||||||
|
ResultOrException = resultOrException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents errors that occur during an interop call from .NET to JavaScript.
|
||||||
|
/// </summary>
|
||||||
|
public class JSException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs an instance of <see cref="JSException"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The exception message.</param>
|
||||||
|
public JSException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// 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 Microsoft.JSInterop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract base class for an in-process JavaScript runtime.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class JSInProcessRuntimeBase : JSRuntimeBase, IJSInProcessRuntime
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the specified JavaScript function synchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The JSON-serializable return type.</typeparam>
|
||||||
|
/// <param name="identifier">An identifier for the function to invoke. For example, the value <code>"someScope.someFunction"</code> will invoke the function <code>window.someScope.someFunction</code>.</param>
|
||||||
|
/// <param name="args">JSON-serializable arguments.</param>
|
||||||
|
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns>
|
||||||
|
public T Invoke<T>(string identifier, params object[] args)
|
||||||
|
{
|
||||||
|
var resultJson = InvokeJS(identifier, Json.Serialize(args, ArgSerializerStrategy));
|
||||||
|
return Json.Deserialize<T>(resultJson, ArgSerializerStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs a synchronous function invocation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identifier">The identifier for the function to invoke.</param>
|
||||||
|
/// <param name="argsJson">A JSON representation of the arguments.</param>
|
||||||
|
/// <returns>A JSON representation of the result.</returns>
|
||||||
|
protected abstract string InvokeJS(string identifier, string argsJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
// 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.JSInterop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies a .NET method as allowing invocation from JavaScript code.
|
||||||
|
/// Any method marked with this attribute may receive arbitrary parameter values
|
||||||
|
/// from untrusted callers. All inputs should be validated carefully.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
|
||||||
|
public class JSInvokableAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the identifier for the method. The identifier must be unique within the scope
|
||||||
|
/// of an assembly.
|
||||||
|
///
|
||||||
|
/// If not set, the identifier is taken from the name of the method. In this case the
|
||||||
|
/// method name must be unique within the assembly.
|
||||||
|
/// </summary>
|
||||||
|
public string Identifier { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs an instance of <see cref="JSInvokableAttribute"/> without setting
|
||||||
|
/// an identifier for the method.
|
||||||
|
/// </summary>
|
||||||
|
public JSInvokableAttribute()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs an instance of <see cref="JSInvokableAttribute"/> using the specified
|
||||||
|
/// identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identifier">An identifier for the method, which must be unique within the scope of the assembly.</param>
|
||||||
|
public JSInvokableAttribute(string identifier)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(identifier))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Cannot be null or empty", nameof(identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
Identifier = identifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
// 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.Threading;
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides mechanisms for accessing the current <see cref="IJSRuntime"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class JSRuntime
|
||||||
|
{
|
||||||
|
private static AsyncLocal<IJSRuntime> _currentJSRuntime
|
||||||
|
= new AsyncLocal<IJSRuntime>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current <see cref="IJSRuntime"/>, if any.
|
||||||
|
/// </summary>
|
||||||
|
public static IJSRuntime Current => _currentJSRuntime.Value;
|
||||||
|
|
||||||
|
/// <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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
// 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;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract base class for a JavaScript runtime.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class JSRuntimeBase : IJSRuntime
|
||||||
|
{
|
||||||
|
private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed"
|
||||||
|
private readonly ConcurrentDictionary<long, object> _pendingTasks
|
||||||
|
= new ConcurrentDictionary<long, object>();
|
||||||
|
|
||||||
|
internal InteropArgSerializerStrategy ArgSerializerStrategy { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs an instance of <see cref="JSRuntimeBase"/>.
|
||||||
|
/// </summary>
|
||||||
|
public JSRuntimeBase()
|
||||||
|
{
|
||||||
|
ArgSerializerStrategy = new InteropArgSerializerStrategy(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
|
||||||
|
=> ArgSerializerStrategy.ReleaseDotNetObject(dotNetObjectRef);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the specified JavaScript function asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The JSON-serializable return type.</typeparam>
|
||||||
|
/// <param name="identifier">An identifier for the function to invoke. For example, the value <code>"someScope.someFunction"</code> will invoke the function <code>window.someScope.someFunction</code>.</param>
|
||||||
|
/// <param name="args">JSON-serializable arguments.</param>
|
||||||
|
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns>
|
||||||
|
public Task<T> InvokeAsync<T>(string identifier, params object[] args)
|
||||||
|
{
|
||||||
|
// We might consider also adding a default timeout here in case we don't want to
|
||||||
|
// risk a memory leak in the scenario where the JS-side code is failing to complete
|
||||||
|
// the operation.
|
||||||
|
|
||||||
|
var taskId = Interlocked.Increment(ref _nextPendingTaskId);
|
||||||
|
var tcs = new TaskCompletionSource<T>();
|
||||||
|
_pendingTasks[taskId] = tcs;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var argsJson = args?.Length > 0
|
||||||
|
? Json.Serialize(args, ArgSerializerStrategy)
|
||||||
|
: null;
|
||||||
|
BeginInvokeJS(taskId, identifier, argsJson);
|
||||||
|
return tcs.Task;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_pendingTasks.TryRemove(taskId, out _);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Begins an asynchronous function invocation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="asyncHandle">The identifier for the function invocation, or zero if no async callback is required.</param>
|
||||||
|
/// <param name="identifier">The identifier for the function to invoke.</param>
|
||||||
|
/// <param name="argsJson">A JSON representation of the arguments.</param>
|
||||||
|
protected abstract void BeginInvokeJS(long asyncHandle, string identifier, string argsJson);
|
||||||
|
|
||||||
|
internal void EndInvokeDotNet(string callId, bool success, object resultOrException)
|
||||||
|
{
|
||||||
|
// For failures, the common case is to call EndInvokeDotNet with the Exception object.
|
||||||
|
// For these we'll serialize as something that's useful to receive on the JS side.
|
||||||
|
// If the value is not an Exception, we'll just rely on it being directly JSON-serializable.
|
||||||
|
if (!success && resultOrException is Exception)
|
||||||
|
{
|
||||||
|
resultOrException = resultOrException.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", Json.Serialize(new[] {
|
||||||
|
callId,
|
||||||
|
success,
|
||||||
|
resultOrException
|
||||||
|
}, ArgSerializerStrategy));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void EndInvokeJS(long asyncHandle, bool succeeded, object resultOrException)
|
||||||
|
{
|
||||||
|
if (!_pendingTasks.TryRemove(asyncHandle, out var tcs))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"There is no pending task with handle '{asyncHandle}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (succeeded)
|
||||||
|
{
|
||||||
|
var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);
|
||||||
|
if (resultOrException is SimpleJson.JsonObject || resultOrException is SimpleJson.JsonArray)
|
||||||
|
{
|
||||||
|
resultOrException = ArgSerializerStrategy.DeserializeObject(resultOrException, resultType);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, resultOrException);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(resultOrException.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
// 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.JSInterop
|
||||||
|
{
|
||||||
|
internal static class CamelCase
|
||||||
|
{
|
||||||
|
public static string MemberNameToCamelCase(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"The value '{value ?? "null"}' is not a valid member name.",
|
||||||
|
nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't need to modify the value, bail out without creating a char array
|
||||||
|
if (!char.IsUpper(value[0]))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to modify at least one character
|
||||||
|
var chars = value.ToCharArray();
|
||||||
|
|
||||||
|
var length = chars.Length;
|
||||||
|
if (length < 2 || !char.IsUpper(chars[1]))
|
||||||
|
{
|
||||||
|
// Only the first character needs to be modified
|
||||||
|
// Note that this branch is functionally necessary, because the 'else' branch below
|
||||||
|
// never looks at char[1]. It's always looking at the n+2 character.
|
||||||
|
chars[0] = char.ToLowerInvariant(chars[0]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If chars[0] and chars[1] are both upper, then we'll lowercase the first char plus
|
||||||
|
// any consecutive uppercase ones, stopping if we find any char that is followed by a
|
||||||
|
// non-uppercase one
|
||||||
|
var i = 0;
|
||||||
|
while (i < length)
|
||||||
|
{
|
||||||
|
chars[i] = char.ToLowerInvariant(chars[i]);
|
||||||
|
|
||||||
|
i++;
|
||||||
|
|
||||||
|
// If the next-plus-one char isn't also uppercase, then we're now on the last uppercase, so stop
|
||||||
|
if (i < length - 1 && !char.IsUpper(chars[i + 1]))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new string(chars);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
// 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 Microsoft.JSInterop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides mechanisms for converting between .NET objects and JSON strings for use
|
||||||
|
/// when making calls to JavaScript functions via <see cref="IJSRuntime"/>.
|
||||||
|
///
|
||||||
|
/// Warning: This is not intended as a general-purpose JSON library. It is only intended
|
||||||
|
/// for use when making calls via <see cref="IJSRuntime"/>. Eventually its implementation
|
||||||
|
/// will be replaced by something more general-purpose.
|
||||||
|
/// </summary>
|
||||||
|
public static class Json
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Serializes the value as a JSON string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value to serialize.</param>
|
||||||
|
/// <returns>The JSON string.</returns>
|
||||||
|
public static string Serialize(object value)
|
||||||
|
=> SimpleJson.SimpleJson.SerializeObject(value);
|
||||||
|
|
||||||
|
internal static string Serialize(object value, SimpleJson.IJsonSerializerStrategy serializerStrategy)
|
||||||
|
=> SimpleJson.SimpleJson.SerializeObject(value, serializerStrategy);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserializes the JSON string, creating an object of the specified generic type.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of object to create.</typeparam>
|
||||||
|
/// <param name="json">The JSON string.</param>
|
||||||
|
/// <returns>An object of the specified type.</returns>
|
||||||
|
public static T Deserialize<T>(string json)
|
||||||
|
=> SimpleJson.SimpleJson.DeserializeObject<T>(json);
|
||||||
|
|
||||||
|
internal static T Deserialize<T>(string json, SimpleJson.IJsonSerializerStrategy serializerStrategy)
|
||||||
|
=> SimpleJson.SimpleJson.DeserializeObject<T>(json, serializerStrategy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
SimpleJson is from https://github.com/facebook-csharp-sdk/simple-json
|
||||||
|
|
||||||
|
LICENSE (from https://github.com/facebook-csharp-sdk/simple-json/blob/08b6871e8f63e866810d25e7a03c48502c9a234b/LICENSE.txt):
|
||||||
|
=====
|
||||||
|
Copyright (c) 2011, The Outercurve Foundation
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Microsoft.JSInterop.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop
|
||||||
|
{
|
||||||
|
internal static class TaskGenericsUtil
|
||||||
|
{
|
||||||
|
private static ConcurrentDictionary<Type, ITaskResultGetter> _cachedResultGetters
|
||||||
|
= new ConcurrentDictionary<Type, ITaskResultGetter>();
|
||||||
|
|
||||||
|
private static ConcurrentDictionary<Type, ITcsResultSetter> _cachedResultSetters
|
||||||
|
= new ConcurrentDictionary<Type, ITcsResultSetter>();
|
||||||
|
|
||||||
|
public static void SetTaskCompletionSourceResult(object taskCompletionSource, object result)
|
||||||
|
=> CreateResultSetter(taskCompletionSource).SetResult(taskCompletionSource, result);
|
||||||
|
|
||||||
|
public static void SetTaskCompletionSourceException(object taskCompletionSource, Exception exception)
|
||||||
|
=> CreateResultSetter(taskCompletionSource).SetException(taskCompletionSource, exception);
|
||||||
|
|
||||||
|
public static Type GetTaskCompletionSourceResultType(object taskCompletionSource)
|
||||||
|
=> CreateResultSetter(taskCompletionSource).ResultType;
|
||||||
|
|
||||||
|
public static object GetTaskResult(Task task)
|
||||||
|
{
|
||||||
|
var getter = _cachedResultGetters.GetOrAdd(task.GetType(), taskInstanceType =>
|
||||||
|
{
|
||||||
|
var resultType = GetTaskResultType(taskInstanceType);
|
||||||
|
return resultType == null
|
||||||
|
? new VoidTaskResultGetter()
|
||||||
|
: (ITaskResultGetter)Activator.CreateInstance(
|
||||||
|
typeof(TaskResultGetter<>).MakeGenericType(resultType));
|
||||||
|
});
|
||||||
|
return getter.GetResult(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type GetTaskResultType(Type taskType)
|
||||||
|
{
|
||||||
|
// It might be something derived from Task or Task<T>, so we have to scan
|
||||||
|
// up the inheritance hierarchy to find the Task or Task<T>
|
||||||
|
while (taskType != typeof(Task) &&
|
||||||
|
(!taskType.IsGenericType || taskType.GetGenericTypeDefinition() != typeof(Task<>)))
|
||||||
|
{
|
||||||
|
taskType = taskType.BaseType
|
||||||
|
?? throw new ArgumentException($"The type '{taskType.FullName}' is not inherited from '{typeof(Task).FullName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return taskType.IsGenericType
|
||||||
|
? taskType.GetGenericArguments().Single()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITcsResultSetter
|
||||||
|
{
|
||||||
|
Type ResultType { get; }
|
||||||
|
void SetResult(object taskCompletionSource, object result);
|
||||||
|
void SetException(object taskCompletionSource, Exception exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ITaskResultGetter
|
||||||
|
{
|
||||||
|
object GetResult(Task task);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TaskResultGetter<T> : ITaskResultGetter
|
||||||
|
{
|
||||||
|
public object GetResult(Task task) => ((Task<T>)task).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class VoidTaskResultGetter : ITaskResultGetter
|
||||||
|
{
|
||||||
|
public object GetResult(Task task)
|
||||||
|
{
|
||||||
|
task.Wait(); // Throw if the task failed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TcsResultSetter<T> : ITcsResultSetter
|
||||||
|
{
|
||||||
|
public Type ResultType => typeof(T);
|
||||||
|
|
||||||
|
public void SetResult(object tcs, object result)
|
||||||
|
{
|
||||||
|
var typedTcs = (TaskCompletionSource<T>)tcs;
|
||||||
|
|
||||||
|
// If necessary, attempt a cast
|
||||||
|
var typedResult = result is T resultT
|
||||||
|
? resultT
|
||||||
|
: (T)Convert.ChangeType(result, typeof(T));
|
||||||
|
|
||||||
|
typedTcs.SetResult(typedResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetException(object tcs, Exception exception)
|
||||||
|
{
|
||||||
|
var typedTcs = (TaskCompletionSource<T>)tcs;
|
||||||
|
typedTcs.SetException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ITcsResultSetter CreateResultSetter(object taskCompletionSource)
|
||||||
|
{
|
||||||
|
return _cachedResultSetters.GetOrAdd(taskCompletionSource.GetType(), tcsType =>
|
||||||
|
{
|
||||||
|
var resultType = tcsType.GetGenericArguments().Single();
|
||||||
|
return (ITcsResultSetter)Activator.CreateInstance(
|
||||||
|
typeof(TcsResultSetter<>).MakeGenericType(resultType));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace WebAssembly.JSInterop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Methods that map to the functions compiled into the Mono WebAssembly runtime,
|
||||||
|
/// as defined by 'mono_add_internal_call' calls in driver.c
|
||||||
|
/// </summary>
|
||||||
|
internal class InternalCalls
|
||||||
|
{
|
||||||
|
// The exact namespace, type, and method names must match the corresponding entries
|
||||||
|
// in driver.c in the Mono distribution
|
||||||
|
|
||||||
|
// We're passing asyncHandle by ref not because we want it to be writable, but so it gets
|
||||||
|
// passed as a pointer (4 bytes). We can pass 4-byte values, but not 8-byte ones.
|
||||||
|
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||||
|
public static extern string InvokeJSMarshalled(out string exception, ref long asyncHandle, string functionIdentifier, string argsJson);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||||
|
public static extern TRes InvokeJSUnmarshalled<T0, T1, T2, TRes>(out string exception, string functionIdentifier, T0 arg0, T1 arg1, T2 arg2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Microsoft.JSInterop\Microsoft.JSInterop.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
// 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.JSInterop;
|
||||||
|
using WebAssembly.JSInterop;
|
||||||
|
|
||||||
|
namespace Mono.WebAssembly.Interop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides methods for invoking JavaScript functions for applications running
|
||||||
|
/// on the Mono WebAssembly runtime.
|
||||||
|
/// </summary>
|
||||||
|
public class MonoWebAssemblyJSRuntime : JSInProcessRuntimeBase
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string InvokeJS(string identifier, string argsJson)
|
||||||
|
{
|
||||||
|
var noAsyncHandle = default(long);
|
||||||
|
var result = InternalCalls.InvokeJSMarshalled(out var exception, ref noAsyncHandle, identifier, argsJson);
|
||||||
|
return exception != null
|
||||||
|
? throw new JSException(exception)
|
||||||
|
: result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
|
||||||
|
{
|
||||||
|
InternalCalls.InvokeJSMarshalled(out _, ref asyncHandle, identifier, argsJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoked via Mono's JS interop mechanism (invoke_method)
|
||||||
|
private static string InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson)
|
||||||
|
=> DotNetDispatcher.Invoke(assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson);
|
||||||
|
|
||||||
|
// Invoked via Mono's JS interop mechanism (invoke_method)
|
||||||
|
private static void BeginInvokeDotNet(string callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson)
|
||||||
|
{
|
||||||
|
// Figure out whether 'assemblyNameOrDotNetObjectId' is the assembly name or the instance ID
|
||||||
|
// We only need one for any given call. This helps to work around the limitation that we can
|
||||||
|
// only pass a maximum of 4 args in a call from JS to Mono WebAssembly.
|
||||||
|
string assemblyName;
|
||||||
|
long dotNetObjectId;
|
||||||
|
if (char.IsDigit(assemblyNameOrDotNetObjectId[0]))
|
||||||
|
{
|
||||||
|
dotNetObjectId = long.Parse(assemblyNameOrDotNetObjectId);
|
||||||
|
assemblyName = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dotNetObjectId = default;
|
||||||
|
assemblyName = assemblyNameOrDotNetObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Custom MonoWebAssemblyJSRuntime methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the JavaScript function registered with the specified identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TRes">The .NET type corresponding to the function's return value type.</typeparam>
|
||||||
|
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||||
|
/// <returns>The result of the function invocation.</returns>
|
||||||
|
public TRes InvokeUnmarshalled<TRes>(string identifier)
|
||||||
|
=> InvokeUnmarshalled<object, object, object, TRes>(identifier, null, null, null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the JavaScript function registered with the specified identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
||||||
|
/// <typeparam name="TRes">The .NET type corresponding to the function's return value type.</typeparam>
|
||||||
|
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||||
|
/// <param name="arg0">The first argument.</param>
|
||||||
|
/// <returns>The result of the function invocation.</returns>
|
||||||
|
public TRes InvokeUnmarshalled<T0, TRes>(string identifier, T0 arg0)
|
||||||
|
=> InvokeUnmarshalled<T0, object, object, TRes>(identifier, arg0, null, null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the JavaScript function registered with the specified identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
||||||
|
/// <typeparam name="T1">The type of the second argument.</typeparam>
|
||||||
|
/// <typeparam name="TRes">The .NET type corresponding to the function's return value type.</typeparam>
|
||||||
|
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||||
|
/// <param name="arg0">The first argument.</param>
|
||||||
|
/// <param name="arg1">The second argument.</param>
|
||||||
|
/// <returns>The result of the function invocation.</returns>
|
||||||
|
public TRes InvokeUnmarshalled<T0, T1, TRes>(string identifier, T0 arg0, T1 arg1)
|
||||||
|
=> InvokeUnmarshalled<T0, T1, object, TRes>(identifier, arg0, arg1, null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the JavaScript function registered with the specified identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
||||||
|
/// <typeparam name="T1">The type of the second argument.</typeparam>
|
||||||
|
/// <typeparam name="T2">The type of the third argument.</typeparam>
|
||||||
|
/// <typeparam name="TRes">The .NET type corresponding to the function's return value type.</typeparam>
|
||||||
|
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||||
|
/// <param name="arg0">The first argument.</param>
|
||||||
|
/// <param name="arg1">The second argument.</param>
|
||||||
|
/// <param name="arg2">The third argument.</param>
|
||||||
|
/// <returns>The result of the function invocation.</returns>
|
||||||
|
public TRes InvokeUnmarshalled<T0, T1, T2, TRes>(string identifier, T0 arg0, T1 arg1, T2 arg2)
|
||||||
|
{
|
||||||
|
var result = InternalCalls.InvokeJSUnmarshalled<T0, T1, T2, TRes>(out var exception, identifier, arg0, arg1, arg2);
|
||||||
|
return exception != null
|
||||||
|
? throw new JSException(exception)
|
||||||
|
: result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,443 @@
|
||||||
|
// 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.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop.Test
|
||||||
|
{
|
||||||
|
public class DotNetDispatcherTest
|
||||||
|
{
|
||||||
|
private readonly static string thisAssemblyName
|
||||||
|
= typeof(DotNetDispatcherTest).Assembly.GetName().Name;
|
||||||
|
private readonly TestJSRuntime jsRuntime
|
||||||
|
= new TestJSRuntime();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CannotInvokeWithEmptyAssemblyName()
|
||||||
|
{
|
||||||
|
var ex = Assert.Throws<ArgumentException>(() =>
|
||||||
|
{
|
||||||
|
DotNetDispatcher.Invoke(" ", "SomeMethod", default, "[]");
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
|
||||||
|
Assert.Equal("assemblyName", ex.ParamName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CannotInvokeWithEmptyMethodIdentifier()
|
||||||
|
{
|
||||||
|
var ex = Assert.Throws<ArgumentException>(() =>
|
||||||
|
{
|
||||||
|
DotNetDispatcher.Invoke("SomeAssembly", " ", default, "[]");
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
|
||||||
|
Assert.Equal("methodIdentifier", ex.ParamName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CannotInvokeMethodsOnUnloadedAssembly()
|
||||||
|
{
|
||||||
|
var assemblyName = "Some.Fake.Assembly";
|
||||||
|
var ex = Assert.Throws<ArgumentException>(() =>
|
||||||
|
{
|
||||||
|
DotNetDispatcher.Invoke(assemblyName, "SomeMethod", default, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal($"There is no loaded assembly with the name '{assemblyName}'.", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Currently it's also not possible to invoke generic methods.
|
||||||
|
// That's not something determined by DotNetDispatcher, but rather by the fact that we
|
||||||
|
// don't close over the generics in the reflection code.
|
||||||
|
// Not defining this behavior through unit tests because the default outcome is
|
||||||
|
// fine (an exception stating what info is missing).
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("MethodOnInternalType")]
|
||||||
|
[InlineData("PrivateMethod")]
|
||||||
|
[InlineData("ProtectedMethod")]
|
||||||
|
[InlineData("StaticMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it
|
||||||
|
[InlineData("InstanceMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it
|
||||||
|
public void CannotInvokeUnsuitableMethods(string methodIdentifier)
|
||||||
|
{
|
||||||
|
var ex = Assert.Throws<ArgumentException>(() =>
|
||||||
|
{
|
||||||
|
DotNetDispatcher.Invoke(thisAssemblyName, methodIdentifier, default, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task CanInvokeStaticVoidMethod() => WithJSRuntime(jsRuntime =>
|
||||||
|
{
|
||||||
|
// Arrange/Act
|
||||||
|
SomePublicType.DidInvokeMyInvocableStaticVoid = false;
|
||||||
|
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticVoid", default, null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(resultJson);
|
||||||
|
Assert.True(SomePublicType.DidInvokeMyInvocableStaticVoid);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task CanInvokeStaticNonVoidMethod() => WithJSRuntime(jsRuntime =>
|
||||||
|
{
|
||||||
|
// Arrange/Act
|
||||||
|
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", default, null);
|
||||||
|
var result = Json.Deserialize<TestDTO>(resultJson);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Test", result.StringVal);
|
||||||
|
Assert.Equal(123, result.IntVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task CanInvokeStaticNonVoidMethodWithoutCustomIdentifier() => WithJSRuntime(jsRuntime =>
|
||||||
|
{
|
||||||
|
// Arrange/Act
|
||||||
|
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null);
|
||||||
|
var result = Json.Deserialize<TestDTO>(resultJson);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal);
|
||||||
|
Assert.Equal(456, result.IntVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task CanInvokeStaticWithParams() => WithJSRuntime(jsRuntime =>
|
||||||
|
{
|
||||||
|
// Arrange: Track a .NET object to use as an arg
|
||||||
|
var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" };
|
||||||
|
jsRuntime.Invoke<object>("unimportant", new DotNetObjectRef(arg3));
|
||||||
|
|
||||||
|
// Arrange: Remaining args
|
||||||
|
var argsJson = Json.Serialize(new object[] {
|
||||||
|
new TestDTO { StringVal = "Another string", IntVal = 456 },
|
||||||
|
new[] { 100, 200 },
|
||||||
|
"__dotNetObject:1"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson);
|
||||||
|
var result = Json.Deserialize<object[]>(resultJson);
|
||||||
|
|
||||||
|
// Assert: First result value marshalled via JSON
|
||||||
|
var resultDto1 = (TestDTO)jsRuntime.ArgSerializerStrategy.DeserializeObject(result[0], typeof(TestDTO));
|
||||||
|
Assert.Equal("ANOTHER STRING", resultDto1.StringVal);
|
||||||
|
Assert.Equal(756, resultDto1.IntVal);
|
||||||
|
|
||||||
|
// Assert: Second result value marshalled by ref
|
||||||
|
var resultDto2Ref = (string)result[1];
|
||||||
|
Assert.Equal("__dotNetObject:2", resultDto2Ref);
|
||||||
|
var resultDto2 = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(2);
|
||||||
|
Assert.Equal("MY STRING", resultDto2.StringVal);
|
||||||
|
Assert.Equal(1299, resultDto2.IntVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task CanInvokeInstanceVoidMethod() => WithJSRuntime(jsRuntime =>
|
||||||
|
{
|
||||||
|
// Arrange: Track some instance
|
||||||
|
var targetInstance = new SomePublicType();
|
||||||
|
jsRuntime.Invoke<object>("unimportant", new DotNetObjectRef(targetInstance));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(resultJson);
|
||||||
|
Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task CanInvokeBaseInstanceVoidMethod() => WithJSRuntime(jsRuntime =>
|
||||||
|
{
|
||||||
|
// Arrange: Track some instance
|
||||||
|
var targetInstance = new DerivedClass();
|
||||||
|
jsRuntime.Invoke<object>("unimportant", new DotNetObjectRef(targetInstance));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var resultJson = DotNetDispatcher.Invoke(null, "BaseClassInvokableInstanceVoid", 1, null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(resultJson);
|
||||||
|
Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task CannotUseDotNetObjectRefAfterDisposal() => WithJSRuntime(jsRuntime =>
|
||||||
|
{
|
||||||
|
// This test addresses the case where the developer calls objectRef.Dispose()
|
||||||
|
// from .NET code, as opposed to .dispose() from JS code
|
||||||
|
|
||||||
|
// Arrange: Track some instance, then dispose it
|
||||||
|
var targetInstance = new SomePublicType();
|
||||||
|
var objectRef = new DotNetObjectRef(targetInstance);
|
||||||
|
jsRuntime.Invoke<object>("unimportant", objectRef);
|
||||||
|
objectRef.Dispose();
|
||||||
|
|
||||||
|
// Act/Assert
|
||||||
|
var ex = Assert.Throws<ArgumentException>(
|
||||||
|
() => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null));
|
||||||
|
Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task CannotUseDotNetObjectRefAfterReleaseDotNetObject() => WithJSRuntime(jsRuntime =>
|
||||||
|
{
|
||||||
|
// This test addresses the case where the developer calls .dispose()
|
||||||
|
// from JS code, as opposed to objectRef.Dispose() from .NET code
|
||||||
|
|
||||||
|
// Arrange: Track some instance, then dispose it
|
||||||
|
var targetInstance = new SomePublicType();
|
||||||
|
var objectRef = new DotNetObjectRef(targetInstance);
|
||||||
|
jsRuntime.Invoke<object>("unimportant", objectRef);
|
||||||
|
DotNetDispatcher.ReleaseDotNetObject(1);
|
||||||
|
|
||||||
|
// Act/Assert
|
||||||
|
var ex = Assert.Throws<ArgumentException>(
|
||||||
|
() => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null));
|
||||||
|
Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task CanInvokeInstanceMethodWithParams() => WithJSRuntime(jsRuntime =>
|
||||||
|
{
|
||||||
|
// Arrange: Track some instance plus another object we'll pass as a param
|
||||||
|
var targetInstance = new SomePublicType();
|
||||||
|
var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" };
|
||||||
|
jsRuntime.Invoke<object>("unimportant",
|
||||||
|
new DotNetObjectRef(targetInstance),
|
||||||
|
new DotNetObjectRef(arg2));
|
||||||
|
var argsJson = "[\"myvalue\",\"__dotNetObject:2\"]";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceMethod", 1, argsJson);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("[\"You passed myvalue\",\"__dotNetObject:3\"]", resultJson);
|
||||||
|
var resultDto = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(3);
|
||||||
|
Assert.Equal(1235, resultDto.IntVal);
|
||||||
|
Assert.Equal("MY STRING", resultDto.StringVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CannotInvokeWithIncorrectNumberOfParams()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var argsJson = Json.Serialize(new object[] { 1, 2, 3, 4 });
|
||||||
|
|
||||||
|
// Act/Assert
|
||||||
|
var ex = Assert.Throws<ArgumentException>(() =>
|
||||||
|
{
|
||||||
|
DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("In call to 'InvocableStaticWithParams', expected 3 parameters but received 4.", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task CanInvokeAsyncMethod() => WithJSRuntime(async jsRuntime =>
|
||||||
|
{
|
||||||
|
// Arrange: Track some instance plus another object we'll pass as a param
|
||||||
|
var targetInstance = new SomePublicType();
|
||||||
|
var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" };
|
||||||
|
jsRuntime.Invoke<object>("unimportant", new DotNetObjectRef(targetInstance), new DotNetObjectRef(arg2));
|
||||||
|
|
||||||
|
// Arrange: all args
|
||||||
|
var argsJson = Json.Serialize(new object[]
|
||||||
|
{
|
||||||
|
new TestDTO { IntVal = 1000, StringVal = "String via JSON" },
|
||||||
|
"__dotNetObject:2"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var callId = "123";
|
||||||
|
var resultTask = jsRuntime.NextInvocationTask;
|
||||||
|
DotNetDispatcher.BeginInvoke(callId, null, "InvokableAsyncMethod", 1, argsJson);
|
||||||
|
await resultTask;
|
||||||
|
var result = Json.Deserialize<SimpleJson.JsonArray>(jsRuntime.LastInvocationArgsJson);
|
||||||
|
var resultValue = (SimpleJson.JsonArray)result[2];
|
||||||
|
|
||||||
|
// Assert: Correct info to complete the async call
|
||||||
|
Assert.Equal(0, jsRuntime.LastInvocationAsyncHandle); // 0 because it doesn't want a further callback from JS to .NET
|
||||||
|
Assert.Equal("DotNet.jsCallDispatcher.endInvokeDotNetFromJS", jsRuntime.LastInvocationIdentifier);
|
||||||
|
Assert.Equal(3, result.Count);
|
||||||
|
Assert.Equal(callId, result[0]);
|
||||||
|
Assert.True((bool)result[1]); // Success flag
|
||||||
|
|
||||||
|
// Assert: First result value marshalled via JSON
|
||||||
|
var resultDto1 = (TestDTO)jsRuntime.ArgSerializerStrategy.DeserializeObject(resultValue[0], typeof(TestDTO));
|
||||||
|
Assert.Equal("STRING VIA JSON", resultDto1.StringVal);
|
||||||
|
Assert.Equal(2000, resultDto1.IntVal);
|
||||||
|
|
||||||
|
// Assert: Second result value marshalled by ref
|
||||||
|
var resultDto2Ref = (string)resultValue[1];
|
||||||
|
Assert.Equal("__dotNetObject:3", resultDto2Ref);
|
||||||
|
var resultDto2 = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(3);
|
||||||
|
Assert.Equal("MY STRING", resultDto2.StringVal);
|
||||||
|
Assert.Equal(2468, resultDto2.IntVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
[JSInvokable("MethodOnInternalType")] public void MyMethod() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SomePublicType
|
||||||
|
{
|
||||||
|
public static bool DidInvokeMyInvocableStaticVoid;
|
||||||
|
public bool DidInvokeMyInvocableInstanceVoid;
|
||||||
|
|
||||||
|
[JSInvokable("PrivateMethod")] private static void MyPrivateMethod() { }
|
||||||
|
[JSInvokable("ProtectedMethod")] protected static void MyProtectedMethod() { }
|
||||||
|
protected static void StaticMethodWithoutAttribute() { }
|
||||||
|
protected static void InstanceMethodWithoutAttribute() { }
|
||||||
|
|
||||||
|
[JSInvokable("InvocableStaticVoid")] public static void MyInvocableVoid()
|
||||||
|
{
|
||||||
|
DidInvokeMyInvocableStaticVoid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable("InvocableStaticNonVoid")]
|
||||||
|
public static object MyInvocableNonVoid()
|
||||||
|
=> new TestDTO { StringVal = "Test", IntVal = 123 };
|
||||||
|
|
||||||
|
[JSInvokable("InvocableStaticWithParams")]
|
||||||
|
public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, TestDTO dtoByRef)
|
||||||
|
=> new object[]
|
||||||
|
{
|
||||||
|
new TestDTO // Return via JSON marshalling
|
||||||
|
{
|
||||||
|
StringVal = dtoViaJson.StringVal.ToUpperInvariant(),
|
||||||
|
IntVal = dtoViaJson.IntVal + incrementAmounts.Sum()
|
||||||
|
},
|
||||||
|
new DotNetObjectRef(new TestDTO // Return by ref
|
||||||
|
{
|
||||||
|
StringVal = dtoByRef.StringVal.ToUpperInvariant(),
|
||||||
|
IntVal = dtoByRef.IntVal + incrementAmounts.Sum()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public static TestDTO InvokableMethodWithoutCustomIdentifier()
|
||||||
|
=> new TestDTO { StringVal = "InvokableMethodWithoutCustomIdentifier", IntVal = 456 };
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public void InvokableInstanceVoid()
|
||||||
|
{
|
||||||
|
DidInvokeMyInvocableInstanceVoid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public object[] InvokableInstanceMethod(string someString, TestDTO someDTO)
|
||||||
|
{
|
||||||
|
// Returning an array to make the point that object references
|
||||||
|
// can be embedded anywhere in the result
|
||||||
|
return new object[]
|
||||||
|
{
|
||||||
|
$"You passed {someString}",
|
||||||
|
new DotNetObjectRef(new TestDTO
|
||||||
|
{
|
||||||
|
IntVal = someDTO.IntVal + 1,
|
||||||
|
StringVal = someDTO.StringVal.ToUpperInvariant()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public async Task<object[]> InvokableAsyncMethod(TestDTO dtoViaJson, TestDTO dtoByRef)
|
||||||
|
{
|
||||||
|
await Task.Delay(50);
|
||||||
|
return new object[]
|
||||||
|
{
|
||||||
|
new TestDTO // Return via JSON
|
||||||
|
{
|
||||||
|
StringVal = dtoViaJson.StringVal.ToUpperInvariant(),
|
||||||
|
IntVal = dtoViaJson.IntVal * 2,
|
||||||
|
},
|
||||||
|
new DotNetObjectRef(new TestDTO // Return by ref
|
||||||
|
{
|
||||||
|
StringVal = dtoByRef.StringVal.ToUpperInvariant(),
|
||||||
|
IntVal = dtoByRef.IntVal * 2,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BaseClass
|
||||||
|
{
|
||||||
|
public bool DidInvokeMyBaseClassInvocableInstanceVoid;
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public void BaseClassInvokableInstanceVoid()
|
||||||
|
{
|
||||||
|
DidInvokeMyBaseClassInvocableInstanceVoid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DerivedClass : BaseClass
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestDTO
|
||||||
|
{
|
||||||
|
public string StringVal { get; set; }
|
||||||
|
public int IntVal { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestJSRuntime : JSInProcessRuntimeBase
|
||||||
|
{
|
||||||
|
private TaskCompletionSource<object> _nextInvocationTcs = new TaskCompletionSource<object>();
|
||||||
|
public Task NextInvocationTask => _nextInvocationTcs.Task;
|
||||||
|
public long LastInvocationAsyncHandle { get; private set; }
|
||||||
|
public string LastInvocationIdentifier { get; private set; }
|
||||||
|
public string LastInvocationArgsJson { get; private set; }
|
||||||
|
|
||||||
|
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
|
||||||
|
{
|
||||||
|
LastInvocationAsyncHandle = asyncHandle;
|
||||||
|
LastInvocationIdentifier = identifier;
|
||||||
|
LastInvocationArgsJson = argsJson;
|
||||||
|
_nextInvocationTcs.SetResult(null);
|
||||||
|
_nextInvocationTcs = new TaskCompletionSource<object>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string InvokeJS(string identifier, string argsJson)
|
||||||
|
{
|
||||||
|
LastInvocationAsyncHandle = default;
|
||||||
|
LastInvocationIdentifier = identifier;
|
||||||
|
LastInvocationArgsJson = argsJson;
|
||||||
|
_nextInvocationTcs.SetResult(null);
|
||||||
|
_nextInvocationTcs = new TaskCompletionSource<object>();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
// 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.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop.Test
|
||||||
|
{
|
||||||
|
public class DotNetObjectRefTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CanAccessValue()
|
||||||
|
{
|
||||||
|
var obj = new object();
|
||||||
|
Assert.Same(obj, new DotNetObjectRef(obj).Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanAssociateWithSameRuntimeMultipleTimes()
|
||||||
|
{
|
||||||
|
var objRef = new DotNetObjectRef(new object());
|
||||||
|
var jsRuntime = new TestJsRuntime();
|
||||||
|
objRef.EnsureAttachedToJsRuntime(jsRuntime);
|
||||||
|
objRef.EnsureAttachedToJsRuntime(jsRuntime);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CannotAssociateWithDifferentRuntimes()
|
||||||
|
{
|
||||||
|
var objRef = new DotNetObjectRef(new object());
|
||||||
|
var jsRuntime1 = new TestJsRuntime();
|
||||||
|
var jsRuntime2 = new TestJsRuntime();
|
||||||
|
objRef.EnsureAttachedToJsRuntime(jsRuntime1);
|
||||||
|
|
||||||
|
var ex = Assert.Throws<InvalidOperationException>(
|
||||||
|
() => objRef.EnsureAttachedToJsRuntime(jsRuntime2));
|
||||||
|
Assert.Contains("Do not attempt to re-use", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NotifiesAssociatedJsRuntimeOfDisposal()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var objRef = new DotNetObjectRef(new object());
|
||||||
|
var jsRuntime = new TestJsRuntime();
|
||||||
|
objRef.EnsureAttachedToJsRuntime(jsRuntime);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
objRef.Dispose();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(new[] { objRef }, jsRuntime.UntrackedRefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestJsRuntime : IJSRuntime
|
||||||
|
{
|
||||||
|
public List<DotNetObjectRef> UntrackedRefs = new List<DotNetObjectRef>();
|
||||||
|
|
||||||
|
public Task<T> InvokeAsync<T>(string identifier, params object[] args)
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
|
||||||
|
=> UntrackedRefs.Add(dotNetObjectRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
// 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 Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop.Test
|
||||||
|
{
|
||||||
|
public class JSInProcessRuntimeBaseTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void DispatchesSyncCallsAndDeserializesResults()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var runtime = new TestJSInProcessRuntime
|
||||||
|
{
|
||||||
|
NextResultJson = Json.Serialize(
|
||||||
|
new TestDTO { IntValue = 123, StringValue = "Hello" })
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var syncResult = runtime.Invoke<TestDTO>("test identifier 1", "arg1", 123, true );
|
||||||
|
var call = runtime.InvokeCalls.Single();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(123, syncResult.IntValue);
|
||||||
|
Assert.Equal("Hello", syncResult.StringValue);
|
||||||
|
Assert.Equal("test identifier 1", call.Identifier);
|
||||||
|
Assert.Equal("[\"arg1\",123,true]", call.ArgsJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SerializesDotNetObjectWrappersInKnownFormat()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var runtime = new TestJSInProcessRuntime { NextResultJson = null };
|
||||||
|
var obj1 = new object();
|
||||||
|
var obj2 = new object();
|
||||||
|
var obj3 = new object();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
// Showing we can pass the DotNetObject either as top-level args or nested
|
||||||
|
var syncResult = runtime.Invoke<object>("test identifier",
|
||||||
|
new DotNetObjectRef(obj1),
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "obj2", new DotNetObjectRef(obj2) },
|
||||||
|
{ "obj3", new DotNetObjectRef(obj3) }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert: Handles null result string
|
||||||
|
Assert.Null(syncResult);
|
||||||
|
|
||||||
|
// Assert: Serialized as expected
|
||||||
|
var call = runtime.InvokeCalls.Single();
|
||||||
|
Assert.Equal("test identifier", call.Identifier);
|
||||||
|
Assert.Equal("[\"__dotNetObject:1\",{\"obj2\":\"__dotNetObject:2\",\"obj3\":\"__dotNetObject:3\"}]", call.ArgsJson);
|
||||||
|
|
||||||
|
// Assert: Objects were tracked
|
||||||
|
Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(1));
|
||||||
|
Assert.Same(obj2, runtime.ArgSerializerStrategy.FindDotNetObject(2));
|
||||||
|
Assert.Same(obj3, runtime.ArgSerializerStrategy.FindDotNetObject(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SyncCallResultCanIncludeDotNetObjects()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var runtime = new TestJSInProcessRuntime
|
||||||
|
{
|
||||||
|
NextResultJson = "[\"__dotNetObject:2\",\"__dotNetObject:1\"]"
|
||||||
|
};
|
||||||
|
var obj1 = new object();
|
||||||
|
var obj2 = new object();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var syncResult = runtime.Invoke<object[]>("test identifier",
|
||||||
|
new DotNetObjectRef(obj1),
|
||||||
|
"some other arg",
|
||||||
|
new DotNetObjectRef(obj2));
|
||||||
|
var call = runtime.InvokeCalls.Single();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(new[] { obj2, obj1 }, syncResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestDTO
|
||||||
|
{
|
||||||
|
public int IntValue { get; set; }
|
||||||
|
public string StringValue { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestJSInProcessRuntime : JSInProcessRuntimeBase
|
||||||
|
{
|
||||||
|
public List<InvokeArgs> InvokeCalls { get; set; } = new List<InvokeArgs>();
|
||||||
|
|
||||||
|
public string NextResultJson { get; set; }
|
||||||
|
|
||||||
|
protected override string InvokeJS(string identifier, string argsJson)
|
||||||
|
{
|
||||||
|
InvokeCalls.Add(new InvokeArgs { Identifier = identifier, ArgsJson = argsJson });
|
||||||
|
return NextResultJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InvokeArgs
|
||||||
|
{
|
||||||
|
public string Identifier { get; set; }
|
||||||
|
public string ArgsJson { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
|
||||||
|
=> throw new NotImplementedException("This test only covers sync calls");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
// 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.JSInterop.Internal;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop.Test
|
||||||
|
{
|
||||||
|
public class JSRuntimeBaseTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void DispatchesAsyncCallsWithDistinctAsyncHandles()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var runtime = new TestJSRuntime();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
runtime.InvokeAsync<object>("test identifier 1", "arg1", 123, true );
|
||||||
|
runtime.InvokeAsync<object>("test identifier 2", "some other arg");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(runtime.BeginInvokeCalls,
|
||||||
|
call =>
|
||||||
|
{
|
||||||
|
Assert.Equal("test identifier 1", call.Identifier);
|
||||||
|
Assert.Equal("[\"arg1\",123,true]", call.ArgsJson);
|
||||||
|
},
|
||||||
|
call =>
|
||||||
|
{
|
||||||
|
Assert.Equal("test identifier 2", call.Identifier);
|
||||||
|
Assert.Equal("[\"some other arg\"]", call.ArgsJson);
|
||||||
|
Assert.NotEqual(runtime.BeginInvokeCalls[0].AsyncHandle, call.AsyncHandle);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanCompleteAsyncCallsAsSuccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var runtime = new TestJSRuntime();
|
||||||
|
|
||||||
|
// Act/Assert: Tasks not initially completed
|
||||||
|
var unrelatedTask = runtime.InvokeAsync<string>("unrelated call", Array.Empty<object>());
|
||||||
|
var task = runtime.InvokeAsync<string>("test identifier", Array.Empty<object>());
|
||||||
|
Assert.False(unrelatedTask.IsCompleted);
|
||||||
|
Assert.False(task.IsCompleted);
|
||||||
|
|
||||||
|
// Act/Assert: Task can be completed
|
||||||
|
runtime.OnEndInvoke(
|
||||||
|
runtime.BeginInvokeCalls[1].AsyncHandle,
|
||||||
|
/* succeeded: */ true,
|
||||||
|
"my result");
|
||||||
|
Assert.False(unrelatedTask.IsCompleted);
|
||||||
|
Assert.True(task.IsCompleted);
|
||||||
|
Assert.Equal("my result", task.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanCompleteAsyncCallsAsFailure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var runtime = new TestJSRuntime();
|
||||||
|
|
||||||
|
// Act/Assert: Tasks not initially completed
|
||||||
|
var unrelatedTask = runtime.InvokeAsync<string>("unrelated call", Array.Empty<object>());
|
||||||
|
var task = runtime.InvokeAsync<string>("test identifier", Array.Empty<object>());
|
||||||
|
Assert.False(unrelatedTask.IsCompleted);
|
||||||
|
Assert.False(task.IsCompleted);
|
||||||
|
|
||||||
|
// Act/Assert: Task can be failed
|
||||||
|
runtime.OnEndInvoke(
|
||||||
|
runtime.BeginInvokeCalls[1].AsyncHandle,
|
||||||
|
/* succeeded: */ false,
|
||||||
|
"This is a test exception");
|
||||||
|
Assert.False(unrelatedTask.IsCompleted);
|
||||||
|
Assert.True(task.IsCompleted);
|
||||||
|
|
||||||
|
Assert.IsType<AggregateException>(task.Exception);
|
||||||
|
Assert.IsType<JSException>(task.Exception.InnerException);
|
||||||
|
Assert.Equal("This is a test exception", ((JSException)task.Exception.InnerException).Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CannotCompleteSameAsyncCallMoreThanOnce()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var runtime = new TestJSRuntime();
|
||||||
|
|
||||||
|
// Act/Assert
|
||||||
|
runtime.InvokeAsync<string>("test identifier", Array.Empty<object>());
|
||||||
|
var asyncHandle = runtime.BeginInvokeCalls[0].AsyncHandle;
|
||||||
|
runtime.OnEndInvoke(asyncHandle, true, null);
|
||||||
|
var ex = Assert.Throws<ArgumentException>(() =>
|
||||||
|
{
|
||||||
|
// Second "end invoke" will fail
|
||||||
|
runtime.OnEndInvoke(asyncHandle, true, null);
|
||||||
|
});
|
||||||
|
Assert.Equal($"There is no pending task with handle '{asyncHandle}'.", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SerializesDotNetObjectWrappersInKnownFormat()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var runtime = new TestJSRuntime();
|
||||||
|
var obj1 = new object();
|
||||||
|
var obj2 = new object();
|
||||||
|
var obj3 = new object();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
// Showing we can pass the DotNetObject either as top-level args or nested
|
||||||
|
var obj1Ref = new DotNetObjectRef(obj1);
|
||||||
|
var obj1DifferentRef = new DotNetObjectRef(obj1);
|
||||||
|
runtime.InvokeAsync<object>("test identifier",
|
||||||
|
obj1Ref,
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "obj2", new DotNetObjectRef(obj2) },
|
||||||
|
{ "obj3", new DotNetObjectRef(obj3) },
|
||||||
|
{ "obj1SameRef", obj1Ref },
|
||||||
|
{ "obj1DifferentRef", obj1DifferentRef },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert: Serialized as expected
|
||||||
|
var call = runtime.BeginInvokeCalls.Single();
|
||||||
|
Assert.Equal("test identifier", call.Identifier);
|
||||||
|
Assert.Equal("[\"__dotNetObject:1\",{\"obj2\":\"__dotNetObject:2\",\"obj3\":\"__dotNetObject:3\",\"obj1SameRef\":\"__dotNetObject:1\",\"obj1DifferentRef\":\"__dotNetObject:4\"}]", call.ArgsJson);
|
||||||
|
|
||||||
|
// Assert: Objects were tracked
|
||||||
|
Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(1));
|
||||||
|
Assert.Same(obj2, runtime.ArgSerializerStrategy.FindDotNetObject(2));
|
||||||
|
Assert.Same(obj3, runtime.ArgSerializerStrategy.FindDotNetObject(3));
|
||||||
|
Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SupportsCustomSerializationForArguments()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var runtime = new TestJSRuntime();
|
||||||
|
|
||||||
|
// Arrange/Act
|
||||||
|
runtime.InvokeAsync<object>("test identifier",
|
||||||
|
new WithCustomArgSerializer());
|
||||||
|
|
||||||
|
// Asssert
|
||||||
|
var call = runtime.BeginInvokeCalls.Single();
|
||||||
|
Assert.Equal("[{\"key1\":\"value1\",\"key2\":123}]", call.ArgsJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestJSRuntime : JSRuntimeBase
|
||||||
|
{
|
||||||
|
public List<BeginInvokeAsyncArgs> BeginInvokeCalls = new List<BeginInvokeAsyncArgs>();
|
||||||
|
|
||||||
|
public class BeginInvokeAsyncArgs
|
||||||
|
{
|
||||||
|
public long AsyncHandle { get; set; }
|
||||||
|
public string Identifier { get; set; }
|
||||||
|
public string ArgsJson { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
|
||||||
|
{
|
||||||
|
BeginInvokeCalls.Add(new BeginInvokeAsyncArgs
|
||||||
|
{
|
||||||
|
AsyncHandle = asyncHandle,
|
||||||
|
Identifier = identifier,
|
||||||
|
ArgsJson = argsJson,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnEndInvoke(long asyncHandle, bool succeeded, object resultOrException)
|
||||||
|
=> EndInvokeJS(asyncHandle, succeeded, resultOrException);
|
||||||
|
}
|
||||||
|
|
||||||
|
class WithCustomArgSerializer : ICustomArgSerializer
|
||||||
|
{
|
||||||
|
public object ToJsonPrimitive()
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "key1", "value1" },
|
||||||
|
{ "key2", 123 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
// 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.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop.Test
|
||||||
|
{
|
||||||
|
public class JSRuntimeTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task CanHaveDistinctJSRuntimeInstancesInEachAsyncContext()
|
||||||
|
{
|
||||||
|
var tasks = Enumerable.Range(0, 20).Select(async _ =>
|
||||||
|
{
|
||||||
|
var jsRuntime = new FakeJSRuntime();
|
||||||
|
JSRuntime.SetCurrentJSRuntime(jsRuntime);
|
||||||
|
await Task.Delay(50).ConfigureAwait(false);
|
||||||
|
Assert.Same(jsRuntime, JSRuntime.Current);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
Assert.Null(JSRuntime.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeJSRuntime : IJSRuntime
|
||||||
|
{
|
||||||
|
public Task<T> InvokeAsync<T>(string identifier, params object[] args)
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,349 @@
|
||||||
|
// 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.JSInterop.Internal;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop.Test
|
||||||
|
{
|
||||||
|
public class JsonUtilTest
|
||||||
|
{
|
||||||
|
// It's not useful to have a complete set of behavior specifications for
|
||||||
|
// what the JSON serializer/deserializer does in all cases here. We merely
|
||||||
|
// expose a simple wrapper over a third-party library that maintains its
|
||||||
|
// own specs and tests.
|
||||||
|
//
|
||||||
|
// We should only add tests here to cover behaviors that Blazor itself
|
||||||
|
// depends on.
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null, "null")]
|
||||||
|
[InlineData("My string", "\"My string\"")]
|
||||||
|
[InlineData(123, "123")]
|
||||||
|
[InlineData(123.456f, "123.456")]
|
||||||
|
[InlineData(123.456d, "123.456")]
|
||||||
|
[InlineData(true, "true")]
|
||||||
|
public void CanSerializePrimitivesToJson(object value, string expectedJson)
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedJson, Json.Serialize(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("null", null)]
|
||||||
|
[InlineData("\"My string\"", "My string")]
|
||||||
|
[InlineData("123", 123L)] // Would also accept 123 as a System.Int32, but Int64 is fine as a default
|
||||||
|
[InlineData("123.456", 123.456d)]
|
||||||
|
[InlineData("true", true)]
|
||||||
|
public void CanDeserializePrimitivesFromJson(string json, object expectedValue)
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedValue, Json.Deserialize<object>(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanSerializeClassToJson()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var person = new Person
|
||||||
|
{
|
||||||
|
Id = 1844,
|
||||||
|
Name = "Athos",
|
||||||
|
Pets = new[] { "Aramis", "Porthos", "D'Artagnan" },
|
||||||
|
Hobby = Hobbies.Swordfighting,
|
||||||
|
SecondaryHobby = Hobbies.Reading,
|
||||||
|
Nicknames = new List<string> { "Comte de la Fère", "Armand" },
|
||||||
|
BirthInstant = new DateTimeOffset(1825, 8, 6, 18, 45, 21, TimeSpan.FromHours(-6)),
|
||||||
|
Age = new TimeSpan(7665, 1, 30, 0),
|
||||||
|
Allergies = new Dictionary<string, object> { { "Ducks", true }, { "Geese", false } },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act/Assert
|
||||||
|
Assert.Equal(
|
||||||
|
"{\"id\":1844,\"name\":\"Athos\",\"pets\":[\"Aramis\",\"Porthos\",\"D'Artagnan\"],\"hobby\":2,\"secondaryHobby\":1,\"nullHobby\":null,\"nicknames\":[\"Comte de la Fère\",\"Armand\"],\"birthInstant\":\"1825-08-06T18:45:21.0000000-06:00\",\"age\":\"7665.01:30:00\",\"allergies\":{\"Ducks\":true,\"Geese\":false}}",
|
||||||
|
Json.Serialize(person));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanDeserializeClassFromJson()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = "{\"id\":1844,\"name\":\"Athos\",\"pets\":[\"Aramis\",\"Porthos\",\"D'Artagnan\"],\"hobby\":2,\"secondaryHobby\":1,\"nullHobby\":null,\"nicknames\":[\"Comte de la Fère\",\"Armand\"],\"birthInstant\":\"1825-08-06T18:45:21.0000000-06:00\",\"age\":\"7665.01:30:00\",\"allergies\":{\"Ducks\":true,\"Geese\":false}}";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var person = Json.Deserialize<Person>(json);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(1844, person.Id);
|
||||||
|
Assert.Equal("Athos", person.Name);
|
||||||
|
Assert.Equal(new[] { "Aramis", "Porthos", "D'Artagnan" }, person.Pets);
|
||||||
|
Assert.Equal(Hobbies.Swordfighting, person.Hobby);
|
||||||
|
Assert.Equal(Hobbies.Reading, person.SecondaryHobby);
|
||||||
|
Assert.Null(person.NullHobby);
|
||||||
|
Assert.Equal(new[] { "Comte de la Fère", "Armand" }, person.Nicknames);
|
||||||
|
Assert.Equal(new DateTimeOffset(1825, 8, 6, 18, 45, 21, TimeSpan.FromHours(-6)), person.BirthInstant);
|
||||||
|
Assert.Equal(new TimeSpan(7665, 1, 30, 0), person.Age);
|
||||||
|
Assert.Equal(new Dictionary<string, object> { { "Ducks", true }, { "Geese", false } }, person.Allergies);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanDeserializeWithCaseInsensitiveKeys()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = "{\"ID\":1844,\"NamE\":\"Athos\"}";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var person = Json.Deserialize<Person>(json);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(1844, person.Id);
|
||||||
|
Assert.Equal("Athos", person.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeserializationPrefersPropertiesOverFields()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = "{\"member1\":\"Hello\"}";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var person = Json.Deserialize<PrefersPropertiesOverFields>(json);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Hello", person.Member1);
|
||||||
|
Assert.Null(person.member1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanSerializeStructToJson()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var commandResult = new SimpleStruct
|
||||||
|
{
|
||||||
|
StringProperty = "Test",
|
||||||
|
BoolProperty = true,
|
||||||
|
NullableIntProperty = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = Json.Serialize(commandResult);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("{\"stringProperty\":\"Test\",\"boolProperty\":true,\"nullableIntProperty\":1}", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanDeserializeStructFromJson()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = "{\"stringProperty\":\"Test\",\"boolProperty\":true,\"nullableIntProperty\":1}";
|
||||||
|
|
||||||
|
//Act
|
||||||
|
var simpleError = Json.Deserialize<SimpleStruct>(json);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Test", simpleError.StringProperty);
|
||||||
|
Assert.True(simpleError.BoolProperty);
|
||||||
|
Assert.Equal(1, simpleError.NullableIntProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanCreateInstanceOfClassWithPrivateConstructor()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedName = "NameValue";
|
||||||
|
var json = $"{{\"Name\":\"{expectedName}\"}}";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var instance = Json.Deserialize<PrivateConstructor>(json);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedName, instance.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanSetValueOfPublicPropertiesWithNonPublicSetters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedPrivateValue = "PrivateValue";
|
||||||
|
var expectedProtectedValue = "ProtectedValue";
|
||||||
|
var expectedInternalValue = "InternalValue";
|
||||||
|
|
||||||
|
var json = "{" +
|
||||||
|
$"\"PrivateSetter\":\"{expectedPrivateValue}\"," +
|
||||||
|
$"\"ProtectedSetter\":\"{expectedProtectedValue}\"," +
|
||||||
|
$"\"InternalSetter\":\"{expectedInternalValue}\"," +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var instance = Json.Deserialize<NonPublicSetterOnPublicProperty>(json);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedPrivateValue, instance.PrivateSetter);
|
||||||
|
Assert.Equal(expectedProtectedValue, instance.ProtectedSetter);
|
||||||
|
Assert.Equal(expectedInternalValue, instance.InternalSetter);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectsTypesWithAmbiguouslyNamedProperties()
|
||||||
|
{
|
||||||
|
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
{
|
||||||
|
Json.Deserialize<ClashingProperties>("{}");
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal($"The type '{typeof(ClashingProperties).FullName}' contains multiple public properties " +
|
||||||
|
$"with names case-insensitively matching '{nameof(ClashingProperties.PROP1).ToLowerInvariant()}'. " +
|
||||||
|
$"Such types cannot be used for JSON deserialization.",
|
||||||
|
ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectsTypesWithAmbiguouslyNamedFields()
|
||||||
|
{
|
||||||
|
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
{
|
||||||
|
Json.Deserialize<ClashingFields>("{}");
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal($"The type '{typeof(ClashingFields).FullName}' contains multiple public fields " +
|
||||||
|
$"with names case-insensitively matching '{nameof(ClashingFields.Field1).ToLowerInvariant()}'. " +
|
||||||
|
$"Such types cannot be used for JSON deserialization.",
|
||||||
|
ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NonEmptyConstructorThrowsUsefulException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = "{\"Property\":1}";
|
||||||
|
var type = typeof(NonEmptyConstructorPoco);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
{
|
||||||
|
Json.Deserialize<NonEmptyConstructorPoco>(json);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
$"Cannot deserialize JSON into type '{type.FullName}' because it does not have a public parameterless constructor.",
|
||||||
|
exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test cases based on https://github.com/JamesNK/Newtonsoft.Json/blob/122afba9908832bd5ac207164ee6c303bfd65cf1/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs#L41
|
||||||
|
// The only difference is that our logic doesn't have to handle space-separated words,
|
||||||
|
// because we're only use this for camelcasing .NET member names
|
||||||
|
//
|
||||||
|
// Not all of the following cases are really valid .NET member names, but we have no reason
|
||||||
|
// to implement more logic to detect invalid member names besides the basics (null or empty).
|
||||||
|
[Theory]
|
||||||
|
[InlineData("URLValue", "urlValue")]
|
||||||
|
[InlineData("URL", "url")]
|
||||||
|
[InlineData("ID", "id")]
|
||||||
|
[InlineData("I", "i")]
|
||||||
|
[InlineData("Person", "person")]
|
||||||
|
[InlineData("xPhone", "xPhone")]
|
||||||
|
[InlineData("XPhone", "xPhone")]
|
||||||
|
[InlineData("X_Phone", "x_Phone")]
|
||||||
|
[InlineData("X__Phone", "x__Phone")]
|
||||||
|
[InlineData("IsCIA", "isCIA")]
|
||||||
|
[InlineData("VmQ", "vmQ")]
|
||||||
|
[InlineData("Xml2Json", "xml2Json")]
|
||||||
|
[InlineData("SnAkEcAsE", "snAkEcAsE")]
|
||||||
|
[InlineData("SnA__kEcAsE", "snA__kEcAsE")]
|
||||||
|
[InlineData("already_snake_case_", "already_snake_case_")]
|
||||||
|
[InlineData("IsJSONProperty", "isJSONProperty")]
|
||||||
|
[InlineData("SHOUTING_CASE", "shoutinG_CASE")]
|
||||||
|
[InlineData("9999-12-31T23:59:59.9999999Z", "9999-12-31T23:59:59.9999999Z")]
|
||||||
|
[InlineData("Hi!! This is text. Time to test.", "hi!! This is text. Time to test.")]
|
||||||
|
[InlineData("BUILDING", "building")]
|
||||||
|
[InlineData("BUILDINGProperty", "buildingProperty")]
|
||||||
|
public void MemberNameToCamelCase_Valid(string input, string expectedOutput)
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedOutput, CamelCase.MemberNameToCamelCase(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(null)]
|
||||||
|
public void MemberNameToCamelCase_Invalid(string input)
|
||||||
|
{
|
||||||
|
var ex = Assert.Throws<ArgumentException>(() =>
|
||||||
|
CamelCase.MemberNameToCamelCase(input));
|
||||||
|
Assert.Equal("value", ex.ParamName);
|
||||||
|
Assert.StartsWith($"The value '{input ?? "null"}' is not a valid member name.", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NonEmptyConstructorPoco
|
||||||
|
{
|
||||||
|
public NonEmptyConstructorPoco(int parameter) {}
|
||||||
|
|
||||||
|
public int Property { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SimpleStruct
|
||||||
|
{
|
||||||
|
public string StringProperty { get; set; }
|
||||||
|
public bool BoolProperty { get; set; }
|
||||||
|
public int? NullableIntProperty { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Person
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string[] Pets { get; set; }
|
||||||
|
public Hobbies Hobby { get; set; }
|
||||||
|
public Hobbies? SecondaryHobby { get; set; }
|
||||||
|
public Hobbies? NullHobby { get; set; }
|
||||||
|
public IList<string> Nicknames { get; set; }
|
||||||
|
public DateTimeOffset BirthInstant { get; set; }
|
||||||
|
public TimeSpan Age { get; set; }
|
||||||
|
public IDictionary<string, object> Allergies { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Hobbies { Reading = 1, Swordfighting = 2 }
|
||||||
|
|
||||||
|
#pragma warning disable 0649
|
||||||
|
class ClashingProperties
|
||||||
|
{
|
||||||
|
public string Prop1 { get; set; }
|
||||||
|
public int PROP1 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClashingFields
|
||||||
|
{
|
||||||
|
public string Field1;
|
||||||
|
public int field1;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrefersPropertiesOverFields
|
||||||
|
{
|
||||||
|
public string member1;
|
||||||
|
public string Member1 { get; set; }
|
||||||
|
}
|
||||||
|
#pragma warning restore 0649
|
||||||
|
|
||||||
|
class PrivateConstructor
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
private PrivateConstructor()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrivateConstructor(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NonPublicSetterOnPublicProperty
|
||||||
|
{
|
||||||
|
public string PrivateSetter { get; private set; }
|
||||||
|
public string ProtectedSetter { get; protected set; }
|
||||||
|
public string InternalSetter { get; internal set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<LangVersion>7.3</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.3.1" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||||
|
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Microsoft.JSInterop\Microsoft.JSInterop.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Loading…
Reference in New Issue