Consume jsinterop from submodule (#1351)
* Remove JSInterop files from this repo * Add jsinterop submodule * In Blazor.sln, reference jsinterop projects from submodule * Update other references to jsinterop * Fix TypeScript warning * Include submodules in test/pack * Update to newer jsinterop to fix JS pack issue * Update to newer jsinterop to obtain strong naming * Allow jsinterop submodule to inherit Directory.Build.props/targets * Get latest jsinterop * For AppVeyor builds, restore git submodules (happens automatically elsewhere) * Update README.md with instructions to restore submodules
This commit is contained in:
parent
520d47316f
commit
cf59ed94ad
|
|
@ -1,6 +1,7 @@
|
|||
init:
|
||||
- git config --global core.autocrlf true
|
||||
- git config --global core.autocrlf true
|
||||
install:
|
||||
- git submodule update --init --recursive
|
||||
- ps: Install-Product node 8 x64
|
||||
branches:
|
||||
only:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "modules/jsinterop"]
|
||||
path = modules/jsinterop
|
||||
url = https://github.com/dotnet/jsinterop.git
|
||||
82
Blazor.sln
82
Blazor.sln
|
|
@ -95,12 +95,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Analyzers.Test", "test\Microsoft.AspNetCore.Blazor.Analyzers.Test\Microsoft.AspNetCore.Blazor.Analyzers.Test.csproj", "{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop", "src\Microsoft.JSInterop\Microsoft.JSInterop.csproj", "{C866B19D-AFFF-45B7-8DAB-71805F39D516}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.Test", "test\Microsoft.JSInterop.Test\Microsoft.JSInterop.Test.csproj", "{BA1CE1FD-89D8-423F-A21B-6B212674EB39}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.WebAssembly.Interop", "src\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj", "{C56873E6-8F49-476E-AF51-B5D187832CF5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspnetCore.Blazor.Server.Test", "test\Microsoft.AspnetCore.Blazor.Server.Test\Microsoft.AspnetCore.Blazor.Server.Test.csproj", "{142AA6BC-5110-486B-A34D-6878E5E2CE95}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ServerSideBlazor", "ServerSideBlazor", "{3173A9C0-4F66-4064-83EC-3C206F1430FB}"
|
||||
|
|
@ -119,6 +113,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.TagHelperWorkaround", "src\Microsoft.AspNetCore.Blazor.TagHelperWorkaround\Microsoft.AspNetCore.Blazor.TagHelperWorkaround.csproj", "{F71D78AB-A07E-415D-BF3F-1B9991628214}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.WebAssembly.Interop", "modules\jsinterop\src\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj", "{37856984-9702-4062-B8B7-91A38AD8F2BF}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop", "modules\jsinterop\src\Microsoft.JSInterop\Microsoft.JSInterop.csproj", "{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.JS", "modules\jsinterop\src\Microsoft.JSInterop.JS\Microsoft.JSInterop.JS.csproj", "{4A5D7F9D-9CED-44C1-983E-054D8E99A292}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "jsinterop", "jsinterop", "{1386F99B-3862-40C2-B24D-796C07DC7921}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "modules", "modules", "{F380B6B6-9486-42BC-9B24-C388F8BF13A3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.Test", "modules\jsinterop\test\Microsoft.JSInterop.Test\Microsoft.JSInterop.Test.csproj", "{ECF02708-4CA4-44B3-B23F-274F4B417FA5}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -362,30 +368,6 @@ Global
|
|||
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C866B19D-AFFF-45B7-8DAB-71805F39D516}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BA1CE1FD-89D8-423F-A21B-6B212674EB39}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{C56873E6-8F49-476E-AF51-B5D187832CF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C56873E6-8F49-476E-AF51-B5D187832CF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C56873E6-8F49-476E-AF51-B5D187832CF5}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C56873E6-8F49-476E-AF51-B5D187832CF5}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C56873E6-8F49-476E-AF51-B5D187832CF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C56873E6-8F49-476E-AF51-B5D187832CF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C56873E6-8F49-476E-AF51-B5D187832CF5}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C56873E6-8F49-476E-AF51-B5D187832CF5}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{142AA6BC-5110-486B-A34D-6878E5E2CE95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{142AA6BC-5110-486B-A34D-6878E5E2CE95}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{142AA6BC-5110-486B-A34D-6878E5E2CE95}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
|
|
@ -442,6 +424,38 @@ Global
|
|||
{F71D78AB-A07E-415D-BF3F-1B9991628214}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F71D78AB-A07E-415D-BF3F-1B9991628214}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F71D78AB-A07E-415D-BF3F-1B9991628214}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{37856984-9702-4062-B8B7-91A38AD8F2BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{37856984-9702-4062-B8B7-91A38AD8F2BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{37856984-9702-4062-B8B7-91A38AD8F2BF}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{37856984-9702-4062-B8B7-91A38AD8F2BF}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{37856984-9702-4062-B8B7-91A38AD8F2BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{37856984-9702-4062-B8B7-91A38AD8F2BF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{37856984-9702-4062-B8B7-91A38AD8F2BF}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{37856984-9702-4062-B8B7-91A38AD8F2BF}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4A5D7F9D-9CED-44C1-983E-054D8E99A292}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{ECF02708-4CA4-44B3-B23F-274F4B417FA5}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -484,9 +498,6 @@ Global
|
|||
{3A457B14-D91B-4FFF-A81A-8F350BDB911F} = {E8EBA72C-D555-43AE-BC98-F0B2D05F6A07}
|
||||
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
|
||||
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
|
||||
{C866B19D-AFFF-45B7-8DAB-71805F39D516} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
|
||||
{BA1CE1FD-89D8-423F-A21B-6B212674EB39} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
|
||||
{C56873E6-8F49-476E-AF51-B5D187832CF5} = {7B5CAAB1-A3EB-44F7-87E3-A13ED89FC17D}
|
||||
{142AA6BC-5110-486B-A34D-6878E5E2CE95} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
|
||||
{3173A9C0-4F66-4064-83EC-3C206F1430FB} = {F5FDD4E5-6A52-4A86-BE5E-5E42CB1DC8DA}
|
||||
{5655AFF9-612C-4947-8221-7DB6949A6CA4} = {3173A9C0-4F66-4064-83EC-3C206F1430FB}
|
||||
|
|
@ -496,6 +507,11 @@ Global
|
|||
{72004416-E278-4787-B84F-40C7E5668D74} = {6BDD3018-3961-488E-97D3-1FB7320A8AC6}
|
||||
{CCEC81C4-1A3C-40DC-952F-074712C46180} = {36A7DEB7-5F88-4BFB-B57E-79EEC9950E25}
|
||||
{F71D78AB-A07E-415D-BF3F-1B9991628214} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
|
||||
{37856984-9702-4062-B8B7-91A38AD8F2BF} = {1386F99B-3862-40C2-B24D-796C07DC7921}
|
||||
{5F992F3C-4980-4E8E-BEE0-6EC08E369A57} = {1386F99B-3862-40C2-B24D-796C07DC7921}
|
||||
{4A5D7F9D-9CED-44C1-983E-054D8E99A292} = {1386F99B-3862-40C2-B24D-796C07DC7921}
|
||||
{1386F99B-3862-40C2-B24D-796C07DC7921} = {F380B6B6-9486-42BC-9B24-C388F8BF13A3}
|
||||
{ECF02708-4CA4-44B3-B23F-274F4B417FA5} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -34,11 +34,16 @@ To get started with Blazor and build your first Blazor web app check out our [ge
|
|||
|
||||
Prerequisites:
|
||||
- [Node.js](https://nodejs.org/) (>8.3)
|
||||
- Restore Git submodules by running the following command at the repo root:
|
||||
|
||||
git submodule update --init --recursive
|
||||
|
||||
The Blazor repository uses the same set of build tools as the other ASP.NET Core projects. The [developer documentation](https://github.com/aspnet/Home/wiki/Building-from-source) for building is the authoritative guide. **Please read this document and check your PATH setup if you have trouble building or using Visual Studio**
|
||||
|
||||
To build at the command line, run `build.cmd` or `build.sh` from the solution directory.
|
||||
|
||||
If you get a build error similar to *The project file "(some path)\blazor\modules\jsinterop\src\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj" was not found.*, it's because you didn't yet restore the Git submodules. Please see *Prerequisites* above.
|
||||
|
||||
## Run unit tests
|
||||
|
||||
Run `build.cmd /t:Test` or `build.sh /t:Test`
|
||||
|
|
@ -62,14 +67,14 @@ Prerequisites:
|
|||
- Follow the steps [here](https://github.com/aspnet/Home/wiki/Building-from-source) to set up a local copy of dotnet
|
||||
- Visual Studio 2017 15.7 latest preview - [download](https://www.visualstudio.com/thank-you-downloading-visual-studio/?ch=pre&sku=Enterprise&rel=15)
|
||||
|
||||
We recommend getting the latest preview version of Visual Studio and updating it frequently. The Blazor
|
||||
editing experience in Visual Studio depends on new features of the Razor language tooling and
|
||||
will be updated frequently.
|
||||
|
||||
When installing Visual Studio choose the following workloads:
|
||||
- ASP.NET and Web Development
|
||||
- Visual Studio extension development features
|
||||
|
||||
Before attempting to open the Blazor repo in Visual Studio, restore Git submodules by running the following command at the repo root:
|
||||
|
||||
git submodule update --init --recursive
|
||||
|
||||
If you have problems using Visual Studio with `Blazor.sln` please refer to the [developer documentation](https://github.com/aspnet/Home/wiki/Building-from-source).
|
||||
|
||||
## Developing the Blazor VS Tooling
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@
|
|||
<DisablePackageReferenceRestrictions>true</DisablePackageReferenceRestrictions>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Submodule support -->
|
||||
<ItemGroup>
|
||||
<ProjectsToTest Include="$(RepositoryRoot)modules\*\test\*\*.csproj" />
|
||||
<ProjectsToPack Include="$(RepositoryRoot)modules\*\src\*\*.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
By default, this excludes the end-to-end tests from the repo-level build command.
|
||||
The CI will script these directly by passing BlazorAllTests=true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
<Project>
|
||||
<Import Project="..\Directory.Build.props" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<Project>
|
||||
<Import Project="..\Directory.Build.targets" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit bba427d9af709017a44ebdca905916ce1d2c96f7
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<!-- Share the InternalCalls.cs source here so we get access to the same interop externs -->
|
||||
<Compile Include="..\..\src\Mono.WebAssembly.Interop\InternalCalls.cs" />
|
||||
<Compile Include="..\..\modules\jsinterop\src\Mono.WebAssembly.Interop\InternalCalls.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="$(AspNetCorePackageVersion)" />
|
||||
<WebpackInputs Include="**\*.ts" Exclude="node_modules\**" />
|
||||
<WebpackInputs Include="..\Microsoft.JSInterop\TypeScript\src\**" />
|
||||
<WebpackInputs Include="..\..\modules\jsinterop\src\Microsoft.JSInterop.JS\src\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\Microsoft.AspNetCore.Blazor.BuildTools\ReferenceFromSource.props" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import '../../Microsoft.JSInterop/JavaScriptRuntime/src/Microsoft.JSInterop';
|
||||
import '../../../modules/jsinterop/src/Microsoft.JSInterop.JS/src/Microsoft.JSInterop';
|
||||
import './GlobalExports';
|
||||
import * as Environment from './Environment';
|
||||
import * as signalR from '@aspnet/signalr';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import '../../Microsoft.JSInterop/JavaScriptRuntime/src/Microsoft.JSInterop';
|
||||
import '../../../modules/jsinterop/src/Microsoft.JSInterop.JS/src/Microsoft.JSInterop';
|
||||
import './GlobalExports';
|
||||
import * as Environment from './Environment';
|
||||
import { monoPlatform } from './Platform/Mono/MonoPlatform';
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ function addScriptTagsToDocument() {
|
|||
const meminitXHR = Module['memoryInitializerRequest'] = new XMLHttpRequest();
|
||||
meminitXHR.open('GET', `${monoRuntimeUrlBase}/mono.js.mem`);
|
||||
meminitXHR.responseType = 'arraybuffer';
|
||||
meminitXHR.send(null);
|
||||
meminitXHR.send(undefined);
|
||||
}
|
||||
|
||||
const scriptElem = document.createElement('script');
|
||||
|
|
@ -283,7 +283,7 @@ function asyncLoad(url) {
|
|||
}
|
||||
};
|
||||
xhr.onerror = reject;
|
||||
xhr.send(null);
|
||||
xhr.send(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Blazor\Microsoft.AspNetCore.Blazor.csproj" />
|
||||
<ProjectReference Include="..\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj" />
|
||||
<ProjectReference Include="..\..\modules\jsinterop\src\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.JSInterop\Microsoft.JSInterop.csproj" />
|
||||
<ProjectReference Include="..\..\modules\jsinterop\src\Microsoft.JSInterop\Microsoft.JSInterop.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
JavaScriptRuntime/dist/
|
||||
|
|
@ -1,299 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.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 automaticallly by <see cref="DotNetObjectRef.Dispose"/>.</remarks>
|
||||
void UntrackObjectRef(DotNetObjectRef dotNetObjectRef);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
// 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
SimpleJson is from https://github.com/facebook-csharp-sdk/simple-json
|
||||
|
||||
CHANGES MADE
|
||||
============
|
||||
* Better handling of System.DateTime serialized by Json.NET
|
||||
as suggested in https://github.com/facebook-csharp-sdk/simple-json/issues/78
|
||||
|
||||
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
|
|
@ -1,12 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="2.9.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.JSInterop.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"noEmitOnError": true,
|
||||
"removeComments": false,
|
||||
"sourceMap": true,
|
||||
"target": "es5",
|
||||
"lib": ["es2015", "dom", "es2015.promise"],
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"outDir": "JavaScriptRuntime/dist"
|
||||
},
|
||||
"include": [
|
||||
"JavaScriptRuntime/src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"JavaScriptRuntime/dist/**"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Mono.WebAssembly.Interop
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.JSInterop\Microsoft.JSInterop.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,443 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using 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 },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,286 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using 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,
|
||||
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,\"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,\"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(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 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 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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</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