├── .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 | 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 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | @foreach (var forecast in forecasts) 26 | { 27 | 28 | 29 | 30 | 31 | 32 | 33 | } 34 | 35 |
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
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 |
9 | 26 |
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 | icon name 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 = '

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 | 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 | 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 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | @foreach (var forecast in forecasts) 26 | { 27 | 28 | 29 | 30 | 31 | 32 | 33 | } 34 | 35 |
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
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 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 |

Window location

23 |
24 |
25 | 26 | 27 |
28 |
29 | 30 | 31 |
32 |
33 |

Window properties

34 |
35 |
36 | 37 | 38 |
39 |
40 | 41 | 42 |
43 |
44 |

Icon

45 |
46 |
47 | 48 | 49 |
50 |
51 | 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 |
9 | About 10 |
11 | 12 |
13 | @Body 14 |
15 |
16 | -------------------------------------------------------------------------------- /testassets/MyBlazorApp/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  7 | 8 |
9 | 31 |
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 | icon name 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 | --------------------------------------------------------------------------------