├── .gitignore
├── Directory.Build.props
├── LICENSE.txt
├── README.md
├── WebWindow.Dev.sln
├── WebWindow.Native.sln
├── WebWindow.Samples.sln
├── azure-pipelines.yml
├── samples
├── BlazorDesktopApp
│ ├── App.razor
│ ├── BlazorDesktopApp.csproj
│ ├── Pages
│ │ ├── Counter.razor
│ │ ├── FetchData.razor
│ │ └── Index.razor
│ ├── Program.cs
│ ├── Shared
│ │ ├── MainLayout.razor
│ │ └── NavMenu.razor
│ ├── Startup.cs
│ ├── _Imports.razor
│ └── wwwroot
│ │ ├── css
│ │ ├── bootstrap
│ │ │ ├── bootstrap.min.css
│ │ │ └── bootstrap.min.css.map
│ │ ├── open-iconic
│ │ │ ├── FONT-LICENSE
│ │ │ ├── ICON-LICENSE
│ │ │ ├── README.md
│ │ │ └── font
│ │ │ │ ├── css
│ │ │ │ └── open-iconic-bootstrap.min.css
│ │ │ │ └── fonts
│ │ │ │ ├── open-iconic.eot
│ │ │ │ ├── open-iconic.otf
│ │ │ │ ├── open-iconic.svg
│ │ │ │ ├── open-iconic.ttf
│ │ │ │ └── open-iconic.woff
│ │ └── site.css
│ │ ├── index.html
│ │ └── sample-data
│ │ └── weather.json
├── HelloWorldApp
│ ├── HelloWorldApp.csproj
│ ├── Program.cs
│ └── wwwroot
│ │ └── index.html
└── VueFileExplorer
│ ├── Program.cs
│ ├── VueFileExplorer.csproj
│ └── wwwroot
│ ├── app.js
│ ├── index.html
│ ├── styles.css
│ └── vue.js
├── src
├── WebWindow.Blazor.JS
│ ├── .gitignore
│ ├── HowToUpdateUpstreamFiles.md
│ ├── WebWindow.Blazor.JS.csproj
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── Boot.Desktop.ts
│ │ └── IPC.ts
│ ├── tsconfig.json
│ ├── upstream
│ │ └── aspnetcore
│ │ │ └── web.js
│ │ │ ├── .gitignore
│ │ │ ├── .npmrc
│ │ │ ├── Microsoft.AspNetCore.Components.Web.JS.npmproj
│ │ │ ├── jest.config.js
│ │ │ ├── package.json
│ │ │ ├── src
│ │ │ ├── .eslintrc.js
│ │ │ ├── Boot.Server.ts
│ │ │ ├── Boot.WebAssembly.ts
│ │ │ ├── BootCommon.ts
│ │ │ ├── BootErrors.ts
│ │ │ ├── Environment.ts
│ │ │ ├── GlobalExports.ts
│ │ │ ├── Platform
│ │ │ │ ├── Circuits
│ │ │ │ │ ├── BlazorOptions.ts
│ │ │ │ │ ├── CircuitManager.ts
│ │ │ │ │ ├── DefaultReconnectDisplay.ts
│ │ │ │ │ ├── DefaultReconnectionHandler.ts
│ │ │ │ │ ├── ReconnectDisplay.ts
│ │ │ │ │ ├── RenderQueue.ts
│ │ │ │ │ └── UserSpecifiedDisplay.ts
│ │ │ │ ├── Logging
│ │ │ │ │ ├── Logger.ts
│ │ │ │ │ └── Loggers.ts
│ │ │ │ ├── Mono
│ │ │ │ │ ├── MonoDebugger.ts
│ │ │ │ │ ├── MonoPlatform.ts
│ │ │ │ │ └── MonoTypes.d.ts
│ │ │ │ ├── Platform.ts
│ │ │ │ └── Url.ts
│ │ │ ├── Rendering
│ │ │ │ ├── BrowserRenderer.ts
│ │ │ │ ├── ElementReferenceCapture.ts
│ │ │ │ ├── EventDelegator.ts
│ │ │ │ ├── EventFieldInfo.ts
│ │ │ │ ├── EventForDotNet.ts
│ │ │ │ ├── LogicalElements.ts
│ │ │ │ ├── RenderBatch
│ │ │ │ │ ├── OutOfProcessRenderBatch.ts
│ │ │ │ │ ├── RenderBatch.ts
│ │ │ │ │ ├── SharedMemoryRenderBatch.ts
│ │ │ │ │ └── Utf8Decoder.ts
│ │ │ │ ├── Renderer.ts
│ │ │ │ └── RendererEventDispatcher.ts
│ │ │ ├── Services
│ │ │ │ └── NavigationManager.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── webpack.config.js
│ │ │ └── yarn.lock
│ │ │ ├── tests
│ │ │ ├── DefaultReconnectDisplay.test.ts
│ │ │ ├── DefaultReconnectionHandler.test.ts
│ │ │ ├── RenderQueue.test.ts
│ │ │ └── tsconfig.json
│ │ │ ├── tsconfig.json
│ │ │ └── yarn.lock
│ └── webpack.config.js
├── WebWindow.Blazor
│ ├── ComponentsDesktop.cs
│ ├── ConventionBasedStartup.cs
│ ├── DesktopApplicationBuilder.cs
│ ├── DesktopJSRuntime.cs
│ ├── DesktopNavigationInterception.cs
│ ├── DesktopNavigationManager.cs
│ ├── DesktopRenderer.cs
│ ├── DesktopSynchronizationContext.cs
│ ├── GlobalSuppressions.cs
│ ├── IPC.cs
│ ├── JSInteropMethods.cs
│ ├── NullDispatcher.cs
│ ├── SharedSource
│ │ ├── ArrayBuilder.cs
│ │ ├── JsonSerializerOptionsProvider.cs
│ │ ├── RenderBatchWriter.cs
│ │ └── WebEventData.cs
│ └── WebWindow.Blazor.csproj
├── WebWindow.Native
│ ├── Exports.cpp
│ ├── WebWindow.Linux.cpp
│ ├── WebWindow.Mac.AppDelegate.h
│ ├── WebWindow.Mac.AppDelegate.mm
│ ├── WebWindow.Mac.UiDelegate.h
│ ├── WebWindow.Mac.UiDelegate.mm
│ ├── WebWindow.Mac.UrlSchemeHandler.h
│ ├── WebWindow.Mac.UrlSchemeHandler.m
│ ├── WebWindow.Mac.mm
│ ├── WebWindow.Native.vcxproj
│ ├── WebWindow.Native.vcxproj.filters
│ ├── WebWindow.Windows.cpp
│ ├── WebWindow.h
│ └── packages.config
└── WebWindow
│ ├── WebWindow.cs
│ ├── WebWindow.csproj
│ └── WebWindowOptions.cs
└── testassets
├── HelloWorldApp
├── HelloWorldApp.csproj
├── Program.cs
├── Properties
│ └── launchSettings.json
└── wwwroot
│ ├── image.png
│ └── index.html
└── MyBlazorApp
├── App.razor
├── MyBlazorApp.csproj
├── Pages
├── Counter.razor
├── FetchData.razor
├── Index.razor
└── WindowProp.razor
├── Program.cs
├── Shared
├── MainLayout.razor
└── NavMenu.razor
├── Startup.cs
├── _Imports.razor
└── wwwroot
├── css
├── bootstrap
│ ├── bootstrap.min.css
│ └── bootstrap.min.css.map
├── open-iconic
│ ├── FONT-LICENSE
│ ├── ICON-LICENSE
│ ├── README.md
│ └── font
│ │ ├── css
│ │ └── open-iconic-bootstrap.min.css
│ │ └── fonts
│ │ ├── open-iconic.eot
│ │ ├── open-iconic.otf
│ │ ├── open-iconic.svg
│ │ ├── open-iconic.ttf
│ │ └── open-iconic.woff
└── site.css
├── index.html
└── sample-data
└── weather.json
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vs/
3 | .vscode/
4 | bin/
5 | obj/
6 | *.user
7 | Debug/
8 | Release/
9 | packages/
10 | artifacts/
11 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 0.1.0-20200214.9
5 | 3.2.0-preview1.20073.1
6 | $(MSBuildThisFileDirectory)artifacts
7 | 0.1.0
8 | dev
9 |
10 |
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Important
2 |
3 | **I'm not directly maintaining or developing WebWindow currently or for the forseeable future.** The primary reason is that it's mostly fulfilled its purpose, which is to inspire and kickstart serious efforts to make cross-platform hybrid desktop+web apps with .NET Core a reality. Read more at https://github.com/SteveSandersonMS/WebWindow/issues/86.
4 |
5 | People who want to build real cross-platform hybrid desktop+web apps with .NET Core should consider the following alternatives:
6 |
7 | * [Photino](https://www.tryphotino.io/), which is based on this WebWindow project and is the successor to it. Photino is maintained by the team at CODE Magazine and the project's open source community. It supports Windows, Mac, and Linux, along with UIs built using either Blazor (for .NET Core) or any JavaScript-based framework.
8 | * Official support for [Blazor hybrid desktop apps coming in .NET 6](https://devblogs.microsoft.com/dotnet/announcing-net-6-preview-1/#blazor-desktop-apps).
9 |
10 | # WebWindow
11 |
12 | For information, see [this blog post](https://blog.stevensanderson.com/2019/11/18/2019-11-18-webwindow-a-cross-platform-webview-for-dotnet-core/).
13 |
14 | # Usage instructions
15 |
16 | Unless you want to change the `WebWindow` library itself, you do not need to build this repo yourself. If you just want to use it in an app, grab the [prebuilt NuGet package](https://www.nuget.org/packages/WebWindow) or follow [these 'hello world' example steps](https://blog.stevensanderson.com/2019/11/18/2019-11-18-webwindow-a-cross-platform-webview-for-dotnet-core/).
17 |
18 | # Samples
19 |
20 | For samples, open the `WebWindow.Samples.sln` solution
21 |
22 | These projects reference the prebuilt NuGet package so can be built without building the native code in this repo.
23 |
24 | # How to build this repo
25 |
26 | If you want to build the `WebWindow` library itself, you will need:
27 |
28 | * Windows, Mac, or Linux
29 | * Node.js (because `WebWindow.Blazor.JS` includes TypeScript code, so the build process involves calling Node to perform a Webpack build)
30 | * If you're on Windows:
31 | * Use Visual Studio with C++ support enabled. You *must* build in x64 configuration (*not* AnyCPU, which is the default).
32 | * If things don't seem to be updating, try right-clicking one of the `testassets` projects and choose *Rebuild* to force it to rebuild the native assets.
33 | * If you're on macOS:
34 | * Install Xcode so that you have the whole `gcc` toolchain available on the command line.
35 | * From the repo root, run `dotnet build src/WebWindow/WebWindow.csproj`
36 | * Then you can `cd testassets/HelloWorldApp` and `dotnet run`
37 | * If you're on Linux (tested with Ubuntu 18.04):
38 | * Install dependencies: `sudo apt-get update && sudo apt-get install libgtk-3-dev libwebkit2gtk-4.0-dev`
39 | * From the repo root, run `dotnet build src/WebWindow/WebWindow.csproj`
40 | * Then you can `cd testassets/HelloWorldApp` and `dotnet run`
41 | * If you're on Windows Subsystem for Linux (WSL), then as well as the above, you will need a local X server ([example setup](https://virtualizationreview.com/articles/2017/02/08/graphical-programs-on-windows-subsystem-on-linux.aspx)).
42 |
43 |
--------------------------------------------------------------------------------
/WebWindow.Native.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29319.158
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebWindow.Native", "src\WebWindow.Native\WebWindow.Native.vcxproj", "{B326B50A-F623-40F1-92F7-1EC6A5A48DAC}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Debug|x64 = Debug|x64
12 | Debug|x86 = Debug|x86
13 | Release|Any CPU = Release|Any CPU
14 | Release|x64 = Release|x64
15 | Release|x86 = Release|x86
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {B326B50A-F623-40F1-92F7-1EC6A5A48DAC}.Debug|Any CPU.ActiveCfg = Debug|Win32
19 | {B326B50A-F623-40F1-92F7-1EC6A5A48DAC}.Debug|x64.ActiveCfg = Debug|x64
20 | {B326B50A-F623-40F1-92F7-1EC6A5A48DAC}.Debug|x64.Build.0 = Debug|x64
21 | {B326B50A-F623-40F1-92F7-1EC6A5A48DAC}.Debug|x86.ActiveCfg = Debug|Win32
22 | {B326B50A-F623-40F1-92F7-1EC6A5A48DAC}.Debug|x86.Build.0 = Debug|Win32
23 | {B326B50A-F623-40F1-92F7-1EC6A5A48DAC}.Release|Any CPU.ActiveCfg = Release|Win32
24 | {B326B50A-F623-40F1-92F7-1EC6A5A48DAC}.Release|x64.ActiveCfg = Release|x64
25 | {B326B50A-F623-40F1-92F7-1EC6A5A48DAC}.Release|x64.Build.0 = Release|x64
26 | {B326B50A-F623-40F1-92F7-1EC6A5A48DAC}.Release|x86.ActiveCfg = Release|Win32
27 | {B326B50A-F623-40F1-92F7-1EC6A5A48DAC}.Release|x86.Build.0 = Release|Win32
28 | EndGlobalSection
29 | GlobalSection(SolutionProperties) = preSolution
30 | HideSolutionNode = FALSE
31 | EndGlobalSection
32 | GlobalSection(ExtensibilityGlobals) = postSolution
33 | SolutionGuid = {C46DB0A4-91F9-4A64-B9AE-E217EEDF82ED}
34 | EndGlobalSection
35 | EndGlobal
36 |
--------------------------------------------------------------------------------
/WebWindow.Samples.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29319.158
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloWorldApp", "samples\HelloWorldApp\HelloWorldApp.csproj", "{CFC44203-0D5E-4D59-A614-6AAD28D7B780}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorDesktopApp", "samples\BlazorDesktopApp\BlazorDesktopApp.csproj", "{FDF64575-084D-4D53-BB47-39B37397E634}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VueFileExplorer", "samples\VueFileExplorer\VueFileExplorer.csproj", "{C73415D2-A246-49C0-98F8-E87E1462E93E}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Debug|x64 = Debug|x64
16 | Debug|x86 = Debug|x86
17 | Release|Any CPU = Release|Any CPU
18 | Release|x64 = Release|x64
19 | Release|x86 = Release|x86
20 | EndGlobalSection
21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
22 | {CFC44203-0D5E-4D59-A614-6AAD28D7B780}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {CFC44203-0D5E-4D59-A614-6AAD28D7B780}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {CFC44203-0D5E-4D59-A614-6AAD28D7B780}.Debug|x64.ActiveCfg = Debug|Any CPU
25 | {CFC44203-0D5E-4D59-A614-6AAD28D7B780}.Debug|x64.Build.0 = Debug|Any CPU
26 | {CFC44203-0D5E-4D59-A614-6AAD28D7B780}.Debug|x86.ActiveCfg = Debug|Any CPU
27 | {CFC44203-0D5E-4D59-A614-6AAD28D7B780}.Debug|x86.Build.0 = Debug|Any CPU
28 | {CFC44203-0D5E-4D59-A614-6AAD28D7B780}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {CFC44203-0D5E-4D59-A614-6AAD28D7B780}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {CFC44203-0D5E-4D59-A614-6AAD28D7B780}.Release|x64.ActiveCfg = Release|Any CPU
31 | {CFC44203-0D5E-4D59-A614-6AAD28D7B780}.Release|x64.Build.0 = Release|Any CPU
32 | {CFC44203-0D5E-4D59-A614-6AAD28D7B780}.Release|x86.ActiveCfg = Release|Any CPU
33 | {CFC44203-0D5E-4D59-A614-6AAD28D7B780}.Release|x86.Build.0 = Release|Any CPU
34 | {FDF64575-084D-4D53-BB47-39B37397E634}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {FDF64575-084D-4D53-BB47-39B37397E634}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {FDF64575-084D-4D53-BB47-39B37397E634}.Debug|x64.ActiveCfg = Debug|Any CPU
37 | {FDF64575-084D-4D53-BB47-39B37397E634}.Debug|x64.Build.0 = Debug|Any CPU
38 | {FDF64575-084D-4D53-BB47-39B37397E634}.Debug|x86.ActiveCfg = Debug|Any CPU
39 | {FDF64575-084D-4D53-BB47-39B37397E634}.Debug|x86.Build.0 = Debug|Any CPU
40 | {FDF64575-084D-4D53-BB47-39B37397E634}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {FDF64575-084D-4D53-BB47-39B37397E634}.Release|Any CPU.Build.0 = Release|Any CPU
42 | {FDF64575-084D-4D53-BB47-39B37397E634}.Release|x64.ActiveCfg = Release|Any CPU
43 | {FDF64575-084D-4D53-BB47-39B37397E634}.Release|x64.Build.0 = Release|Any CPU
44 | {FDF64575-084D-4D53-BB47-39B37397E634}.Release|x86.ActiveCfg = Release|Any CPU
45 | {FDF64575-084D-4D53-BB47-39B37397E634}.Release|x86.Build.0 = Release|Any CPU
46 | {C73415D2-A246-49C0-98F8-E87E1462E93E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
47 | {C73415D2-A246-49C0-98F8-E87E1462E93E}.Debug|Any CPU.Build.0 = Debug|Any CPU
48 | {C73415D2-A246-49C0-98F8-E87E1462E93E}.Debug|x64.ActiveCfg = Debug|Any CPU
49 | {C73415D2-A246-49C0-98F8-E87E1462E93E}.Debug|x64.Build.0 = Debug|Any CPU
50 | {C73415D2-A246-49C0-98F8-E87E1462E93E}.Debug|x86.ActiveCfg = Debug|Any CPU
51 | {C73415D2-A246-49C0-98F8-E87E1462E93E}.Debug|x86.Build.0 = Debug|Any CPU
52 | {C73415D2-A246-49C0-98F8-E87E1462E93E}.Release|Any CPU.ActiveCfg = Release|Any CPU
53 | {C73415D2-A246-49C0-98F8-E87E1462E93E}.Release|Any CPU.Build.0 = Release|Any CPU
54 | {C73415D2-A246-49C0-98F8-E87E1462E93E}.Release|x64.ActiveCfg = Release|Any CPU
55 | {C73415D2-A246-49C0-98F8-E87E1462E93E}.Release|x64.Build.0 = Release|Any CPU
56 | {C73415D2-A246-49C0-98F8-E87E1462E93E}.Release|x86.ActiveCfg = Release|Any CPU
57 | {C73415D2-A246-49C0-98F8-E87E1462E93E}.Release|x86.Build.0 = Release|Any CPU
58 | EndGlobalSection
59 | GlobalSection(SolutionProperties) = preSolution
60 | HideSolutionNode = FALSE
61 | EndGlobalSection
62 | GlobalSection(ExtensibilityGlobals) = postSolution
63 | SolutionGuid = {688C3F20-8845-4828-AD29-993FD448DFB3}
64 | EndGlobalSection
65 | EndGlobal
66 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # ASP.NET Core
2 | # Build and test ASP.NET Core projects targeting .NET Core.
3 | # Add steps that run tests, create a NuGet package, deploy, and more:
4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core
5 |
6 | trigger:
7 | - master
8 |
9 | variables:
10 | versionprefix: 0.1.0
11 |
12 | jobs:
13 | - job: 'BuildPackage'
14 | strategy:
15 | matrix:
16 | linux:
17 | imageName: 'ubuntu-18.04'
18 | rid: 'linux-x64'
19 | mac:
20 | imageName: 'macos-10.14'
21 | rid: 'osx-x64'
22 | windows:
23 | rid: 'windows-x64'
24 | imageName: 'windows-2019'
25 |
26 | pool:
27 | vmImage: $(imageName)
28 |
29 | variables:
30 | buildConfiguration: 'Release'
31 |
32 | steps:
33 | - task: UseDotNet@2
34 | displayName: 'Use .NET Core sdk'
35 | inputs:
36 | packageType: sdk
37 | version: 3.0.100
38 | installationPath: $(Agent.ToolsDirectory)/dotnet
39 | - task: CmdLine@2
40 | displayName: 'Install linux dependencies'
41 | condition: eq(variables.rid, 'linux-x64')
42 | inputs:
43 | script: 'sudo apt-get update && sudo apt-get install libgtk-3-dev libwebkit2gtk-4.0-dev'
44 | - task: NuGetCommand@2
45 | displayName: 'NuGet package restore for Windows native packages'
46 | condition: eq(variables.rid, 'windows-x64')
47 | inputs:
48 | command: 'restore'
49 | restoreSolution: 'WebWindow.Native.sln'
50 | feedsToUse: 'select'
51 | - task: VSBuild@1
52 | displayName: 'Build Windows native assets'
53 | condition: eq(variables.rid, 'windows-x64')
54 | inputs:
55 | solution: 'WebWindow.Native.sln'
56 | platform: 'x64'
57 | configuration: '$(buildConfiguration)'
58 | - task: CmdLine@2
59 | displayName: 'Build Linux/macOS native assets'
60 | condition: ne(variables.rid, 'windows-x64')
61 | inputs:
62 | script: 'dotnet build -c $(buildConfiguration) src/WebWindow/WebWindow.csproj /t:BuildNonWindowsNative'
63 | - task: CmdLine@2
64 | condition: eq(variables.rid, 'windows-x64')
65 | displayName: 'Build .js artifact for WebWindow.Blazor.JS'
66 | inputs:
67 | script: 'dotnet build -c $(buildConfiguration) src/WebWindow.Blazor.JS'
68 | - task: CmdLine@2
69 | displayName: 'dotnet pack WebWindow'
70 | inputs:
71 | script: 'dotnet pack -c $(buildConfiguration) src/WebWindow/WebWindow.csproj /p:VersionPrefix=$(versionprefix) /p:VersionSuffix=$(Build.BuildNumber)'
72 | - task: CmdLine@2
73 | condition: eq(variables.rid, 'windows-x64')
74 | displayName: 'dotnet pack WebWindow.Blazor'
75 | inputs:
76 | script: 'dotnet pack -c $(buildConfiguration) src/WebWindow.Blazor/WebWindow.Blazor.csproj /p:VersionPrefix=$(versionprefix) /p:VersionSuffix=$(Build.BuildNumber)'
77 | - task: PublishBuildArtifacts@1
78 | inputs:
79 | PathtoPublish: 'artifacts'
80 | ArtifactName: 'artifacts-$(rid)'
81 | publishLocation: 'Container'
82 | - job: 'CombinePackages'
83 | dependsOn: 'BuildPackage'
84 | steps:
85 | - task: DownloadBuildArtifacts@0
86 | inputs:
87 | downloadPath: 'artifacts'
88 | artifactName: 'artifacts-windows-x64'
89 | - task: DownloadBuildArtifacts@0
90 | inputs:
91 | downloadPath: 'artifacts'
92 | artifactName: 'artifacts-linux-x64'
93 | - task: DownloadBuildArtifacts@0
94 | inputs:
95 | downloadPath: 'artifacts'
96 | artifactName: 'artifacts-osx-x64'
97 | - task: CmdLine@2
98 | inputs:
99 | script: 'ls -R artifacts'
100 | - task: CmdLine@2
101 | displayName: 'Merge .nupkg files'
102 | inputs:
103 | script: 'sudo apt install zipmerge && mkdir combined && zipmerge combined/WebWindow.$(versionprefix)-$(Build.BuildNumber).nupkg artifacts/*/WebWindow.$(versionprefix)-$(Build.BuildNumber).nupkg'
104 | - task: CmdLine@2
105 | displayName: 'Copy WebWindow.Blazor nupkg to output'
106 | inputs:
107 | script: 'cp artifacts/artifacts-windows-x64/WebWindow.Blazor.*.nupkg combined/'
108 | - task: PublishBuildArtifacts@1
109 | inputs:
110 | PathtoPublish: 'combined'
111 | ArtifactName: 'artifacts-combined'
112 | publishLocation: 'Container'
113 |
114 | # Uploads the NuGet package file to nuget.org
115 | # Important notes:
116 | # 1. For this to work, you need to create a 'service connection' with the same name
117 | # as the 'publishFeedCredentials' value.
118 | # 2. For security, you *must* ensure that 'Make secrets available to builds of forks'
119 | # is disabled in your PR validation settings (inside build -> Edit -> Triggers).
120 | # Otherwise, PRs would be able to push new packages even without being merged.
121 | - job: 'PublishToNuGet'
122 | dependsOn: 'CombinePackages'
123 | steps:
124 | - task: DownloadBuildArtifacts@0
125 | inputs:
126 | downloadPath: 'artifacts'
127 | artifactName: 'artifacts-combined'
128 | - task: NuGetCommand@2
129 | displayName: 'Publish to nuget.org'
130 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
131 | inputs:
132 | command: push
133 | packagesToPush: 'artifacts/artifacts-combined/*.nupkg'
134 | nuGetFeedType: external
135 | publishFeedCredentials: 'WebWindowNuGet'
136 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/App.razor:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Sorry, there's nothing at this address.
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/BlazorDesktopApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.0
5 | WinExe
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | PreserveNewest
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/Pages/Counter.razor:
--------------------------------------------------------------------------------
1 | @page "/counter"
2 |
3 |
Counter
4 |
5 | Current count: @currentCount
6 |
7 | Click me
8 |
9 | @code {
10 | int currentCount = 0;
11 |
12 | void IncrementCount()
13 | {
14 | currentCount++;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/Pages/FetchData.razor:
--------------------------------------------------------------------------------
1 | @page "/fetchdata"
2 | @using System.IO
3 | @using System.Text.Json
4 |
5 | Weather forecast
6 |
7 | This component demonstrates fetching data from the server.
8 |
9 | @if (forecasts == null)
10 | {
11 | Loading...
12 | }
13 | else
14 | {
15 |
16 |
17 |
18 | Date
19 | Temp. (C)
20 | Temp. (F)
21 | Summary
22 |
23 |
24 |
25 | @foreach (var forecast in forecasts)
26 | {
27 |
28 | @forecast.Date.ToShortDateString()
29 | @forecast.TemperatureC
30 | @forecast.TemperatureF
31 | @forecast.Summary
32 |
33 | }
34 |
35 |
36 | }
37 |
38 | @code {
39 | WeatherForecast[] forecasts;
40 |
41 | protected override async Task OnInitializedAsync()
42 | {
43 | var forecastsJson = await File.ReadAllTextAsync("wwwroot/sample-data/weather.json");
44 | forecasts = JsonSerializer.Deserialize(forecastsJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
45 | }
46 |
47 | public class WeatherForecast
48 | {
49 | public DateTime Date { get; set; }
50 |
51 | public int TemperatureC { get; set; }
52 |
53 | public string Summary { get; set; }
54 |
55 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/Pages/Index.razor:
--------------------------------------------------------------------------------
1 | @page "/"
2 |
3 | Hello, world!
4 |
5 | Welcome to your new app.
6 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/Program.cs:
--------------------------------------------------------------------------------
1 | using WebWindows.Blazor;
2 | using System;
3 |
4 | namespace BlazorDesktopApp
5 | {
6 | public class Program
7 | {
8 | static void Main(string[] args)
9 | {
10 | ComponentsDesktop.Run("My Blazor App", "wwwroot/index.html");
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/Shared/MainLayout.razor:
--------------------------------------------------------------------------------
1 | @inherits LayoutComponentBase
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | @Body
13 |
14 |
15 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/Shared/NavMenu.razor:
--------------------------------------------------------------------------------
1 |
7 |
8 |
27 |
28 | @code {
29 | bool collapseNavMenu = true;
30 |
31 | string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
32 |
33 | void ToggleNavMenu()
34 | {
35 | collapseNavMenu = !collapseNavMenu;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using WebWindows.Blazor;
3 |
4 | namespace BlazorDesktopApp
5 | {
6 | public class Startup
7 | {
8 | public void ConfigureServices(IServiceCollection services)
9 | {
10 | }
11 |
12 | public void Configure(DesktopApplicationBuilder app)
13 | {
14 | app.AddComponent("app");
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/_Imports.razor:
--------------------------------------------------------------------------------
1 | @using System.Net.Http
2 | @using Microsoft.AspNetCore.Authorization
3 | @using Microsoft.AspNetCore.Components.Forms
4 | @using Microsoft.AspNetCore.Components.Routing
5 | @using Microsoft.AspNetCore.Components.Web
6 | @using Microsoft.JSInterop
7 | @using BlazorDesktopApp
8 | @using BlazorDesktopApp.Shared
9 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/wwwroot/css/open-iconic/FONT-LICENSE:
--------------------------------------------------------------------------------
1 | SIL OPEN FONT LICENSE Version 1.1
2 |
3 | Copyright (c) 2014 Waybury
4 |
5 | PREAMBLE
6 | The goals of the Open Font License (OFL) are to stimulate worldwide
7 | development of collaborative font projects, to support the font creation
8 | efforts of academic and linguistic communities, and to provide a free and
9 | open framework in which fonts may be shared and improved in partnership
10 | with others.
11 |
12 | The OFL allows the licensed fonts to be used, studied, modified and
13 | redistributed freely as long as they are not sold by themselves. The
14 | fonts, including any derivative works, can be bundled, embedded,
15 | redistributed and/or sold with any software provided that any reserved
16 | names are not used by derivative works. The fonts and derivatives,
17 | however, cannot be released under any other type of license. The
18 | requirement for fonts to remain under this license does not apply
19 | to any document created using the fonts or their derivatives.
20 |
21 | DEFINITIONS
22 | "Font Software" refers to the set of files released by the Copyright
23 | Holder(s) under this license and clearly marked as such. This may
24 | include source files, build scripts and documentation.
25 |
26 | "Reserved Font Name" refers to any names specified as such after the
27 | copyright statement(s).
28 |
29 | "Original Version" refers to the collection of Font Software components as
30 | distributed by the Copyright Holder(s).
31 |
32 | "Modified Version" refers to any derivative made by adding to, deleting,
33 | or substituting -- in part or in whole -- any of the components of the
34 | Original Version, by changing formats or by porting the Font Software to a
35 | new environment.
36 |
37 | "Author" refers to any designer, engineer, programmer, technical
38 | writer or other person who contributed to the Font Software.
39 |
40 | PERMISSION & CONDITIONS
41 | Permission is hereby granted, free of charge, to any person obtaining
42 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
43 | redistribute, and sell modified and unmodified copies of the Font
44 | Software, subject to the following conditions:
45 |
46 | 1) Neither the Font Software nor any of its individual components,
47 | in Original or Modified Versions, may be sold by itself.
48 |
49 | 2) Original or Modified Versions of the Font Software may be bundled,
50 | redistributed and/or sold with any software, provided that each copy
51 | contains the above copyright notice and this license. These can be
52 | included either as stand-alone text files, human-readable headers or
53 | in the appropriate machine-readable metadata fields within text or
54 | binary files as long as those fields can be easily viewed by the user.
55 |
56 | 3) No Modified Version of the Font Software may use the Reserved Font
57 | Name(s) unless explicit written permission is granted by the corresponding
58 | Copyright Holder. This restriction only applies to the primary font name as
59 | presented to the users.
60 |
61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
62 | Software shall not be used to promote, endorse or advertise any
63 | Modified Version, except to acknowledge the contribution(s) of the
64 | Copyright Holder(s) and the Author(s) or with their explicit written
65 | permission.
66 |
67 | 5) The Font Software, modified or unmodified, in part or in whole,
68 | must be distributed entirely under this license, and must not be
69 | distributed under any other license. The requirement for fonts to
70 | remain under this license does not apply to any document created
71 | using the Font Software.
72 |
73 | TERMINATION
74 | This license becomes null and void if any of the above conditions are
75 | not met.
76 |
77 | DISCLAIMER
78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
86 | OTHER DEALINGS IN THE FONT SOFTWARE.
87 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/wwwroot/css/open-iconic/ICON-LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Waybury
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/wwwroot/css/open-iconic/README.md:
--------------------------------------------------------------------------------
1 | [Open Iconic v1.1.1](http://useiconic.com/open)
2 | ===========
3 |
4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons)
5 |
6 |
7 |
8 | ## What's in Open Iconic?
9 |
10 | * 223 icons designed to be legible down to 8 pixels
11 | * Super-light SVG files - 61.8 for the entire set
12 | * SVG sprite—the modern replacement for icon fonts
13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats
14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats
15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px.
16 |
17 |
18 | ## Getting Started
19 |
20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections.
21 |
22 | ### General Usage
23 |
24 | #### Using Open Iconic's SVGs
25 |
26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute).
27 |
28 | ```
29 |
30 | ```
31 |
32 | #### Using Open Iconic's SVG Sprite
33 |
34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack.
35 |
36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.*
37 |
38 | ```
39 |
40 |
41 |
42 | ```
43 |
44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions.
45 |
46 | ```
47 | .icon {
48 | width: 16px;
49 | height: 16px;
50 | }
51 | ```
52 |
53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag.
54 |
55 | ```
56 | .icon-account-login {
57 | fill: #f00;
58 | }
59 | ```
60 |
61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/).
62 |
63 | #### Using Open Iconic's Icon Font...
64 |
65 |
66 | ##### …with Bootstrap
67 |
68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}`
69 |
70 |
71 | ```
72 |
73 | ```
74 |
75 |
76 | ```
77 |
78 | ```
79 |
80 | ##### …with Foundation
81 |
82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}`
83 |
84 | ```
85 |
86 | ```
87 |
88 |
89 | ```
90 |
91 | ```
92 |
93 | ##### …on its own
94 |
95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}`
96 |
97 | ```
98 |
99 | ```
100 |
101 | ```
102 |
103 | ```
104 |
105 |
106 | ## License
107 |
108 | ### Icons
109 |
110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT).
111 |
112 | ### Fonts
113 |
114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web).
115 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/wwwroot/css/open-iconic/font/fonts/open-iconic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteveSandersonMS/WebWindow/ccdf6e7dda39fe98c7d9fec7f848c8fd8edd682d/samples/BlazorDesktopApp/wwwroot/css/open-iconic/font/fonts/open-iconic.eot
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/wwwroot/css/open-iconic/font/fonts/open-iconic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteveSandersonMS/WebWindow/ccdf6e7dda39fe98c7d9fec7f848c8fd8edd682d/samples/BlazorDesktopApp/wwwroot/css/open-iconic/font/fonts/open-iconic.otf
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteveSandersonMS/WebWindow/ccdf6e7dda39fe98c7d9fec7f848c8fd8edd682d/samples/BlazorDesktopApp/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/wwwroot/css/open-iconic/font/fonts/open-iconic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteveSandersonMS/WebWindow/ccdf6e7dda39fe98c7d9fec7f848c8fd8edd682d/samples/BlazorDesktopApp/wwwroot/css/open-iconic/font/fonts/open-iconic.woff
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/wwwroot/css/site.css:
--------------------------------------------------------------------------------
1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
2 |
3 | html, body {
4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
5 | }
6 |
7 | a, .btn-link {
8 | color: #0366d6;
9 | }
10 |
11 | .btn-primary {
12 | color: #fff;
13 | background-color: #1b6ec2;
14 | border-color: #1861ac;
15 | }
16 |
17 | app {
18 | position: relative;
19 | display: flex;
20 | flex-direction: column;
21 | }
22 |
23 | .top-row {
24 | height: 3.5rem;
25 | display: flex;
26 | align-items: center;
27 | }
28 |
29 | .main {
30 | flex: 1;
31 | }
32 |
33 | .main .top-row {
34 | background-color: #f7f7f7;
35 | border-bottom: 1px solid #d6d5d5;
36 | justify-content: flex-end;
37 | }
38 |
39 | .main .top-row > a {
40 | margin-left: 1.5rem;
41 | }
42 |
43 | .sidebar {
44 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
45 | }
46 |
47 | .sidebar .top-row {
48 | background-color: rgba(0,0,0,0.4);
49 | }
50 |
51 | .sidebar .navbar-brand {
52 | font-size: 1.1rem;
53 | }
54 |
55 | .sidebar .oi {
56 | width: 2rem;
57 | font-size: 1.1rem;
58 | vertical-align: text-top;
59 | top: -2px;
60 | }
61 |
62 | .nav-item {
63 | font-size: 0.9rem;
64 | padding-bottom: 0.5rem;
65 | }
66 |
67 | .nav-item:first-of-type {
68 | padding-top: 1rem;
69 | }
70 |
71 | .nav-item:last-of-type {
72 | padding-bottom: 1rem;
73 | }
74 |
75 | .nav-item a {
76 | color: #d7d7d7;
77 | border-radius: 4px;
78 | height: 3rem;
79 | display: flex;
80 | align-items: center;
81 | line-height: 3rem;
82 | }
83 |
84 | .nav-item a.active {
85 | background-color: rgba(255,255,255,0.25);
86 | color: white;
87 | }
88 |
89 | .nav-item a:hover {
90 | background-color: rgba(255,255,255,0.1);
91 | color: white;
92 | }
93 |
94 | .content {
95 | padding-top: 1.1rem;
96 | }
97 |
98 | .navbar-toggler {
99 | background-color: rgba(255, 255, 255, 0.1);
100 | }
101 |
102 | .valid.modified:not([type=checkbox]) {
103 | outline: 1px solid #26b050;
104 | }
105 |
106 | .invalid {
107 | outline: 1px solid red;
108 | }
109 |
110 | .validation-message {
111 | color: red;
112 | }
113 |
114 | @media (max-width: 767.98px) {
115 | .main .top-row {
116 | display: none;
117 | }
118 | }
119 |
120 | @media (min-width: 768px) {
121 | app {
122 | flex-direction: row;
123 | }
124 |
125 | .sidebar {
126 | width: 250px;
127 | height: 100vh;
128 | position: sticky;
129 | top: 0;
130 | }
131 |
132 | .main .top-row {
133 | position: sticky;
134 | top: 0;
135 | }
136 |
137 | .main > div {
138 | padding-left: 2rem !important;
139 | padding-right: 1.5rem !important;
140 | }
141 |
142 | .navbar-toggler {
143 | display: none;
144 | }
145 |
146 | .sidebar .collapse {
147 | /* Never collapse the sidebar for wide screens */
148 | display: block;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/wwwroot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | MyDesktopApp
7 |
8 |
9 |
10 |
11 |
12 | Loading...
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/samples/BlazorDesktopApp/wwwroot/sample-data/weather.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "date": "2018-05-06",
4 | "temperatureC": 1,
5 | "summary": "Freezing",
6 | "temperatureF": 33
7 | },
8 | {
9 | "date": "2018-05-07",
10 | "temperatureC": 14,
11 | "summary": "Bracing",
12 | "temperatureF": 57
13 | },
14 | {
15 | "date": "2018-05-08",
16 | "temperatureC": -13,
17 | "summary": "Freezing",
18 | "temperatureF": 9
19 | },
20 | {
21 | "date": "2018-05-09",
22 | "temperatureC": -16,
23 | "summary": "Balmy",
24 | "temperatureF": 4
25 | },
26 | {
27 | "date": "2018-05-10",
28 | "temperatureC": -2,
29 | "summary": "Chilly",
30 | "temperatureF": 29
31 | }
32 | ]
33 |
--------------------------------------------------------------------------------
/samples/HelloWorldApp/HelloWorldApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | netcoreapp3.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | PreserveNewest
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/samples/HelloWorldApp/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using WebWindows;
3 |
4 | namespace HelloWorldApp
5 | {
6 | class Program
7 | {
8 | static void Main(string[] args)
9 | {
10 | var window = new WebWindow("My first WebWindow app");
11 | window.NavigateToLocalFile("wwwroot/index.html");
12 | window.WaitForExit();
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/samples/HelloWorldApp/wwwroot/index.html:
--------------------------------------------------------------------------------
1 | Hello, world!
2 |
3 |
4 | This is a .NET Core application. It is displaying a native OS window that contains
5 | whatever webview technology is present in your OS:
6 |
7 |
8 | When running on Windows, it uses webview2 (backed by the new Chromium-based Edge)
9 | When running on Mac, it uses WKWebView (backed by Safari)
10 | When running on Linux, it uses WebKitGTK+2 (backed by WebKit)
11 |
12 |
13 |
--------------------------------------------------------------------------------
/samples/VueFileExplorer/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Text.Json;
6 | using WebWindows;
7 |
8 | namespace VueFileExplorer
9 | {
10 | class Program
11 | {
12 | static void Main(string[] args)
13 | {
14 | var window = new WebWindow(".NET Core + Vue.js file explorer");
15 | window.OnWebMessageReceived += HandleWebMessageReceived;
16 | window.NavigateToLocalFile("wwwroot/index.html");
17 | window.WaitForExit();
18 | }
19 |
20 | static void HandleWebMessageReceived(object sender, string message)
21 | {
22 | var window = (WebWindow)sender;
23 | var parsedMessage = JsonDocument.Parse(message).RootElement;
24 | switch (parsedMessage.GetProperty("command").GetString())
25 | {
26 | case "ready":
27 | ShowDirectoryInfo(window, Directory.GetCurrentDirectory());
28 | break;
29 | case "navigateTo":
30 | var basePath = parsedMessage.GetProperty("basePath").GetString();
31 | var relativePath = parsedMessage.GetProperty("relativePath").GetString();
32 | var destinationPath = Path.GetFullPath(Path.Combine(basePath, relativePath)).TrimEnd(Path.DirectorySeparatorChar);
33 | ShowDirectoryInfo(window, destinationPath);
34 | break;
35 | case "showFile":
36 | var fullName = parsedMessage.GetProperty("fullName").GetString();
37 | ShowFileContents(window, fullName);
38 | break;
39 | }
40 | }
41 |
42 | static void ShowDirectoryInfo(WebWindow window, string path)
43 | {
44 | window.Title = Path.GetFileName(path);
45 |
46 | var directoryInfo = new DirectoryInfo(path);
47 | SendCommand(window, "showDirectory", new
48 | {
49 | name = path,
50 | isRoot = Path.GetDirectoryName(path) == null,
51 | directories = directoryInfo.GetDirectories().Select(directoryInfo => new
52 | {
53 | name = directoryInfo.Name + Path.DirectorySeparatorChar,
54 | }),
55 | files = directoryInfo.GetFiles().Select(fileInfo => new
56 | {
57 | name = fileInfo.Name,
58 | size = fileInfo.Length,
59 | fullName = fileInfo.FullName,
60 | }),
61 | });
62 | }
63 |
64 | private static void ShowFileContents(WebWindow window, string fullName)
65 | {
66 | var fileInfo = new FileInfo(fullName);
67 | SendCommand(window, "showFile", null); // Clear the old display first
68 | SendCommand(window, "showFile", new
69 | {
70 | name = fileInfo.Name,
71 | size = fileInfo.Length,
72 | fullName = fileInfo.FullName,
73 | text = ReadTextFile(fullName, maxChars: 100000),
74 | });
75 | }
76 |
77 | private static string ReadTextFile(string fullName, int maxChars)
78 | {
79 | var stringBuilder = new StringBuilder();
80 | var buffer = new char[4096];
81 | using (var file = File.OpenText(fullName))
82 | {
83 | int charsRead = int.MaxValue;
84 | while (maxChars > 0 && charsRead > 0)
85 | {
86 | charsRead = file.ReadBlock(buffer, 0, Math.Min(maxChars, buffer.Length));
87 | stringBuilder.Append(buffer, 0, charsRead);
88 | maxChars -= charsRead;
89 | }
90 |
91 | return stringBuilder.ToString();
92 | }
93 | }
94 |
95 | static void SendCommand(WebWindow window, string commandName, object arg)
96 | {
97 | window.SendMessage(JsonSerializer.Serialize(new { command = commandName, arg = arg }));
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/samples/VueFileExplorer/VueFileExplorer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | netcoreapp3.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | PreserveNewest
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/samples/VueFileExplorer/wwwroot/app.js:
--------------------------------------------------------------------------------
1 | var app = new Vue({
2 | el: '#app',
3 | data: {
4 | directoryInfo: null,
5 | fileInfo: null
6 | },
7 | methods: {
8 | navigateTo: function (relativePath, event) {
9 | event.preventDefault();
10 | window.external.sendMessage(JSON.stringify({
11 | command: 'navigateTo',
12 | basePath: app.directoryInfo.name,
13 | relativePath: relativePath
14 | }));
15 | },
16 | showFile: function (fullName, event) {
17 | event.preventDefault();
18 | window.external.sendMessage(JSON.stringify({
19 | command: 'showFile',
20 | fullName: fullName
21 | }));
22 | }
23 | }
24 | });
25 |
26 | window.external.receiveMessage(function (messageJson) {
27 | var message = JSON.parse(messageJson);
28 | switch (message.command) {
29 | case 'showDirectory':
30 | app.directoryInfo = message.arg;
31 | break;
32 | case 'showFile':
33 | app.fileInfo = message.arg;
34 | break;
35 | }
36 | });
37 |
38 | window.external.sendMessage(JSON.stringify({ command: 'ready' }));
39 |
--------------------------------------------------------------------------------
/samples/VueFileExplorer/wwwroot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
22 |
23 |
24 |
{{ fileInfo.name }} ({{ fileInfo.size }} bytes)
25 |
{{ fileInfo.text }}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/samples/VueFileExplorer/wwwroot/styles.css:
--------------------------------------------------------------------------------
1 | html, body
2 | {
3 | margin: 0;
4 | padding: 0;
5 | height: 100%;
6 | }
7 |
8 | body {
9 | display: flex;
10 | font-family: Arial, Helvetica, sans-serif;
11 | }
12 |
13 | #app {
14 | flex-grow: 1;
15 | display: flex;
16 | overflow: hidden;
17 | }
18 |
19 | .sidebar {
20 | width: 30%; height: 100%;
21 | overflow-y: scroll;
22 | overflow-x: hidden;
23 | }
24 |
25 | .viewer {
26 | width: 70%;
27 | box-shadow: inset 0 0 4px rgba(0,0,0,0.3);
28 | overflow-y: scroll;
29 | overflow-x: auto;
30 | padding: 1rem 2rem;
31 | }
32 |
33 | .entries {
34 | margin: 0;
35 | padding: 0;
36 | }
37 |
38 | .entries a {
39 | display: block;
40 | background-color: #eee;
41 | margin: 0 0.2rem 0.2rem;
42 | padding: 0.3rem 1rem;
43 | border-radius: 2px;
44 | overflow: hidden;
45 | text-overflow: ellipsis;
46 | }
47 |
48 | .entries a:hover {
49 | background-color: #448844;
50 | color: white;
51 | }
52 |
53 | .file-contents {
54 | white-space: pre;
55 | font-family: monospace;
56 | }
57 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/HowToUpdateUpstreamFiles.md:
--------------------------------------------------------------------------------
1 | ## Updating the upstream/aspnetcore/web.js directory
2 |
3 | The contents of this directory come from https://github.com/aspnet/AspNetCore repo. I didn't want to use a real git submodule because that's such a giant repo, and I only need a few files from it here. So instead I used the `git read-tree` technique described at https://stackoverflow.com/a/30386041
4 |
5 | One-time setup per working copy:
6 |
7 | git remote add -t master --no-tags aspnetcore https://github.com/aspnet/AspNetCore.git
8 |
9 | Then, to update the contents of upstream/aspnetcore/web.js to the latest:
10 |
11 | cd
12 | git rm -rf upstream/aspnetcore
13 | git fetch --depth 1 aspnetcore
14 | git read-tree --prefix=src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js -u aspnetcore/master:src/Components/Web.JS
15 | git commit -m "Get Web.JS files from commit a294d64a45f"
16 |
17 | When using these commands, replace:
18 |
19 | * `master` with the branch you want to fetch from
20 | * `a294d64a45f` with the SHA of the commit you're fetching from
21 |
22 | Longer term, we may consider publishing Components.Web.JS as a NuGet package
23 | with embedded .ts sources, so that it's possible to use inside a WebPack build
24 | without needing to clone its sources.
25 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/WebWindow.Blazor.JS.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | true
6 | Latest
7 | ${DefaultItemExcludes};node_modules\**
8 | false
9 |
10 |
11 | true
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "components.desktop.client",
3 | "private": true,
4 | "version": "0.0.1",
5 | "description": "",
6 | "main": "index.js",
7 | "scripts": {
8 | "build:debug": "webpack --mode development",
9 | "build:production": "webpack --mode production",
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "devDependencies": {
13 | "@dotnet/jsinterop": "3.0.0-preview9.19423.4",
14 | "@types/base64-arraybuffer": "^0.1.0",
15 | "base64-arraybuffer": "^0.1.5",
16 | "ts-loader": "^4.4.1",
17 | "tsconfig-paths-webpack-plugin": "^3.2.0",
18 | "typescript": "^3.5.3",
19 | "webpack": "^4.36.1",
20 | "webpack-cli": "^3.3.6"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/src/Boot.Desktop.ts:
--------------------------------------------------------------------------------
1 | import '@dotnet/jsinterop/dist/Microsoft.JSInterop';
2 | import '@browserjs/GlobalExports';
3 | import { OutOfProcessRenderBatch } from '@browserjs/Rendering/RenderBatch/OutOfProcessRenderBatch';
4 | import { setEventDispatcher } from '@browserjs/Rendering/RendererEventDispatcher';
5 | import { internalFunctions as navigationManagerFunctions } from '@browserjs/Services/NavigationManager';
6 | import { renderBatch } from '@browserjs/Rendering/Renderer';
7 | import { decode } from 'base64-arraybuffer';
8 | import * as ipc from './IPC';
9 |
10 | function boot() {
11 | setEventDispatcher((eventDescriptor, eventArgs) => DotNet.invokeMethodAsync('WebWindow.Blazor', 'DispatchEvent', eventDescriptor, JSON.stringify(eventArgs)));
12 | navigationManagerFunctions.listenForNavigationEvents((uri: string, intercepted: boolean) => {
13 | return DotNet.invokeMethodAsync('WebWindow.Blazor', 'NotifyLocationChanged', uri, intercepted);
14 | });
15 |
16 | // Configure the mechanism for JS<->NET calls
17 | DotNet.attachDispatcher({
18 | beginInvokeDotNetFromJS: (callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string) => {
19 | ipc.send('BeginInvokeDotNetFromJS', [callId ? callId.toString() : null, assemblyName, methodIdentifier, dotNetObjectId || 0, argsJson]);
20 | },
21 | endInvokeJSFromDotNet: (callId: number, succeeded: boolean, resultOrError: any) => {
22 | ipc.send('EndInvokeJSFromDotNet', [callId, succeeded, resultOrError]);
23 | }
24 | });
25 |
26 | navigationManagerFunctions.enableNavigationInterception();
27 |
28 | ipc.on('JS.BeginInvokeJS', (asyncHandle, identifier, argsJson) => {
29 | DotNet.jsCallDispatcher.beginInvokeJSFromDotNet(asyncHandle, identifier, argsJson);
30 | });
31 |
32 | ipc.on('JS.EndInvokeDotNet', (callId, success, resultOrError) => {
33 | DotNet.jsCallDispatcher.endInvokeDotNetFromJS(callId, success, resultOrError);
34 | });
35 |
36 | ipc.on('JS.RenderBatch', (rendererId, batchBase64) => {
37 | var batchData = new Uint8Array(decode(batchBase64));
38 | renderBatch(rendererId, new OutOfProcessRenderBatch(batchData));
39 | });
40 |
41 | ipc.on('JS.Error', (message) => {
42 | console.error(message);
43 | });
44 |
45 | // Confirm that the JS side is ready for the app to start
46 | ipc.send('components:init', [
47 | navigationManagerFunctions.getLocationHref().replace(/\/index\.html$/, ''),
48 | navigationManagerFunctions.getBaseURI()]);
49 | }
50 |
51 | boot();
52 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/src/IPC.ts:
--------------------------------------------------------------------------------
1 | interface Callback {
2 | (...args: any[]): void;
3 | }
4 |
5 | const registrations = {} as { [eventName: string]: Callback[] };
6 |
7 | export function on(eventName: string, callback: Callback): void {
8 | if (!(eventName in registrations)) {
9 | registrations[eventName] = [];
10 | }
11 |
12 | registrations[eventName].push(callback);
13 | }
14 |
15 | export function off(eventName: string, callback: Callback): void {
16 | const group = registrations[eventName];
17 | const index = group.indexOf(callback);
18 | if (index >= 0) {
19 | group.splice(index, 1);
20 | }
21 | }
22 |
23 | export function once(eventName: string, callback: Callback): void {
24 | const callbackOnce: Callback = (...args: any[]) => {
25 | off(eventName, callbackOnce);
26 | callback.apply(null, args);
27 | };
28 |
29 | on(eventName, callbackOnce);
30 | }
31 |
32 | export function send(eventName: string, args: any): void {
33 | (window as any).external.sendMessage(`ipc:${eventName} ${JSON.stringify(args)}`);
34 | }
35 |
36 | (window as any).external.receiveMessage((message: string) => {
37 | const colonPos = message.indexOf(':');
38 | const eventName = message.substring(0, colonPos);
39 | const argsJson = message.substr(colonPos + 1);
40 |
41 | const group = registrations[eventName];
42 | if (group) {
43 | const args: any[] = JSON.parse(argsJson);
44 | group.forEach(callback => callback.apply(null, args));
45 | }
46 | });
47 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./upstream/aspnetcore/web.js/src/tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@browserjs/*": [ "./upstream/aspnetcore/web.js/src/*" ]
7 | }
8 | },
9 | "include": [
10 | "src/**/*"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/Debug/
3 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/.npmrc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteveSandersonMS/WebWindow/ccdf6e7dda39fe98c7d9fec7f848c8fd8edd682d/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/.npmrc
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/Microsoft.AspNetCore.Components.Web.JS.npmproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | true
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/jest.config.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | module.exports = {
5 | globals: {
6 | "ts-jest": {
7 | "tsConfig": "./tests/tsconfig.json",
8 | "babeConfig": true,
9 | "diagnostics": true
10 | }
11 | },
12 | preset: 'ts-jest',
13 | testEnvironment: 'jsdom'
14 | };
15 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "microsoft.aspnetcore.components.web.js",
3 | "private": true,
4 | "version": "0.0.1",
5 | "description": "",
6 | "main": "index.js",
7 | "scripts": {
8 | "preclean": "yarn install --mutex network",
9 | "clean": "node node_modules/rimraf/bin.js ./dist/Debug ./dist/Release",
10 | "prebuild": "yarn run clean && yarn install --mutex network",
11 | "build": "yarn run build:debug && yarn run build:production",
12 | "build:debug": "cd src && node ../node_modules/webpack-cli/bin/cli.js --mode development --config ./webpack.config.js",
13 | "build:production": "cd src && node ../node_modules/webpack-cli/bin/cli.js --mode production --config ./webpack.config.js",
14 | "test": "jest"
15 | },
16 | "devDependencies": {
17 | "@aspnet/signalr": "link:../../SignalR/clients/ts/signalr",
18 | "@aspnet/signalr-protocol-msgpack": "link:../../SignalR/clients/ts/signalr-protocol-msgpack",
19 | "@dotnet/jsinterop": "https://dotnet.myget.org/F/aspnetcore-dev/npm/@dotnet/jsinterop/-/@dotnet/jsinterop-3.0.0-preview9.19415.3.tgz",
20 | "@types/emscripten": "0.0.31",
21 | "@types/jest": "^24.0.6",
22 | "@types/jsdom": "11.0.6",
23 | "@typescript-eslint/eslint-plugin": "^1.5.0",
24 | "@typescript-eslint/parser": "^1.5.0",
25 | "eslint": "^5.16.0",
26 | "jest": "^24.8.0",
27 | "rimraf": "^2.6.2",
28 | "ts-jest": "^24.0.0",
29 | "ts-loader": "^4.4.1",
30 | "typescript": "^3.5.3",
31 | "webpack": "^4.36.1",
32 | "webpack-cli": "^3.3.6"
33 | },
34 | "resolutions": {
35 | "**/set-value": "^2.0.1"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser
3 | plugins: ['@typescript-eslint'],
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
7 | ],
8 | env: {
9 | browser: true,
10 | es6: true,
11 | },
12 | rules: {
13 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
14 | // e.g. "@typescript-eslint/explicit-function-return-type": "off",
15 | "@typescript-eslint/indent": ["error", 2],
16 | "@typescript-eslint/no-use-before-define": [ "off" ],
17 | "@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }],
18 | "no-var": "error",
19 | "prefer-const": "error",
20 | "quotes": ["error", "single", { "avoidEscape": true }],
21 | "semi": ["error", "always"],
22 | "semi-style": ["error", "last"],
23 | "semi-spacing": ["error", { "after": true }],
24 | "spaced-comment": ["error", "always"],
25 | "unicode-bom": ["error", "never"],
26 | "brace-style": ["error", "1tbs"],
27 | "comma-dangle": ["error", {
28 | "arrays": "always-multiline",
29 | "objects": "always-multiline",
30 | "imports": "always-multiline",
31 | "exports": "always-multiline",
32 | "functions": "ignore"
33 | }],
34 | "comma-style": ["error", "last"],
35 | "comma-spacing": ["error", { "after": true }],
36 | "no-trailing-spaces": ["error"],
37 | "curly": ["error"],
38 | "dot-location": ["error", "property"],
39 | "eqeqeq": ["error", "always"],
40 | "no-eq-null": ["error"],
41 | "no-multi-spaces": ["error"],
42 | "no-unused-labels": ["error"],
43 | "require-await": ["error"],
44 | "array-bracket-newline": ["error", { "multiline": true, "minItems": 4 }],
45 | "array-bracket-spacing": ["error", "never"],
46 | "array-element-newline": ["error", { "minItems": 3 }],
47 | "block-spacing": ["error"],
48 | "func-call-spacing": ["error", "never"],
49 | "function-paren-newline": ["error", "multiline"],
50 | "key-spacing": ["error", { "mode": "strict" }],
51 | "keyword-spacing": ["error", { "before": true }],
52 | "lines-between-class-members": ["error", "always"],
53 | "new-parens": ["error"],
54 | "no-multi-assign": ["error"],
55 | "no-multiple-empty-lines": ["error"],
56 | "no-unneeded-ternary": ["error"],
57 | "no-whitespace-before-property": ["error"],
58 | "one-var": ["error", "never"],
59 | "space-before-function-paren": ["error", {
60 | "anonymous": "never",
61 | "named": "never",
62 | "asyncArrow": "always"
63 | }],
64 | "space-in-parens": ["error", "never"],
65 | "space-infix-ops": ["error"]
66 |
67 | },
68 | globals: {
69 | DotNet: "readonly"
70 | }
71 | };
72 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Boot.Server.ts:
--------------------------------------------------------------------------------
1 | import '@dotnet/jsinterop';
2 | import './GlobalExports';
3 | import * as signalR from '@aspnet/signalr';
4 | import { MessagePackHubProtocol } from '@aspnet/signalr-protocol-msgpack';
5 | import { showErrorNotification } from './BootErrors';
6 | import { shouldAutoStart } from './BootCommon';
7 | import { RenderQueue } from './Platform/Circuits/RenderQueue';
8 | import { ConsoleLogger } from './Platform/Logging/Loggers';
9 | import { LogLevel, Logger } from './Platform/Logging/Logger';
10 | import { discoverComponents, CircuitDescriptor } from './Platform/Circuits/CircuitManager';
11 | import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
12 | import { resolveOptions, BlazorOptions } from './Platform/Circuits/BlazorOptions';
13 | import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
14 | import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
15 |
16 | let renderingFailed = false;
17 | let started = false;
18 |
19 | async function boot(userOptions?: Partial): Promise {
20 | if (started) {
21 | throw new Error('Blazor has already started.');
22 | }
23 | started = true;
24 |
25 | // Establish options to be used
26 | const options = resolveOptions(userOptions);
27 | const logger = new ConsoleLogger(options.logLevel);
28 | window['Blazor'].defaultReconnectionHandler = new DefaultReconnectionHandler(logger);
29 | options.reconnectionHandler = options.reconnectionHandler || window['Blazor'].defaultReconnectionHandler;
30 | logger.log(LogLevel.Information, 'Starting up blazor server-side application.');
31 |
32 | const components = discoverComponents(document);
33 | const circuit = new CircuitDescriptor(components);
34 |
35 | const initialConnection = await initializeConnection(options, logger, circuit);
36 | const circuitStarted = await circuit.startCircuit(initialConnection);
37 | if (!circuitStarted) {
38 | logger.log(LogLevel.Error, 'Failed to start the circuit.');
39 | return;
40 | }
41 |
42 | const reconnect = async (existingConnection?: signalR.HubConnection): Promise => {
43 | if (renderingFailed) {
44 | // We can't reconnect after a failure, so exit early.
45 | return false;
46 | }
47 |
48 | const reconnection = existingConnection || await initializeConnection(options, logger, circuit);
49 | if (!(await circuit.reconnect(reconnection))) {
50 | logger.log(LogLevel.Information, 'Reconnection attempt to the circuit was rejected by the server. This may indicate that the associated state is no longer available on the server.');
51 | return false;
52 | }
53 |
54 | options.reconnectionHandler!.onConnectionUp();
55 |
56 | return true;
57 | };
58 |
59 | window.addEventListener(
60 | 'unload',
61 | () => {
62 | const data = new FormData();
63 | const circuitId = circuit.circuitId!;
64 | data.append('circuitId', circuitId);
65 | navigator.sendBeacon('_blazor/disconnect', data);
66 | },
67 | false
68 | );
69 |
70 | window['Blazor'].reconnect = reconnect;
71 |
72 | logger.log(LogLevel.Information, 'Blazor server-side application started.');
73 | }
74 |
75 | async function initializeConnection(options: BlazorOptions, logger: Logger, circuit: CircuitDescriptor): Promise {
76 | const hubProtocol = new MessagePackHubProtocol();
77 | (hubProtocol as unknown as { name: string }).name = 'blazorpack';
78 |
79 | const connectionBuilder = new signalR.HubConnectionBuilder()
80 | .withUrl('_blazor')
81 | .withHubProtocol(hubProtocol);
82 |
83 | options.configureSignalR(connectionBuilder);
84 |
85 | const connection = connectionBuilder.build();
86 |
87 | setEventDispatcher((descriptor, args) => {
88 | return connection.send('DispatchBrowserEvent', JSON.stringify(descriptor), JSON.stringify(args));
89 | });
90 |
91 | // Configure navigation via SignalR
92 | window['Blazor']._internal.navigationManager.listenForNavigationEvents((uri: string, intercepted: boolean): Promise => {
93 | return connection.send('OnLocationChanged', uri, intercepted);
94 | });
95 |
96 | connection.on('JS.AttachComponent', (componentId, selector) => attachRootComponentToLogicalElement(0, circuit.resolveElement(selector), componentId));
97 | connection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet);
98 | connection.on('JS.EndInvokeDotNet', (args: string) => DotNet.jsCallDispatcher.endInvokeDotNetFromJS(...(JSON.parse(args) as [string, boolean, unknown])));
99 |
100 | const renderQueue = RenderQueue.getOrCreate(logger);
101 | connection.on('JS.RenderBatch', (batchId: number, batchData: Uint8Array) => {
102 | logger.log(LogLevel.Debug, `Received render batch with id ${batchId} and ${batchData.byteLength} bytes.`);
103 | renderQueue.processBatch(batchId, batchData, connection);
104 | });
105 |
106 | connection.onclose(error => !renderingFailed && options.reconnectionHandler!.onConnectionDown(options.reconnectionOptions, error));
107 | connection.on('JS.Error', error => {
108 | renderingFailed = true;
109 | unhandledError(connection, error, logger);
110 | showErrorNotification();
111 | });
112 |
113 | window['Blazor']._internal.forceCloseConnection = () => connection.stop();
114 |
115 | try {
116 | await connection.start();
117 | } catch (ex) {
118 | unhandledError(connection, ex, logger);
119 | }
120 |
121 | DotNet.attachDispatcher({
122 | beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson): void => {
123 | connection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, dotNetObjectId || 0, argsJson);
124 | },
125 | endInvokeJSFromDotNet: (asyncHandle, succeeded, argsJson): void => {
126 | connection.send('EndInvokeJSFromDotNet', asyncHandle, succeeded, argsJson);
127 | },
128 | });
129 |
130 | return connection;
131 | }
132 |
133 | function unhandledError(connection: signalR.HubConnection, err: Error, logger: Logger): void {
134 | logger.log(LogLevel.Error, err);
135 |
136 | // Disconnect on errors.
137 | //
138 | // Trying to call methods on the connection after its been closed will throw.
139 | if (connection) {
140 | connection.stop();
141 | }
142 | }
143 |
144 | window['Blazor'].start = boot;
145 |
146 | if (shouldAutoStart()) {
147 | boot();
148 | }
149 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Boot.WebAssembly.ts:
--------------------------------------------------------------------------------
1 | import '@dotnet/jsinterop';
2 | import './GlobalExports';
3 | import * as Environment from './Environment';
4 | import { monoPlatform } from './Platform/Mono/MonoPlatform';
5 | import { getAssemblyNameFromUrl } from './Platform/Url';
6 | import { renderBatch } from './Rendering/Renderer';
7 | import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch';
8 | import { Pointer } from './Platform/Platform';
9 | import { shouldAutoStart } from './BootCommon';
10 | import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
11 |
12 | let started = false;
13 |
14 | async function boot(options?: any): Promise {
15 |
16 | if (started) {
17 | throw new Error('Blazor has already started.');
18 | }
19 | started = true;
20 |
21 | setEventDispatcher((eventDescriptor, eventArgs) => DotNet.invokeMethodAsync('Microsoft.AspNetCore.Blazor', 'DispatchEvent', eventDescriptor, JSON.stringify(eventArgs)));
22 |
23 | // Configure environment for execution under Mono WebAssembly with shared-memory rendering
24 | const platform = Environment.setPlatform(monoPlatform);
25 | window['Blazor'].platform = platform;
26 | window['Blazor']._internal.renderBatch = (browserRendererId: number, batchAddress: Pointer) => {
27 | renderBatch(browserRendererId, new SharedMemoryRenderBatch(batchAddress));
28 | };
29 |
30 | // Configure navigation via JS Interop
31 | window['Blazor']._internal.navigationManager.listenForNavigationEvents(async (uri: string, intercepted: boolean): Promise => {
32 | await DotNet.invokeMethodAsync(
33 | 'Microsoft.AspNetCore.Blazor',
34 | 'NotifyLocationChanged',
35 | uri,
36 | intercepted
37 | );
38 | });
39 |
40 | // Fetch the boot JSON file
41 | const bootConfig = await fetchBootConfigAsync();
42 | const embeddedResourcesPromise = loadEmbeddedResourcesAsync(bootConfig);
43 |
44 | if (!bootConfig.linkerEnabled) {
45 | console.info('Blazor is running in dev mode without IL stripping. To make the bundle size significantly smaller, publish the application or see https://go.microsoft.com/fwlink/?linkid=870414');
46 | }
47 |
48 | // Determine the URLs of the assemblies we want to load, then begin fetching them all
49 | const loadAssemblyUrls = [bootConfig.main]
50 | .concat(bootConfig.assemblyReferences)
51 | .map(filename => `_framework/_bin/${filename}`);
52 |
53 | try {
54 | await platform.start(loadAssemblyUrls);
55 | } catch (ex) {
56 | throw new Error(`Failed to start platform. Reason: ${ex}`);
57 | }
58 |
59 | // Before we start running .NET code, be sure embedded content resources are all loaded
60 | await embeddedResourcesPromise;
61 |
62 | // Start up the application
63 | const mainAssemblyName = getAssemblyNameFromUrl(bootConfig.main);
64 | platform.callEntryPoint(mainAssemblyName, bootConfig.entryPoint, []);
65 | }
66 |
67 | async function fetchBootConfigAsync() {
68 | // Later we might make the location of this configurable (e.g., as an attribute on the
2 | export function shouldAutoStart() {
3 | return !!(document &&
4 | document.currentScript &&
5 | document.currentScript.getAttribute('autostart') !== 'false');
6 | }
7 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/BootErrors.ts:
--------------------------------------------------------------------------------
1 | let hasFailed = false;
2 |
3 | export async function showErrorNotification() {
4 | let errorUi = document.querySelector('#blazor-error-ui') as HTMLElement;
5 | if (errorUi) {
6 | errorUi.style.display = 'block';
7 | }
8 |
9 | if (!hasFailed) {
10 | hasFailed = true;
11 | const errorUiReloads = document.querySelectorAll('#blazor-error-ui .reload');
12 | errorUiReloads.forEach(reload => {
13 | reload.onclick = function (e) {
14 | location.reload();
15 | e.preventDefault();
16 | };
17 | });
18 |
19 | let errorUiDismiss = document.querySelectorAll('#blazor-error-ui .dismiss');
20 | errorUiDismiss.forEach(dismiss => {
21 | dismiss.onclick = function (e) {
22 | const errorUi = document.querySelector('#blazor-error-ui');
23 | if (errorUi) {
24 | errorUi.style.display = 'none';
25 | }
26 | e.preventDefault();
27 | };
28 | });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Environment.ts:
--------------------------------------------------------------------------------
1 | // Expose an export called 'platform' of the interface type 'Platform',
2 | // so that consumers can be agnostic about which implementation they use.
3 | // Basic alternative to having an actual DI container.
4 | import { Platform } from './Platform/Platform';
5 |
6 | export let platform: Platform;
7 |
8 | export function setPlatform(platformInstance: Platform) {
9 | platform = platformInstance;
10 | return platform;
11 | }
12 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/GlobalExports.ts:
--------------------------------------------------------------------------------
1 | import { navigateTo, internalFunctions as navigationManagerInternalFunctions } from './Services/NavigationManager';
2 | import { attachRootComponentToElement } from './Rendering/Renderer';
3 |
4 | // Make the following APIs available in global scope for invocation from JS
5 | window['Blazor'] = {
6 | navigateTo,
7 |
8 | _internal: {
9 | attachRootComponentToElement,
10 | navigationManager: navigationManagerInternalFunctions,
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Platform/Circuits/BlazorOptions.ts:
--------------------------------------------------------------------------------
1 | import { LogLevel } from '../Logging/Logger';
2 |
3 | export interface BlazorOptions {
4 | configureSignalR: (builder: signalR.HubConnectionBuilder) => void;
5 | logLevel: LogLevel;
6 | reconnectionOptions: ReconnectionOptions;
7 | reconnectionHandler?: ReconnectionHandler;
8 | }
9 |
10 | export function resolveOptions(userOptions?: Partial): BlazorOptions {
11 | const result = { ...defaultOptions, ...userOptions };
12 |
13 | // The spread operator can't be used for a deep merge, so do the same for subproperties
14 | if (userOptions && userOptions.reconnectionOptions) {
15 | result.reconnectionOptions = { ...defaultOptions.reconnectionOptions, ...userOptions.reconnectionOptions };
16 | }
17 |
18 | return result;
19 | }
20 |
21 | export interface ReconnectionOptions {
22 | maxRetries: number;
23 | retryIntervalMilliseconds: number;
24 | dialogId: string;
25 | }
26 |
27 | export interface ReconnectionHandler {
28 | onConnectionDown(options: ReconnectionOptions, error?: Error): void;
29 | onConnectionUp(): void;
30 | }
31 |
32 | const defaultOptions: BlazorOptions = {
33 | configureSignalR: (_) => { },
34 | logLevel: LogLevel.Warning,
35 | reconnectionOptions: {
36 | maxRetries: 5,
37 | retryIntervalMilliseconds: 3000,
38 | dialogId: 'components-reconnect-modal',
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Platform/Circuits/DefaultReconnectDisplay.ts:
--------------------------------------------------------------------------------
1 | import { ReconnectDisplay } from './ReconnectDisplay';
2 | import { Logger, LogLevel } from '../Logging/Logger';
3 |
4 | export class DefaultReconnectDisplay implements ReconnectDisplay {
5 | modal: HTMLDivElement;
6 |
7 | message: HTMLHeadingElement;
8 |
9 | button: HTMLButtonElement;
10 |
11 | addedToDom: boolean = false;
12 |
13 | reloadParagraph: HTMLParagraphElement;
14 |
15 | constructor(dialogId: string, private readonly document: Document, private readonly logger: Logger) {
16 | this.modal = this.document.createElement('div');
17 | this.modal.id = dialogId;
18 |
19 | const modalStyles = [
20 | 'position: fixed',
21 | 'top: 0',
22 | 'right: 0',
23 | 'bottom: 0',
24 | 'left: 0',
25 | 'z-index: 1000',
26 | 'display: none',
27 | 'overflow: hidden',
28 | 'background-color: #fff',
29 | 'opacity: 0.8',
30 | 'text-align: center',
31 | 'font-weight: bold',
32 | ];
33 |
34 | this.modal.style.cssText = modalStyles.join(';');
35 | this.modal.innerHTML = 'Retry Alternatively, reload
';
36 | this.message = this.modal.querySelector('h5')!;
37 | this.button = this.modal.querySelector('button')!;
38 | this.reloadParagraph = this.modal.querySelector('p')!;
39 |
40 | this.button.addEventListener('click', async () => {
41 | this.show();
42 |
43 | try {
44 | // reconnect will asynchronously return:
45 | // - true to mean success
46 | // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
47 | // - exception to mean we didn't reach the server (this can be sync or async)
48 | const successful = await window['Blazor'].reconnect();
49 | if (!successful) {
50 | this.rejected();
51 | }
52 | } catch (err) {
53 | // We got an exception, server is currently unavailable
54 | this.logger.log(LogLevel.Error, err);
55 | this.failed();
56 | }
57 | });
58 | this.reloadParagraph.querySelector('a')!.addEventListener('click', () => location.reload());
59 | }
60 |
61 | show(): void {
62 | if (!this.addedToDom) {
63 | this.addedToDom = true;
64 | this.document.body.appendChild(this.modal);
65 | }
66 | this.modal.style.display = 'block';
67 | this.button.style.display = 'none';
68 | this.reloadParagraph.style.display = 'none';
69 | this.message.textContent = 'Attempting to reconnect to the server...';
70 | }
71 |
72 | hide(): void {
73 | this.modal.style.display = 'none';
74 | }
75 |
76 | failed(): void {
77 | this.button.style.display = 'block';
78 | this.reloadParagraph.style.display = 'none';
79 | this.message.innerHTML = 'Reconnection failed. Try reloading the page if you\'re unable to reconnect.';
80 | this.message.querySelector('a')!.addEventListener('click', () => location.reload());
81 | }
82 |
83 | rejected(): void {
84 | this.button.style.display = 'none';
85 | this.reloadParagraph.style.display = 'none';
86 | this.message.innerHTML = 'Could not reconnect to the server. Reload the page to restore functionality.';
87 | this.message.querySelector('a')!.addEventListener('click', () => location.reload());
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Platform/Circuits/DefaultReconnectionHandler.ts:
--------------------------------------------------------------------------------
1 | import { ReconnectionHandler, ReconnectionOptions } from './BlazorOptions';
2 | import { ReconnectDisplay } from './ReconnectDisplay';
3 | import { DefaultReconnectDisplay } from './DefaultReconnectDisplay';
4 | import { UserSpecifiedDisplay } from './UserSpecifiedDisplay';
5 | import { Logger, LogLevel } from '../Logging/Logger';
6 |
7 | export class DefaultReconnectionHandler implements ReconnectionHandler {
8 | private readonly _logger: Logger;
9 | private readonly _reconnectCallback: () => Promise;
10 | private _currentReconnectionProcess: ReconnectionProcess | null = null;
11 | private _reconnectionDisplay?: ReconnectDisplay;
12 |
13 | constructor(logger: Logger, overrideDisplay?: ReconnectDisplay, reconnectCallback?: () => Promise) {
14 | this._logger = logger;
15 | this._reconnectionDisplay = overrideDisplay;
16 | this._reconnectCallback = reconnectCallback || (() => window['Blazor'].reconnect());
17 | }
18 |
19 | onConnectionDown (options: ReconnectionOptions, error?: Error) {
20 | if (!this._reconnectionDisplay) {
21 | const modal = document.getElementById(options.dialogId);
22 | this._reconnectionDisplay = modal
23 | ? new UserSpecifiedDisplay(modal)
24 | : new DefaultReconnectDisplay(options.dialogId, document, this._logger);
25 | }
26 |
27 | if (!this._currentReconnectionProcess) {
28 | this._currentReconnectionProcess = new ReconnectionProcess(options, this._logger, this._reconnectCallback, this._reconnectionDisplay!);
29 | }
30 | }
31 |
32 | onConnectionUp() {
33 | if (this._currentReconnectionProcess) {
34 | this._currentReconnectionProcess.dispose();
35 | this._currentReconnectionProcess = null;
36 | }
37 | }
38 | };
39 |
40 | class ReconnectionProcess {
41 | readonly reconnectDisplay: ReconnectDisplay;
42 | isDisposed = false;
43 |
44 | constructor(options: ReconnectionOptions, private logger: Logger, private reconnectCallback: () => Promise, display: ReconnectDisplay) {
45 | this.reconnectDisplay = display;
46 | this.reconnectDisplay.show();
47 | this.attemptPeriodicReconnection(options);
48 | }
49 |
50 | public dispose() {
51 | this.isDisposed = true;
52 | this.reconnectDisplay.hide();
53 | }
54 |
55 | async attemptPeriodicReconnection(options: ReconnectionOptions) {
56 | for (let i = 0; i < options.maxRetries; i++) {
57 | await this.delay(options.retryIntervalMilliseconds);
58 | if (this.isDisposed) {
59 | break;
60 | }
61 |
62 | try {
63 | // reconnectCallback will asynchronously return:
64 | // - true to mean success
65 | // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
66 | // - exception to mean we didn't reach the server (this can be sync or async)
67 | const result = await this.reconnectCallback();
68 | if (!result) {
69 | // If the server responded and refused to reconnect, stop auto-retrying.
70 | this.reconnectDisplay.rejected();
71 | return;
72 | }
73 | return;
74 | } catch (err) {
75 | // We got an exception so will try again momentarily
76 | this.logger.log(LogLevel.Error, err);
77 | }
78 | }
79 |
80 | this.reconnectDisplay.failed();
81 | }
82 |
83 | delay(durationMilliseconds: number): Promise {
84 | return new Promise(resolve => setTimeout(resolve, durationMilliseconds));
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Platform/Circuits/ReconnectDisplay.ts:
--------------------------------------------------------------------------------
1 | export interface ReconnectDisplay {
2 | show(): void;
3 | hide(): void;
4 | failed(): void;
5 | rejected(): void;
6 | }
7 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Platform/Circuits/RenderQueue.ts:
--------------------------------------------------------------------------------
1 | import { renderBatch } from '../../Rendering/Renderer';
2 | import { OutOfProcessRenderBatch } from '../../Rendering/RenderBatch/OutOfProcessRenderBatch';
3 | import { Logger, LogLevel } from '../Logging/Logger';
4 | import { HubConnection } from '@aspnet/signalr';
5 |
6 | export class RenderQueue {
7 | private static instance: RenderQueue;
8 |
9 | private nextBatchId = 2;
10 |
11 | private fatalError?: string;
12 |
13 | public browserRendererId: number;
14 |
15 | public logger: Logger;
16 |
17 | public constructor(browserRendererId: number, logger: Logger) {
18 | this.browserRendererId = browserRendererId;
19 | this.logger = logger;
20 | }
21 |
22 | public static getOrCreate(logger: Logger): RenderQueue {
23 | if (!RenderQueue.instance) {
24 | RenderQueue.instance = new RenderQueue(0, logger);
25 | }
26 |
27 | return this.instance;
28 | }
29 |
30 | public async processBatch(receivedBatchId: number, batchData: Uint8Array, connection: HubConnection): Promise {
31 | if (receivedBatchId < this.nextBatchId) {
32 | // SignalR delivers messages in order, but it does not guarantee that the message gets delivered.
33 | // For that reason, if the server re-sends a batch (for example during a reconnection because it didn't get an ack)
34 | // we simply acknowledge it to get back in sync with the server.
35 | await this.completeBatch(connection, receivedBatchId);
36 | this.logger.log(LogLevel.Debug, `Batch ${receivedBatchId} already processed. Waiting for batch ${this.nextBatchId}.`);
37 | return;
38 | }
39 |
40 | if (receivedBatchId > this.nextBatchId) {
41 | if (this.fatalError) {
42 | this.logger.log(LogLevel.Debug, `Received a new batch ${receivedBatchId} but errored out on a previous batch ${this.nextBatchId - 1}`);
43 | await connection.send('OnRenderCompleted', this.nextBatchId - 1, this.fatalError.toString());
44 | return;
45 | }
46 |
47 | this.logger.log(LogLevel.Debug, `Waiting for batch ${this.nextBatchId}. Batch ${receivedBatchId} not processed.`);
48 | return;
49 | }
50 |
51 | try {
52 | this.nextBatchId++;
53 | this.logger.log(LogLevel.Debug, `Applying batch ${receivedBatchId}.`);
54 | renderBatch(this.browserRendererId, new OutOfProcessRenderBatch(batchData));
55 | await this.completeBatch(connection, receivedBatchId);
56 | } catch (error) {
57 | this.fatalError = error.toString();
58 | this.logger.log(LogLevel.Error, `There was an error applying batch ${receivedBatchId}.`);
59 |
60 | // If there's a rendering exception, notify server *and* throw on client
61 | connection.send('OnRenderCompleted', receivedBatchId, error.toString());
62 | throw error;
63 | }
64 | }
65 |
66 | public getLastBatchid(): number {
67 | return this.nextBatchId - 1;
68 | }
69 |
70 | private async completeBatch(connection: signalR.HubConnection, batchId: number): Promise {
71 | try {
72 | await connection.send('OnRenderCompleted', batchId, null);
73 | } catch {
74 | this.logger.log(LogLevel.Warning, `Failed to deliver completion notification for render '${batchId}'.`);
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Platform/Circuits/UserSpecifiedDisplay.ts:
--------------------------------------------------------------------------------
1 | import { ReconnectDisplay } from './ReconnectDisplay';
2 | export class UserSpecifiedDisplay implements ReconnectDisplay {
3 | static readonly ShowClassName = 'components-reconnect-show';
4 |
5 | static readonly HideClassName = 'components-reconnect-hide';
6 |
7 | static readonly FailedClassName = 'components-reconnect-failed';
8 |
9 | static readonly RejectedClassName = 'components-reconnect-rejected';
10 |
11 | constructor(private dialog: HTMLElement) {
12 | }
13 |
14 | show(): void {
15 | this.removeClasses();
16 | this.dialog.classList.add(UserSpecifiedDisplay.ShowClassName);
17 | }
18 |
19 | hide(): void {
20 | this.removeClasses();
21 | this.dialog.classList.add(UserSpecifiedDisplay.HideClassName);
22 | }
23 |
24 | failed(): void {
25 | this.removeClasses();
26 | this.dialog.classList.add(UserSpecifiedDisplay.FailedClassName);
27 | }
28 |
29 | rejected(): void {
30 | this.removeClasses();
31 | this.dialog.classList.add(UserSpecifiedDisplay.RejectedClassName);
32 | }
33 |
34 | private removeClasses() {
35 | this.dialog.classList.remove(UserSpecifiedDisplay.ShowClassName, UserSpecifiedDisplay.HideClassName, UserSpecifiedDisplay.FailedClassName, UserSpecifiedDisplay.RejectedClassName);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Platform/Logging/Logger.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | // These values are designed to match the ASP.NET Log Levels since that's the pattern we're emulating here.
5 | /** Indicates the severity of a log message.
6 | *
7 | * Log Levels are ordered in increasing severity. So `Debug` is more severe than `Trace`, etc.
8 | */
9 | export enum LogLevel {
10 | /** Log level for very low severity diagnostic messages. */
11 | Trace = 0,
12 | /** Log level for low severity diagnostic messages. */
13 | Debug = 1,
14 | /** Log level for informational diagnostic messages. */
15 | Information = 2,
16 | /** Log level for diagnostic messages that indicate a non-fatal problem. */
17 | Warning = 3,
18 | /** Log level for diagnostic messages that indicate a failure in the current operation. */
19 | Error = 4,
20 | /** Log level for diagnostic messages that indicate a failure that will terminate the entire application. */
21 | Critical = 5,
22 | /** The highest possible log level. Used when configuring logging to indicate that no log messages should be emitted. */
23 | None = 6,
24 | }
25 |
26 | /** An abstraction that provides a sink for diagnostic messages. */
27 | export interface Logger { // eslint-disable-line @typescript-eslint/interface-name-prefix
28 | /** Called by the framework to emit a diagnostic message.
29 | *
30 | * @param {LogLevel} logLevel The severity level of the message.
31 | * @param {string} message The message.
32 | */
33 | log(logLevel: LogLevel, message: string | Error): void;
34 | }
35 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Platform/Logging/Loggers.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import { Logger, LogLevel } from './Logger';
4 |
5 | export class NullLogger implements Logger {
6 | public static instance: Logger = new NullLogger();
7 |
8 | private constructor() { }
9 |
10 | public log(_logLevel: LogLevel, _message: string): void { // eslint-disable-line @typescript-eslint/no-unused-vars
11 | }
12 | }
13 |
14 | export class ConsoleLogger implements Logger {
15 | private readonly minimumLogLevel: LogLevel;
16 |
17 | public constructor(minimumLogLevel: LogLevel) {
18 | this.minimumLogLevel = minimumLogLevel;
19 | }
20 |
21 | public log(logLevel: LogLevel, message: string | Error): void {
22 | if (logLevel >= this.minimumLogLevel) {
23 | switch (logLevel) {
24 | case LogLevel.Critical:
25 | case LogLevel.Error:
26 | console.error(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
27 | break;
28 | case LogLevel.Warning:
29 | console.warn(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
30 | break;
31 | case LogLevel.Information:
32 | console.info(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
33 | break;
34 | default:
35 | // console.debug only goes to attached debuggers in Node, so we use console.log for Trace and Debug
36 | console.log(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
37 | break;
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Platform/Mono/MonoDebugger.ts:
--------------------------------------------------------------------------------
1 | import { getAssemblyNameFromUrl, getFileNameFromUrl } from '../Url';
2 |
3 | const currentBrowserIsChrome = (window as any).chrome
4 | && navigator.userAgent.indexOf('Edge') < 0; // Edge pretends to be Chrome
5 |
6 | let hasReferencedPdbs = false;
7 |
8 | export function hasDebuggingEnabled() {
9 | return hasReferencedPdbs && currentBrowserIsChrome;
10 | }
11 |
12 | export function attachDebuggerHotkey(loadAssemblyUrls: string[]) {
13 | hasReferencedPdbs = loadAssemblyUrls
14 | .some(url => /\.pdb$/.test(getFileNameFromUrl(url)));
15 |
16 | // Use the combination shift+alt+D because it isn't used by the major browsers
17 | // for anything else by default
18 | const altKeyName = navigator.platform.match(/^Mac/i) ? 'Cmd' : 'Alt';
19 | if (hasDebuggingEnabled()) {
20 | console.info(`Debugging hotkey: Shift+${altKeyName}+D (when application has focus)`);
21 | }
22 |
23 | // Even if debugging isn't enabled, we register the hotkey so we can report why it's not enabled
24 | document.addEventListener('keydown', evt => {
25 | if (evt.shiftKey && (evt.metaKey || evt.altKey) && evt.code === 'KeyD') {
26 | if (!hasReferencedPdbs) {
27 | console.error('Cannot start debugging, because the application was not compiled with debugging enabled.');
28 | } else if (!currentBrowserIsChrome) {
29 | console.error('Currently, only Edge(Chromium) or Chrome is supported for debugging.');
30 | } else {
31 | launchDebugger();
32 | }
33 | }
34 | });
35 | }
36 |
37 | function launchDebugger() {
38 | // The noopener flag is essential, because otherwise Chrome tracks the association with the
39 | // parent tab, and then when the parent tab pauses in the debugger, the child tab does so
40 | // too (even if it's since navigated to a different page). This means that the debugger
41 | // itself freezes, and not just the page being debugged.
42 | //
43 | // We have to construct a link element and simulate a click on it, because the more obvious
44 | // window.open(..., 'noopener') always opens a new window instead of a new tab.
45 | const link = document.createElement('a');
46 | link.href = `_framework/debug?url=${encodeURIComponent(location.href)}`;
47 | link.target = '_blank';
48 | link.rel = 'noopener noreferrer';
49 | link.click();
50 | }
51 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Platform/Mono/MonoTypes.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace Module {
2 | function UTF8ToString(utf8: Mono.Utf8Ptr): string;
3 | var preloadPlugins: any[];
4 |
5 | function stackSave(): Mono.StackSaveHandle;
6 | function stackAlloc(length: number): number;
7 | function stackRestore(handle: Mono.StackSaveHandle): void;
8 |
9 | // These should probably be in @types/emscripten
10 | function FS_createPath(parent, path, canRead, canWrite);
11 | function FS_createDataFile(parent, name, data, canRead, canWrite, canOwn);
12 | }
13 |
14 | // Emscripten declares these globals
15 | declare const addRunDependency: any;
16 | declare const removeRunDependency: any;
17 |
18 | declare namespace Mono {
19 | interface Utf8Ptr { Utf8Ptr__DO_NOT_IMPLEMENT: any }
20 | interface StackSaveHandle { StackSaveHandle__DO_NOT_IMPLEMENT: any }
21 | }
22 |
23 | // Mono uses this global to hang various debugging-related items on
24 | declare namespace MONO {
25 | var loaded_files: string[];
26 | var mono_wasm_runtime_is_ready: boolean;
27 | function mono_wasm_setenv (name: string, value: string): void;
28 | }
29 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Platform/Platform.ts:
--------------------------------------------------------------------------------
1 | export interface Platform {
2 | start(loadAssemblyUrls: string[]): Promise;
3 |
4 | callEntryPoint(assemblyName: string, entrypointMethod: string, args: (System_Object | null)[]);
5 | findMethod(assemblyName: string, namespace: string, className: string, methodName: string): MethodHandle;
6 | callMethod(method: MethodHandle, target: System_Object | null, args: (System_Object | null)[]): System_Object;
7 |
8 | toJavaScriptString(dotNetString: System_String): string;
9 | toDotNetString(javaScriptString: string): System_String;
10 |
11 | toUint8Array(array: System_Array): Uint8Array;
12 |
13 | getArrayLength(array: System_Array): number;
14 | getArrayEntryPtr(array: System_Array, index: number, itemSize: number): TPtr;
15 |
16 | getObjectFieldsBaseAddress(referenceTypedObject: System_Object): Pointer;
17 | readInt16Field(baseAddress: Pointer, fieldOffset?: number): number;
18 | readInt32Field(baseAddress: Pointer, fieldOffset?: number): number;
19 | readUint64Field(baseAddress: Pointer, fieldOffset?: number): number;
20 | readFloatField(baseAddress: Pointer, fieldOffset?: number): number;
21 | readObjectField(baseAddress: Pointer, fieldOffset?: number): T;
22 | readStringField(baseAddress: Pointer, fieldOffset?: number): string | null;
23 | readStructField(baseAddress: Pointer, fieldOffset?: number): T;
24 | }
25 |
26 | // We don't actually instantiate any of these at runtime. For perf it's preferable to
27 | // use the original 'number' instances without any boxing. The definitions are just
28 | // for compile-time checking, since TypeScript doesn't support nominal types.
29 | export interface MethodHandle { MethodHandle__DO_NOT_IMPLEMENT: any }
30 | export interface System_Object { System_Object__DO_NOT_IMPLEMENT: any }
31 | export interface System_String extends System_Object { System_String__DO_NOT_IMPLEMENT: any }
32 | export interface System_Array extends System_Object { System_Array__DO_NOT_IMPLEMENT: any }
33 | export interface Pointer { Pointer__DO_NOT_IMPLEMENT: any }
34 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Platform/Url.ts:
--------------------------------------------------------------------------------
1 | export function getFileNameFromUrl(url: string) {
2 | // This could also be called "get last path segment from URL", but the primary
3 | // use case is to extract things that look like filenames
4 | const lastSegment = url.substring(url.lastIndexOf('/') + 1);
5 | const queryStringStartPos = lastSegment.indexOf('?');
6 | return queryStringStartPos < 0 ? lastSegment : lastSegment.substring(0, queryStringStartPos);
7 | }
8 |
9 | export function getAssemblyNameFromUrl(url: string) {
10 | return getFileNameFromUrl(url).replace(/\.dll$/, '');
11 | }
12 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Rendering/ElementReferenceCapture.ts:
--------------------------------------------------------------------------------
1 | export function applyCaptureIdToElement(element: Element, referenceCaptureId: string) {
2 | element.setAttribute(getCaptureIdAttributeName(referenceCaptureId), '');
3 | }
4 |
5 | function getElementByCaptureId(referenceCaptureId: string) {
6 | const selector = `[${getCaptureIdAttributeName(referenceCaptureId)}]`;
7 | return document.querySelector(selector);
8 | }
9 |
10 | function getCaptureIdAttributeName(referenceCaptureId: string) {
11 | return `_bl_${referenceCaptureId}`;
12 | }
13 |
14 | // Support receiving ElementRef instances as args in interop calls
15 | const elementRefKey = '__internalId'; // Keep in sync with ElementRef.cs
16 | DotNet.attachReviver((key, value) => {
17 | if (value && typeof value === 'object' && value.hasOwnProperty(elementRefKey) && typeof value[elementRefKey] === 'string') {
18 | return getElementByCaptureId(value[elementRefKey]);
19 | } else {
20 | return value;
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Rendering/EventFieldInfo.ts:
--------------------------------------------------------------------------------
1 | export class EventFieldInfo {
2 | constructor(public componentId: number, public fieldValue: string | boolean) {
3 | }
4 |
5 | public static fromEvent(componentId: number, event: Event): EventFieldInfo | null {
6 | const elem = event.target;
7 | if (elem instanceof Element) {
8 | const fieldData = getFormFieldData(elem);
9 | if (fieldData) {
10 | return new EventFieldInfo(componentId, fieldData.value);
11 | }
12 | }
13 |
14 | // This event isn't happening on a form field that we can reverse-map back to some incoming attribute
15 | return null;
16 | }
17 | }
18 |
19 | function getFormFieldData(elem: Element) {
20 | // The logic in here should be the inverse of the logic in BrowserRenderer's tryApplySpecialProperty.
21 | // That is, we're doing the reverse mapping, starting from an HTML property and reconstructing which
22 | // "special" attribute would have been mapped to that property.
23 | if (elem instanceof HTMLInputElement) {
24 | return (elem.type && elem.type.toLowerCase() === 'checkbox')
25 | ? { value: elem.checked }
26 | : { value: elem.value };
27 | }
28 |
29 | if (elem instanceof HTMLSelectElement || elem instanceof HTMLTextAreaElement) {
30 | return { value: elem.value };
31 | }
32 |
33 | return null;
34 | }
35 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Rendering/RenderBatch/RenderBatch.ts:
--------------------------------------------------------------------------------
1 | export interface RenderBatch {
2 | updatedComponents(): ArrayRange;
3 | referenceFrames(): ArrayRange;
4 | disposedComponentIds(): ArrayRange;
5 | disposedEventHandlerIds(): ArrayRange;
6 |
7 | updatedComponentsEntry(values: ArrayValues, index: number): RenderTreeDiff;
8 | referenceFramesEntry(values: ArrayValues, index: number): RenderTreeFrame;
9 | disposedComponentIdsEntry(values: ArrayValues, index: number): number;
10 | disposedEventHandlerIdsEntry(values: ArrayValues, index: number): number;
11 |
12 | diffReader: RenderTreeDiffReader;
13 | editReader: RenderTreeEditReader;
14 | frameReader: RenderTreeFrameReader;
15 | arrayRangeReader: ArrayRangeReader;
16 | arrayBuilderSegmentReader: ArrayBuilderSegmentReader;
17 | }
18 |
19 | export interface ArrayRangeReader {
20 | count(arrayRange: ArrayRange): number;
21 | values(arrayRange: ArrayRange): ArrayValues;
22 | }
23 |
24 | export interface ArrayBuilderSegmentReader {
25 | offset(arrayBuilderSegment: ArrayBuilderSegment): number;
26 | count(arrayBuilderSegment: ArrayBuilderSegment): number;
27 | values(arrayBuilderSegment: ArrayBuilderSegment): ArrayValues;
28 | }
29 |
30 | export interface RenderTreeDiffReader {
31 | componentId(diff: RenderTreeDiff): number;
32 | edits(diff: RenderTreeDiff): ArrayBuilderSegment;
33 | editsEntry(values: ArrayValues, index: number): RenderTreeEdit;
34 | }
35 |
36 | export interface RenderTreeEditReader {
37 | editType(edit: RenderTreeEdit): EditType;
38 | siblingIndex(edit: RenderTreeEdit): number;
39 | newTreeIndex(edit: RenderTreeEdit): number;
40 | moveToSiblingIndex(edit: RenderTreeEdit): number;
41 | removedAttributeName(edit: RenderTreeEdit): string | null;
42 | }
43 |
44 | export interface RenderTreeFrameReader {
45 | frameType(frame: RenderTreeFrame): FrameType;
46 | subtreeLength(frame: RenderTreeFrame): number;
47 | elementReferenceCaptureId(frame: RenderTreeFrame): string | null;
48 | componentId(frame: RenderTreeFrame): number;
49 | elementName(frame: RenderTreeFrame): string | null;
50 | textContent(frame: RenderTreeFrame): string | null;
51 | markupContent(frame: RenderTreeFrame): string;
52 | attributeName(frame: RenderTreeFrame): string | null;
53 | attributeValue(frame: RenderTreeFrame): string | null;
54 | attributeEventHandlerId(frame: RenderTreeFrame): number;
55 | }
56 |
57 | export interface ArrayRange { ArrayRange__DO_NOT_IMPLEMENT: any }
58 | export interface ArrayBuilderSegment { ArrayBuilderSegment__DO_NOT_IMPLEMENT: any }
59 | export interface ArrayValues { ArrayValues__DO_NOT_IMPLEMENT: any }
60 |
61 | export interface RenderTreeDiff { RenderTreeDiff__DO_NOT_IMPLEMENT: any }
62 | export interface RenderTreeFrame { RenderTreeFrame__DO_NOT_IMPLEMENT: any }
63 | export interface RenderTreeEdit { RenderTreeEdit__DO_NOT_IMPLEMENT: any }
64 |
65 | export enum EditType {
66 | // The values must be kept in sync with the .NET equivalent in RenderTreeEditType.cs
67 | prependFrame = 1,
68 | removeFrame = 2,
69 | setAttribute = 3,
70 | removeAttribute = 4,
71 | updateText = 5,
72 | stepIn = 6,
73 | stepOut = 7,
74 | updateMarkup = 8,
75 | permutationListEntry = 9,
76 | permutationListEnd = 10,
77 | }
78 |
79 | export enum FrameType {
80 | // The values must be kept in sync with the .NET equivalent in RenderTreeFrameType.cs
81 | element = 1,
82 | text = 2,
83 | attribute = 3,
84 | component = 4,
85 | region = 5,
86 | elementReferenceCapture = 6,
87 | markup = 8,
88 | }
89 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Rendering/RenderBatch/SharedMemoryRenderBatch.ts:
--------------------------------------------------------------------------------
1 | import { platform } from '../../Environment';
2 | import { RenderBatch, ArrayRange, ArrayRangeReader, ArrayBuilderSegment, RenderTreeDiff, RenderTreeEdit, RenderTreeFrame, ArrayValues, EditType, FrameType, RenderTreeFrameReader } from './RenderBatch';
3 | import { Pointer, System_Array, System_Object } from '../../Platform/Platform';
4 |
5 | // Used when running on Mono WebAssembly for shared-memory interop. The code here encapsulates
6 | // our knowledge of the memory layout of RenderBatch and all referenced types.
7 | //
8 | // In this implementation, all the DTO types are really heap pointers at runtime, hence all
9 | // the casts to 'any' whenever we pass them to platform.read.
10 |
11 | export class SharedMemoryRenderBatch implements RenderBatch {
12 | constructor(private batchAddress: Pointer) {
13 | }
14 |
15 | // Keep in sync with memory layout in RenderBatch.cs
16 | updatedComponents() {
17 | return platform.readStructField(this.batchAddress, 0) as any as ArrayRange;
18 | }
19 |
20 | referenceFrames() {
21 | return platform.readStructField(this.batchAddress, arrayRangeReader.structLength) as any as ArrayRange;
22 | }
23 |
24 | disposedComponentIds() {
25 | return platform.readStructField(this.batchAddress, arrayRangeReader.structLength * 2) as any as ArrayRange;
26 | }
27 |
28 | disposedEventHandlerIds() {
29 | return platform.readStructField(this.batchAddress, arrayRangeReader.structLength * 3) as any as ArrayRange;
30 | }
31 |
32 | updatedComponentsEntry(values: ArrayValues, index: number) {
33 | return arrayValuesEntry(values, index, diffReader.structLength);
34 | }
35 |
36 | referenceFramesEntry(values: ArrayValues, index: number) {
37 | return arrayValuesEntry(values, index, frameReader.structLength);
38 | }
39 |
40 | disposedComponentIdsEntry(values: ArrayValues, index: number) {
41 | const pointer = arrayValuesEntry(values, index, /* int length */ 4);
42 | return platform.readInt32Field(pointer as any as Pointer);
43 | }
44 |
45 | disposedEventHandlerIdsEntry(values: ArrayValues, index: number) {
46 | const pointer = arrayValuesEntry(values, index, /* long length */ 8);
47 | return platform.readUint64Field(pointer as any as Pointer);
48 | }
49 |
50 | arrayRangeReader = arrayRangeReader;
51 |
52 | arrayBuilderSegmentReader = arrayBuilderSegmentReader;
53 |
54 | diffReader = diffReader;
55 |
56 | editReader = editReader;
57 |
58 | frameReader = frameReader;
59 | }
60 |
61 | // Keep in sync with memory layout in ArrayRange.cs
62 | const arrayRangeReader = {
63 | structLength: 8,
64 | values: (arrayRange: ArrayRange) => platform.readObjectField>(arrayRange as any, 0) as any as ArrayValues,
65 | count: (arrayRange: ArrayRange) => platform.readInt32Field(arrayRange as any, 4),
66 | };
67 |
68 | // Keep in sync with memory layout in ArrayBuilderSegment
69 | const arrayBuilderSegmentReader = {
70 | structLength: 12,
71 | values: (arrayBuilderSegment: ArrayBuilderSegment) => {
72 | // Evaluate arrayBuilderSegment->_builder->_items, i.e., two dereferences needed
73 | const builder = platform.readObjectField(arrayBuilderSegment as any, 0);
74 | const builderFieldsAddress = platform.getObjectFieldsBaseAddress(builder);
75 | return platform.readObjectField>(builderFieldsAddress, 0) as any as ArrayValues;
76 | },
77 | offset: (arrayBuilderSegment: ArrayBuilderSegment) => platform.readInt32Field(arrayBuilderSegment as any, 4),
78 | count: (arrayBuilderSegment: ArrayBuilderSegment) => platform.readInt32Field(arrayBuilderSegment as any, 8),
79 | };
80 |
81 | // Keep in sync with memory layout in RenderTreeDiff.cs
82 | const diffReader = {
83 | structLength: 4 + arrayBuilderSegmentReader.structLength,
84 | componentId: (diff: RenderTreeDiff) => platform.readInt32Field(diff as any, 0),
85 | edits: (diff: RenderTreeDiff) => platform.readStructField(diff as any, 4) as any as ArrayBuilderSegment,
86 | editsEntry: (values: ArrayValues, index: number) => arrayValuesEntry(values, index, editReader.structLength),
87 | };
88 |
89 | // Keep in sync with memory layout in RenderTreeEdit.cs
90 | const editReader = {
91 | structLength: 20,
92 | editType: (edit: RenderTreeEdit) => platform.readInt32Field(edit as any, 0) as EditType,
93 | siblingIndex: (edit: RenderTreeEdit) => platform.readInt32Field(edit as any, 4),
94 | newTreeIndex: (edit: RenderTreeEdit) => platform.readInt32Field(edit as any, 8),
95 | moveToSiblingIndex: (edit: RenderTreeEdit) => platform.readInt32Field(edit as any, 8),
96 | removedAttributeName: (edit: RenderTreeEdit) => platform.readStringField(edit as any, 16),
97 | };
98 |
99 | // Keep in sync with memory layout in RenderTreeFrame.cs
100 | const frameReader = {
101 | structLength: 36,
102 | frameType: (frame: RenderTreeFrame) => platform.readInt16Field(frame as any, 4) as FrameType,
103 | subtreeLength: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 8),
104 | elementReferenceCaptureId: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
105 | componentId: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 12),
106 | elementName: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
107 | textContent: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
108 | markupContent: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16)!,
109 | attributeName: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
110 | attributeValue: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 24),
111 | attributeEventHandlerId: (frame: RenderTreeFrame) => platform.readUint64Field(frame as any, 8),
112 | };
113 |
114 | function arrayValuesEntry(arrayValues: ArrayValues, index: number, itemSize: number): T {
115 | return platform.getArrayEntryPtr(arrayValues as any as System_Array, index, itemSize) as any as T;
116 | }
117 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Rendering/RenderBatch/Utf8Decoder.ts:
--------------------------------------------------------------------------------
1 | const nativeDecoder = typeof TextDecoder === 'function'
2 | ? new TextDecoder('utf-8')
3 | : null;
4 |
5 | export const decodeUtf8: (bytes: Uint8Array) => string
6 | = nativeDecoder ? nativeDecoder.decode.bind(nativeDecoder) : decodeImpl;
7 |
8 | /* !
9 | Logic in decodeImpl is derived from fast-text-encoding
10 | https://github.com/samthor/fast-text-encoding
11 |
12 | License for fast-text-encoding: Apache 2.0
13 | https://github.com/samthor/fast-text-encoding/blob/master/LICENSE
14 | */
15 |
16 | function decodeImpl(bytes: Uint8Array): string {
17 | let pos = 0;
18 | const len = bytes.length;
19 | const out: number[] = [];
20 | const substrings: string[] = [];
21 |
22 | while (pos < len) {
23 | const byte1 = bytes[pos++];
24 | if (byte1 === 0) {
25 | break; // NULL
26 | }
27 |
28 | if ((byte1 & 0x80) === 0) { // 1-byte
29 | out.push(byte1);
30 | } else if ((byte1 & 0xe0) === 0xc0) { // 2-byte
31 | const byte2 = bytes[pos++] & 0x3f;
32 | out.push(((byte1 & 0x1f) << 6) | byte2);
33 | } else if ((byte1 & 0xf0) === 0xe0) {
34 | const byte2 = bytes[pos++] & 0x3f;
35 | const byte3 = bytes[pos++] & 0x3f;
36 | out.push(((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3);
37 | } else if ((byte1 & 0xf8) === 0xf0) {
38 | const byte2 = bytes[pos++] & 0x3f;
39 | const byte3 = bytes[pos++] & 0x3f;
40 | const byte4 = bytes[pos++] & 0x3f;
41 |
42 | // this can be > 0xffff, so possibly generate surrogates
43 | let codepoint = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
44 | if (codepoint > 0xffff) {
45 | // codepoint &= ~0x10000;
46 | codepoint -= 0x10000;
47 | out.push((codepoint >>> 10) & 0x3ff | 0xd800);
48 | codepoint = 0xdc00 | codepoint & 0x3ff;
49 | }
50 | out.push(codepoint);
51 | } else {
52 | // FIXME: we're ignoring this
53 | }
54 |
55 | // As a workaround for https://github.com/samthor/fast-text-encoding/issues/1,
56 | // make sure the 'out' array never gets too long. When it reaches a limit, we
57 | // stringify what we have so far and append to a list of outputs.
58 | if (out.length > 1024) {
59 | substrings.push(String.fromCharCode.apply(null, out));
60 | out.length = 0;
61 | }
62 | }
63 |
64 | substrings.push(String.fromCharCode.apply(null, out));
65 | return substrings.join('');
66 | }
67 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Rendering/Renderer.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/camelcase */
2 | import '../Platform/Platform';
3 | import '../Environment';
4 | import { RenderBatch } from './RenderBatch/RenderBatch';
5 | import { BrowserRenderer } from './BrowserRenderer';
6 | import { toLogicalElement, LogicalElement } from './LogicalElements';
7 |
8 | interface BrowserRendererRegistry {
9 | [browserRendererId: number]: BrowserRenderer;
10 | }
11 | const browserRenderers: BrowserRendererRegistry = {};
12 | let shouldResetScrollAfterNextBatch = false;
13 |
14 | export function attachRootComponentToLogicalElement(browserRendererId: number, logicalElement: LogicalElement, componentId: number): void {
15 | let browserRenderer = browserRenderers[browserRendererId];
16 | if (!browserRenderer) {
17 | browserRenderer = browserRenderers[browserRendererId] = new BrowserRenderer(browserRendererId);
18 | }
19 |
20 | browserRenderer.attachRootComponentToLogicalElement(componentId, logicalElement);
21 | }
22 |
23 | export function attachRootComponentToElement(elementSelector: string, componentId: number, browserRendererId?: number): void {
24 | const element = document.querySelector(elementSelector);
25 | if (!element) {
26 | throw new Error(`Could not find any element matching selector '${elementSelector}'.`);
27 | }
28 |
29 | // 'allowExistingContents' to keep any prerendered content until we do the first client-side render
30 | // Only client-side Blazor supplies a browser renderer ID
31 | attachRootComponentToLogicalElement(browserRendererId || 0, toLogicalElement(element, /* allow existing contents */ true), componentId);
32 | }
33 |
34 | export function renderBatch(browserRendererId: number, batch: RenderBatch): void {
35 | const browserRenderer = browserRenderers[browserRendererId];
36 | if (!browserRenderer) {
37 | throw new Error(`There is no browser renderer with ID ${browserRendererId}.`);
38 | }
39 |
40 | const arrayRangeReader = batch.arrayRangeReader;
41 | const updatedComponentsRange = batch.updatedComponents();
42 | const updatedComponentsValues = arrayRangeReader.values(updatedComponentsRange);
43 | const updatedComponentsLength = arrayRangeReader.count(updatedComponentsRange);
44 | const referenceFrames = batch.referenceFrames();
45 | const referenceFramesValues = arrayRangeReader.values(referenceFrames);
46 | const diffReader = batch.diffReader;
47 |
48 | for (let i = 0; i < updatedComponentsLength; i++) {
49 | const diff = batch.updatedComponentsEntry(updatedComponentsValues, i);
50 | const componentId = diffReader.componentId(diff);
51 | const edits = diffReader.edits(diff);
52 | browserRenderer.updateComponent(batch, componentId, edits, referenceFramesValues);
53 | }
54 |
55 | const disposedComponentIdsRange = batch.disposedComponentIds();
56 | const disposedComponentIdsValues = arrayRangeReader.values(disposedComponentIdsRange);
57 | const disposedComponentIdsLength = arrayRangeReader.count(disposedComponentIdsRange);
58 | for (let i = 0; i < disposedComponentIdsLength; i++) {
59 | const componentId = batch.disposedComponentIdsEntry(disposedComponentIdsValues, i);
60 | browserRenderer.disposeComponent(componentId);
61 | }
62 |
63 | const disposedEventHandlerIdsRange = batch.disposedEventHandlerIds();
64 | const disposedEventHandlerIdsValues = arrayRangeReader.values(disposedEventHandlerIdsRange);
65 | const disposedEventHandlerIdsLength = arrayRangeReader.count(disposedEventHandlerIdsRange);
66 | for (let i = 0; i < disposedEventHandlerIdsLength; i++) {
67 | const eventHandlerId = batch.disposedEventHandlerIdsEntry(disposedEventHandlerIdsValues, i);
68 | browserRenderer.disposeEventHandler(eventHandlerId);
69 | }
70 |
71 | resetScrollIfNeeded();
72 | }
73 |
74 | export function resetScrollAfterNextBatch() {
75 | shouldResetScrollAfterNextBatch = true;
76 | }
77 |
78 | function resetScrollIfNeeded() {
79 | if (shouldResetScrollAfterNextBatch) {
80 | shouldResetScrollAfterNextBatch = false;
81 |
82 | // This assumes the scroller is on the window itself. There isn't a general way to know
83 | // if some other element is playing the role of the primary scroll region.
84 | window.scrollTo && window.scrollTo(0, 0);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Rendering/RendererEventDispatcher.ts:
--------------------------------------------------------------------------------
1 | import { EventDescriptor } from './BrowserRenderer';
2 | import { UIEventArgs } from './EventForDotNet';
3 |
4 | type EventDispatcher = (eventDescriptor: EventDescriptor, eventArgs: UIEventArgs) => void;
5 |
6 | let eventDispatcherInstance: EventDispatcher;
7 |
8 | export function dispatchEvent(eventDescriptor: EventDescriptor, eventArgs: UIEventArgs): void {
9 | if (!eventDispatcherInstance) {
10 | throw new Error('eventDispatcher not initialized. Call \'setEventDispatcher\' to configure it.');
11 | }
12 |
13 | return eventDispatcherInstance(eventDescriptor, eventArgs);
14 | }
15 |
16 | export function setEventDispatcher(newDispatcher: (eventDescriptor: EventDescriptor, eventArgs: UIEventArgs) => Promise): void {
17 | eventDispatcherInstance = newDispatcher;
18 | }
19 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/Services/NavigationManager.ts:
--------------------------------------------------------------------------------
1 | // import '@dotnet/jsinterop'; Imported elsewhere
2 | import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
3 | import { EventDelegator } from '../Rendering/EventDelegator';
4 |
5 | let hasEnabledNavigationInterception = false;
6 | let hasRegisteredNavigationEventListeners = false;
7 |
8 | // Will be initialized once someone registers
9 | let notifyLocationChangedCallback: ((uri: string, intercepted: boolean) => Promise) | null = null;
10 |
11 | // These are the functions we're making available for invocation from .NET
12 | export const internalFunctions = {
13 | listenForNavigationEvents,
14 | enableNavigationInterception,
15 | navigateTo,
16 | getBaseURI: () => document.baseURI,
17 | getLocationHref: () => location.href,
18 | };
19 |
20 | function listenForNavigationEvents(callback: (uri: string, intercepted: boolean) => Promise) {
21 | notifyLocationChangedCallback = callback;
22 |
23 | if (hasRegisteredNavigationEventListeners) {
24 | return;
25 | }
26 |
27 | hasRegisteredNavigationEventListeners = true;
28 | window.addEventListener('popstate', () => notifyLocationChanged(false));
29 | }
30 |
31 | function enableNavigationInterception() {
32 | hasEnabledNavigationInterception = true;
33 | }
34 |
35 | export function attachToEventDelegator(eventDelegator: EventDelegator) {
36 | // We need to respond to clicks on elements *after* the EventDelegator has finished
37 | // running its simulated bubbling process so that we can respect any preventDefault requests.
38 | // So instead of registering our own native event, register using the EventDelegator.
39 | eventDelegator.notifyAfterClick(event => {
40 | if (!hasEnabledNavigationInterception) {
41 | return;
42 | }
43 |
44 | if (event.button !== 0 || eventHasSpecialKey(event)) {
45 | // Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
46 | return;
47 | }
48 |
49 | if (event.defaultPrevented) {
50 | return;
51 | }
52 |
53 | // Intercept clicks on all elements where the href is within the URI space
54 | // We must explicitly check if it has an 'href' attribute, because if it doesn't, the result might be null or an empty string depending on the browser
55 | const anchorTarget = findClosestAncestor(event.target as Element | null, 'A') as HTMLAnchorElement | null;
56 | const hrefAttributeName = 'href';
57 | if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
58 | const targetAttributeValue = anchorTarget.getAttribute('target');
59 | const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
60 | if (!opensInSameFrame) {
61 | return;
62 | }
63 |
64 | const href = anchorTarget.getAttribute(hrefAttributeName)!;
65 | const absoluteHref = toAbsoluteUri(href);
66 |
67 | if (isWithinBaseUriSpace(absoluteHref)) {
68 | event.preventDefault();
69 | performInternalNavigation(absoluteHref, true);
70 | }
71 | }
72 | });
73 | }
74 |
75 | export function navigateTo(uri: string, forceLoad: boolean) {
76 | const absoluteUri = toAbsoluteUri(uri);
77 |
78 | if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) {
79 | // It's an internal URL, so do client-side navigation
80 | performInternalNavigation(absoluteUri, false);
81 | } else if (forceLoad && location.href === uri) {
82 | // Force-loading the same URL you're already on requires special handling to avoid
83 | // triggering browser-specific behavior issues.
84 | // For details about what this fixes and why, see https://github.com/aspnet/AspNetCore/pull/10839
85 | const temporaryUri = uri + '?';
86 | history.replaceState(null, '', temporaryUri);
87 | location.replace(uri);
88 | } else {
89 | // It's either an external URL, or forceLoad is requested, so do a full page load
90 | location.href = uri;
91 | }
92 | }
93 |
94 | function performInternalNavigation(absoluteInternalHref: string, interceptedLink: boolean) {
95 | // Since this was *not* triggered by a back/forward gesture (that goes through a different
96 | // code path starting with a popstate event), we don't want to preserve the current scroll
97 | // position, so reset it.
98 | // To avoid ugly flickering effects, we don't want to change the scroll position until the
99 | // we render the new page. As a best approximation, wait until the next batch.
100 | resetScrollAfterNextBatch();
101 |
102 | history.pushState(null, /* ignored title */ '', absoluteInternalHref);
103 | notifyLocationChanged(interceptedLink);
104 | }
105 |
106 | async function notifyLocationChanged(interceptedLink: boolean) {
107 | if (notifyLocationChangedCallback) {
108 | await notifyLocationChangedCallback(location.href, interceptedLink);
109 | }
110 | }
111 |
112 | let testAnchor: HTMLAnchorElement;
113 | function toAbsoluteUri(relativeUri: string) {
114 | testAnchor = testAnchor || document.createElement('a');
115 | testAnchor.href = relativeUri;
116 | return testAnchor.href;
117 | }
118 |
119 | function findClosestAncestor(element: Element | null, tagName: string) {
120 | return !element
121 | ? null
122 | : element.tagName === tagName
123 | ? element
124 | : findClosestAncestor(element.parentElement, tagName);
125 | }
126 |
127 | function isWithinBaseUriSpace(href: string) {
128 | const baseUriWithTrailingSlash = toBaseUriWithTrailingSlash(document.baseURI!); // TODO: Might baseURI really be null?
129 | return href.startsWith(baseUriWithTrailingSlash);
130 | }
131 |
132 | function toBaseUriWithTrailingSlash(baseUri: string) {
133 | return baseUri.substr(0, baseUri.lastIndexOf('/') + 1);
134 | }
135 |
136 | function eventHasSpecialKey(event: MouseEvent) {
137 | return event.ctrlKey || event.shiftKey || event.altKey || event.metaKey;
138 | }
139 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/src/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | module.exports = (env, args) => ({
5 | resolve: { extensions: ['.ts', '.js'] },
6 | devtool: args.mode === 'development' ? 'source-map' : 'none',
7 | module: {
8 | rules: [{ test: /\.ts?$/, loader: 'ts-loader' }]
9 | },
10 | entry: {
11 | 'blazor.webassembly': './Boot.WebAssembly.ts',
12 | 'blazor.server': './Boot.Server.ts',
13 | },
14 | output: { path: path.join(__dirname, '/..', '/dist', args.mode == 'development' ? '/Debug' : '/Release'), filename: '[name].js' }
15 | });
16 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/tests/DefaultReconnectDisplay.test.ts:
--------------------------------------------------------------------------------
1 | import { DefaultReconnectDisplay } from "../src/Platform/Circuits/DefaultReconnectDisplay";
2 | import {JSDOM} from 'jsdom';
3 | import { NullLogger} from '../src/Platform/Logging/Loggers';
4 |
5 | describe('DefaultReconnectDisplay', () => {
6 |
7 | it ('adds element to the body on show', () => {
8 | const testDocument = new JSDOM().window.document;
9 | const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
10 |
11 | display.show();
12 |
13 | const element = testDocument.body.querySelector('div');
14 | expect(element).toBeDefined();
15 | expect(element!.id).toBe('test-dialog-id');
16 | expect(element!.style.display).toBe('block');
17 |
18 | expect(display.message.textContent).toBe('Attempting to reconnect to the server...');
19 | expect(display.button.style.display).toBe('none');
20 | });
21 |
22 | it ('does not add element to the body multiple times', () => {
23 | const testDocument = new JSDOM().window.document;
24 | const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
25 |
26 | display.show();
27 | display.show();
28 |
29 | expect(testDocument.body.childElementCount).toBe(1);
30 | });
31 |
32 | it ('hides element', () => {
33 | const testDocument = new JSDOM().window.document;
34 | const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
35 |
36 | display.hide();
37 |
38 | expect(display.modal.style.display).toBe('none');
39 | });
40 |
41 | it ('updates message on fail', () => {
42 | const testDocument = new JSDOM().window.document;
43 | const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
44 |
45 | display.show();
46 | display.failed();
47 |
48 | expect(display.modal.style.display).toBe('block');
49 | expect(display.message.innerHTML).toBe('Reconnection failed. Try reloading the page if you\'re unable to reconnect.');
50 | expect(display.button.style.display).toBe('block');
51 | });
52 |
53 | it ('updates message on refused', () => {
54 | const testDocument = new JSDOM().window.document;
55 | const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
56 |
57 | display.show();
58 | display.rejected();
59 |
60 | expect(display.modal.style.display).toBe('block');
61 | expect(display.message.innerHTML).toBe('Could not reconnect to the server. Reload the page to restore functionality.');
62 | expect(display.button.style.display).toBe('none');
63 | });
64 |
65 | });
66 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/tests/DefaultReconnectionHandler.test.ts:
--------------------------------------------------------------------------------
1 | import '../src/GlobalExports';
2 | import { UserSpecifiedDisplay } from '../src/Platform/Circuits/UserSpecifiedDisplay';
3 | import { DefaultReconnectionHandler } from '../src/Platform/Circuits/DefaultReconnectionHandler';
4 | import { NullLogger} from '../src/Platform/Logging/Loggers';
5 | import { resolveOptions, ReconnectionOptions } from "../src/Platform/Circuits/BlazorOptions";
6 | import { ReconnectDisplay } from '../src/Platform/Circuits/ReconnectDisplay';
7 |
8 | const defaultReconnectionOptions = resolveOptions().reconnectionOptions;
9 |
10 | describe('DefaultReconnectionHandler', () => {
11 | it('toggles user-specified UI on disconnection/connection', () => {
12 | const element = attachUserSpecifiedUI(defaultReconnectionOptions);
13 | const handler = new DefaultReconnectionHandler(NullLogger.instance);
14 |
15 | // Shows on disconnection
16 | handler.onConnectionDown(defaultReconnectionOptions);
17 | expect(element.className).toBe(UserSpecifiedDisplay.ShowClassName);
18 |
19 | // Hides on reconnection
20 | handler.onConnectionUp();
21 | expect(element.className).toBe(UserSpecifiedDisplay.HideClassName);
22 |
23 | document.body.removeChild(element);
24 | });
25 |
26 | it('hides display on connection up, and stops retrying', async () => {
27 | const testDisplay = createTestDisplay();
28 | const reconnect = jest.fn().mockResolvedValue(true);
29 | const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect);
30 |
31 | handler.onConnectionDown({
32 | maxRetries: 1000,
33 | retryIntervalMilliseconds: 100,
34 | dialogId: 'ignored'
35 | });
36 | handler.onConnectionUp();
37 |
38 | expect(testDisplay.hide).toHaveBeenCalled();
39 | await delay(200);
40 | expect(reconnect).not.toHaveBeenCalled();
41 | });
42 |
43 | it('shows display on connection down', async () => {
44 | const testDisplay = createTestDisplay();
45 | const reconnect = jest.fn().mockResolvedValue(true);
46 | const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect);
47 |
48 | handler.onConnectionDown({
49 | maxRetries: 1000,
50 | retryIntervalMilliseconds: 100,
51 | dialogId: 'ignored'
52 | });
53 | expect(testDisplay.show).toHaveBeenCalled();
54 | expect(testDisplay.failed).not.toHaveBeenCalled();
55 | expect(reconnect).not.toHaveBeenCalled();
56 |
57 | await delay(150);
58 | expect(reconnect).toHaveBeenCalledTimes(1);
59 | });
60 |
61 | it('invokes failed if reconnect fails', async () => {
62 | const testDisplay = createTestDisplay();
63 | const reconnect = jest.fn().mockRejectedValue(null);
64 | const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect);
65 | window.console.error = jest.fn();
66 |
67 | handler.onConnectionDown({
68 | maxRetries: 2,
69 | retryIntervalMilliseconds: 5,
70 | dialogId: 'ignored'
71 | });
72 |
73 | await delay(500);
74 | expect(testDisplay.show).toHaveBeenCalled();
75 | expect(testDisplay.failed).toHaveBeenCalled();
76 | expect(reconnect).toHaveBeenCalledTimes(2);
77 | });
78 | });
79 |
80 | function attachUserSpecifiedUI(options: ReconnectionOptions): Element {
81 | const element = document.createElement('div');
82 | element.id = options.dialogId;
83 | element.className = UserSpecifiedDisplay.HideClassName;
84 | document.body.appendChild(element);
85 | return element;
86 | }
87 |
88 | function delay(durationMilliseconds: number) {
89 | return new Promise(resolve => setTimeout(resolve, durationMilliseconds));
90 | }
91 |
92 | function createTestDisplay(): ReconnectDisplay {
93 | return {
94 | show: jest.fn(),
95 | hide: jest.fn(),
96 | failed: jest.fn(),
97 | rejected: jest.fn()
98 | };
99 | }
100 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/tests/RenderQueue.test.ts:
--------------------------------------------------------------------------------
1 | (global as any).DotNet = { attachReviver: jest.fn() };
2 |
3 | import { RenderQueue } from '../src/Platform/Circuits/RenderQueue';
4 | import { NullLogger } from '../src/Platform/Logging/Loggers';
5 | import * as signalR from '@aspnet/signalr';
6 |
7 | jest.mock('../src/Rendering/Renderer', () => ({
8 | renderBatch: jest.fn()
9 | }));
10 |
11 | describe('RenderQueue', () => {
12 |
13 | it('processBatch acknowledges previously rendered batches', () => {
14 | const queue = new RenderQueue(0, NullLogger.instance);
15 |
16 | const sendMock = jest.fn();
17 | const connection = { send: sendMock } as any as signalR.HubConnection;
18 | queue.processBatch(2, new Uint8Array(0), connection);
19 |
20 | expect(sendMock.mock.calls.length).toEqual(1);
21 | expect(queue.getLastBatchid()).toEqual(2);
22 | });
23 |
24 | it('processBatch does not render out of order batches', () => {
25 | const queue = new RenderQueue(0, NullLogger.instance);
26 |
27 | const sendMock = jest.fn();
28 | const connection = { send: sendMock } as any as signalR.HubConnection;
29 | queue.processBatch(3, new Uint8Array(0), connection);
30 |
31 | expect(sendMock.mock.calls.length).toEqual(0);
32 | });
33 |
34 | it('processBatch renders pending batches', () => {
35 | const queue = new RenderQueue(0, NullLogger.instance);
36 |
37 | const sendMock = jest.fn();
38 | const connection = { send: sendMock } as any as signalR.HubConnection;
39 | queue.processBatch(2, new Uint8Array(0), connection);
40 |
41 | expect(sendMock.mock.calls.length).toEqual(1);
42 | expect(queue.getLastBatchid()).toEqual(2);
43 | });
44 |
45 | });
46 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | }
4 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/upstream/aspnetcore/web.js/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "noImplicitAny": false,
4 | "noEmitOnError": true,
5 | "removeComments": false,
6 | "sourceMap": true,
7 | "downlevelIteration": true,
8 | "target": "es5",
9 | "lib": ["es2015", "dom"],
10 | "strict": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor.JS/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
3 |
4 | module.exports = (env, args) => ({
5 | resolve: {
6 | extensions: ['.ts', '.js'],
7 | plugins: [new TsconfigPathsPlugin()]
8 | },
9 | devtool: 'inline-source-map',
10 | module: {
11 | rules: [{ test: /\.ts?$/, loader: 'ts-loader' }]
12 | },
13 | entry: {
14 | 'blazor.desktop': './src/Boot.Desktop.ts'
15 | },
16 | output: { path: path.join(__dirname, '/dist'), filename: '[name].js' }
17 | });
18 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor/ConventionBasedStartup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using System;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Runtime.ExceptionServices;
7 |
8 | namespace WebWindows.Blazor
9 | {
10 | internal class ConventionBasedStartup
11 | {
12 | public ConventionBasedStartup(object instance)
13 | {
14 | Instance = instance ?? throw new ArgumentNullException(nameof(instance));
15 | }
16 |
17 | public object Instance { get; }
18 |
19 | public void Configure(DesktopApplicationBuilder app, IServiceProvider services)
20 | {
21 | try
22 | {
23 | var method = GetConfigureMethod();
24 | Debug.Assert(method != null);
25 |
26 | var parameters = method.GetParameters();
27 | var arguments = new object[parameters.Length];
28 | for (var i = 0; i < parameters.Length; i++)
29 | {
30 | var parameter = parameters[i];
31 | arguments[i] = parameter.ParameterType == typeof(DesktopApplicationBuilder)
32 | ? app
33 | : services.GetRequiredService(parameter.ParameterType);
34 | }
35 |
36 | method.Invoke(Instance, arguments);
37 | }
38 | catch (Exception ex)
39 | {
40 | if (ex is TargetInvocationException)
41 | {
42 | ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
43 | }
44 |
45 | throw;
46 | }
47 | }
48 |
49 | internal MethodInfo GetConfigureMethod()
50 | {
51 | var methods = Instance.GetType()
52 | .GetMethods(BindingFlags.Instance | BindingFlags.Public)
53 | .Where(m => string.Equals(m.Name, "Configure", StringComparison.Ordinal))
54 | .ToArray();
55 |
56 | if (methods.Length == 1)
57 | {
58 | return methods[0];
59 | }
60 | else if (methods.Length == 0)
61 | {
62 | throw new InvalidOperationException("The startup class must define a 'Configure' method.");
63 | }
64 | else
65 | {
66 | throw new InvalidOperationException("Overloading the 'Configure' method is not supported.");
67 | }
68 | }
69 |
70 | public void ConfigureServices(IServiceCollection services)
71 | {
72 | try
73 | {
74 | var method = GetConfigureServicesMethod();
75 | if (method != null)
76 | {
77 | method.Invoke(Instance, new object[] { services });
78 | }
79 | }
80 | catch (Exception ex)
81 | {
82 | if (ex is TargetInvocationException)
83 | {
84 | ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
85 | }
86 |
87 | throw;
88 | }
89 | }
90 |
91 | internal MethodInfo GetConfigureServicesMethod()
92 | {
93 | return Instance.GetType()
94 | .GetMethod(
95 | "ConfigureServices",
96 | BindingFlags.Public | BindingFlags.Instance,
97 | null,
98 | new Type[] { typeof(IServiceCollection), },
99 | Array.Empty());
100 | }
101 | }
102 | }
--------------------------------------------------------------------------------
/src/WebWindow.Blazor/DesktopApplicationBuilder.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Components;
2 | using System;
3 | using System.Collections.Generic;
4 |
5 | namespace WebWindows.Blazor
6 | {
7 | public class DesktopApplicationBuilder
8 | {
9 | public DesktopApplicationBuilder(IServiceProvider services)
10 | {
11 | Services = services;
12 | Entries = new List<(Type componentType, string domElementSelector)>();
13 | }
14 |
15 | public List<(Type componentType, string domElementSelector)> Entries { get; }
16 |
17 | public IServiceProvider Services { get; }
18 |
19 | public void AddComponent(Type componentType, string domElementSelector)
20 | {
21 | if (componentType == null)
22 | {
23 | throw new ArgumentNullException(nameof(componentType));
24 | }
25 |
26 | if (domElementSelector == null)
27 | {
28 | throw new ArgumentNullException(nameof(domElementSelector));
29 | }
30 |
31 | Entries.Add((componentType, domElementSelector));
32 | }
33 |
34 | public void AddComponent(string domElementSelector) where T : IComponent
35 | => AddComponent(typeof(T), domElementSelector);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor/DesktopJSRuntime.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.JSInterop;
2 | using Microsoft.JSInterop.Infrastructure;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace WebWindows.Blazor
9 | {
10 | internal class DesktopJSRuntime : JSRuntime
11 | {
12 | private readonly IPC _ipc;
13 | private static Type VoidTaskResultType = typeof(Task).Assembly
14 | .GetType("System.Threading.Tasks.VoidTaskResult", true);
15 |
16 | public DesktopJSRuntime(IPC ipc)
17 | {
18 | _ipc = ipc ?? throw new ArgumentNullException(nameof(ipc));
19 | }
20 |
21 | protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
22 | {
23 | _ipc.Send("JS.BeginInvokeJS", asyncHandle, identifier, argsJson);
24 | }
25 |
26 | protected override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult)
27 | {
28 | // The other params aren't strictly required and are only used for logging
29 | var resultOrError = invocationResult.Success ? HandlePossibleVoidTaskResult(invocationResult.Result) : invocationResult.Exception.ToString();
30 | if (resultOrError != null)
31 | {
32 | _ipc.Send("JS.EndInvokeDotNet", invocationInfo.CallId, invocationResult.Success, resultOrError);
33 | }
34 | else
35 | {
36 | _ipc.Send("JS.EndInvokeDotNet", invocationInfo.CallId, invocationResult.Success);
37 | }
38 | }
39 |
40 | private static object HandlePossibleVoidTaskResult(object result)
41 | {
42 | // Looks like the TaskGenericsUtil logic in Microsoft.JSInterop doesn't know how to
43 | // understand System.Threading.Tasks.VoidTaskResult
44 | return result?.GetType() == VoidTaskResultType ? null : result;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor/DesktopNavigationInterception.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Components.Routing;
2 | using System.Threading.Tasks;
3 |
4 | namespace WebWindows.Blazor
5 | {
6 | internal class DesktopNavigationInterception : INavigationInterception
7 | {
8 | public Task EnableNavigationInterceptionAsync()
9 | {
10 | // We don't actually need to set anything up in this environment
11 | return Task.CompletedTask;
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/WebWindow.Blazor/DesktopNavigationManager.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.JSInterop;
2 | using Microsoft.AspNetCore.Components;
3 |
4 | namespace WebWindows.Blazor
5 | {
6 | internal class DesktopNavigationManager : NavigationManager
7 | {
8 | public static readonly DesktopNavigationManager Instance = new DesktopNavigationManager();
9 |
10 | private static readonly string InteropPrefix = "Blazor._internal.navigationManager.";
11 | private static readonly string InteropNavigateTo = InteropPrefix + "navigateTo";
12 |
13 | protected override void EnsureInitialized()
14 | {
15 | Initialize(ComponentsDesktop.BaseUriAbsolute, ComponentsDesktop.InitialUriAbsolute);
16 | }
17 |
18 | protected override void NavigateToCore(string uri, bool forceLoad)
19 | {
20 | ComponentsDesktop.DesktopJSRuntime.InvokeAsync(InteropNavigateTo, uri, forceLoad);
21 | }
22 |
23 | public void SetLocation(string uri, bool isInterceptedLink)
24 | {
25 | Uri = uri;
26 | NotifyLocationChanged(isInterceptedLink);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor/DesktopRenderer.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Components;
2 | using Microsoft.AspNetCore.Components.RenderTree;
3 | using Microsoft.AspNetCore.Components.Server.Circuits;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Logging;
6 | using Microsoft.JSInterop;
7 | using System;
8 | using System.IO;
9 | using System.Reflection;
10 | using System.Threading.Tasks;
11 |
12 | namespace WebWindows.Blazor
13 | {
14 | // Many aspects of the layering here are not what we really want, but it won't affect
15 | // people prototyping applications with it. We can put more work into restructuring the
16 | // hosting and startup models in the future if it's justified.
17 |
18 | internal class DesktopRenderer : Renderer
19 | {
20 | private const int RendererId = 0; // Not relevant, since we have only one renderer in Desktop
21 | private readonly IPC _ipc;
22 | private readonly IJSRuntime _jsRuntime;
23 | private static readonly Type _writer;
24 | private static readonly MethodInfo _writeMethod;
25 |
26 | public override Dispatcher Dispatcher { get; } = NullDispatcher.Instance;
27 |
28 | static DesktopRenderer()
29 | {
30 | _writer = typeof(RenderBatchWriter);
31 | _writeMethod = _writer.GetMethod("Write", new[] { typeof(RenderBatch).MakeByRefType() });
32 | }
33 |
34 | public DesktopRenderer(IServiceProvider serviceProvider, IPC ipc, ILoggerFactory loggerFactory)
35 | : base(serviceProvider, loggerFactory)
36 | {
37 | _ipc = ipc ?? throw new ArgumentNullException(nameof(ipc));
38 | _jsRuntime = serviceProvider.GetRequiredService();
39 | }
40 |
41 | ///
42 | /// Notifies when a rendering exception occured.
43 | ///
44 | public event EventHandler UnhandledException;
45 |
46 | ///
47 | /// Attaches a new root component to the renderer,
48 | /// causing it to be displayed in the specified DOM element.
49 | ///
50 | /// The type of the component.
51 | /// A CSS selector that uniquely identifies a DOM element.
52 | public Task AddComponentAsync(string domElementSelector)
53 | where TComponent : IComponent
54 | {
55 | return AddComponentAsync(typeof(TComponent), domElementSelector);
56 | }
57 |
58 | ///
59 | /// Associates the with the ,
60 | /// causing it to be displayed in the specified DOM element.
61 | ///
62 | /// The type of the component.
63 | /// A CSS selector that uniquely identifies a DOM element.
64 | public Task AddComponentAsync(Type componentType, string domElementSelector)
65 | {
66 | var component = InstantiateComponent(componentType);
67 | var componentId = AssignRootComponentId(component);
68 |
69 | var attachComponentTask = _jsRuntime.InvokeAsync(
70 | "Blazor._internal.attachRootComponentToElement",
71 | domElementSelector,
72 | componentId,
73 | RendererId);
74 | CaptureAsyncExceptions(attachComponentTask);
75 | return RenderRootComponentAsync(componentId);
76 | }
77 |
78 | ///
79 | protected override Task UpdateDisplayAsync(in RenderBatch batch)
80 | {
81 | string base64;
82 | using (var memoryStream = new MemoryStream())
83 | {
84 | object renderBatchWriter = Activator.CreateInstance(_writer, new object[] { memoryStream, false });
85 | using (renderBatchWriter as IDisposable)
86 | {
87 | _writeMethod.Invoke(renderBatchWriter, new object[] { batch });
88 | }
89 |
90 | var batchBytes = memoryStream.ToArray();
91 | base64 = Convert.ToBase64String(batchBytes);
92 | }
93 |
94 | _ipc.Send("JS.RenderBatch", RendererId, base64);
95 |
96 | // TODO: Consider finding a way to get back a completion message from the Desktop side
97 | // in case there was an error. We don't really need to wait for anything to happen, since
98 | // this is not prerendering and we don't care how quickly the UI is updated, but it would
99 | // be desirable to flow back errors.
100 | return Task.CompletedTask;
101 | }
102 |
103 | private async void CaptureAsyncExceptions(ValueTask task)
104 | {
105 | try
106 | {
107 | await task;
108 | }
109 | catch (Exception ex)
110 | {
111 | UnhandledException?.Invoke(this, ex);
112 | }
113 | }
114 |
115 | protected override void HandleException(Exception exception)
116 | {
117 | Console.WriteLine(exception.ToString());
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor/DesktopSynchronizationContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace WebWindows.Blazor
7 | {
8 | internal class DesktopSynchronizationContext : SynchronizationContext
9 | {
10 | public static event EventHandler UnhandledException;
11 |
12 | private readonly WorkQueue _work;
13 |
14 | public DesktopSynchronizationContext(CancellationToken cancellationToken)
15 | {
16 | _work = new WorkQueue(cancellationToken);
17 | }
18 |
19 | public override SynchronizationContext CreateCopy()
20 | {
21 | return this;
22 | }
23 |
24 | public override void Post(SendOrPostCallback d, object state)
25 | {
26 | _work.Queue.Add(new WorkItem() { Callback = d, Context = this, State = state, });
27 | }
28 |
29 | public override void Send(SendOrPostCallback d, object state)
30 | {
31 | if (_work.CheckAccess())
32 | {
33 | _work.ProcessWorkitemInline(d, state);
34 | }
35 | else
36 | {
37 | var completed = new ManualResetEventSlim();
38 | _work.Queue.Add(new WorkItem() { Callback = d, Context = this, State = state, Completed = completed, });
39 | completed.Wait();
40 | }
41 | }
42 |
43 | public void Stop()
44 | {
45 | _work.Queue.CompleteAdding();
46 | }
47 |
48 | public static void CheckAccess()
49 | {
50 | var synchronizationContext = Current as DesktopSynchronizationContext;
51 | if (synchronizationContext == null)
52 | {
53 | throw new InvalidOperationException("Not in the right context.");
54 | }
55 |
56 | synchronizationContext._work.CheckAccess();
57 | }
58 |
59 | private class WorkQueue
60 | {
61 | private readonly Thread _thread;
62 | private readonly CancellationToken _cancellationToken;
63 |
64 | public WorkQueue(CancellationToken cancellationToken)
65 | {
66 | _cancellationToken = cancellationToken;
67 | _thread = new Thread(ProcessQueue);
68 | _thread.Start();
69 | }
70 |
71 | public BlockingCollection Queue { get; } = new BlockingCollection();
72 |
73 | public bool CheckAccess()
74 | {
75 | return Thread.CurrentThread == _thread;
76 | }
77 |
78 | private void ProcessQueue()
79 | {
80 | while (!Queue.IsCompleted)
81 | {
82 | WorkItem item;
83 | try
84 | {
85 | item = Queue.Take(_cancellationToken);
86 | }
87 | catch (InvalidOperationException)
88 | {
89 | return;
90 | }
91 | catch (OperationCanceledException)
92 | {
93 | return;
94 | }
95 |
96 | var current = Current;
97 | SetSynchronizationContext(item.Context);
98 |
99 | try
100 | {
101 | ProcessWorkitemInline(item.Callback, item.State);
102 | }
103 | finally
104 | {
105 | if (item.Completed != null)
106 | {
107 | item.Completed.Set();
108 | }
109 |
110 | SetSynchronizationContext(current);
111 | }
112 | }
113 | }
114 |
115 | public void ProcessWorkitemInline(SendOrPostCallback callback, object state)
116 | {
117 | try
118 | {
119 | callback(state);
120 | }
121 | catch (Exception e)
122 | {
123 | UnhandledException?.Invoke(this, e);
124 | }
125 | }
126 | }
127 |
128 | private class WorkItem
129 | {
130 | public SendOrPostCallback Callback;
131 | public object State;
132 | public SynchronizationContext Context;
133 | public ManualResetEventSlim Completed;
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor/GlobalSuppressions.cs:
--------------------------------------------------------------------------------
1 | // This file is used by Code Analysis to maintain SuppressMessage
2 | // attributes that are applied to this project.
3 | // Project-level suppressions either have no target or are given
4 | // a specific target and scoped to a namespace, type, member, etc.
5 |
6 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "BL0006:Do not use RenderTree types", Justification = "")]
7 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor/IPC.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.Text.Json;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using WebWindows;
8 |
9 | namespace WebWindows.Blazor
10 | {
11 | internal class IPC
12 | {
13 | private readonly Dictionary>> _registrations = new Dictionary>>();
14 | private readonly WebWindow _webWindow;
15 |
16 | public IPC(WebWindow webWindow)
17 | {
18 | _webWindow = webWindow ?? throw new ArgumentNullException(nameof(webWindow));
19 | _webWindow.OnWebMessageReceived += HandleScriptNotify;
20 | }
21 |
22 | public void Send(string eventName, params object[] args)
23 | {
24 | try
25 | {
26 | _webWindow.Invoke(() =>
27 | {
28 | _webWindow.SendMessage($"{eventName}:{JsonSerializer.Serialize(args)}");
29 | });
30 | }
31 | catch (Exception ex)
32 | {
33 | Console.WriteLine(ex.Message);
34 | }
35 | }
36 |
37 | public void On(string eventName, Action callback)
38 | {
39 | lock (_registrations)
40 | {
41 | if (!_registrations.TryGetValue(eventName, out var group))
42 | {
43 | group = new List>();
44 | _registrations.Add(eventName, group);
45 | }
46 |
47 | group.Add(callback);
48 | }
49 | }
50 |
51 | public void Once(string eventName, Action callback)
52 | {
53 | Action callbackOnce = null;
54 | callbackOnce = arg =>
55 | {
56 | Off(eventName, callbackOnce);
57 | callback(arg);
58 | };
59 |
60 | On(eventName, callbackOnce);
61 | }
62 |
63 | public void Off(string eventName, Action callback)
64 | {
65 | lock (_registrations)
66 | {
67 | if (_registrations.TryGetValue(eventName, out var group))
68 | {
69 | group.Remove(callback);
70 | }
71 | }
72 | }
73 |
74 | private void HandleScriptNotify(object sender, string message)
75 | {
76 | var value = message;
77 |
78 | // Move off the browser UI thread
79 | Task.Factory.StartNew(() =>
80 | {
81 | if (value.StartsWith("ipc:"))
82 | {
83 | var spacePos = value.IndexOf(' ');
84 | var eventName = value.Substring(4, spacePos - 4);
85 | var argsJson = value.Substring(spacePos + 1);
86 | var args = JsonSerializer.Deserialize(argsJson);
87 |
88 | Action[] callbacksCopy;
89 | lock (_registrations)
90 | {
91 | if (!_registrations.TryGetValue(eventName, out var callbacks))
92 | {
93 | return;
94 | }
95 |
96 | callbacksCopy = callbacks.ToArray();
97 | }
98 |
99 | foreach (var callback in callbacksCopy)
100 | {
101 | callback(args);
102 | }
103 | }
104 | });
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor/JSInteropMethods.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Components.RenderTree;
2 | using Microsoft.AspNetCore.Components.Web;
3 | using Microsoft.JSInterop;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace WebWindows.Blazor
10 | {
11 | public static class JSInteropMethods
12 | {
13 | [JSInvokable(nameof(DispatchEvent))]
14 | public static async Task DispatchEvent(WebEventDescriptor eventDescriptor, string eventArgsJson)
15 | {
16 | var webEvent = WebEventData.Parse(eventDescriptor, eventArgsJson);
17 | var renderer = ComponentsDesktop.DesktopRenderer;
18 | await renderer.DispatchEventAsync(
19 | webEvent.EventHandlerId,
20 | webEvent.EventFieldInfo,
21 | webEvent.EventArgs);
22 | }
23 |
24 | [JSInvokable(nameof(NotifyLocationChanged))]
25 | public static void NotifyLocationChanged(string uri, bool isInterceptedLink)
26 | {
27 | DesktopNavigationManager.Instance.SetLocation(uri, isInterceptedLink);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor/NullDispatcher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.AspNetCore.Components;
4 |
5 | namespace WebWindows.Blazor
6 | {
7 | internal class NullDispatcher : Dispatcher
8 | {
9 | public static readonly Dispatcher Instance = new NullDispatcher();
10 |
11 | private NullDispatcher()
12 | {
13 | }
14 |
15 | public override bool CheckAccess() => true;
16 |
17 | public override Task InvokeAsync(Action workItem)
18 | {
19 | if (workItem is null)
20 | {
21 | throw new ArgumentNullException(nameof(workItem));
22 | }
23 |
24 | workItem();
25 | return Task.CompletedTask;
26 | }
27 |
28 | public override Task InvokeAsync(Func workItem)
29 | {
30 | if (workItem is null)
31 | {
32 | throw new ArgumentNullException(nameof(workItem));
33 | }
34 |
35 | return workItem();
36 | }
37 |
38 | public override Task InvokeAsync(Func workItem)
39 | {
40 | if (workItem is null)
41 | {
42 | throw new ArgumentNullException(nameof(workItem));
43 | }
44 |
45 | return Task.FromResult(workItem());
46 | }
47 |
48 | public override Task InvokeAsync(Func> workItem)
49 | {
50 | if (workItem is null)
51 | {
52 | throw new ArgumentNullException(nameof(workItem));
53 | }
54 |
55 | return workItem();
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/src/WebWindow.Blazor/SharedSource/JsonSerializerOptionsProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System.Text.Json;
5 |
6 | namespace Microsoft.AspNetCore.Components
7 | {
8 | internal static class JsonSerializerOptionsProvider
9 | {
10 | public static readonly JsonSerializerOptions Options = new JsonSerializerOptions
11 | {
12 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
13 | PropertyNameCaseInsensitive = true,
14 | };
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor/SharedSource/WebEventData.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Text.Json;
6 | using Microsoft.AspNetCore.Components.RenderTree;
7 |
8 | namespace Microsoft.AspNetCore.Components.Web
9 | {
10 | internal class WebEventData
11 | {
12 | // This class represents the second half of parsing incoming event data,
13 | // once the type of the eventArgs becomes known.
14 | public static WebEventData Parse(string eventDescriptorJson, string eventArgsJson)
15 | {
16 | WebEventDescriptor eventDescriptor;
17 | try
18 | {
19 | eventDescriptor = Deserialize(eventDescriptorJson);
20 | }
21 | catch (Exception e)
22 | {
23 | throw new InvalidOperationException("Error parsing the event descriptor", e);
24 | }
25 |
26 | return Parse(
27 | eventDescriptor,
28 | eventArgsJson);
29 | }
30 |
31 | public static WebEventData Parse(WebEventDescriptor eventDescriptor, string eventArgsJson)
32 | {
33 | return new WebEventData(
34 | eventDescriptor.BrowserRendererId,
35 | eventDescriptor.EventHandlerId,
36 | InterpretEventFieldInfo(eventDescriptor.EventFieldInfo),
37 | ParseEventArgsJson(eventDescriptor.EventHandlerId, eventDescriptor.EventArgsType, eventArgsJson));
38 | }
39 |
40 | private WebEventData(int browserRendererId, ulong eventHandlerId, EventFieldInfo eventFieldInfo, EventArgs eventArgs)
41 | {
42 | BrowserRendererId = browserRendererId;
43 | EventHandlerId = eventHandlerId;
44 | EventFieldInfo = eventFieldInfo;
45 | EventArgs = eventArgs;
46 | }
47 |
48 | public int BrowserRendererId { get; }
49 |
50 | public ulong EventHandlerId { get; }
51 |
52 | public EventFieldInfo EventFieldInfo { get; }
53 |
54 | public EventArgs EventArgs { get; }
55 |
56 | private static EventArgs ParseEventArgsJson(ulong eventHandlerId, string eventArgsType, string eventArgsJson)
57 | {
58 | try
59 | {
60 | return eventArgsType switch
61 | {
62 | "change" => DeserializeChangeEventArgs(eventArgsJson),
63 | "clipboard" => Deserialize(eventArgsJson),
64 | "drag" => Deserialize(eventArgsJson),
65 | "error" => Deserialize(eventArgsJson),
66 | "focus" => Deserialize(eventArgsJson),
67 | "keyboard" => Deserialize(eventArgsJson),
68 | "mouse" => Deserialize(eventArgsJson),
69 | "pointer" => Deserialize(eventArgsJson),
70 | "progress" => Deserialize(eventArgsJson),
71 | "touch" => Deserialize(eventArgsJson),
72 | "unknown" => EventArgs.Empty,
73 | "wheel" => Deserialize(eventArgsJson),
74 | _ => throw new InvalidOperationException($"Unsupported event type '{eventArgsType}'. EventId: '{eventHandlerId}'."),
75 | };
76 | }
77 | catch (Exception e)
78 | {
79 | throw new InvalidOperationException($"There was an error parsing the event arguments. EventId: '{eventHandlerId}'.", e);
80 | }
81 | }
82 |
83 | private static T Deserialize(string json) => JsonSerializer.Deserialize(json, JsonSerializerOptionsProvider.Options);
84 |
85 | private static EventFieldInfo InterpretEventFieldInfo(EventFieldInfo fieldInfo)
86 | {
87 | // The incoming field value can be either a bool or a string, but since the .NET property
88 | // type is 'object', it will deserialize initially as a JsonElement
89 | if (fieldInfo?.FieldValue is JsonElement attributeValueJsonElement)
90 | {
91 | switch (attributeValueJsonElement.ValueKind)
92 | {
93 | case JsonValueKind.True:
94 | case JsonValueKind.False:
95 | return new EventFieldInfo
96 | {
97 | ComponentId = fieldInfo.ComponentId,
98 | FieldValue = attributeValueJsonElement.GetBoolean()
99 | };
100 | default:
101 | return new EventFieldInfo
102 | {
103 | ComponentId = fieldInfo.ComponentId,
104 | FieldValue = attributeValueJsonElement.GetString()
105 | };
106 | }
107 | }
108 |
109 | return null;
110 | }
111 |
112 | private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson)
113 | {
114 | var changeArgs = Deserialize(eventArgsJson);
115 | var jsonElement = (JsonElement)changeArgs.Value;
116 | switch (jsonElement.ValueKind)
117 | {
118 | case JsonValueKind.Null:
119 | changeArgs.Value = null;
120 | break;
121 | case JsonValueKind.String:
122 | changeArgs.Value = jsonElement.GetString();
123 | break;
124 | case JsonValueKind.True:
125 | case JsonValueKind.False:
126 | changeArgs.Value = jsonElement.GetBoolean();
127 | break;
128 | default:
129 | throw new ArgumentException($"Unsupported {nameof(ChangeEventArgs)} value {jsonElement}.");
130 | }
131 | return changeArgs;
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/WebWindow.Blazor/WebWindow.Blazor.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.0
5 | WebWindow.Blazor
6 | Host a Blazor application inside a native OS window on Windows, Mac, and Linux
7 | Apache-2.0
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/WebWindow.Native/Exports.cpp:
--------------------------------------------------------------------------------
1 | #include "WebWindow.h"
2 |
3 | #ifdef _WIN32
4 | # define EXPORTED __declspec(dllexport)
5 | #else
6 | # define EXPORTED
7 | #endif
8 |
9 | extern "C"
10 | {
11 | #ifdef _WIN32
12 | EXPORTED void WebWindow_register_win32(HINSTANCE hInstance)
13 | {
14 | WebWindow::Register(hInstance);
15 | }
16 |
17 | EXPORTED HWND WebWindow_getHwnd_win32(WebWindow* instance)
18 | {
19 | return instance->getHwnd();
20 | }
21 | #elif OS_MAC
22 | EXPORTED void WebWindow_register_mac()
23 | {
24 | WebWindow::Register();
25 | }
26 | #endif
27 |
28 | EXPORTED WebWindow* WebWindow_ctor(AutoString title, WebWindow* parent, WebMessageReceivedCallback webMessageReceivedCallback)
29 | {
30 | return new WebWindow(title, parent, webMessageReceivedCallback);
31 | }
32 |
33 | EXPORTED void WebWindow_dtor(WebWindow* instance)
34 | {
35 | delete instance;
36 | }
37 |
38 | EXPORTED void WebWindow_SetTitle(WebWindow* instance, AutoString title)
39 | {
40 | instance->SetTitle(title);
41 | }
42 |
43 | EXPORTED void WebWindow_Show(WebWindow* instance)
44 | {
45 | instance->Show();
46 | }
47 |
48 | EXPORTED void WebWindow_WaitForExit(WebWindow* instance)
49 | {
50 | instance->WaitForExit();
51 | }
52 |
53 | EXPORTED void WebWindow_ShowMessage(WebWindow* instance, AutoString title, AutoString body, unsigned int type)
54 | {
55 | instance->ShowMessage(title, body, type);
56 | }
57 |
58 | EXPORTED void WebWindow_Invoke(WebWindow* instance, ACTION callback)
59 | {
60 | instance->Invoke(callback);
61 | }
62 |
63 | EXPORTED void WebWindow_NavigateToString(WebWindow* instance, AutoString content)
64 | {
65 | instance->NavigateToString(content);
66 | }
67 |
68 | EXPORTED void WebWindow_NavigateToUrl(WebWindow* instance, AutoString url)
69 | {
70 | instance->NavigateToUrl(url);
71 | }
72 |
73 | EXPORTED void WebWindow_SendMessage(WebWindow* instance, AutoString message)
74 | {
75 | instance->SendMessage(message);
76 | }
77 |
78 | EXPORTED void WebWindow_AddCustomScheme(WebWindow* instance, AutoString scheme, WebResourceRequestedCallback requestHandler)
79 | {
80 | instance->AddCustomScheme(scheme, requestHandler);
81 | }
82 |
83 | EXPORTED void WebWindow_SetResizable(WebWindow* instance, int resizable)
84 | {
85 | instance->SetResizable(resizable);
86 | }
87 |
88 | EXPORTED void WebWindow_GetSize(WebWindow* instance, int* width, int* height)
89 | {
90 | instance->GetSize(width, height);
91 | }
92 |
93 | EXPORTED void WebWindow_SetSize(WebWindow* instance, int width, int height)
94 | {
95 | instance->SetSize(width, height);
96 | }
97 |
98 | EXPORTED void WebWindow_SetResizedCallback(WebWindow* instance, ResizedCallback callback)
99 | {
100 | instance->SetResizedCallback(callback);
101 | }
102 |
103 | EXPORTED void WebWindow_GetAllMonitors(WebWindow* instance, GetAllMonitorsCallback callback)
104 | {
105 | instance->GetAllMonitors(callback);
106 | }
107 |
108 | EXPORTED unsigned int WebWindow_GetScreenDpi(WebWindow* instance)
109 | {
110 | return instance->GetScreenDpi();
111 | }
112 |
113 | EXPORTED void WebWindow_GetPosition(WebWindow* instance, int* x, int* y)
114 | {
115 | instance->GetPosition(x, y);
116 | }
117 |
118 | EXPORTED void WebWindow_SetPosition(WebWindow* instance, int x, int y)
119 | {
120 | instance->SetPosition(x, y);
121 | }
122 |
123 | EXPORTED void WebWindow_SetMovedCallback(WebWindow* instance, MovedCallback callback)
124 | {
125 | instance->SetMovedCallback(callback);
126 | }
127 |
128 | EXPORTED void WebWindow_SetTopmost(WebWindow* instance, int topmost)
129 | {
130 | instance->SetTopmost(topmost);
131 | }
132 |
133 | EXPORTED void WebWindow_SetIconFile(WebWindow* instance, AutoString filename)
134 | {
135 | instance->SetIconFile(filename);
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/WebWindow.Native/WebWindow.Mac.AppDelegate.h:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | @interface MyApplicationDelegate : NSObject {
4 | NSWindow * window;
5 | }
6 | @end
7 |
--------------------------------------------------------------------------------
/src/WebWindow.Native/WebWindow.Mac.AppDelegate.mm:
--------------------------------------------------------------------------------
1 | #import "WebWindow.Mac.AppDelegate.h"
2 |
3 | @implementation MyApplicationDelegate : NSObject
4 | - (id)init {
5 | if (self = [super init]) {
6 | // allocate and initialize window and stuff here ..
7 | }
8 | return self;
9 | }
10 |
11 | - (void)applicationDidFinishLaunching:(NSNotification *)notification {
12 | [window makeKeyAndOrderFront:nil];
13 | [NSApp activateIgnoringOtherApps:YES];
14 | }
15 |
16 | - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {
17 | return true;
18 | }
19 |
20 | - (void)dealloc {
21 | [window release];
22 | [super dealloc];
23 | }
24 |
25 | @end
26 |
--------------------------------------------------------------------------------
/src/WebWindow.Native/WebWindow.Mac.UiDelegate.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 | #include "WebWindow.h"
4 |
5 | typedef void (*WebMessageReceivedCallback) (char* message);
6 |
7 | @interface MyUiDelegate : NSObject {
8 | @public
9 | NSWindow * window;
10 | WebWindow * webWindow;
11 | WebMessageReceivedCallback webMessageReceivedCallback;
12 | }
13 | @end
14 |
--------------------------------------------------------------------------------
/src/WebWindow.Native/WebWindow.Mac.UiDelegate.mm:
--------------------------------------------------------------------------------
1 | #import "WebWindow.Mac.UiDelegate.h"
2 |
3 | @implementation MyUiDelegate : NSObject
4 |
5 | - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
6 | {
7 | char *messageUtf8 = (char *)[message.body UTF8String];
8 | webMessageReceivedCallback(messageUtf8);
9 | }
10 |
11 | - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
12 | {
13 | NSAlert* alert = [[NSAlert alloc] init];
14 |
15 | [alert setMessageText:[NSString stringWithFormat:@"Alert: %@.", [frame.request.URL absoluteString]]];
16 | [alert setInformativeText:message];
17 | [alert addButtonWithTitle:@"OK"];
18 |
19 | [alert beginSheetModalForWindow:window completionHandler:^void (NSModalResponse response) {
20 | completionHandler();
21 | [alert release];
22 | }];
23 | }
24 |
25 | - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler
26 | {
27 | NSAlert* alert = [[NSAlert alloc] init];
28 |
29 | [alert setMessageText:[NSString stringWithFormat:@"Confirm: %@.", [frame.request.URL absoluteString]]];
30 | [alert setInformativeText:message];
31 |
32 | [alert addButtonWithTitle:@"OK"];
33 | [alert addButtonWithTitle:@"Cancel"];
34 |
35 | [alert beginSheetModalForWindow:window completionHandler:^void (NSModalResponse response) {
36 | completionHandler(response == NSAlertFirstButtonReturn);
37 | [alert release];
38 | }];
39 | }
40 |
41 | - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler
42 | {
43 | NSAlert* alert = [[NSAlert alloc] init];
44 |
45 | [alert setMessageText:[NSString stringWithFormat:@"Prompt: %@.", [frame.request.URL absoluteString]]];
46 | [alert setInformativeText:prompt];
47 |
48 | [alert addButtonWithTitle:@"OK"];
49 | [alert addButtonWithTitle:@"Cancel"];
50 |
51 | NSTextField* input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 24)];
52 | [input setStringValue:defaultText];
53 | [alert setAccessoryView:input];
54 |
55 | [alert beginSheetModalForWindow:window completionHandler:^void (NSModalResponse response) {
56 | [input validateEditing];
57 | completionHandler(response == NSAlertFirstButtonReturn ? [input stringValue] : nil);
58 | [alert release];
59 | }];
60 | }
61 |
62 | - (void)windowDidResize:(NSNotification *)notification {
63 | int width, height;
64 | webWindow->GetSize(&width, &height);
65 | webWindow->InvokeResized(width, height);
66 | }
67 |
68 | - (void)windowDidMove:(NSNotification *)notification {
69 | int x, y;
70 | webWindow->GetPosition(&x, &y);
71 | webWindow->InvokeMoved(x, y);
72 | }
73 |
74 | @end
75 |
--------------------------------------------------------------------------------
/src/WebWindow.Native/WebWindow.Mac.UrlSchemeHandler.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | typedef void* (*WebResourceRequestedCallback) (char* url, int* outNumBytes, char** outContentType);
5 |
6 | @interface MyUrlSchemeHandler : NSObject {
7 | @public
8 | WebResourceRequestedCallback requestHandler;
9 | }
10 | @end
11 |
--------------------------------------------------------------------------------
/src/WebWindow.Native/WebWindow.Mac.UrlSchemeHandler.m:
--------------------------------------------------------------------------------
1 | #import "WebWindow.Mac.UrlSchemeHandler.h"
2 |
3 | @implementation MyUrlSchemeHandler : NSObject
4 |
5 | - (void)webView:(WKWebView *)webView startURLSchemeTask:(id )urlSchemeTask
6 | {
7 | NSURL *url = [[urlSchemeTask request] URL];
8 | char *urlUtf8 = (char *)[url.absoluteString UTF8String];
9 | int numBytes;
10 | char* contentType;
11 | void* dotNetResponse = requestHandler(urlUtf8, &numBytes, &contentType);
12 |
13 | NSInteger statusCode = dotNetResponse == NULL ? 404 : 200;
14 |
15 | NSString* nsContentType = [[NSString stringWithUTF8String:contentType] autorelease];
16 |
17 | NSDictionary* headers = @{ @"Content-Type" : nsContentType, @"Cache-Control": @"no-cache" };
18 | NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:statusCode HTTPVersion:nil headerFields:headers];
19 | [urlSchemeTask didReceiveResponse:response];
20 | [urlSchemeTask didReceiveData:[NSData dataWithBytes:dotNetResponse length:numBytes]];
21 | [urlSchemeTask didFinish];
22 |
23 | free(dotNetResponse);
24 | free(contentType);
25 | }
26 |
27 | - (void)webView:(WKWebView *)webView stopURLSchemeTask:(id )urlSchemeTask
28 | {
29 |
30 | }
31 |
32 | @end
33 |
--------------------------------------------------------------------------------
/src/WebWindow.Native/WebWindow.Native.vcxproj.filters:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx
7 |
8 |
9 | {93995380-89BD-4b04-88EB-625FBE52EBFB}
10 | h;hh;hpp;hxx;hm;inl;inc;ipp;xsd
11 |
12 |
13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
15 |
16 |
17 |
18 |
19 | Source Files
20 |
21 |
22 | Source Files
23 |
24 |
25 | Source Files
26 |
27 |
28 |
29 |
30 | Header Files
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/WebWindow.Native/WebWindow.h:
--------------------------------------------------------------------------------
1 | #ifndef WEBWINDOW_H
2 | #define WEBWINDOW_H
3 |
4 | #ifdef _WIN32
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | typedef const wchar_t* AutoString;
12 | #else
13 | #ifdef OS_LINUX
14 | #include
15 | #endif
16 | typedef char* AutoString;
17 | #endif
18 |
19 | struct Monitor
20 | {
21 | struct MonitorRect
22 | {
23 | int x, y;
24 | int width, height;
25 | } monitor, work;
26 | };
27 |
28 | typedef void (*ACTION)();
29 | typedef void (*WebMessageReceivedCallback)(AutoString message);
30 | typedef void* (*WebResourceRequestedCallback)(AutoString url, int* outNumBytes, AutoString* outContentType);
31 | typedef int (*GetAllMonitorsCallback)(const Monitor* monitor);
32 | typedef void (*ResizedCallback)(int width, int height);
33 | typedef void (*MovedCallback)(int x, int y);
34 |
35 | class WebWindow
36 | {
37 | private:
38 | WebMessageReceivedCallback _webMessageReceivedCallback;
39 | MovedCallback _movedCallback;
40 | ResizedCallback _resizedCallback;
41 | #ifdef _WIN32
42 | static HINSTANCE _hInstance;
43 | HWND _hWnd;
44 | WebWindow* _parent;
45 | wil::com_ptr _webviewEnvironment;
46 | wil::com_ptr _webviewWindow;
47 | std::map _schemeToRequestHandler;
48 | void AttachWebView();
49 | #elif OS_LINUX
50 | GtkWidget* _window;
51 | GtkWidget* _webview;
52 | #elif OS_MAC
53 | void* _window;
54 | void* _webview;
55 | void* _webviewConfiguration;
56 | void AttachWebView();
57 | #endif
58 |
59 | public:
60 | #ifdef _WIN32
61 | static void Register(HINSTANCE hInstance);
62 | HWND getHwnd();
63 | void RefitContent();
64 | #elif OS_MAC
65 | static void Register();
66 | #endif
67 |
68 | WebWindow(AutoString title, WebWindow* parent, WebMessageReceivedCallback webMessageReceivedCallback);
69 | ~WebWindow();
70 | void SetTitle(AutoString title);
71 | void Show();
72 | void WaitForExit();
73 | void ShowMessage(AutoString title, AutoString body, unsigned int type);
74 | void Invoke(ACTION callback);
75 | void NavigateToUrl(AutoString url);
76 | void NavigateToString(AutoString content);
77 | void SendMessage(AutoString message);
78 | void AddCustomScheme(AutoString scheme, WebResourceRequestedCallback requestHandler);
79 | void SetResizable(bool resizable);
80 | void GetSize(int* width, int* height);
81 | void SetSize(int width, int height);
82 | void SetResizedCallback(ResizedCallback callback) { _resizedCallback = callback; }
83 | void InvokeResized(int width, int height) { if (_resizedCallback) _resizedCallback(width, height); }
84 | void GetAllMonitors(GetAllMonitorsCallback callback);
85 | unsigned int GetScreenDpi();
86 | void GetPosition(int* x, int* y);
87 | void SetPosition(int x, int y);
88 | void SetMovedCallback(MovedCallback callback) { _movedCallback = callback; }
89 | void InvokeMoved(int x, int y) { if (_movedCallback) _movedCallback(x, y); }
90 | void SetTopmost(bool topmost);
91 | void SetIconFile(AutoString filename);
92 | };
93 |
94 | #endif // !WEBWINDOW_H
95 |
--------------------------------------------------------------------------------
/src/WebWindow.Native/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/WebWindow/WebWindow.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WebWindow
5 | Open native OS windows hosting web UI on Windows, Mac, and Linux
6 | Apache-2.0
7 | netstandard2.1
8 | ..\WebWindow.Native\x64\$(Configuration)\
9 | $([MSBuild]::IsOsPlatform('OSX'))
10 | win-x64
11 | linux-x64
12 | osx-x64
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 |
27 |
28 |
29 |
30 | <_NativeLibraries Include="$(NativeOutputDir)WebWindow.Native.dll" Condition="Exists('$(NativeOutputDir)WebWindow.Native.dll')" />
31 | <_NativeLibraries Include="$(NativeOutputDir)WebView2Loader.dll" Condition="Exists('$(NativeOutputDir)WebView2Loader.dll')" />
32 | <_NativeLibraries Include="$(NativeOutputDir)WebWindow.Native.so" Condition="Exists('$(NativeOutputDir)WebWindow.Native.so')" />
33 | <_NativeLibraries Include="$(NativeOutputDir)WebWindow.Native.dylib" Condition="Exists('$(NativeOutputDir)WebWindow.Native.dylib')" />
34 |
35 | PreserveNewest
36 | %(Filename)%(Extension)
37 | true
38 | runtimes/$(NativeAssetRuntimeIdentifier)/native/%(Filename)%(Extension)
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/WebWindow/WebWindowOptions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 |
4 | namespace WebWindows
5 | {
6 | public class WebWindowOptions
7 | {
8 | public WebWindow Parent { get; set; }
9 |
10 | public IDictionary SchemeHandlers { get; }
11 | = new Dictionary();
12 | }
13 |
14 | public delegate Stream ResolveWebResourceDelegate(string url, out string contentType);
15 | }
16 |
--------------------------------------------------------------------------------
/testassets/HelloWorldApp/HelloWorldApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp3.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | PreserveNewest
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/testassets/HelloWorldApp/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 | using WebWindows;
5 |
6 | namespace HelloWorldApp
7 | {
8 | class Program
9 | {
10 | static void Main(string[] args)
11 | {
12 | var window = new WebWindow("My great app", options =>
13 | {
14 | options.SchemeHandlers.Add("app", (string url, out string contentType) =>
15 | {
16 | contentType = "text/javascript";
17 | return new MemoryStream(Encoding.UTF8.GetBytes("alert('super')"));
18 | });
19 | });
20 |
21 | window.OnWebMessageReceived += (sender, message) =>
22 | {
23 | window.SendMessage("Got message: " + message);
24 | };
25 |
26 | window.NavigateToLocalFile("wwwroot/index.html");
27 | window.WaitForExit();
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/testassets/HelloWorldApp/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "HelloWorldApp": {
4 | "commandName": "Project",
5 | "nativeDebugging": true
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/testassets/HelloWorldApp/wwwroot/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteveSandersonMS/WebWindow/ccdf6e7dda39fe98c7d9fec7f848c8fd8edd682d/testassets/HelloWorldApp/wwwroot/image.png
--------------------------------------------------------------------------------
/testassets/HelloWorldApp/wwwroot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hello
4 | This is a local file
5 |
6 |
7 |
8 | Call .NET
9 |
10 |
11 |
12 |
13 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/App.razor:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Sorry, there's nothing at this address.
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/MyBlazorApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.0
5 | WinExe
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | PreserveNewest
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/Pages/Counter.razor:
--------------------------------------------------------------------------------
1 | @page "/counter"
2 |
3 | Counter
4 |
5 | Current count: @currentCount
6 |
7 | Click me
8 |
9 | @code {
10 | int currentCount = 0;
11 |
12 | void IncrementCount()
13 | {
14 | currentCount++;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/Pages/FetchData.razor:
--------------------------------------------------------------------------------
1 | @page "/fetchdata"
2 | @using System.IO
3 | @using System.Text.Json
4 |
5 | Weather forecast
6 |
7 | This component demonstrates fetching data from the server.
8 |
9 | @if (forecasts == null)
10 | {
11 | Loading...
12 | }
13 | else
14 | {
15 |
16 |
17 |
18 | Date
19 | Temp. (C)
20 | Temp. (F)
21 | Summary
22 |
23 |
24 |
25 | @foreach (var forecast in forecasts)
26 | {
27 |
28 | @forecast.Date.ToShortDateString()
29 | @forecast.TemperatureC
30 | @forecast.TemperatureF
31 | @forecast.Summary
32 |
33 | }
34 |
35 |
36 | }
37 |
38 | @code {
39 | WeatherForecast[] forecasts;
40 |
41 | protected override async Task OnInitializedAsync()
42 | {
43 | var forecastsJson = await File.ReadAllTextAsync("wwwroot/sample-data/weather.json");
44 | forecasts = JsonSerializer.Deserialize(forecastsJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
45 | }
46 |
47 | public class WeatherForecast
48 | {
49 | public DateTime Date { get; set; }
50 |
51 | public int TemperatureC { get; set; }
52 |
53 | public string Summary { get; set; }
54 |
55 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/Pages/Index.razor:
--------------------------------------------------------------------------------
1 | @page "/"
2 |
3 | Hello, world!
4 |
5 | Welcome to your new app.
6 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/Pages/WindowProp.razor:
--------------------------------------------------------------------------------
1 | @page "/window"
2 | @inject WebWindow Window
3 |
4 | Window properties
5 | Screen size
6 | DPI: @Window.ScreenDpi
7 | @foreach (var (m, i) in Window.Monitors.Select((monitor, index) => (monitor, index)))
8 | {
9 | Monitor @i: Width: @m.MonitorArea.Width, Height: @m.MonitorArea.Height
10 | }
11 | Window size
12 |
22 | Window location
23 |
33 | Window properties
34 |
44 | Icon
45 |
46 |
47 | Icon file
48 |
49 |
50 |
51 | Change
52 |
53 |
54 |
55 | @code {
56 | protected override void OnInitialized()
57 | {
58 | Window.SizeChanged += (sender, e) => StateHasChanged();
59 | Window.LocationChanged += (sender, e) => StateHasChanged();
60 | }
61 |
62 | string iconFilename;
63 |
64 | void ChangeIconFile()
65 | {
66 | if (!string.IsNullOrEmpty(iconFilename))
67 | {
68 | Window.SetIconFile(iconFilename);
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/Program.cs:
--------------------------------------------------------------------------------
1 | using WebWindows.Blazor;
2 | using System;
3 |
4 | namespace MyBlazorApp
5 | {
6 | public class Program
7 | {
8 | static void Main(string[] args)
9 | {
10 | ComponentsDesktop.Run("My Blazor App", "wwwroot/index.html");
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/Shared/MainLayout.razor:
--------------------------------------------------------------------------------
1 | @inherits LayoutComponentBase
2 |
3 |
6 |
7 |
8 |
11 |
12 |
13 | @Body
14 |
15 |
16 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/Shared/NavMenu.razor:
--------------------------------------------------------------------------------
1 |
7 |
8 |
32 |
33 | @code {
34 | bool collapseNavMenu = true;
35 |
36 | string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
37 |
38 | void ToggleNavMenu()
39 | {
40 | collapseNavMenu = !collapseNavMenu;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using WebWindows.Blazor;
3 |
4 | namespace MyBlazorApp
5 | {
6 | public class Startup
7 | {
8 | public void ConfigureServices(IServiceCollection services)
9 | {
10 | }
11 |
12 | public void Configure(DesktopApplicationBuilder app)
13 | {
14 | app.AddComponent("app");
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/_Imports.razor:
--------------------------------------------------------------------------------
1 | @using System.Net.Http
2 | @using Microsoft.AspNetCore.Authorization
3 | @using Microsoft.AspNetCore.Components.Forms
4 | @using Microsoft.AspNetCore.Components.Routing
5 | @using Microsoft.AspNetCore.Components.Web
6 | @using Microsoft.JSInterop
7 | @using MyBlazorApp
8 | @using MyBlazorApp.Shared
9 | @using WebWindows;
10 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/wwwroot/css/open-iconic/FONT-LICENSE:
--------------------------------------------------------------------------------
1 | SIL OPEN FONT LICENSE Version 1.1
2 |
3 | Copyright (c) 2014 Waybury
4 |
5 | PREAMBLE
6 | The goals of the Open Font License (OFL) are to stimulate worldwide
7 | development of collaborative font projects, to support the font creation
8 | efforts of academic and linguistic communities, and to provide a free and
9 | open framework in which fonts may be shared and improved in partnership
10 | with others.
11 |
12 | The OFL allows the licensed fonts to be used, studied, modified and
13 | redistributed freely as long as they are not sold by themselves. The
14 | fonts, including any derivative works, can be bundled, embedded,
15 | redistributed and/or sold with any software provided that any reserved
16 | names are not used by derivative works. The fonts and derivatives,
17 | however, cannot be released under any other type of license. The
18 | requirement for fonts to remain under this license does not apply
19 | to any document created using the fonts or their derivatives.
20 |
21 | DEFINITIONS
22 | "Font Software" refers to the set of files released by the Copyright
23 | Holder(s) under this license and clearly marked as such. This may
24 | include source files, build scripts and documentation.
25 |
26 | "Reserved Font Name" refers to any names specified as such after the
27 | copyright statement(s).
28 |
29 | "Original Version" refers to the collection of Font Software components as
30 | distributed by the Copyright Holder(s).
31 |
32 | "Modified Version" refers to any derivative made by adding to, deleting,
33 | or substituting -- in part or in whole -- any of the components of the
34 | Original Version, by changing formats or by porting the Font Software to a
35 | new environment.
36 |
37 | "Author" refers to any designer, engineer, programmer, technical
38 | writer or other person who contributed to the Font Software.
39 |
40 | PERMISSION & CONDITIONS
41 | Permission is hereby granted, free of charge, to any person obtaining
42 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
43 | redistribute, and sell modified and unmodified copies of the Font
44 | Software, subject to the following conditions:
45 |
46 | 1) Neither the Font Software nor any of its individual components,
47 | in Original or Modified Versions, may be sold by itself.
48 |
49 | 2) Original or Modified Versions of the Font Software may be bundled,
50 | redistributed and/or sold with any software, provided that each copy
51 | contains the above copyright notice and this license. These can be
52 | included either as stand-alone text files, human-readable headers or
53 | in the appropriate machine-readable metadata fields within text or
54 | binary files as long as those fields can be easily viewed by the user.
55 |
56 | 3) No Modified Version of the Font Software may use the Reserved Font
57 | Name(s) unless explicit written permission is granted by the corresponding
58 | Copyright Holder. This restriction only applies to the primary font name as
59 | presented to the users.
60 |
61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
62 | Software shall not be used to promote, endorse or advertise any
63 | Modified Version, except to acknowledge the contribution(s) of the
64 | Copyright Holder(s) and the Author(s) or with their explicit written
65 | permission.
66 |
67 | 5) The Font Software, modified or unmodified, in part or in whole,
68 | must be distributed entirely under this license, and must not be
69 | distributed under any other license. The requirement for fonts to
70 | remain under this license does not apply to any document created
71 | using the Font Software.
72 |
73 | TERMINATION
74 | This license becomes null and void if any of the above conditions are
75 | not met.
76 |
77 | DISCLAIMER
78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
86 | OTHER DEALINGS IN THE FONT SOFTWARE.
87 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/wwwroot/css/open-iconic/ICON-LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Waybury
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/wwwroot/css/open-iconic/README.md:
--------------------------------------------------------------------------------
1 | [Open Iconic v1.1.1](http://useiconic.com/open)
2 | ===========
3 |
4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons)
5 |
6 |
7 |
8 | ## What's in Open Iconic?
9 |
10 | * 223 icons designed to be legible down to 8 pixels
11 | * Super-light SVG files - 61.8 for the entire set
12 | * SVG sprite—the modern replacement for icon fonts
13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats
14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats
15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px.
16 |
17 |
18 | ## Getting Started
19 |
20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections.
21 |
22 | ### General Usage
23 |
24 | #### Using Open Iconic's SVGs
25 |
26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute).
27 |
28 | ```
29 |
30 | ```
31 |
32 | #### Using Open Iconic's SVG Sprite
33 |
34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack.
35 |
36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.*
37 |
38 | ```
39 |
40 |
41 |
42 | ```
43 |
44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions.
45 |
46 | ```
47 | .icon {
48 | width: 16px;
49 | height: 16px;
50 | }
51 | ```
52 |
53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag.
54 |
55 | ```
56 | .icon-account-login {
57 | fill: #f00;
58 | }
59 | ```
60 |
61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/).
62 |
63 | #### Using Open Iconic's Icon Font...
64 |
65 |
66 | ##### …with Bootstrap
67 |
68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}`
69 |
70 |
71 | ```
72 |
73 | ```
74 |
75 |
76 | ```
77 |
78 | ```
79 |
80 | ##### …with Foundation
81 |
82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}`
83 |
84 | ```
85 |
86 | ```
87 |
88 |
89 | ```
90 |
91 | ```
92 |
93 | ##### …on its own
94 |
95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}`
96 |
97 | ```
98 |
99 | ```
100 |
101 | ```
102 |
103 | ```
104 |
105 |
106 | ## License
107 |
108 | ### Icons
109 |
110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT).
111 |
112 | ### Fonts
113 |
114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web).
115 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/wwwroot/css/open-iconic/font/fonts/open-iconic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteveSandersonMS/WebWindow/ccdf6e7dda39fe98c7d9fec7f848c8fd8edd682d/testassets/MyBlazorApp/wwwroot/css/open-iconic/font/fonts/open-iconic.eot
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/wwwroot/css/open-iconic/font/fonts/open-iconic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteveSandersonMS/WebWindow/ccdf6e7dda39fe98c7d9fec7f848c8fd8edd682d/testassets/MyBlazorApp/wwwroot/css/open-iconic/font/fonts/open-iconic.otf
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteveSandersonMS/WebWindow/ccdf6e7dda39fe98c7d9fec7f848c8fd8edd682d/testassets/MyBlazorApp/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/wwwroot/css/open-iconic/font/fonts/open-iconic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteveSandersonMS/WebWindow/ccdf6e7dda39fe98c7d9fec7f848c8fd8edd682d/testassets/MyBlazorApp/wwwroot/css/open-iconic/font/fonts/open-iconic.woff
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/wwwroot/css/site.css:
--------------------------------------------------------------------------------
1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
2 |
3 | html, body {
4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
5 | }
6 |
7 | a, .btn-link {
8 | color: #0366d6;
9 | }
10 |
11 | .btn-primary {
12 | color: #fff;
13 | background-color: #1b6ec2;
14 | border-color: #1861ac;
15 | }
16 |
17 | app {
18 | position: relative;
19 | display: flex;
20 | flex-direction: column;
21 | }
22 |
23 | .top-row {
24 | height: 3.5rem;
25 | display: flex;
26 | align-items: center;
27 | }
28 |
29 | .main {
30 | flex: 1;
31 | }
32 |
33 | .main .top-row {
34 | background-color: #f7f7f7;
35 | border-bottom: 1px solid #d6d5d5;
36 | justify-content: flex-end;
37 | }
38 |
39 | .main .top-row > a {
40 | margin-left: 1.5rem;
41 | }
42 |
43 | .sidebar {
44 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
45 | }
46 |
47 | .sidebar .top-row {
48 | background-color: rgba(0,0,0,0.4);
49 | }
50 |
51 | .sidebar .navbar-brand {
52 | font-size: 1.1rem;
53 | }
54 |
55 | .sidebar .oi {
56 | width: 2rem;
57 | font-size: 1.1rem;
58 | vertical-align: text-top;
59 | top: -2px;
60 | }
61 |
62 | .nav-item {
63 | font-size: 0.9rem;
64 | padding-bottom: 0.5rem;
65 | }
66 |
67 | .nav-item:first-of-type {
68 | padding-top: 1rem;
69 | }
70 |
71 | .nav-item:last-of-type {
72 | padding-bottom: 1rem;
73 | }
74 |
75 | .nav-item a {
76 | color: #d7d7d7;
77 | border-radius: 4px;
78 | height: 3rem;
79 | display: flex;
80 | align-items: center;
81 | line-height: 3rem;
82 | }
83 |
84 | .nav-item a.active {
85 | background-color: rgba(255,255,255,0.25);
86 | color: white;
87 | }
88 |
89 | .nav-item a:hover {
90 | background-color: rgba(255,255,255,0.1);
91 | color: white;
92 | }
93 |
94 | .content {
95 | padding-top: 1.1rem;
96 | }
97 |
98 | .navbar-toggler {
99 | background-color: rgba(255, 255, 255, 0.1);
100 | }
101 |
102 | .valid.modified:not([type=checkbox]) {
103 | outline: 1px solid #26b050;
104 | }
105 |
106 | .invalid {
107 | outline: 1px solid red;
108 | }
109 |
110 | .validation-message {
111 | color: red;
112 | }
113 |
114 | @media (max-width: 767.98px) {
115 | .main .top-row {
116 | display: none;
117 | }
118 | }
119 |
120 | @media (min-width: 768px) {
121 | app {
122 | flex-direction: row;
123 | }
124 |
125 | .sidebar {
126 | width: 250px;
127 | height: 100vh;
128 | position: sticky;
129 | top: 0;
130 | }
131 |
132 | .main .top-row {
133 | position: sticky;
134 | top: 0;
135 | }
136 |
137 | .main > div {
138 | padding-left: 2rem !important;
139 | padding-right: 1.5rem !important;
140 | }
141 |
142 | .navbar-toggler {
143 | display: none;
144 | }
145 |
146 | .sidebar .collapse {
147 | /* Never collapse the sidebar for wide screens */
148 | display: block;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/wwwroot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | MyDesktopApp
7 |
8 |
9 |
10 |
11 |
12 | Loading...
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/testassets/MyBlazorApp/wwwroot/sample-data/weather.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "date": "2018-05-06",
4 | "temperatureC": 1,
5 | "summary": "Freezing",
6 | "temperatureF": 33
7 | },
8 | {
9 | "date": "2018-05-07",
10 | "temperatureC": 14,
11 | "summary": "Bracing",
12 | "temperatureF": 57
13 | },
14 | {
15 | "date": "2018-05-08",
16 | "temperatureC": -13,
17 | "summary": "Freezing",
18 | "temperatureF": 9
19 | },
20 | {
21 | "date": "2018-05-09",
22 | "temperatureC": -16,
23 | "summary": "Balmy",
24 | "temperatureF": 4
25 | },
26 | {
27 | "date": "2018-05-10",
28 | "temperatureC": -2,
29 | "summary": "Chilly",
30 | "temperatureF": 29
31 | }
32 | ]
33 |
--------------------------------------------------------------------------------