├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codeql-analysis.yml │ └── dotnet.yml ├── .gitignore ├── .gitmodules ├── .yarnrc.yml ├── Directory.Build.props ├── README.md ├── Readme.txt ├── Release.md ├── RemoteBlazorWebView.sln ├── RunTests.ps1 ├── doc ├── LICENSE.txt ├── README.txt ├── Release.md ├── ReleaseNotes.txt ├── RemoteBlazor.pptx ├── RemoteBlazorWebView.gif ├── TODO.txt └── UpdateCertificate.txt ├── global.json ├── icons ├── 462-4623623_blazor-icon-blazor-logo-svg-hd-png-download.png ├── RemoteBlazorWebView.gif ├── Spin-1s-200px.gif ├── blazor.png ├── blazor.xcf ├── blazor240.png ├── blazor60.png ├── blazor60wide.png ├── favicon-32x32.png ├── favicon.ico ├── locked.png ├── wide.png └── wide.xcf ├── package.json ├── src ├── Benchmarks │ ├── ClientBenchmark │ │ ├── Blog.txt │ │ ├── ClientBenchmark.csproj │ │ ├── HttpClientWrapper.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ └── TimeVsNumClients.html │ ├── FilePOC │ │ ├── Client │ │ │ ├── Client.csproj │ │ │ ├── GlobalSuppressions.cs │ │ │ ├── Program.cs │ │ │ ├── Properties │ │ │ │ ├── PublishProfiles │ │ │ │ │ └── FolderProfile.pubxml │ │ │ │ └── launchSettings.json │ │ │ └── Utilities.cs │ │ └── FileWatcherClientService │ │ │ ├── FileWatcherClient.csproj │ │ │ ├── Program.cs │ │ │ ├── Properties │ │ │ ├── PublishProfiles │ │ │ │ └── FolderProfile.pubxml │ │ │ └── launchSettings.json │ │ │ ├── Worker.cs │ │ │ ├── app.manifest │ │ │ ├── appsettings.Development.json │ │ │ ├── appsettings.json │ │ │ └── protos │ │ │ └── filewatcher.proto │ ├── FileRead │ │ ├── FileRead.csproj │ │ ├── FileReadInitRequestBenchmark.cs │ │ ├── Program.cs │ │ └── Temperature.cs │ ├── Startup │ │ ├── Program.cs │ │ ├── Startup.csproj │ │ └── results.md │ ├── StressClient │ │ ├── Program.cs │ │ ├── Properties │ │ │ ├── PublishProfiles │ │ │ │ └── FolderProfile.pubxml │ │ │ └── launchSettings.json │ │ └── StressClient.csproj │ └── StressServer │ │ ├── Blog.md │ │ ├── ExecutableManager.cs │ │ ├── GlobalSuppressions.cs │ │ ├── Logging.cs │ │ ├── Program.cs │ │ ├── Properties │ │ ├── PublishProfiles │ │ │ └── FolderProfile.pubxml │ │ └── launchSettings.json │ │ ├── Resources │ │ └── .playwright.zip │ │ ├── StressServer.csproj │ │ └── app.manifest ├── EditWebView │ ├── EditWebView.csproj │ ├── Editor.cs │ ├── Program.cs │ ├── Utility.cs │ └── UtilityHelpers.cs ├── Protos │ └── webview.proto ├── RemoteBlazorWebView.WinForms │ ├── BlazorWebView.cs │ ├── BlazorWebViewFormBase.cs │ ├── IWindowsFormsBlazorWebViewBuilder.cs │ ├── PublicAPI.Shipped.txt │ ├── PublicAPI.Unshipped.txt │ ├── RemoteBlazorWebView.WindowsForms.csproj │ ├── RootComponent.cs │ ├── RootComponentCollectionExtensions.cs │ ├── RootComponentsCollection.cs │ ├── WindowsFormsBlazorMarkerService.cs │ ├── WindowsFormsBlazorWebViewBuilder.cs │ └── WindowsFormsDispatcher.cs ├── RemoteBlazorWebView.Wpf │ ├── AssemblyInfo.cs │ ├── BlazorWebView.cs │ ├── BlazorWebViewBase.cs │ ├── GlobalSuppressions.cs │ ├── IWpfBlazorWebViewBuilder.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── PublicAPI.Shipped.txt │ ├── PublicAPI.Unshipped.txt │ ├── RemoteBlazorWebView.Wpf.csproj │ ├── RootComponent.cs │ ├── RootComponentCollectionExtensions.cs │ ├── RootComponentsCollection.cs │ ├── WpfBlazorMarkerService.cs │ ├── WpfBlazorWebViewBuilder.cs │ └── WpfDispatcher.cs ├── RemoteBlazorWebView │ ├── GlobalSuppressions.cs │ ├── PhotinoWebViewManager.cs │ ├── RemoteBlazorWebView.csproj │ ├── RemoteBlazorWebViewAppBuilder.cs │ ├── RemoteBlazorWebViewWindow.cs │ ├── RemotePhotinoBlazorApp.cs │ ├── RemotePhotinoDispatcher.cs │ ├── RemotePhotinoSyncrhronizationContext.cs │ ├── RemotePhotinoWebViewManager.cs │ └── RemotePhotinoWindowRootComponents.cs ├── RemoteWebView.Blazor.JS │ ├── .gitignore │ ├── .yarnrc.yml │ ├── Build.md │ ├── HowToUpdateUpstreamFiles.md │ ├── RemoteWebView.Blazor.JS.csproj │ ├── Shared.JS │ │ ├── rollup.config.mjs │ │ └── tsconfig.json │ ├── TransferControl.txt │ ├── Web.JS │ │ ├── .gitignore │ │ ├── babel.config.js │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── rollup.config.mjs │ │ ├── src │ │ │ ├── .eslintrc.js │ │ │ ├── BinaryDecoder.ts │ │ │ ├── Boot.Server.Common.ts │ │ │ ├── Boot.Server.ts │ │ │ ├── Boot.Web.ts │ │ │ ├── Boot.WebAssembly.Common.ts │ │ │ ├── Boot.WebAssembly.ts │ │ │ ├── Boot.WebView.ts │ │ │ ├── BootCommon.ts │ │ │ ├── BootErrors.ts │ │ │ ├── DomWrapper.ts │ │ │ ├── Environment.ts │ │ │ ├── GlobalExports.ts │ │ │ ├── InputFile.ts │ │ │ ├── JSInitializers │ │ │ │ ├── JSInitializers.Server.ts │ │ │ │ ├── JSInitializers.Web.ts │ │ │ │ ├── JSInitializers.WebAssembly.ts │ │ │ │ ├── JSInitializers.WebView.ts │ │ │ │ └── JSInitializers.ts │ │ │ ├── NavigationLock.ts │ │ │ ├── PageTitle.ts │ │ │ ├── Platform │ │ │ │ ├── Circuits │ │ │ │ │ ├── CircuitManager.ts │ │ │ │ │ ├── CircuitStartOptions.ts │ │ │ │ │ ├── CircuitStreamingInterop.ts │ │ │ │ │ ├── DefaultReconnectDisplay.ts │ │ │ │ │ ├── DefaultReconnectionHandler.ts │ │ │ │ │ ├── ReconnectDisplay.ts │ │ │ │ │ ├── RenderQueue.ts │ │ │ │ │ └── UserSpecifiedDisplay.ts │ │ │ │ ├── Logging │ │ │ │ │ ├── Logger.ts │ │ │ │ │ └── Loggers.ts │ │ │ │ ├── Mono │ │ │ │ │ ├── MonoDebugger.ts │ │ │ │ │ ├── MonoNavigatorUserAgentData.ts │ │ │ │ │ └── MonoPlatform.ts │ │ │ │ ├── Platform.ts │ │ │ │ ├── SsrStartOptions.ts │ │ │ │ ├── WebAssemblyComponentAttacher.ts │ │ │ │ ├── WebAssemblyStartOptions.ts │ │ │ │ ├── WebStartOptions.ts │ │ │ │ └── WebView │ │ │ │ │ ├── WebViewIpcCommon.ts │ │ │ │ │ ├── WebViewIpcReceiver.ts │ │ │ │ │ └── WebViewIpcSender.ts │ │ │ ├── Rendering │ │ │ │ ├── BrowserRenderer.ts │ │ │ │ ├── DomMerging │ │ │ │ │ ├── AttributeSync.ts │ │ │ │ │ ├── DataPermanentElementSync.ts │ │ │ │ │ ├── DomSync.ts │ │ │ │ │ └── EditScript.ts │ │ │ │ ├── DomSpecialPropertyUtil.ts │ │ │ │ ├── ElementReferenceCapture.ts │ │ │ │ ├── Events │ │ │ │ │ ├── EventDelegator.ts │ │ │ │ │ ├── EventFieldInfo.ts │ │ │ │ │ └── EventTypes.ts │ │ │ │ ├── FocusOnNavigate.ts │ │ │ │ ├── JSRootComponents.ts │ │ │ │ ├── LogicalElements.ts │ │ │ │ ├── RenderBatch │ │ │ │ │ ├── OutOfProcessRenderBatch.ts │ │ │ │ │ ├── RenderBatch.ts │ │ │ │ │ └── SharedMemoryRenderBatch.ts │ │ │ │ ├── Renderer.ts │ │ │ │ ├── StreamingRendering.ts │ │ │ │ ├── WebRendererId.ts │ │ │ │ └── WebRendererInteropMethods.ts │ │ │ ├── Services │ │ │ │ ├── ComponentDescriptorDiscovery.ts │ │ │ │ ├── InitialRootComponentsList.ts │ │ │ │ ├── JSEventRegistry.ts │ │ │ │ ├── NavigationEnhancement.ts │ │ │ │ ├── NavigationManager.ts │ │ │ │ ├── NavigationUtils.ts │ │ │ │ ├── RootComponentManager.ts │ │ │ │ └── WebRootComponentManager.ts │ │ │ ├── StreamingInterop.ts │ │ │ ├── Utf8Decoder.ts │ │ │ ├── Virtualize.ts │ │ │ ├── tsconfig.json │ │ │ └── webpack.config.js │ │ ├── test │ │ │ ├── DomSync.test.ts │ │ │ └── EditDistance.test.ts │ │ ├── tsconfig.jest.json │ │ └── tsconfig.json │ ├── dotnet-runtime │ │ ├── dotnet-legacy.d.ts │ │ ├── dotnet.d.ts │ │ └── package.json │ ├── package.json │ ├── protoc │ │ ├── bin │ │ │ └── protoc.exe │ │ ├── include │ │ │ └── google │ │ │ │ └── protobuf │ │ │ │ ├── any.proto │ │ │ │ ├── api.proto │ │ │ │ ├── compiler │ │ │ │ └── plugin.proto │ │ │ │ ├── descriptor.proto │ │ │ │ ├── duration.proto │ │ │ │ ├── empty.proto │ │ │ │ ├── field_mask.proto │ │ │ │ ├── source_context.proto │ │ │ │ ├── struct.proto │ │ │ │ ├── timestamp.proto │ │ │ │ ├── type.proto │ │ │ │ └── wrappers.proto │ │ └── readme.txt │ ├── src │ │ ├── Boot.Desktop.js │ │ ├── Boot.Desktop.js.map │ │ ├── Boot.Desktop.ts │ │ ├── IPC.ts │ │ ├── RemoteWebView.js │ │ ├── RemoteWebView.js.map │ │ └── RemoteWebView.ts │ ├── tsconfig.json │ ├── webpack.config.js │ └── yarn.lock ├── RemoteWebView │ ├── ClientFileSyncManager.cs │ ├── ConnectedEventArgs.cs │ ├── DisconnectedEventArgs.cs │ ├── EtagGenerator.cs │ ├── FixedManifestEmbeddedAssembly.cs │ ├── GlobalSuppressions.cs │ ├── HealthCheck.cs │ ├── IBlazorWebView.cs │ ├── ManifestStaticWebAssetFileProvider.cs │ ├── ReadyToConnectEventArgs.cs │ ├── RefreshedEventArgs.cs │ ├── RemoteWebView.cs │ ├── RemoteWebView.csproj │ ├── StaticWebAssetsLoader.cs │ └── _framework │ │ └── blazor.modules.json ├── RemoteWebViewService │ ├── .config │ │ └── dotnet-tools.json │ ├── BrowserIPCState.cs │ ├── BrowserResponseNode.cs │ ├── CreateCert.ps1 │ ├── EndPoints │ │ ├── Contact.cs │ │ ├── Favicon.cs │ │ ├── GrpcBaseUri.cs │ │ ├── Health.cs │ │ ├── Mirror.cs │ │ ├── ResetStats.cs │ │ ├── Start.cs │ │ ├── StartOrRefresh.cs │ │ ├── Stats.cs │ │ ├── Status.cs │ │ └── Wait.cs │ ├── FileEntry.cs │ ├── GlobalSuppressions.cs │ ├── IPC.cs │ ├── IUserService.cs │ ├── JsonContext.cs │ ├── MockUserService.cs │ ├── Pages │ │ ├── HtmlPageGenerator.cs │ │ ├── LockedPage.cs │ │ ├── RestartFailedPage.cs │ │ ├── RestartPage.cs │ │ └── Shared │ │ │ └── _LoginPartial.cshtml │ ├── Performance.md │ ├── Program.cs │ ├── Properties │ │ ├── PublishProfiles │ │ │ ├── FolderProfileAuth.pubxml │ │ │ ├── FolderProfileNoAuth.pubxml │ │ │ ├── RemoteWebViewServer - Web Deploy1.pubxml │ │ │ ├── RemoteWebViewServer - Zip Deploy.pubxml │ │ │ └── RemoteWebViewServer.PublishSettings │ │ ├── ServiceDependencies │ │ │ ├── RemoteWebViewServer - Web Deploy1 │ │ │ │ └── profile.arm.json │ │ │ └── RemoteWebViewServerDeployAzure │ │ │ │ └── profile.arm.json │ │ ├── launchSettings.json │ │ └── serviceDependencies.json │ ├── ProtectedApiCallHelper.cs │ ├── RemoteStaticFiles │ │ ├── FileStats.cs │ │ ├── FileStream.cs │ │ ├── IFileProvider.cs │ │ ├── RemoteFileResolver.cs │ │ ├── RemoteFilesMiddleware.cs │ │ ├── RemoteFilesMiddlewareExtensions.cs │ │ ├── RemoteFilesOptions.cs │ │ └── ServerFileSyncManager.cs │ ├── RemoteWebViewService.csproj │ ├── RenewCert.txt │ ├── ServerStats.cs │ ├── ServiceState.cs │ ├── Services │ │ ├── BrowserIPCService.cs │ │ ├── ClientIPCService.cs │ │ ├── FileWatcherService.cs │ │ ├── RemoteWebViewService.cs │ │ ├── ShutdownService.cs │ │ └── StatsInterceptor.cs │ ├── Startup.cs │ ├── StaticMethods.cs │ ├── TODO.txt │ ├── TrustCert.ps1 │ ├── UserService.cs │ ├── appsettings.Development.json │ ├── appsettings.json.save │ └── start.bat ├── SharedSource │ ├── AutoCloseOnReadCompleteStream.cs │ ├── BlazorWebViewDeveloperTools.cs │ ├── BlazorWebViewInitializedEventArgs.cs │ ├── BlazorWebViewInitializingEventArgs.cs │ ├── BlazorWebViewServiceCollectionExtensions.cs │ ├── HostAddressHelper.cs │ ├── Log.cs │ ├── QueryStringHelper.cs │ ├── RemoteWebView2Manager.cs │ ├── StaticContentHotReloadManager.cs │ ├── UrlLoadingEventArgs.cs │ ├── UrlLoadingStrategy.cs │ └── WebView2WebViewManager.cs └── node_modules │ └── .yarn-integrity └── test └── FileSyncServer.Tests ├── AssemblyInfo.cs ├── BlazorWebViewFactory.cs ├── BlazorWebViewFormFactory.cs ├── ClientCaching.cs ├── ClientCachingCollection.cs ├── ClientFixture.cs ├── ConcurrentRequestsTests .cs ├── FileFetchingTests.cs ├── FileSyncServer.Tests.csproj ├── GlobalSuppressions.cs ├── Home.razor ├── Home.razor.cs ├── LargeFileSetup.cs ├── LoadTest.cs ├── Local ├── BaseTestClicks.cs ├── BaseTestFixture.cs └── TestLocal.cs ├── PerformanceCollection.cs ├── PerformanceTests .cs ├── Remote ├── BaseTestRemote.cs ├── BaseTestRemoteFixture.cs ├── TestRemote.cs ├── TestServerForm.cs └── TestServerWpf.cs ├── ServerCaching.cs ├── ServerCachingCollection.cs ├── ServerCollection.cs ├── ServerFixture.cs ├── TestBlazorFormControl.cs ├── TestBlazorFormControlFixture.cs ├── TestBlazorWpfControl.cs ├── TestBlazorWpfControlFixture.cs ├── TestMisc.cs ├── Utilities.cs └── wwwroot ├── css ├── app.css └── site.css ├── favicon.ico └── index.html /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ net8 ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ net8 ] 20 | schedule: 21 | - cron: '28 12 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'csharp', 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ net8 ] 6 | pull_request: 7 | branches: [ net8 ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 8.0.x 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --no-restore 24 | - name: Test 25 | run: dotnet test --no-build --verbosity normal 26 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/.gitmodules -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildThisFileDirectory)artifacts 5 | 9.0.5 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RemoteBlazorWebView.Wpf.BlazorWebView 2 | 3 | RemoteBlazorWebView.Wpf.BlazorWebView is a powerful control based on the .NET 9 Blazor WebView Control for WPF applications. It allows you to interact with the user interface of a program developed using the BlazorWebView WPF control through a web browser by leveraging a cloud-based server. 4 | 5 | The RemoteBlazorWebView.Wpf.BlazorWebView control facilitates remote interaction with your application's user interface by setting up a server (RemoteWebViewService) in the cloud and connecting your browser to it. 6 | 7 | As a drop-in replacement for the Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView control, RemoteBlazorWebView.Wpf requires only minimal changes to your existing application to enable remote control capabilities. This makes it an efficient and convenient solution for extending your application's functionality. 8 | 9 | 10 | # RemoteBlazorWebView.WindowsForms.BlazorWebView 11 | 12 | RemoteBlazorWebView.WindowsForms.BlazorWebView is a robust control built on the .NET 9 Blazor WebView WinForms Control, designed for Windows Forms applications. It allows you to engage with the user interface of a program created using the BlazorWebView WinForms control through a web browser, utilizing a cloud-based server. 13 | 14 | The RemoteBlazorWebView.WindowsForms.BlazorWebView control enables remote interaction with your application's user interface by establishing a server (RemoteWebViewService) in the cloud and directing your browser to connect with it. 15 | 16 | As a seamless replacement for the Microsoft.AspNetCore.Components.WebView.WindowsForms.BlazorWebView control, RemoteBlazorWebView.WindowsForms.BlazorWebView necessitates only minimal adjustments to your current application to empower remote control capabilities. This makes it an effective and user-friendly option for enhancing your application's features. 17 | 18 | # Demo Video 19 |  20 | 21 | # Usage instructions 22 | 23 | You do not need to build this repo unless you want to customize the RemoteWebViewService. Run the following command to install the RemoteWebViewService 24 | 25 | ```console 26 | dotnet tool update -g PeakSWC.RemoteWebViewService --version 9.*-* 27 | ``` 28 | 29 | # Samples 30 | 31 | Check out the tutorial at https://github.com/budcribar/RemoteBlazorWebViewTutorial 32 | 33 | -------------------------------------------------------------------------------- /Readme.txt: -------------------------------------------------------------------------------- 1 | To Run the Wpf or WinForms app in the remote mode 2 | 3 | Open a powershell window and execute the following commands 4 | 5 | RemoteWebViewService.exe 6 | .\RemoteBlazorWebViewTutorial.WinFormsApp.exe -u="https://localhost:5001" 7 | .\RemoteBlazorWebViewTutorial.WpfApp.exe -u="https://localhost:5001" -------------------------------------------------------------------------------- /Release.md: -------------------------------------------------------------------------------- 1 | - Increment VersionPrefix from Directory.Build.Props 2 | - Modify Readme.md in the RemoteBlazorWebView and RemoteBlazorWebViewTutorial repositories 3 | - Modify PackageReleaseNotes in Project Files (Wpf,Forms,RemoteWebView, RemoteWebViewService) 4 | - .\RunTests.ps1 -Mode Developer from RemoteBlazorWebView 5 | ** Note if unable to install RemoteWebViewServer you may need to do dotnet nuget remove source 6 | - Upload the contents of RemoteBlazorWebView\artifacts to nuget.org 7 | - Open the RemoteBlazorWebViewTutorial solution and make sure Package sources are set to all 8 | - Update the RemoteBlazorWebView.WindowsForms, RemoteBlazorWebView.Wpf, RemoteBlazorWebViewTutorial.Shared packages to the latest version 9 | - .\RunTests.ps1 -Mode Release 10 | - Verify RunTests.ps1 passes in release mode 11 | - Update both RemoteBlazorWebView and RemoteBlazorWebViewTutorial repositories with the artifacts/release.zip using the latest tag 12 | - Merge the changes into the dotnet6 branch 13 | - Create a new branch in both repositories RemoteBlazorWebView and RemoteBlazorWebViewTutorial 14 | - Copy the webwindow.proto file to the RemoteWebViewAdmin project 15 | - Copy the RemoteWebViewService to the WebWindow server 16 | - *** Test out the instructions to change the Port !!!!! -------------------------------------------------------------------------------- /doc/README.txt: -------------------------------------------------------------------------------- 1 | To Run the Wpf or WinForms app in the remote mode 2 | 3 | Open a powershell window and execute the following commands 4 | 5 | RemoteWebViewService.exe 6 | .\RemoteBlazorWebViewTutorial.WinFormsApp.exe -u="https://localhost:5001" 7 | .\RemoteBlazorWebViewTutorial.WpfApp.exe -u="https://localhost:5001" 8 | -------------------------------------------------------------------------------- /doc/Release.md: -------------------------------------------------------------------------------- 1 | - Increment VersionPrefix from Directory.Build.Props 2 | - Modify Readme.md in the RemoteBlazorWebView and RemoteBlazorWebViewTutorial repositories 3 | - Modify PackageReleaseNotes in Project Files (Wpf,Forms,RemoteWebView, RemoteWebViewService) 4 | - .\RunTests.ps1 -Mode Developer from RemoteBlazorWebView 5 | ** Note if unable to install RemoteWebViewServer you may need to do dotnet nuget remove source 6 | - Upload the contents of RemoteBlazorWebView\artifacts to nuget.org 7 | - Open the RemoteBlazorWebViewTutorial solution and make sure Package sources are set to all 8 | - Update the RemoteBlazorWebView.WindowsForms, RemoteBlazorWebView.Wpf, RemoteBlazorWebViewTutorial.Shared packages to the latest version 9 | - .\RunTests.ps1 -Mode Release 10 | - Verify RunTests.ps1 passes in release mode 11 | - Update both RemoteBlazorWebView and RemoteBlazorWebViewTutorial repositories with the artifacts/release.zip using the latest tag 12 | - Merge the changes into the dotnet6 branch 13 | - Create a new branch in both repositories RemoteBlazorWebView and RemoteBlazorWebViewTutorial 14 | - Copy the webwindow.proto file to the RemoteWebViewAdmin project 15 | - Copy the RemoteWebViewService to the WebWindow server 16 | - *** Test out the instructions to change the Port !!!!! 17 | 18 | 19 | -------------------------------------------------------------------------------- /doc/ReleaseNotes.txt: -------------------------------------------------------------------------------- 1 | - Added instructions on how to create a single file executable including wwwroot files -------------------------------------------------------------------------------- /doc/RemoteBlazor.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/doc/RemoteBlazor.pptx -------------------------------------------------------------------------------- /doc/RemoteBlazorWebView.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/doc/RemoteBlazorWebView.gif -------------------------------------------------------------------------------- /doc/TODO.txt: -------------------------------------------------------------------------------- 1 | Make the timeout a property on the WebView (Form and WPF) 2 | Fix cannot dispose of task while running exception 3 | Markup and Group cannot call StartWebViewCoreIfPossible because RequiredStartupPropertiesSet does not check if they are set or not. Also, Markup and Group MUST be set only before the remote web view is created!! 4 | WaitForInitializationComplete relies on a 5 second delay 5 | Remove the server from the release package and have user install dotnet tool instead 6 | Add detailed build instructions for the server 7 | 8 | Add detailed instructions for modifying existing Wpf and WinForms app 9 | 10 | 11 | The README file provides a comprehensive overview of the RemoteBlazorWebViewTutorial project. It explains what the project is about, its use cases, and how it works. The steps for downloading and running the demo, building and running the demo, and accessing the application from the cloud server are clearly explained. 12 | 13 | I have a few suggestions that could enhance the readme file: 14 | 15 | Adding a screenshot or an image that shows what the application looks like when running could make it more visually appealing. 16 | 17 | Clarifying the system requirements for running the application (e.g. .NET 7, Windows, Mac, or Linux) would be helpful. 18 | 19 | Adding a section that explains how to deploy the RemoteWebViewService to a cloud server could be useful for developers who want to run the application in production. 20 | 21 | Providing a brief explanation of what photino.Blazor is and how it relates to RemoteBlazorWebView would make it easier for readers to understand the project's background. 22 | 23 | Mentioning the security measures in place to ensure the safety of the data being transmitted through the server could help to build trust with users. 24 | 25 | Adding a section on troubleshooting common issues that might arise during setup or usage of the application would be helpful for users who might encounter problems. -------------------------------------------------------------------------------- /doc/UpdateCertificate.txt: -------------------------------------------------------------------------------- 1 | App Service -> RemoteableWebViewAdmin -> TLS/SSL Binding 2 | Add private key certificate 3 | 4 | Now go back and bind certificate 5 | 6 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.0", 4 | "rollForward": "latestMinor" 5 | } 6 | } -------------------------------------------------------------------------------- /icons/462-4623623_blazor-icon-blazor-logo-svg-hd-png-download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/icons/462-4623623_blazor-icon-blazor-logo-svg-hd-png-download.png -------------------------------------------------------------------------------- /icons/RemoteBlazorWebView.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/icons/RemoteBlazorWebView.gif -------------------------------------------------------------------------------- /icons/Spin-1s-200px.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/icons/Spin-1s-200px.gif -------------------------------------------------------------------------------- /icons/blazor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/icons/blazor.png -------------------------------------------------------------------------------- /icons/blazor.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/icons/blazor.xcf -------------------------------------------------------------------------------- /icons/blazor240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/icons/blazor240.png -------------------------------------------------------------------------------- /icons/blazor60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/icons/blazor60.png -------------------------------------------------------------------------------- /icons/blazor60wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/icons/blazor60wide.png -------------------------------------------------------------------------------- /icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/icons/favicon-32x32.png -------------------------------------------------------------------------------- /icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/icons/favicon.ico -------------------------------------------------------------------------------- /icons/locked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/icons/locked.png -------------------------------------------------------------------------------- /icons/wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/icons/wide.png -------------------------------------------------------------------------------- /icons/wide.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/icons/wide.xcf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/Benchmarks/ClientBenchmark/ClientBenchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | latest 9 | true 10 | x64 11 | $(NoWarn);CA1416 12 | DEBUG_SERVER;PROFILED_SERVER 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | true 35 | true 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/Benchmarks/ClientBenchmark/HttpClientWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace ClientBenchmark 2 | { 3 | using System; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | public class HttpClientWrapper(HttpClient httpClient) 10 | { 11 | private const int MaxRetries = 1; 12 | private const int RetryDelayMilliseconds = 1000; 13 | public int bytes; 14 | public int count; 15 | 16 | public async Task GetWithRetryAsync(string url) 17 | { 18 | int attempts = 0; 19 | while (attempts < MaxRetries) 20 | { 21 | try 22 | { 23 | var response = await httpClient.GetAsync(url); 24 | if (response.IsSuccessStatusCode) 25 | { 26 | var data = await response.Content.ReadAsStringAsync(); 27 | Interlocked.Add(ref bytes, data.Length); 28 | Interlocked.Increment(ref count); 29 | return data; 30 | } 31 | else 32 | { 33 | Console.WriteLine($"Attempt {attempts + 1} failed. Status code: {response.StatusCode}"); 34 | } 35 | } 36 | catch (HttpRequestException ex) 37 | { 38 | Console.WriteLine($"Attempt {attempts + 1} failed. Error: {ex.Message}"); 39 | } 40 | 41 | attempts++; 42 | if (attempts < MaxRetries) 43 | { 44 | await Task.Delay(RetryDelayMilliseconds); 45 | } 46 | } 47 | 48 | throw new Exception($"Failed to get successful response after {MaxRetries} attempts"); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Benchmarks/ClientBenchmark/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "ClientBenchmark": { 4 | "commandName": "Project", 5 | "hotReloadEnabled": false 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Benchmarks/FilePOC/Client/Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | Debug;Release 7 | enable 8 | enable 9 | true 10 | x64 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Benchmarks/FilePOC/Client/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 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "", Scope = "member", Target = "~M:Utilities.CreateSimplePngIcon(System.Int32,System.Int32,System.Drawing.Color)~System.Byte[]")] 9 | [assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "", Scope = "member", Target = "~M:Utilities.ModifyFilePermissions(System.String,System.Boolean,System.Boolean)")] 10 | -------------------------------------------------------------------------------- /src/Benchmarks/FilePOC/Client/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | publish\ 10 | FileSystem 11 | <_TargetId>Folder 12 | net9.0 13 | win-x64 14 | true 15 | true 16 | true 17 | true 18 | 19 | -------------------------------------------------------------------------------- /src/Benchmarks/FilePOC/Client/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Client": { 4 | "commandName": "Project", 5 | "commandLineArgs": "1e32d82e-2333-49ff-8675-15aa1a088bf1" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Benchmarks/FilePOC/FileWatcherClientService/FileWatcherClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | x64 8 | app.manifest 9 | $(NoWarn);CA1416 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Benchmarks/FilePOC/FileWatcherClientService/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | x64 9 | bin\Publish 10 | FileSystem 11 | <_TargetId>Folder 12 | net9.0 13 | win-x64 14 | true 15 | true 16 | false 17 | false 18 | 19 | -------------------------------------------------------------------------------- /src/Benchmarks/FilePOC/FileWatcherClientService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "FileWatcherClient": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "environmentVariables": { 8 | "DOTNET_ENVIRONMENT": "Development" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Benchmarks/FilePOC/FileWatcherClientService/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Benchmarks/FilePOC/FileWatcherClientService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Benchmarks/FilePOC/FileWatcherClientService/protos/filewatcher.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option csharp_namespace = "PeakSWC.RemoteWebView"; 4 | 5 | package webview; 6 | 7 | // The request message from client to server to start watching a file 8 | message WatchFileRequest { 9 | string file_path = 1; 10 | } 11 | 12 | message FileChangedNotification { 13 | string run_arguments = 1; 14 | } 15 | 16 | message FileChunk { 17 | bytes content = 1; 18 | } 19 | 20 | message WatchFileResponse { 21 | oneof response { 22 | FileChangedNotification notification = 1; 23 | FileChunk chunk = 2; 24 | } 25 | } 26 | 27 | service FileWatcherIPC { 28 | // Client sends a WatchFileRequest and server streams WatchFileResponse messages 29 | rpc WatchFile (WatchFileRequest) returns (stream WatchFileResponse); 30 | } -------------------------------------------------------------------------------- /src/Benchmarks/FileRead/FileRead.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | Debug;Release 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Benchmarks/FileRead/FileReadInitRequestBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Channels; 4 | using System.Threading.Tasks; 5 | using BenchmarkDotNet.Attributes; 6 | using BenchmarkDotNet.Running; 7 | using Grpc.Core; 8 | using PeakSWC.RemoteWebView; 9 | 10 | namespace PerformanceTests 11 | { 12 | public class FileReadInitRequestBenchmark 13 | { 14 | //private static readonly Channel channel = new Channel("localhost", 50051, ChannelCredentials.Insecure); 15 | //private static readonly RemoteWebViewService.RemoteWebViewServiceClient client = new RemoteWebViewService.RemoteWebViewServiceClient(channel); 16 | 17 | [Benchmark] 18 | public async Task FileReadInitRequest() 19 | { 20 | // Replace the file path and guid with valid values 21 | //var request = new FileReadInitRequest { File = "C:\\test\\test.txt", Guid = "123456" }; 22 | //await client.FileReadInitAsync(request); 23 | } 24 | } 25 | 26 | class Program 27 | { 28 | //static void Main(string[] args) 29 | //{ 30 | // var summary = BenchmarkRunner.Run(); 31 | //} 32 | } 33 | } 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Benchmarks/Startup/Startup.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0-windows 6 | enable 7 | enable 8 | true 9 | true 10 | x64 11 | $(NoWarn);CA1416 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Benchmarks/StressClient/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | x64 9 | bin\Publish 10 | FileSystem 11 | <_TargetId>Folder 12 | net9.0 13 | win-x64 14 | true 15 | true 16 | true 17 | false 18 | 19 | -------------------------------------------------------------------------------- /src/Benchmarks/StressClient/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "StressClient": { 4 | "commandName": "Project", 5 | "commandLineArgs": "10 1024 10240" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Benchmarks/StressClient/StressClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0-windows 6 | enable 7 | enable 8 | win-x64 9 | true 10 | x64 11 | $(NoWarn);CA1416 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Benchmarks/StressServer/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 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "", Scope = "member", Target = "~M:StressServer.Logging.SetupEventLog")] 9 | [assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "", Scope = "member", Target = "~M:StressServer.Logging.LogEvent(System.String,System.Diagnostics.EventLogEntryType)")] 10 | [assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "", Scope = "member", Target = "~M:StressServer.Program.Main(System.String[])")] 11 | -------------------------------------------------------------------------------- /src/Benchmarks/StressServer/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | x64 9 | publish\ 10 | FileSystem 11 | <_TargetId>Folder 12 | net9-windows 13 | win-x64 14 | 15 | -------------------------------------------------------------------------------- /src/Benchmarks/StressServer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "StressServer": { 4 | "commandName": "Project", 5 | "commandLineArgs": "10 100" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Benchmarks/StressServer/Resources/.playwright.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/src/Benchmarks/StressServer/Resources/.playwright.zip -------------------------------------------------------------------------------- /src/EditWebView/EditWebView.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | x64 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/EditWebView/UtilityHelpers.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | namespace EditWebView 3 | { 4 | public static partial class UtilityHelpers 5 | { 6 | 7 | [GeneratedRegex(@"/(?:v)?([\d.-]+(?:-[\w.]+)?)\.zip$")] 8 | public static partial Regex MyRegex(); 9 | } 10 | } -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.WinForms/IWindowsFormsBlazorWebViewBuilder.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 Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace PeakSWC.RemoteBlazorWebView.WindowsForms 7 | { 8 | /// 9 | /// A builder for Windows Forms Blazor WebViews. 10 | /// 11 | public interface IWindowsFormsBlazorWebViewBuilder 12 | { 13 | /// 14 | /// Gets the builder service collection. 15 | /// 16 | IServiceCollection Services { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.WinForms/PublicAPI.Unshipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.WinForms/RootComponentCollectionExtensions.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 Microsoft.AspNetCore.Components; 6 | using System.Collections.Generic; 7 | 8 | namespace PeakSWC.RemoteBlazorWebView.WindowsForms 9 | { 10 | /// 11 | /// Provides a set of extension methods for modifying collections of objects. 12 | /// 13 | public static class RootComponentCollectionExtensions 14 | { 15 | /// 16 | /// Adds the component specified by to the collection specified by 17 | /// to be associated with the selector specified by 18 | /// and to be instantiated with the parameters specified by . 19 | /// 20 | /// The to add to the collection. 21 | /// The collection to which the component should be added. 22 | /// The selector to which the component will be associated. 23 | /// The optional creation parameters for the component. 24 | public static void Add(this RootComponentsCollection components, string selector, IDictionary? parameters = null) 25 | where TComponent : IComponent 26 | { 27 | components.Add(new RootComponent(selector, typeof(TComponent), parameters)); 28 | } 29 | 30 | /// 31 | /// Removes the component associated with the specified from the collection 32 | /// specified by . 33 | /// 34 | /// The collection from which the component associated with the selector should be removed. 35 | /// The selector associated with the component to be removed. 36 | public static void Remove(this RootComponentsCollection components, string selector) 37 | { 38 | for (var i = 0; i < components.Count; i++) 39 | { 40 | if (components[i].Selector.Equals(selector, StringComparison.Ordinal)) 41 | { 42 | components.RemoveAt(i); 43 | return; 44 | } 45 | } 46 | 47 | throw new ArgumentException($"There is no root component with selector '{selector}'.", nameof(selector)); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.WinForms/RootComponentsCollection.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.Collections.ObjectModel; 5 | using Microsoft.AspNetCore.Components.Web; 6 | 7 | namespace PeakSWC.RemoteBlazorWebView.WindowsForms 8 | { 9 | /// 10 | /// A collection of items. 11 | /// 12 | public class RootComponentsCollection : ObservableCollection, IJSComponentConfiguration 13 | { 14 | /// 15 | public JSComponentConfigurationStore JSComponents { get; } = new(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.WinForms/WindowsFormsBlazorMarkerService.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 | namespace PeakSWC.RemoteBlazorWebView.WindowsForms 5 | { 6 | internal class WindowsFormsBlazorMarkerService 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.WinForms/WindowsFormsBlazorWebViewBuilder.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 Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace PeakSWC.RemoteBlazorWebView.WindowsForms 7 | { 8 | internal class WindowsFormsBlazorWebViewBuilder : IWindowsFormsBlazorWebViewBuilder 9 | { 10 | public IServiceCollection Services { get; } 11 | 12 | public WindowsFormsBlazorWebViewBuilder(IServiceCollection services) 13 | { 14 | Services = services; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.Wpf/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.Wpf/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 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:RemoteBlazorWebView.Wpf.RemoteBlazorWebView.NavigateToUrl(System.String)")] 9 | [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:RemoteBlazorWebView.Wpf.RemoteBlazorWebView.OnServicesPropertyChanged(System.Windows.DependencyPropertyChangedEventArgs)")] 10 | [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:RemoteBlazorWebView.Wpf.RemoteBlazorWebView.OnHostPagePropertyChanged(System.Windows.DependencyPropertyChangedEventArgs)")] 11 | [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:RemoteBlazorWebView.Wpf.RemoteBlazorWebView.OnServerUriPropertyChanged(System.Windows.DependencyPropertyChangedEventArgs)")] 12 | [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:RemoteBlazorWebView.Wpf.RemoteBlazorWebView.OnIdPropertyChanged(System.Windows.DependencyPropertyChangedEventArgs)")] 13 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.Wpf/IWpfBlazorWebViewBuilder.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 Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace PeakSWC.RemoteBlazorWebView.Wpf 7 | { 8 | /// 9 | /// A builder for WPF Blazor WebViews. 10 | /// 11 | public interface IWpfBlazorWebViewBuilder 12 | { 13 | /// 14 | /// Gets the builder service collection. 15 | /// 16 | IServiceCollection Services { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.Wpf/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Markup; 2 | 3 | [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation/blazor", "Microsoft.AspNetCore.Components.WebView.Wpf")] 4 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.Wpf/PublicAPI.Unshipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.Wpf/RootComponent.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 Microsoft.AspNetCore.Components.WebView; 6 | using Microsoft.AspNetCore.Components; 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | using WebView2 = Microsoft.AspNetCore.Components.WebView.WebView2; 10 | 11 | namespace PeakSWC.RemoteBlazorWebView.Wpf 12 | { 13 | /// 14 | /// Describes a root component that can be added to a . 15 | /// 16 | public class RootComponent 17 | { 18 | /// 19 | /// Gets or sets the CSS selector string that specifies where in the document the component should be placed. 20 | /// This must be unique among the root components within the . 21 | /// 22 | public string Selector { get; set; } = default!; 23 | 24 | /// 25 | /// Gets or sets the type of the root component. This type must implement . 26 | /// 27 | public Type ComponentType { get; set; } = default!; 28 | 29 | /// 30 | /// Gets or sets an optional dictionary of parameters to pass to the root component. 31 | /// 32 | public IDictionary? Parameters { get; set; } 33 | 34 | internal Task AddToWebViewManagerAsync(WebViewManager webViewManager) 35 | { 36 | // As a characteristic of XAML,we can't rely on non-default constructors. So we have to 37 | // validate that the required properties were set. We could skip validating this and allow 38 | // the lower-level renderer code to throw, but that would be harder for developers to understand. 39 | 40 | if (string.IsNullOrWhiteSpace(Selector)) 41 | { 42 | throw new InvalidOperationException($"{nameof(RootComponent)} requires a value for its {nameof(Selector)} property, but no value was set."); 43 | } 44 | 45 | if (ComponentType is null) 46 | { 47 | throw new InvalidOperationException($"{nameof(RootComponent)} requires a value for its {nameof(ComponentType)} property, but no value was set."); 48 | } 49 | 50 | var parameterView = Parameters == null ? ParameterView.Empty : ParameterView.FromDictionary(Parameters); 51 | return webViewManager.AddRootComponentAsync(ComponentType, Selector, parameterView); 52 | } 53 | 54 | internal Task RemoveFromWebViewManagerAsync(WebView2WebViewManager webviewManager) 55 | { 56 | return webviewManager.RemoveRootComponentAsync(Selector); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.Wpf/RootComponentCollectionExtensions.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 Microsoft.AspNetCore.Components; 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace PeakSWC.RemoteBlazorWebView.Wpf 9 | { 10 | /// 11 | /// Provides a set of extension methods for modifying collections of objects. 12 | /// 13 | public static class RootComponentCollectionExtensions 14 | { 15 | /// 16 | /// Adds the component specified by to the collection specified by 17 | /// to be associated with the selector specified by 18 | /// and to be instantiated with the parameters specified by . 19 | /// 20 | /// The to add to the collection. 21 | /// The collection to which the component should be added. 22 | /// The selector to which the component will be associated. 23 | /// The optional creation parameters for the component. 24 | public static void Add(this RootComponentsCollection components, string selector, IDictionary parameters = null) 25 | where TComponent : IComponent 26 | { 27 | components.Add(new RootComponent { Selector = selector, ComponentType = typeof(TComponent), Parameters = parameters }); 28 | } 29 | 30 | /// 31 | /// Removes the component associated with the specified from the collection 32 | /// specified by . 33 | /// 34 | /// The collection from which the component associated with the selector should be removed. 35 | /// The selector associated with the component to be removed. 36 | public static void Remove(this RootComponentsCollection components, string selector) 37 | { 38 | for (var i = 0; i < components.Count; i++) 39 | { 40 | if (components[i].Selector.Equals(selector, StringComparison.Ordinal)) 41 | { 42 | components.RemoveAt(i); 43 | return; 44 | } 45 | } 46 | 47 | throw new ArgumentException($"There is no root component with selector '{selector}'.", nameof(selector)); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.Wpf/RootComponentsCollection.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.Collections.ObjectModel; 5 | using Microsoft.AspNetCore.Components.Web; 6 | 7 | namespace PeakSWC.RemoteBlazorWebView.Wpf 8 | { 9 | /// 10 | /// A collection of items. 11 | /// 12 | public class RootComponentsCollection : ObservableCollection, IJSComponentConfiguration 13 | { 14 | /// 15 | public JSComponentConfigurationStore JSComponents { get; } = new(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.Wpf/WpfBlazorMarkerService.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 | namespace PeakSWC.RemoteBlazorWebView.Wpf 5 | { 6 | public class WpfBlazorMarkerService 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView.Wpf/WpfBlazorWebViewBuilder.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 Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace PeakSWC.RemoteBlazorWebView.Wpf 7 | { 8 | internal class WpfBlazorWebViewBuilder : IWpfBlazorWebViewBuilder 9 | { 10 | public IServiceCollection Services { get; } 11 | 12 | public WpfBlazorWebViewBuilder(IServiceCollection services) 13 | { 14 | Services = services; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView/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 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "", Scope = "namespace", Target = "~N:PeakSWC.RemoteWebView")] 9 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView/RemoteBlazorWebViewAppBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Net.Http; 8 | using Photino.Blazor; 9 | using Microsoft.Extensions.Options; 10 | 11 | namespace PeakSWC.RemoteWebView 12 | { 13 | public class RemoteBlazorWebViewAppBuilder 14 | { 15 | internal RemoteBlazorWebViewAppBuilder() 16 | { 17 | RootComponents = new RootComponentList(); 18 | Services = new ServiceCollection(); 19 | } 20 | 21 | public static RemoteBlazorWebViewAppBuilder CreateDefault(string[]? args = default) 22 | { 23 | // We don't use the args for anything right now, but we want to accept them 24 | // here so that it shows up this way in the project templates. 25 | // var jsRuntime = DefaultWebAssemblyJSRuntime.Instance; 26 | var builder = new RemoteBlazorWebViewAppBuilder(); 27 | builder.Services 28 | //.AddScoped(sp => new HttpClient(new PhotinoHttpHandler(sp.GetService())) { BaseAddress = new Uri(PhotinoWebViewManager.AppBaseUri) }) 29 | .AddSingleton() 30 | .AddBlazorWebView(); 31 | 32 | // Right now we don't have conventions or behaviors that are specific to this method 33 | // however, making this the default for the template allows us to add things like that 34 | // in the future, while giving `new BlazorDesktopHostBuilder` as an opt-out of opinionated 35 | // settings. 36 | return builder; 37 | } 38 | 39 | public RootComponentList RootComponents { get; } 40 | 41 | public IServiceCollection Services { get; } 42 | 43 | public RemotePhotinoBlazorApp Build(Uri serverUrl, Guid id) 44 | { 45 | var sp = Services.BuildServiceProvider(); 46 | var app = sp.GetRequiredService(); 47 | app.Initialize(sp, RootComponents, serverUrl, id); 48 | return app; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/RemoteBlazorWebView/RemotePhotinoDispatcher.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Components; 8 | using Photino.NET; 9 | 10 | namespace PeakSWC.RemoteWebView 11 | { 12 | internal class RemotePhotinoDispatcher : Dispatcher 13 | { 14 | private readonly PhotinoSynchronizationContext _context; 15 | 16 | public RemotePhotinoDispatcher(PhotinoWindow window) 17 | { 18 | _context = new PhotinoSynchronizationContext(window); 19 | _context.UnhandledException += (sender, e) => 20 | { 21 | OnUnhandledException(e); 22 | }; 23 | } 24 | 25 | public override bool CheckAccess() => SynchronizationContext.Current == _context; 26 | 27 | public override Task InvokeAsync(Action workItem) 28 | { 29 | if (CheckAccess()) 30 | { 31 | workItem(); 32 | return Task.CompletedTask; 33 | } 34 | 35 | return _context.InvokeAsync(workItem); 36 | } 37 | 38 | public override Task InvokeAsync(Func workItem) 39 | { 40 | if (CheckAccess()) 41 | { 42 | return workItem(); 43 | } 44 | 45 | return _context.InvokeAsync(workItem); 46 | } 47 | 48 | public override Task InvokeAsync(Func workItem) 49 | { 50 | if (CheckAccess()) 51 | { 52 | return Task.FromResult(workItem()); 53 | } 54 | 55 | return _context.InvokeAsync(workItem); 56 | } 57 | 58 | public override Task InvokeAsync(Func> workItem) 59 | { 60 | if (CheckAccess()) 61 | { 62 | return workItem(); 63 | } 64 | 65 | return _context.InvokeAsync(workItem); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/RemoteBlazorWebView/RemotePhotinoWindowRootComponents.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using Microsoft.AspNetCore.Components; 7 | using Microsoft.AspNetCore.Components.Web; 8 | using Photino.Blazor; 9 | 10 | namespace PeakSWC.RemoteWebView 11 | { 12 | /// 13 | /// Configures root components for a . 14 | /// 15 | public sealed class BlazorWindowRootComponents : IJSComponentConfiguration 16 | { 17 | private readonly PhotinoWebViewManager _manager; 18 | 19 | internal BlazorWindowRootComponents(PhotinoWebViewManager manager, JSComponentConfigurationStore jsComponents) 20 | { 21 | _manager = manager; 22 | JSComponents = jsComponents; 23 | } 24 | 25 | public JSComponentConfigurationStore JSComponents { get; } 26 | 27 | /// 28 | /// Adds a root component to the window. 29 | /// 30 | /// The component type. 31 | /// A CSS selector describing where the component should be added in the host page. 32 | /// An optional dictionary of parameters to pass to the component. 33 | public void Add(Type typeComponent, string selector, IDictionary? parameters = null) 34 | { 35 | var parameterView = parameters == null 36 | ? ParameterView.Empty 37 | : ParameterView.FromDictionary(parameters); 38 | 39 | // Dispatch because this is going to be async, and we want to catch any errors 40 | _ = _manager.Dispatcher.InvokeAsync(async () => 41 | { 42 | await _manager.AddRootComponentAsync(typeComponent, selector, parameterView); 43 | }); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Build.md: -------------------------------------------------------------------------------- 1 | # Building remote.blazor.desktop.js 2 | 3 | 4 | * Install Node Version Manager from https://github.com/coreybutler/nvm-windows 5 | 6 | * Verify that you have version 22.3.0 of node installed 7 | ``` 8 | nvm list 9 | ``` 10 | 11 | * If not, run the following command to install it 12 | ``` 13 | nvm install 22.3.0 14 | ``` 15 | 16 | * Using a powershell or cmd window with Admin privileges, set the node version 17 | ``` 18 | nvm use 22.3.0 19 | ``` 20 | 21 | * Install yarn globally and use it to install the dependencies 22 | ``` 23 | 24 | npm install -g yarn 25 | npm install -g rollup 26 | cd RemoteWebView.Blazor.JS 27 | yarn install 28 | cd .\web.js\ 29 | yarn install 30 | ``` 31 | 32 | * npm install -g protoc-gen-ts 33 | * npm install -g protoc-gen-grpc-web 34 | 35 | * Add RemoteWebView.Blazor.JS\protoc\bin to your path variable 36 | * Verify you can run "protoc -h" from a terminal window 37 | * Now you should be able to build remote.blazor.desktop.js using the following command 38 | 39 | ``` 40 | cd RemoteWebView.Blazor.JS 41 | npm run build:production 42 | ``` 43 | 44 | 45 | sudo apt update 46 | 47 | 48 | 49 | open ubuntu 50 | cd /mnt/c/users/budcr/source/repos/RemoteBlazorWebView/src 51 | sudo find . \( -name '*.cs' -o -name '*.txt' -o -name '*.ts' -o -name '*.js' -o -name '.yarnrc' -o -name '*.npmproj' \) -exec unix2dos {} \; 52 | 53 | sudo find . -path './node_modules' -prune -o \( -name '*.cs' -o -name '*.txt' -o -name '*.ts' -o -name '*.js' -o -name '.yarnrc' -o -name '*.npmproj' \) -exec unix2dos {} \; 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/RemoteWebView.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/WebView.Blazor.JS/upstream/aspnetcore/web.js -u aspnetcore/v3.1.4:src/Components/Web.JS 15 | git commit -m "Get Web.JS files from tag v3.1.4" 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 | 26 | 27 | Alternatively, 28 | 29 | * Copy and unzip the aspnetcore source files 30 | * Copy the files from src/Components/Web.JS to RemoteBlazorWebView\src\RemoteWebView.Blazor.JS\upstream\aspnetcore\web.js 31 | 32 | * Edit package.json 33 | 34 | "@microsoft/dotnet-js-interop": "link:../../JSInterop/Microsoft.JSInterop.JS/src", 35 | "@microsoft/signalr": "link:../../SignalR/clients/ts/signalr", 36 | "@microsoft/signalr-protocol-msgpack": "link:../../SignalR/clients/ts/signalr-protocol-msgpack", 37 | 38 | to 39 | // where x is the latest preview 40 | "@microsoft/signalr": "6.0.x", 41 | "@microsoft/signalr-protocol-msgpack":"6.0.x", 42 | "@microsoft/dotnet-js-interop": "6.0.0.x", 43 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/RemoteWebView.Blazor.JS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | true 6 | Latest 7 | ${DefaultItemExcludes};node_modules\** 8 | false 9 | false 10 | Debug;Release 11 | x64 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Shared.JS/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "lib": [ "DOM", "ES2022" ], 6 | "strict": true, 7 | "forceConsistentCasingInFileNames": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/TransferControl.txt: -------------------------------------------------------------------------------- 1 | 2 | setTimeout(() => setPrimaryButton(message.getIsprimary()), 1000); 3 | 4 | 5 | export function setPrimary(): void { 6 | var message = new ClientIdMessageRequest(); 7 | var id = window.location.pathname.split('/')[1]; 8 | message.setId(id); 9 | message.setClientid(clientId); 10 | 11 | grpc.invoke(BrowserIPC.SetPrimary, { 12 | request: message, 13 | host: window.location.origin, 14 | //onMessage: (message: google.E) => { 15 | // //console.info("Set Primary: " + message.getId() + " " + message.getClientid()); 16 | //}, 17 | onEnd: (code: grpc.Code, msg: string | undefined, trailers: grpc.Metadata) => { 18 | if (code == grpc.Code.OK) { 19 | console.log("SetPrimary ok:" + clientId) 20 | } else { 21 | console.error("grpc error", code, msg, trailers); 22 | } 23 | } 24 | }); 25 | } 26 | 27 | function setPrimaryButton(isPrimary: boolean) { 28 | var element = document.getElementById('primaryButton'); 29 | if (element != null) { 30 | if (isPrimary) 31 | element.innerText = "Primary"; 32 | else 33 | element.innerText = "Mirror"; 34 | 35 | element.onclick = setPrimary; 36 | } 37 | 38 | } 39 | 40 | function setPrimaryButton(isPrimary: boolean) { 41 | var element = document.getElementById('primaryButton'); 42 | if (element != null) { 43 | if (isPrimary) 44 | element.innerText = "Primary"; 45 | else 46 | element.innerText = "Mirror"; 47 | 48 | element.onclick = setPrimary; 49 | } 50 | 51 | } 52 | 53 | BrowserIPCService: 54 | 55 | public override Task SetPrimary(ClientIdMessageRequest request, ServerCallContext context) 56 | { 57 | if (!_serviceDictionary.TryGetValue(request.Id, out ServiceState? serviceState)) 58 | { 59 | _shutdownService.Shutdown(request.Id); 60 | return Task.FromResult(new Empty()); 61 | } 62 | 63 | serviceState.ClientId = request.ClientId; 64 | serviceState.IPC.ClientResponseStream?.WriteAsync(new WebMessageResponse { Response = $"primaryChanged:{request.ClientId}" }); 65 | return Task.FromResult(new Empty()); 66 | } 67 | 68 | 69 | 70 | serviceState.IPC.ClientResponseStream?.WriteAsync(new WebMessageResponse { Response = $"primaryChanged:{guid}" }); 71 | 72 | 73 | 74 | 75 | rpc SetPrimary(ClientIdMessageRequest) returns (google.protobuf.Empty); -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/Debug/ 3 | dist/Release/blazor.webassembly.js 4 | dist/Release/blazor.webview.js 5 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | path = require('path'); 7 | 8 | const ROOT_DIR = path.resolve(__dirname, '..', '..', '..'); 9 | 10 | /** @type {import('jest').Config} */ 11 | 12 | module.exports = { 13 | testEnvironment: 'node', 14 | roots: ['/src','/test'], 15 | testMatch: ['**/*.test.ts'], 16 | moduleFileExtensions: ['js', 'ts'], 17 | transform: { 18 | '^.+\\.(js|ts)$': 'babel-jest', 19 | }, 20 | moduleDirectories: ['node_modules', 'src'], 21 | testEnvironment: "jsdom", 22 | reporters: [ 23 | "default", 24 | [path.resolve(ROOT_DIR, "node_modules", "jest-junit", "index.js"), { "outputDirectory": path.resolve(ROOT_DIR, "artifacts", "log"), "outputName": `${process.platform}` + ".components-webjs.junit.xml" }] 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@microsoft/microsoft.aspnetcore.components.web.js", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "", 6 | "main": "index.js", 7 | "sideEffects": [ 8 | "GlobalExports.*", 9 | "MonoPlatform.*" 10 | ], 11 | "scripts": { 12 | "clean": "rimraf ./dist/Debug ./dist/Release", 13 | "prebuild": "npm run clean", 14 | "lint": "eslint -c ./src/.eslintrc.js --ext .ts ./src", 15 | "test": "jest", 16 | "test:watch": "jest --watch", 17 | "test:debug": "node --nolazy --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --colors --verbose", 18 | "build": "npm run build:debug && npm run build:production", 19 | "build:debug": "rollup -c --environment development --forceExit", 20 | "build:production": "rollup -c --environment production --forceExit" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "^7.23.6", 24 | "@babel/preset-env": "^7.23.6", 25 | "@babel/preset-typescript": "^7.23.3", 26 | "@microsoft/dotnet-js-interop": "8.0.0", 27 | "@microsoft/signalr": "8.0.7", 28 | "@microsoft/signalr-protocol-msgpack": "8.0.7", 29 | "@types/jsdom": "^16.2.14", 30 | "@typescript-eslint/eslint-plugin": "^5.26.0", 31 | "@typescript-eslint/parser": "^5.26.0", 32 | "babel-jest": "^29.7.0", 33 | "eslint": "^8.16.0", 34 | "eslint-plugin-header": "^3.1.1", 35 | "jest": "^29.7.0", 36 | "jest-environment-jsdom": "^29.7.0", 37 | "jest-junit": "^16.0.0" 38 | }, 39 | "resolutions": { 40 | "tough-cookie": ">=4.1.3" 41 | }, 42 | "dependencies": { 43 | "@rollup/plugin-terser": "^0.4.4", 44 | "dotnet-runtime": "^0.0.1-security" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import createBaseConfig from '../Shared.JS/rollup.config.mjs'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | export default createBaseConfig({ 9 | inputOutputMap: { 10 | 'blazor.server': './src/Boot.Server.ts', 11 | 'blazor.web': './src/Boot.Web.ts', 12 | 'blazor.webassembly': './src/Boot.WebAssembly.ts', 13 | 'blazor.webview': './src/Boot.WebView.ts', 14 | }, 15 | dir: __dirname, 16 | updateConfig: (config, environment, _, input) => { 17 | if (environment === 'development') { 18 | if (input.includes("WebView")) { 19 | config.output.sourcemap = 'inline'; 20 | } else { 21 | config.output.sourcemap = true; 22 | } 23 | } else { 24 | config.output.sourcemap = false; 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/BinaryDecoder.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | const uint64HighPartShift = Math.pow(2, 32); 5 | const maxSafeNumberHighPart = Math.pow(2, 21) - 1; // The high-order int32 from Number.MAX_SAFE_INTEGER 6 | 7 | export function readInt32LE(buffer: Uint8Array, position: number): any { 8 | return (buffer[position]) 9 | | (buffer[position + 1] << 8) 10 | | (buffer[position + 2] << 16) 11 | | (buffer[position + 3] << 24); 12 | } 13 | 14 | export function readUint32LE(buffer: Uint8Array, position: number): any { 15 | return (buffer[position]) 16 | + (buffer[position + 1] << 8) 17 | + (buffer[position + 2] << 16) 18 | + ((buffer[position + 3] << 24) >>> 0); // The >>> 0 coerces the value to unsigned 19 | } 20 | 21 | export function readUint64LE(buffer: Uint8Array, position: number): any { 22 | // This cannot be done using bit-shift operators in JavaScript, because 23 | // those all implicitly convert to int32 24 | const highPart = readUint32LE(buffer, position + 4); 25 | if (highPart > maxSafeNumberHighPart) { 26 | throw new Error(`Cannot read uint64 with high order part ${highPart}, because the result would exceed Number.MAX_SAFE_INTEGER.`); 27 | } 28 | 29 | return (highPart * uint64HighPartShift) + readUint32LE(buffer, position); 30 | } 31 | 32 | export function readLEB128(buffer: Uint8Array, position: number): number { 33 | let result = 0; 34 | let shift = 0; 35 | for (let index = 0; index < 4; index++) { 36 | const byte = buffer[position + index]; 37 | result |= (byte & 127) << shift; 38 | if (byte < 128) { 39 | break; 40 | } 41 | shift += 7; 42 | } 43 | return result; 44 | } 45 | 46 | export function numLEB128Bytes(value: number): 1 | 2 | 3 | 4 { 47 | return value < 128 ? 1 48 | : value < 16384 ? 2 49 | : value < 2097152 ? 3 : 4; 50 | } 51 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Boot.Server.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { Blazor } from './GlobalExports'; 5 | import { shouldAutoStart } from './BootCommon'; 6 | import { CircuitStartOptions, resolveOptions } from './Platform/Circuits/CircuitStartOptions'; 7 | import { setCircuitOptions, startServer } from './Boot.Server.Common'; 8 | import { ServerComponentDescriptor, discoverComponents } from './Services/ComponentDescriptorDiscovery'; 9 | import { DotNet } from '@microsoft/dotnet-js-interop'; 10 | import { InitialRootComponentsList } from './Services/InitialRootComponentsList'; 11 | import { JSEventRegistry } from './Services/JSEventRegistry'; 12 | 13 | let started = false; 14 | 15 | function boot(userOptions?: Partial): Promise { 16 | if (started) { 17 | throw new Error('Blazor has already started.'); 18 | } 19 | started = true; 20 | 21 | const configuredOptions = resolveOptions(userOptions); 22 | setCircuitOptions(Promise.resolve(configuredOptions || {})); 23 | 24 | JSEventRegistry.create(Blazor); 25 | const serverComponents = discoverComponents(document, 'server') as ServerComponentDescriptor[]; 26 | const components = new InitialRootComponentsList(serverComponents); 27 | return startServer(components); 28 | } 29 | 30 | Blazor.start = boot; 31 | window['DotNet'] = DotNet; 32 | 33 | if (shouldAutoStart()) { 34 | boot(); 35 | } 36 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Boot.WebAssembly.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | /* eslint-disable array-element-newline */ 5 | import { Blazor } from './GlobalExports'; 6 | import { shouldAutoStart } from './BootCommon'; 7 | import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions'; 8 | import { setWebAssemblyOptions, startWebAssembly } from './Boot.WebAssembly.Common'; 9 | import { WebAssemblyComponentDescriptor, discoverComponents } from './Services/ComponentDescriptorDiscovery'; 10 | import { DotNet } from '@microsoft/dotnet-js-interop'; 11 | import { InitialRootComponentsList } from './Services/InitialRootComponentsList'; 12 | import { JSEventRegistry } from './Services/JSEventRegistry'; 13 | import { printErr } from './Platform/Mono/MonoPlatform'; 14 | 15 | let started = false; 16 | 17 | async function boot(options?: Partial): Promise { 18 | if (started) { 19 | throw new Error('Blazor has already started.'); 20 | } 21 | started = true; 22 | 23 | setWebAssemblyOptions(Promise.resolve(options || {})); 24 | 25 | JSEventRegistry.create(Blazor); 26 | const webAssemblyComponents = discoverComponents(document, 'webassembly') as WebAssemblyComponentDescriptor[]; 27 | const components = new InitialRootComponentsList(webAssemblyComponents); 28 | await startWebAssembly(components); 29 | } 30 | 31 | Blazor.start = boot; 32 | window['DotNet'] = DotNet; 33 | 34 | if (shouldAutoStart()) { 35 | boot().catch(printErr); 36 | } 37 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Boot.WebView.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { DotNet } from '@microsoft/dotnet-js-interop'; 5 | import { Blazor } from './GlobalExports'; 6 | import { shouldAutoStart } from './BootCommon'; 7 | import { internalFunctions as navigationManagerFunctions } from './Services/NavigationManager'; 8 | import { startIpcReceiver } from './Platform/WebView/WebViewIpcReceiver'; 9 | import { sendAttachPage, sendBeginInvokeDotNetFromJS, sendEndInvokeJSFromDotNet, sendByteArray, sendLocationChanged, sendLocationChanging } from './Platform/WebView/WebViewIpcSender'; 10 | import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.WebView'; 11 | import { receiveDotNetDataStream } from './StreamingInterop'; 12 | import { WebRendererId } from './Rendering/WebRendererId'; 13 | 14 | let started = false; 15 | 16 | export let dispatcher: DotNet.ICallDispatcher; 17 | 18 | async function boot(): Promise { 19 | if (started) { 20 | throw new Error('Blazor has already started.'); 21 | } 22 | started = true; 23 | 24 | dispatcher = DotNet.attachDispatcher({ 25 | beginInvokeDotNetFromJS: sendBeginInvokeDotNetFromJS, 26 | endInvokeJSFromDotNet: sendEndInvokeJSFromDotNet, 27 | sendByteArray: sendByteArray, 28 | }); 29 | 30 | const jsInitializer = await fetchAndInvokeInitializers(); 31 | 32 | startIpcReceiver(); 33 | 34 | Blazor._internal.receiveWebViewDotNetDataStream = receiveWebViewDotNetDataStream; 35 | 36 | navigationManagerFunctions.enableNavigationInterception(WebRendererId.WebView); 37 | navigationManagerFunctions.listenForNavigationEvents(WebRendererId.WebView, sendLocationChanged, sendLocationChanging); 38 | 39 | sendAttachPage(navigationManagerFunctions.getBaseURI(), navigationManagerFunctions.getLocationHref()); 40 | await jsInitializer.invokeAfterStartedCallbacks(Blazor); 41 | } 42 | 43 | function receiveWebViewDotNetDataStream(streamId: number, data: any, bytesRead: number, errorMessage: string): void { 44 | receiveDotNetDataStream(dispatcher, streamId, data, bytesRead, errorMessage); 45 | } 46 | 47 | Blazor.start = boot; 48 | window['DotNet'] = DotNet; 49 | 50 | if (shouldAutoStart()) { 51 | boot(); 52 | } 53 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/BootCommon.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | // Tells you if the script was added without 5 | export function shouldAutoStart(): boolean { 6 | return !!(document && 7 | document.currentScript && 8 | document.currentScript.getAttribute('autostart') !== 'false'); 9 | } 10 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/BootErrors.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | let hasFailed = false; 5 | 6 | export function showErrorNotification(): void { 7 | const errorUi = document.querySelector('#blazor-error-ui') as HTMLElement; 8 | if (errorUi) { 9 | errorUi.style.display = 'block'; 10 | } 11 | 12 | if (!hasFailed) { 13 | hasFailed = true; 14 | const errorUiReloads = document.querySelectorAll('#blazor-error-ui .reload'); 15 | errorUiReloads.forEach(reload => { 16 | reload.onclick = function(e) { 17 | location.reload(); 18 | e.preventDefault(); 19 | }; 20 | }); 21 | 22 | const errorUiDismiss = document.querySelectorAll('#blazor-error-ui .dismiss'); 23 | errorUiDismiss.forEach(dismiss => { 24 | dismiss.onclick = function(e) { 25 | const errorUi = document.querySelector('#blazor-error-ui'); 26 | if (errorUi) { 27 | errorUi.style.display = 'none'; 28 | } 29 | e.preventDefault(); 30 | }; 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/DomWrapper.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import '@microsoft/dotnet-js-interop'; 5 | 6 | export const domFunctions = { 7 | focus, 8 | focusBySelector, 9 | }; 10 | 11 | function focus(element: HTMLOrSVGElement, preventScroll: boolean): void { 12 | if (element instanceof HTMLElement) { 13 | element.focus({ preventScroll }); 14 | } else if (element instanceof SVGElement) { 15 | if (element.hasAttribute('tabindex')) { 16 | element.focus({ preventScroll }); 17 | } else { 18 | throw new Error('Unable to focus an SVG element that does not have a tabindex.'); 19 | } 20 | } else { 21 | throw new Error('Unable to focus an invalid element.'); 22 | } 23 | } 24 | 25 | function focusBySelector(selector: string) { 26 | const element = document.querySelector(selector) as HTMLElement; 27 | if (element) { 28 | // If no explicit tabindex is defined, mark it as programmatically-focusable. 29 | // This does actually add a new HTML attribute, but it shouldn't interfere with 30 | // diffing because diffing only deals with the attributes you have in your code. 31 | if (!element.hasAttribute('tabindex')) { 32 | element.tabIndex = -1; 33 | } 34 | 35 | element.focus({ preventScroll: true }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Environment.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | // Expose an export called 'platform' of the interface type 'Platform', 5 | // so that consumers can be agnostic about which implementation they use. 6 | // Basic alternative to having an actual DI container. 7 | import { Platform } from './Platform/Platform'; 8 | 9 | export let platform: Platform; 10 | 11 | export function setPlatform(platformInstance: Platform): Platform { 12 | platform = platformInstance; 13 | return platform; 14 | } 15 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/JSInitializers/JSInitializers.Server.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { CircuitStartOptions } from '../Platform/Circuits/CircuitStartOptions'; 5 | import { WebRendererId } from '../Rendering/WebRendererId'; 6 | import { JSInitializer } from './JSInitializers'; 7 | 8 | export async function fetchAndInvokeInitializers(options: Partial) : Promise { 9 | if (options.initializers) { 10 | // Initializers were already resolved, so we don't have to fetch them, we just invoke the beforeStart ones 11 | // and return the list of afterStarted ones so they get processed in the same way as in traditional Blazor Server. 12 | await Promise.all(options.initializers.beforeStart.map(i => i(options))); 13 | return new JSInitializer(/* singleRuntime: */ false, undefined, options.initializers.afterStarted, WebRendererId.Server); 14 | } 15 | 16 | const jsInitializersResponse = await fetch('_blazor/initializers', { 17 | method: 'GET', 18 | credentials: 'include', 19 | cache: 'no-cache', 20 | }); 21 | 22 | const initializers: string[] = await jsInitializersResponse.json(); 23 | const jsInitializer = new JSInitializer(/* singleRuntime: */ true, undefined, undefined, WebRendererId.Server); 24 | await jsInitializer.importInitializersAsync(initializers, [options]); 25 | return jsInitializer; 26 | } 27 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/JSInitializers/JSInitializers.Web.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { Logger } from '../Platform/Logging/Logger'; 5 | import { WebStartOptions } from '../Platform/WebStartOptions'; 6 | import { discoverWebInitializers } from '../Services/ComponentDescriptorDiscovery'; 7 | import { JSInitializer } from './JSInitializers'; 8 | 9 | export async function fetchAndInvokeInitializers(options: Partial, logger: Logger) : Promise { 10 | const initializersElement = discoverWebInitializers(document); 11 | if (!initializersElement) { 12 | return new JSInitializer(false, logger); 13 | } 14 | const initializers: string[] = JSON.parse(atob(initializersElement)) as string[] ?? []; 15 | const jsInitializer = new JSInitializer(false, logger); 16 | await jsInitializer.importInitializersAsync(initializers, [options]); 17 | return jsInitializer; 18 | } 19 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/JSInitializers/JSInitializers.WebAssembly.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { MonoConfig } from '@microsoft/dotnet-runtime'; 5 | import { WebAssemblyStartOptions } from '../Platform/WebAssemblyStartOptions'; 6 | import { WebRendererId } from '../Rendering/WebRendererId'; 7 | import { JSInitializer } from './JSInitializers'; 8 | 9 | export async function fetchAndInvokeInitializers(options: Partial, loadedConfig: MonoConfig): Promise { 10 | if (options.initializers) { 11 | // Initializers were already resolved, so we don't have to fetch them, we just invoke the beforeStart ones 12 | // and return the list of afterStarted ones so they get processed in the same way as in traditional Blazor Server. 13 | await Promise.all(options.initializers.beforeStart.map(i => i(options))); 14 | return new JSInitializer(/* singleRuntime: */ false, undefined, options.initializers.afterStarted, WebRendererId.WebAssembly); 15 | } else { 16 | const initializerArguments = [options, loadedConfig.resources?.extensions ?? {}]; 17 | const jsInitializer = new JSInitializer( 18 | /* singleRuntime: */ true, 19 | undefined, 20 | undefined, 21 | WebRendererId.WebAssembly 22 | ); 23 | const initializers = Object.keys((loadedConfig?.resources?.['libraryInitializers']) || {}); 24 | await jsInitializer.importInitializersAsync(initializers, initializerArguments); 25 | return jsInitializer; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/JSInitializers/JSInitializers.WebView.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { JSInitializer } from './JSInitializers'; 5 | 6 | export async function fetchAndInvokeInitializers() : Promise { 7 | const jsInitializersResponse = await fetch('_framework/blazor.modules.json', { 8 | method: 'GET', 9 | credentials: 'include', 10 | cache: 'no-cache', 11 | }); 12 | 13 | const initializers: string[] = await jsInitializersResponse.json(); 14 | const jsInitializer = new JSInitializer(); 15 | await jsInitializer.importInitializersAsync(initializers, []); 16 | return jsInitializer; 17 | } 18 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/NavigationLock.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | const registeredLocks = new Set(); 5 | 6 | export const NavigationLock = { 7 | enableNavigationPrompt, 8 | disableNavigationPrompt, 9 | }; 10 | 11 | function onBeforeUnload(event: BeforeUnloadEvent) { 12 | event.preventDefault(); 13 | // Modern browsers display a confirmation prompt when returnValue is some value other than 14 | // null or undefined. 15 | // See: https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#compatibility_notes 16 | event.returnValue = true; 17 | } 18 | 19 | function enableNavigationPrompt(id: string) { 20 | if (registeredLocks.size === 0) { 21 | window.addEventListener('beforeunload', onBeforeUnload); 22 | } 23 | 24 | registeredLocks.add(id); 25 | } 26 | 27 | function disableNavigationPrompt(id: string) { 28 | registeredLocks.delete(id); 29 | 30 | if (registeredLocks.size === 0) { 31 | window.removeEventListener('beforeunload', onBeforeUnload); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/PageTitle.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { getLogicalParent, LogicalElement } from './Rendering/LogicalElements'; 5 | 6 | export const PageTitle = { 7 | getAndRemoveExistingTitle, 8 | }; 9 | 10 | function getAndRemoveExistingTitle(): string | null { 11 | // Other elements may exist outside (e.g., inside elements) but they aren't page titles 12 | const titleElements = document.head ? document.head.getElementsByTagName('title') : []; 13 | 14 | if (titleElements.length === 0) { 15 | return null; 16 | } 17 | 18 | let existingTitle: string | null = null; 19 | 20 | for (let index = titleElements.length - 1; index >= 0; index--) { 21 | const currentTitleElement = titleElements[index]; 22 | const previousSibling = currentTitleElement.previousSibling; 23 | const isBlazorTitle = previousSibling instanceof Comment && getLogicalParent(previousSibling as unknown as LogicalElement) !== null; 24 | 25 | if (isBlazorTitle) { 26 | continue; 27 | } 28 | 29 | if (existingTitle === null) { 30 | existingTitle = currentTitleElement.textContent; 31 | } 32 | 33 | currentTitleElement.parentNode?.removeChild(currentTitleElement); 34 | } 35 | 36 | return existingTitle; 37 | } 38 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Platform/Circuits/ReconnectDisplay.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | export interface ReconnectDisplay { 5 | show(): void; 6 | update(currentAttempt: number, secondsToNextAttempt: number): void; 7 | hide(): void; 8 | failed(): void; 9 | rejected(): void; 10 | } 11 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Platform/Circuits/UserSpecifiedDisplay.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { ReconnectDisplay } from './ReconnectDisplay'; 5 | export class UserSpecifiedDisplay implements ReconnectDisplay { 6 | static readonly ShowClassName = 'components-reconnect-show'; 7 | 8 | static readonly HideClassName = 'components-reconnect-hide'; 9 | 10 | static readonly FailedClassName = 'components-reconnect-failed'; 11 | 12 | static readonly RejectedClassName = 'components-reconnect-rejected'; 13 | 14 | static readonly MaxRetriesId = 'components-reconnect-max-retries'; 15 | 16 | static readonly CurrentAttemptId = 'components-reconnect-current-attempt'; 17 | 18 | static readonly SecondsToNextAttemptId = 'components-seconds-to-next-attempt'; 19 | 20 | constructor(private dialog: HTMLElement, private readonly document: Document, maxRetries?: number) { 21 | this.document = document; 22 | 23 | if (maxRetries !== undefined) { 24 | const maxRetriesElement = this.document.getElementById(UserSpecifiedDisplay.MaxRetriesId); 25 | 26 | if (maxRetriesElement) { 27 | maxRetriesElement.innerText = maxRetries.toString(); 28 | } 29 | } 30 | } 31 | 32 | show(): void { 33 | this.removeClasses(); 34 | this.dialog.classList.add(UserSpecifiedDisplay.ShowClassName); 35 | } 36 | 37 | update(currentAttempt: number, secondsToNextAttempt: number): void { 38 | const currentAttemptElement = this.document.getElementById(UserSpecifiedDisplay.CurrentAttemptId); 39 | 40 | if (currentAttemptElement) { 41 | currentAttemptElement.innerText = currentAttempt.toString(); 42 | } 43 | 44 | const secondsToNextAttemptElement = this.document.getElementById(UserSpecifiedDisplay.SecondsToNextAttemptId); 45 | 46 | if (secondsToNextAttemptElement) { 47 | secondsToNextAttemptElement.innerText = secondsToNextAttempt.toString(); 48 | } 49 | } 50 | 51 | hide(): void { 52 | this.removeClasses(); 53 | this.dialog.classList.add(UserSpecifiedDisplay.HideClassName); 54 | } 55 | 56 | failed(): void { 57 | this.removeClasses(); 58 | this.dialog.classList.add(UserSpecifiedDisplay.FailedClassName); 59 | } 60 | 61 | rejected(): void { 62 | this.removeClasses(); 63 | this.dialog.classList.add(UserSpecifiedDisplay.RejectedClassName); 64 | } 65 | 66 | private removeClasses() { 67 | this.dialog.classList.remove(UserSpecifiedDisplay.ShowClassName, UserSpecifiedDisplay.HideClassName, UserSpecifiedDisplay.FailedClassName, UserSpecifiedDisplay.RejectedClassName); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Platform/Logging/Logger.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 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 { 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/RemoteWebView.Blazor.JS/Web.JS/src/Platform/Logging/Loggers.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | /* eslint-disable no-console */ 5 | 6 | import { Logger, LogLevel } from './Logger'; 7 | 8 | export class NullLogger implements Logger { 9 | public static instance: Logger = new NullLogger(); 10 | 11 | public log(_logLevel: LogLevel, _message: string): void { // eslint-disable-line @typescript-eslint/no-unused-vars 12 | } 13 | } 14 | 15 | export class ConsoleLogger implements Logger { 16 | private readonly minLevel: LogLevel; 17 | 18 | public constructor(minimumLogLevel: LogLevel) { 19 | this.minLevel = minimumLogLevel; 20 | } 21 | 22 | public log(logLevel: LogLevel, message: string | Error): void { 23 | if (logLevel >= this.minLevel) { 24 | const msg = `[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`; 25 | switch (logLevel) { 26 | case LogLevel.Critical: 27 | case LogLevel.Error: 28 | console.error(msg); 29 | break; 30 | case LogLevel.Warning: 31 | console.warn(msg); 32 | break; 33 | case LogLevel.Information: 34 | console.info(msg); 35 | break; 36 | default: 37 | // console.debug only goes to attached debuggers in Node, so we use console.log for Trace and Debug 38 | console.log(msg); 39 | break; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Platform/Mono/MonoNavigatorUserAgentData.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | // can be removed once userAgentData is part of lib.dom.d.ts 5 | declare interface MonoNavigatorUserAgent extends Navigator { 6 | readonly userAgentData: MonoUserAgentData; 7 | } 8 | 9 | declare interface MonoUserAgentData { 10 | readonly brands: ReadonlyArray; 11 | readonly platform: string; 12 | } 13 | 14 | declare interface MonoUserAgentDataBrandVersion { 15 | brand?: string; 16 | version?: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Platform/Platform.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | /* eslint-disable @typescript-eslint/ban-types */ 5 | /* eslint-disable @typescript-eslint/no-unused-vars */ 6 | import { MonoObject, MonoString, MonoArray } from '@microsoft/dotnet-runtime/dotnet-legacy'; 7 | import { WebAssemblyStartOptions } from './WebAssemblyStartOptions'; 8 | import { MonoConfig } from '@microsoft/dotnet-runtime'; 9 | 10 | export interface Platform { 11 | load(options: Partial, onConfigLoaded?: (loadedConfig: MonoConfig) => void): Promise; 12 | start(): Promise; 13 | 14 | callEntryPoint(): Promise; 15 | 16 | getArrayEntryPtr(array: System_Array, index: number, itemSize: number): TPtr; 17 | 18 | getObjectFieldsBaseAddress(referenceTypedObject: System_Object): Pointer; 19 | readInt16Field(baseAddress: Pointer, fieldOffset?: number): number; 20 | readInt32Field(baseAddress: Pointer, fieldOffset?: number): number; 21 | readUint64Field(baseAddress: Pointer, fieldOffset?: number): number; 22 | readObjectField(baseAddress: Pointer, fieldOffset?: number): T; 23 | readStringField(baseAddress: Pointer, fieldOffset?: number, readBoolValueAsString?: boolean): string | null; 24 | readStructField(baseAddress: Pointer, fieldOffset?: number): T; 25 | 26 | beginHeapLock(): HeapLock; 27 | invokeWhenHeapUnlocked(callback: Function): void; 28 | } 29 | 30 | export type PlatformApi = { 31 | invokeLibraryInitializers(functionName: string, args: unknown[]): Promise; 32 | } 33 | 34 | export interface HeapLock { 35 | release(); 36 | } 37 | 38 | // We don't actually instantiate any of these at runtime. For perf it's preferable to 39 | // use the original 'number' instances without any boxing. The definitions are just 40 | // for compile-time checking, since TypeScript doesn't support nominal types. 41 | export interface MethodHandle { MethodHandle__DO_NOT_IMPLEMENT: unknown } 42 | export type System_Object = MonoObject; 43 | export interface System_Boolean { System_Boolean__DO_NOT_IMPLEMENT: unknown } 44 | export interface System_Byte { System_Byte__DO_NOT_IMPLEMENT: unknown } 45 | export interface System_Int { System_Int__DO_NOT_IMPLEMENT: unknown } 46 | export interface System_String extends System_Object, MonoString { } 47 | export interface System_Array extends System_Object, MonoArray { } 48 | export interface Pointer { Pointer__DO_NOT_IMPLEMENT: unknown } 49 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Platform/SsrStartOptions.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | export interface SsrStartOptions { 5 | /** 6 | * If true, does not attempt to preserve DOM nodes when performing dynamic updates to SSR content 7 | * (for example, during enhanced navigation or streaming rendering). 8 | */ 9 | disableDomPreservation?: boolean; 10 | 11 | /** 12 | * Configures how long to wait after all Blazor Server components have been removed from the document 13 | * before closing the circuit. 14 | */ 15 | circuitInactivityTimeoutMs?: number; 16 | } 17 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Platform/WebAssemblyComponentAttacher.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { LogicalElement, toLogicalRootCommentElement } from '../Rendering/LogicalElements'; 5 | import { WebAssemblyComponentDescriptor } from '../Services/ComponentDescriptorDiscovery'; 6 | import { RootComponentManager } from '../Services/RootComponentManager'; 7 | 8 | export class WebAssemblyComponentAttacher { 9 | private componentManager: RootComponentManager; 10 | 11 | public constructor(componentManager: RootComponentManager) { 12 | this.componentManager = componentManager; 13 | } 14 | 15 | public resolveRegisteredElement(id: string): LogicalElement | undefined { 16 | const parsedId = Number.parseInt(id); 17 | if (!Number.isNaN(parsedId)) { 18 | const component = this.componentManager.resolveRootComponent(parsedId); 19 | return toLogicalRootCommentElement(component); 20 | } else { 21 | return undefined; 22 | } 23 | } 24 | 25 | public getParameterValues(id: number): string | undefined { 26 | return this.componentManager.initialComponents[id].parameterValues; 27 | } 28 | 29 | public getParameterDefinitions(id: number): string | undefined { 30 | return this.componentManager.initialComponents[id].parameterDefinitions; 31 | } 32 | 33 | public getTypeName(id: number): string { 34 | return this.componentManager.initialComponents[id].typeName; 35 | } 36 | 37 | public getAssembly(id: number): string { 38 | return this.componentManager.initialComponents[id].assembly; 39 | } 40 | 41 | public getCount(): number { 42 | return this.componentManager.initialComponents.length; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Platform/WebAssemblyStartOptions.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { DotnetHostBuilder, AssetBehaviors } from '@microsoft/dotnet-runtime'; 5 | import { IBlazor } from '../GlobalExports'; 6 | 7 | export interface WebAssemblyStartOptions { 8 | /** 9 | * Overrides the built-in boot resource loading mechanism so that boot resources can be fetched 10 | * from a custom source, such as an external CDN. 11 | * @param type The type of the resource to be loaded. 12 | * @param name The name of the resource to be loaded. 13 | * @param defaultUri The URI from which the framework would fetch the resource by default. The URI may be relative or absolute. 14 | * @param integrity The integrity string representing the expected content in the response. 15 | * @param behavior The detailed behavior/type of the resource to be loaded. 16 | * @returns A URI string or a Response promise to override the loading process, or null/undefined to allow the default loading behavior. 17 | * When returned string is not qualified with `./` or absolute URL, it will be resolved against document.baseURI. 18 | */ 19 | loadBootResource(type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string, behavior: AssetBehaviors): string | Promise | null | undefined; 20 | 21 | /** 22 | * Override built-in environment setting on start. 23 | */ 24 | environment?: string; 25 | 26 | /** 27 | * Gets the application culture. This is a name specified in the BCP 47 format. See https://tools.ietf.org/html/bcp47 28 | */ 29 | applicationCulture?: string; 30 | 31 | initializers?: WebAssemblyInitializers; 32 | 33 | /** 34 | * Allows to override .NET runtime configuration. 35 | */ 36 | configureRuntime(builder: DotnetHostBuilder): void; 37 | } 38 | 39 | // This type doesn't have to align with anything in BootConfig. 40 | // Instead, this represents the public API through which certain aspects 41 | // of boot resource loading can be customized. 42 | export type WebAssemblyBootResourceType = 'assembly' | 'pdb' | 'dotnetjs' | 'dotnetwasm' | 'globalization' | 'manifest' | 'configuration'; 43 | 44 | export type BeforeBlazorWebAssemblyStartedCallback = (options: Partial) => Promise; 45 | export type AfterBlazorWebAssemblyStartedCallback = (blazor: IBlazor) => Promise; 46 | 47 | export type WebAssemblyInitializers = { 48 | beforeStart: BeforeBlazorWebAssemblyStartedCallback [], 49 | afterStarted: AfterBlazorWebAssemblyStartedCallback [], 50 | } 51 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Platform/WebStartOptions.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { WebAssemblyStartOptions } from './WebAssemblyStartOptions'; 5 | import { CircuitStartOptions } from './Circuits/CircuitStartOptions'; 6 | import { SsrStartOptions } from './SsrStartOptions'; 7 | import { LogLevel } from './Logging/Logger'; 8 | 9 | export interface WebStartOptions { 10 | enableClassicInitializers?: boolean; 11 | circuit: CircuitStartOptions; 12 | webAssembly: WebAssemblyStartOptions; 13 | logLevel?: LogLevel; 14 | ssr: SsrStartOptions; 15 | } 16 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Platform/WebView/WebViewIpcCommon.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | const ipcMessagePrefix = '__bwv:'; 5 | let applicationIsTerminated = false; 6 | 7 | export function trySerializeMessage(messageType: string, args: any[]): string | null { 8 | return applicationIsTerminated 9 | ? null 10 | : `${ipcMessagePrefix}${JSON.stringify([messageType, ...args])}`; 11 | } 12 | 13 | export function tryDeserializeMessage(message: string): IpcMessage | null { 14 | if (applicationIsTerminated || !message || !message.startsWith(ipcMessagePrefix)) { 15 | return null; 16 | } 17 | 18 | const messageAfterPrefix = message.substring(ipcMessagePrefix.length); 19 | const [messageType, ...args] = JSON.parse(messageAfterPrefix); 20 | return { messageType, args }; 21 | } 22 | 23 | export function setApplicationIsTerminated(): void { 24 | // If there's an unhandled exception, we'll prevent the webview from doing anything else until 25 | // it reloads the page. This is equivalent to what happens in Blazor Server, and avoids anyone 26 | // taking a dependency on being able to continue interacting after a fatal error. 27 | applicationIsTerminated = true; 28 | } 29 | 30 | interface IpcMessage { 31 | messageType: string; 32 | args: unknown[]; 33 | } 34 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Rendering/DomMerging/DataPermanentElementSync.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | const dataPermanentAttributeName = 'data-permanent'; 5 | 6 | export function isDataPermanentElement(elem: Element): boolean { 7 | return elem.hasAttribute(dataPermanentAttributeName); 8 | } 9 | 10 | export function cannotMergeDueToDataPermanentAttributes(elementA: Element, elementB: Element) { 11 | const dataPermanentAttributeValueA = elementA.getAttribute(dataPermanentAttributeName); 12 | const dataPermanentAttributeValueB = elementB.getAttribute(dataPermanentAttributeName); 13 | 14 | return dataPermanentAttributeValueA !== dataPermanentAttributeValueB; 15 | } 16 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Rendering/ElementReferenceCapture.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { DotNet } from '@microsoft/dotnet-js-interop'; 5 | 6 | export function applyCaptureIdToElement(element: Element, referenceCaptureId: string): void { 7 | element.setAttribute(getCaptureIdAttributeName(referenceCaptureId), ''); 8 | } 9 | 10 | function getElementByCaptureId(referenceCaptureId: string) { 11 | const selector = `[${getCaptureIdAttributeName(referenceCaptureId)}]`; 12 | return document.querySelector(selector); 13 | } 14 | 15 | function getCaptureIdAttributeName(referenceCaptureId: string) { 16 | return `_bl_${referenceCaptureId}`; 17 | } 18 | 19 | // Support receiving ElementRef instances as args in interop calls 20 | const elementRefKey = '__internalId'; // Keep in sync with ElementRef.cs 21 | DotNet.attachReviver((key, value) => { 22 | if (value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, elementRefKey) && typeof value[elementRefKey] === 'string') { 23 | return getElementByCaptureId(value[elementRefKey]); 24 | } else { 25 | return value; 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Rendering/Events/EventFieldInfo.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | export class EventFieldInfo { 5 | constructor(public componentId: number, public fieldValue: string | boolean) { 6 | } 7 | 8 | public static fromEvent(componentId: number, event: Event): EventFieldInfo | null { 9 | const elem = event.target; 10 | if (elem instanceof Element) { 11 | const fieldData = getFormFieldData(elem); 12 | if (fieldData) { 13 | return new EventFieldInfo(componentId, fieldData.value); 14 | } 15 | } 16 | 17 | // This event isn't happening on a form field that we can reverse-map back to some incoming attribute 18 | return null; 19 | } 20 | } 21 | 22 | function getFormFieldData(elem: Element) { 23 | // The logic in here should be the inverse of the logic in BrowserRenderer's tryApplySpecialProperty. 24 | // That is, we're doing the reverse mapping, starting from an HTML property and reconstructing which 25 | // "special" attribute would have been mapped to that property. 26 | if (elem instanceof HTMLInputElement) { 27 | return (elem.type && elem.type.toLowerCase() === 'checkbox') 28 | ? { value: elem.checked } 29 | : { value: elem.value }; 30 | } 31 | 32 | if (elem instanceof HTMLSelectElement || elem instanceof HTMLTextAreaElement) { 33 | return { value: elem.value }; 34 | } 35 | 36 | return null; 37 | } 38 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Rendering/WebRendererId.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | // These IDs need to be kept in sync with RendererId.cs 5 | export enum WebRendererId { 6 | Default = 0, 7 | Server = 1, 8 | WebAssembly = 2, 9 | WebView = 3, 10 | } 11 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Services/InitialRootComponentsList.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { ComponentDescriptor } from './ComponentDescriptorDiscovery'; 5 | import { RootComponentManager } from './RootComponentManager'; 6 | 7 | export class InitialRootComponentsList implements RootComponentManager { 8 | constructor(public readonly initialComponents: ComponentDescriptorType[]) { 9 | } 10 | 11 | resolveRootComponent(ssrComponentId: number): ComponentDescriptor { 12 | return this.initialComponents[ssrComponentId]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Services/JSEventRegistry.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { IBlazor } from '../GlobalExports'; 5 | 6 | // The base Blazor event type. 7 | // Properties listed here get assigned by the event registry in 'dispatchEvent'. 8 | interface BlazorEvent { 9 | type: keyof BlazorEventMap; 10 | } 11 | 12 | // Maps Blazor event names to the argument type passed to registered listeners. 13 | export interface BlazorEventMap { 14 | 'enhancedload': BlazorEvent, 15 | 'enhancednavigationstart': BlazorEvent, 16 | 'enhancednavigationend': BlazorEvent, 17 | } 18 | 19 | export class JSEventRegistry { 20 | private readonly _eventListeners = new Map void>>(); 21 | 22 | static create(blazor: IBlazor): JSEventRegistry { 23 | const result = new JSEventRegistry(); 24 | blazor.addEventListener = result.addEventListener.bind(result); 25 | blazor.removeEventListener = result.removeEventListener.bind(result); 26 | return result; 27 | } 28 | 29 | public addEventListener(type: K, listener: (ev: BlazorEventMap[K]) => void): void { 30 | let listenersForEventType = this._eventListeners.get(type); 31 | if (!listenersForEventType) { 32 | listenersForEventType = new Set(); 33 | this._eventListeners.set(type, listenersForEventType); 34 | } 35 | 36 | listenersForEventType.add(listener); 37 | } 38 | 39 | public removeEventListener(type: K, listener: (ev: BlazorEventMap[K]) => void): void { 40 | this._eventListeners.get(type)?.delete(listener); 41 | } 42 | 43 | public dispatchEvent(type: K, ev: Omit): void { 44 | const listenersForEventType = this._eventListeners.get(type); 45 | if (!listenersForEventType) { 46 | return; 47 | } 48 | 49 | const event = { 50 | ...ev, 51 | type, 52 | }; 53 | 54 | for (const listener of listenersForEventType) { 55 | listener(event); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Services/RootComponentManager.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { ComponentDescriptor } from './ComponentDescriptorDiscovery'; 5 | 6 | export interface RootComponentManager { 7 | initialComponents: InitialComponentsDescriptorType[]; 8 | onAfterRenderBatch?(browserRendererId: number): void; 9 | onAfterUpdateRootComponents?(batchId: number): void; 10 | resolveRootComponent(ssrComponentId: number): ComponentDescriptor; 11 | } 12 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/StreamingInterop.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { DotNet } from '@microsoft/dotnet-js-interop'; 5 | 6 | export async function getNextChunk(data: ArrayBufferView | Blob, position: number, nextChunkSize: number): Promise { 7 | if (data instanceof Blob) { 8 | return await getChunkFromBlob(data, position, nextChunkSize); 9 | } else { 10 | return getChunkFromArrayBufferView(data, position, nextChunkSize); 11 | } 12 | } 13 | 14 | async function getChunkFromBlob(data: Blob, position: number, nextChunkSize: number): Promise { 15 | const chunkBlob = data.slice(position, position + nextChunkSize); 16 | const arrayBuffer = await chunkBlob.arrayBuffer(); 17 | const nextChunkData = new Uint8Array(arrayBuffer); 18 | return nextChunkData; 19 | } 20 | 21 | function getChunkFromArrayBufferView(data: ArrayBufferView, position: number, nextChunkSize: number): Uint8Array { 22 | const nextChunkData = new Uint8Array(data.buffer, data.byteOffset + position, nextChunkSize); 23 | return nextChunkData; 24 | } 25 | 26 | const transmittingDotNetToJSStreams = new Map>(); 27 | export function receiveDotNetDataStream(dispatcher: DotNet.ICallDispatcher, streamId: number, data: Uint8Array, bytesRead: number, errorMessage: string): void { 28 | let streamController = transmittingDotNetToJSStreams.get(streamId); 29 | if (!streamController) { 30 | const readableStream = new ReadableStream({ 31 | start(controller) { 32 | transmittingDotNetToJSStreams.set(streamId, controller); 33 | streamController = controller; 34 | }, 35 | }); 36 | 37 | dispatcher.supplyDotNetStream(streamId, readableStream); 38 | } 39 | 40 | if (errorMessage) { 41 | streamController!.error(errorMessage); 42 | transmittingDotNetToJSStreams.delete(streamId); 43 | } else if (bytesRead === 0) { 44 | streamController!.close(); 45 | transmittingDotNetToJSStreams.delete(streamId); 46 | } else { 47 | streamController!.enqueue(data.length === bytesRead ? data : data.subarray(0, bytesRead)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/Utf8Decoder.ts: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | const nativeDecoder = typeof TextDecoder === 'function' 5 | ? new TextDecoder('utf-8') 6 | : null; 7 | 8 | export const decodeUtf8: (bytes: Uint8Array) => string 9 | = nativeDecoder ? nativeDecoder.decode.bind(nativeDecoder) : decodeImpl; 10 | 11 | /* ! 12 | Logic in decodeImpl is derived from fast-text-encoding 13 | https://github.com/samthor/fast-text-encoding 14 | 15 | License for fast-text-encoding: Apache 2.0 16 | https://github.com/samthor/fast-text-encoding/blob/master/LICENSE 17 | */ 18 | 19 | function decodeImpl(bytes: Uint8Array): string { 20 | let pos = 0; 21 | const len = bytes.length; 22 | const out: number[] = []; 23 | const substrings: string[] = []; 24 | 25 | while (pos < len) { 26 | const byte1 = bytes[pos++]; 27 | if (byte1 === 0) { 28 | break; // NULL 29 | } 30 | 31 | if ((byte1 & 0x80) === 0) { // 1-byte 32 | out.push(byte1); 33 | } else if ((byte1 & 0xe0) === 0xc0) { // 2-byte 34 | const byte2 = bytes[pos++] & 0x3f; 35 | out.push(((byte1 & 0x1f) << 6) | byte2); 36 | } else if ((byte1 & 0xf0) === 0xe0) { 37 | const byte2 = bytes[pos++] & 0x3f; 38 | const byte3 = bytes[pos++] & 0x3f; 39 | out.push(((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3); 40 | } else if ((byte1 & 0xf8) === 0xf0) { 41 | const byte2 = bytes[pos++] & 0x3f; 42 | const byte3 = bytes[pos++] & 0x3f; 43 | const byte4 = bytes[pos++] & 0x3f; 44 | 45 | // this can be > 0xffff, so possibly generate surrogates 46 | let codepoint = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4; 47 | if (codepoint > 0xffff) { 48 | // codepoint &= ~0x10000; 49 | codepoint -= 0x10000; 50 | out.push((codepoint >>> 10) & 0x3ff | 0xd800); 51 | codepoint = 0xdc00 | codepoint & 0x3ff; 52 | } 53 | out.push(codepoint); 54 | } else { 55 | // FIXME: we're ignoring this 56 | } 57 | 58 | // As a workaround for https://github.com/samthor/fast-text-encoding/issues/1, 59 | // make sure the 'out' array never gets too long. When it reaches a limit, we 60 | // stringify what we have so far and append to a list of outputs. 61 | if (out.length > 1024) { 62 | substrings.push(String.fromCharCode.apply(null, out)); 63 | out.length = 0; 64 | } 65 | } 66 | 67 | substrings.push(String.fromCharCode.apply(null, out)); 68 | return substrings.join(''); 69 | } 70 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "noImplicitAny": false, 5 | "noEmitOnError": true, 6 | "removeComments": false, 7 | "sourceMap": true, 8 | "target": "es2019", 9 | "module": "es2020", 10 | "lib": ["es2019", "dom"], 11 | "strict": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/tsconfig.jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2015", 4 | "target": "es2019", 5 | "sourceMap": true, 6 | "moduleResolution": "node", 7 | "inlineSources": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "removeComments": false, 11 | "noImplicitAny": false, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": false, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "noEmitOnError": true, 16 | "skipLibCheck": true, 17 | "stripInternal": true, 18 | "strict": true, 19 | "outDir": "./dist", 20 | "types": [ 21 | "jest", 22 | "jasmine", 23 | "node" 24 | ], 25 | "lib": [ 26 | "es2019", 27 | "dom" 28 | ], 29 | "baseUrl": "." 30 | }, 31 | "include": [ 32 | "./test/**/*" 33 | ], 34 | } 35 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/Web.JS/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../Shared.JS/tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitAny": false, 5 | "noEmitOnError": true, 6 | "removeComments": false, 7 | "useDefineForClassFields": false 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/dotnet-runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dotnet-runtime", 3 | "private": true, 4 | "description": ".NET is a developer platform with tools and libraries for building any type of app, including web, mobile, desktop, games, IoT, cloud, and microservices.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git@github.com:dotnet/runtime.git" 8 | }, 9 | "version": "1.0.0", 10 | "main": "dotnet.js", 11 | "types": "dotnet.d.ts", 12 | "keywords": [ 13 | "dotnet", 14 | "runtime", 15 | "wasm" 16 | ], 17 | "author": "Microsoft", 18 | "license": "MIT" 19 | } 20 | -------------------------------------------------------------------------------- /src/RemoteWebView.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": "npm run protoc && webpack --mode production --stats-error-details", 10 | "build:noprotoc": "webpack --mode production", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "protoc": "protoc --plugin=protoc-gen-ts=\".\\node_modules\\.bin\\protoc-gen-ts.cmd\" --plugin=protoc-gen-grpc-web=\".\\node_modules\\protoc-gen-grpc-web\\bin\\protoc-gen-grpc-web.exe\" --js_out=\"import_style=commonjs,binary:src/generated\" --grpc-web_out=\"import_style=typescript,mode=grpcwebtext:src/generated\" --ts_out=\"src/generated/ts\" -I..\\protos -I.\\protoc\\include webview.proto" 13 | }, 14 | "devDependencies": { 15 | "@rollup/plugin-typescript": "11.1.6", 16 | "@types/base64-arraybuffer": "^0.1.0", 17 | "@types/google-protobuf": "3.15.12", 18 | "base64-arraybuffer": "^0.1.5", 19 | "eslint": "^8.42.0", 20 | "google-protobuf": "3.21.4", 21 | "grpc-web": "^1.5.0", 22 | "inspectpack": "^4.7.1", 23 | "source-map-loader": "^5.0.0", 24 | "ts-loader": "^9.2.5", 25 | "ts-protoc-gen": "0.15.0", 26 | "tsconfig-paths-webpack-plugin": "^3.5.1", 27 | "typescript": "^4.4.2", 28 | "webpack": "^5.95.0", 29 | "webpack-bundle-analyzer": "^4.10.2", 30 | "webpack-cli": "^5.1.4" 31 | }, 32 | "dependencies": { 33 | "enhanced-resolve": "^4.5.0", 34 | "protoc": "^1.1.3", 35 | "protoc-gen-grpc-web": "^1.5.0", 36 | "yarn": "^1.22.22" 37 | }, 38 | "packageManager": "yarn@4.3.1" 39 | } 40 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/protoc/bin/protoc.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/src/RemoteWebView.Blazor.JS/protoc/bin/protoc.exe -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/protoc/include/google/protobuf/empty.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option go_package = "google.golang.org/protobuf/types/known/emptypb"; 37 | option java_package = "com.google.protobuf"; 38 | option java_outer_classname = "EmptyProto"; 39 | option java_multiple_files = true; 40 | option objc_class_prefix = "GPB"; 41 | option cc_enable_arenas = true; 42 | 43 | // A generic empty message that you can re-use to avoid defining duplicated 44 | // empty messages in your APIs. A typical example is to use it as the request 45 | // or the response type of an API method. For instance: 46 | // 47 | // service Foo { 48 | // rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); 49 | // } 50 | // 51 | // The JSON representation for `Empty` is empty JSON object `{}`. 52 | message Empty {} 53 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/protoc/include/google/protobuf/source_context.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option java_package = "com.google.protobuf"; 37 | option java_outer_classname = "SourceContextProto"; 38 | option java_multiple_files = true; 39 | option objc_class_prefix = "GPB"; 40 | option go_package = "google.golang.org/protobuf/types/known/sourcecontextpb"; 41 | 42 | // `SourceContext` represents information about the source of a 43 | // protobuf element, like the file in which it is defined. 44 | message SourceContext { 45 | // The path-qualified name of the .proto file that contained the associated 46 | // protobuf element. For example: `"google/protobuf/source_context.proto"`. 47 | string file_name = 1; 48 | } 49 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/protoc/readme.txt: -------------------------------------------------------------------------------- 1 | Protocol Buffers - Google's data interchange format 2 | Copyright 2008 Google Inc. 3 | https://developers.google.com/protocol-buffers/ 4 | 5 | This package contains a precompiled binary version of the protocol buffer 6 | compiler (protoc). This binary is intended for users who want to use Protocol 7 | Buffers in languages other than C++ but do not want to compile protoc 8 | themselves. To install, simply place this binary somewhere in your PATH. 9 | 10 | If you intend to use the included well known types then don't forget to 11 | copy the contents of the 'include' directory somewhere as well, for example 12 | into '/usr/local/include/'. 13 | 14 | Please refer to our official github site for more installation instructions: 15 | https://github.com/protocolbuffers/protobuf 16 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/src/Boot.Desktop.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"Boot.Desktop.js","sourceRoot":"","sources":["Boot.Desktop.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,yFAA6E;AAC7E,6DAAqD;AACrD,uDAA2D;AAC3D,8EAA2G;AAC3G,oFAAmL;AACnL,8FAAiG;AACjG,iDAA0D;AAC1D,mEAAyE;AACzE,uEAAsE;AAGtE,IAAI,OAAO,GAAG,KAAK,CAAC;AAEpB,SAAe,IAAI;;;;;;oBACf,IAAI,OAAO,EAAE,CAAC;wBACV,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;oBACnD,CAAC;oBACD,OAAO,GAAG,IAAI,CAAC;oBAEf,kBAAU,GAAG,0BAAM,CAAC,gBAAgB,CAAC;wBACjC,uBAAuB,EAAE,8CAA2B;wBACpD,qBAAqB,EAAE,4CAAyB;wBAChD,aAAa,EAAE,gCAAa;qBAC/B,CAAC,CAAC;oBAEmB,qBAAM,IAAA,mDAA0B,GAAE,EAAA;;oBAAlD,aAAa,GAAG,SAAkC;oBAExD,IAAA,uCAAuB,GAAE,CAAC;oBAE1B,sBAAM,CAAC,SAAS,CAAC,8BAA8B,GAAG,8BAA8B,CAAC;oBAEjF,qCAA0B,CAAC,4BAA4B,CAAC,6BAAa,CAAC,OAAO,CAAC,CAAC;oBAC/E,qCAA0B,CAAC,yBAAyB,CAAC,6BAAa,CAAC,OAAO,EAAE,sCAAmB,EAAE,uCAAoB,CAAC,CAAC;oBAEvH,sDAAsD;oBAEtD,qBAAM,aAAa,CAAC,2BAA2B,CAAC,sBAAM,CAAC,EAAA;;oBAFvD,sDAAsD;oBAEtD,SAAuD,CAAC;;;;;CAC3D;AAED,SAAS,8BAA8B,CAAC,QAAgB,EAAE,IAAS,EAAE,SAAiB,EAAE,YAAoB;IACxG,IAAA,0CAAuB,EAAC,kBAAU,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;AACjF,CAAC;AAED,sBAAM,CAAC,KAAK,GAAG,IAAI,CAAC;AACpB,MAAM,CAAC,QAAQ,CAAC,GAAG,0BAAM,CAAC;AAG1B,IAAI,IAAA,4BAAe,GAAE,EAAE,CAAC;IACpB,IAAI,EAAE,CAAC;AACX,CAAC"} -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/src/Boot.Desktop.ts: -------------------------------------------------------------------------------- 1 | import { DotNet } from '../web.js/node_modules/@microsoft/dotnet-js-interop'; 2 | import { Blazor } from '../web.js/src/GlobalExports'; 3 | import { shouldAutoStart } from '../web.js/src/BootCommon'; 4 | import { internalFunctions as navigationManagerFunctions } from '../web.js/src/Services/NavigationManager'; 5 | import { sendBeginInvokeDotNetFromJS, sendEndInvokeJSFromDotNet, sendByteArray, sendLocationChanged, sendLocationChanging } from '../web.js/src/Platform/WebView/WebViewIpcSender'; 6 | import { fetchAndInvokeInitializers } from '../web.js/src/JSInitializers/JSInitializers.WebView'; 7 | import { initializeRemoteWebView } from './RemoteWebView'; 8 | import { receiveDotNetDataStream } from '../web.js/src/StreamingInterop'; 9 | import { WebRendererId } from '../web.js/src/Rendering/WebRendererId'; 10 | 11 | 12 | let started = false; 13 | export let dispatcher: DotNet.ICallDispatcher; 14 | async function boot(): Promise { 15 | if (started) { 16 | throw new Error('Blazor has already started.'); 17 | } 18 | started = true; 19 | 20 | dispatcher = DotNet.attachDispatcher({ 21 | beginInvokeDotNetFromJS: sendBeginInvokeDotNetFromJS, 22 | endInvokeJSFromDotNet: sendEndInvokeJSFromDotNet, 23 | sendByteArray: sendByteArray, 24 | }); 25 | 26 | const jsInitializer = await fetchAndInvokeInitializers(); 27 | 28 | initializeRemoteWebView(); 29 | 30 | Blazor._internal.receiveWebViewDotNetDataStream = receiveWebViewDotNetDataStream; 31 | 32 | navigationManagerFunctions.enableNavigationInterception(WebRendererId.WebView); 33 | navigationManagerFunctions.listenForNavigationEvents(WebRendererId.WebView, sendLocationChanged, sendLocationChanging); 34 | 35 | // sendAttachPage is done in initializeRemoteWebView() 36 | 37 | await jsInitializer.invokeAfterStartedCallbacks(Blazor); 38 | } 39 | 40 | function receiveWebViewDotNetDataStream(streamId: number, data: any, bytesRead: number, errorMessage: string): void { 41 | receiveDotNetDataStream(dispatcher, streamId, data, bytesRead, errorMessage); 42 | } 43 | 44 | Blazor.start = boot; 45 | window['DotNet'] = DotNet; 46 | 47 | 48 | if (shouldAutoStart()) { 49 | boot(); 50 | } 51 | -------------------------------------------------------------------------------- /src/RemoteWebView.Blazor.JS/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./web.js/src/tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@browserjs/*": [ "./web.js/src/*" ] 7 | } 8 | }, 9 | "include": [ 10 | "src/**/*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/RemoteWebView/ConnectedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PeakSWC.RemoteWebView 4 | { 5 | public class ConnectedEventArgs(Guid id, Uri url, string ipAddress, string user) : EventArgs 6 | { 7 | public Guid Id { get; } = id; 8 | public Uri Url { get; } = url; 9 | 10 | public string IpAddress { get; } = ipAddress; 11 | 12 | public string User { get; } = user; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/RemoteWebView/DisconnectedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PeakSWC.RemoteWebView 4 | { 5 | public class DisconnectedEventArgs(Guid id, Uri url, Exception exception) : EventArgs 6 | { 7 | public Guid Id { get; } = id; 8 | public Uri Url { get; } = url; 9 | public Exception Exception { get; } = exception; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/RemoteWebView/EtagGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.FileProviders; 3 | using System.Security.Cryptography; 4 | using System.IO; 5 | 6 | namespace PeakSWC.RemoteWebView 7 | { 8 | public static class ETagGenerator 9 | { 10 | public static string GenerateETag(IFileInfo fileInfo) 11 | { 12 | using var stream = fileInfo.CreateReadStream(); 13 | using var hasher = SHA256.Create(); 14 | byte[] hash = hasher.ComputeHash(stream); 15 | stream.Seek(0, SeekOrigin.Begin); 16 | return Convert.ToBase64String(hash); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/RemoteWebView/FixedManifestEmbeddedAssembly.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Xml.Linq; 7 | 8 | namespace PeakSWC.RemoteWebView 9 | { 10 | public class FixedManifestEmbeddedAssembly(Assembly inner) : Assembly 11 | { 12 | // See: 13 | // - https://github.com/dotnet/aspnetcore/issues/29306 14 | // - https://github.com/dotnet/aspnetcore/blob/master/src/FileProviders/Embedded/src/build/netstandard2.0/Microsoft.Extensions.FileProviders.Embedded.targets 15 | // - https://github.com/dotnet/aspnetcore/tree/master/src/FileProviders/Manifest.MSBuildTask/src 16 | // - https://github.com/dotnet/aspnetcore/blob/master/src/FileProviders/Embedded/src/Manifest/ManifestParser.cs 17 | 18 | private const string ManifestName = "Microsoft.Extensions.FileProviders.Embedded.Manifest.xml"; 19 | 20 | public override string Location => inner.Location; 21 | 22 | public override AssemblyName GetName() => inner.GetName(); 23 | 24 | public override Stream? GetManifestResourceStream(string name) 25 | { 26 | var stream = inner.GetManifestResourceStream(name); 27 | 28 | if (name != ManifestName || stream == null) 29 | return stream; 30 | 31 | using var reader = new StreamReader(stream); 32 | 33 | var xml = XDocument.Parse(reader.ReadToEnd()); 34 | 35 | var invalidNode = xml 36 | .Descendants("File") 37 | .FirstOrDefault(node => string.IsNullOrEmpty(node.Attribute("Name")?.Value)); 38 | 39 | if (invalidNode == null) 40 | { 41 | return new MemoryStream(Encoding.UTF8.GetBytes(xml.ToString())); 42 | } 43 | 44 | invalidNode.Remove(); 45 | 46 | return new MemoryStream(Encoding.UTF8.GetBytes(xml.ToString())); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/RemoteWebView/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 | using System.Diagnostics.CodeAnalysis; 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/RemoteWebView/HealthCheck.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http.Headers; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace PeakSWC.RemoteWebView 10 | { 11 | internal static class HealthCheck 12 | { 13 | /// 14 | /// Waits until the server's health endpoint returns a successful response or until a timeout occurs. 15 | /// 16 | /// The URL of the server's health check endpoint. 17 | /// The HTTP handler configured to bypass SSL certificate validation. 18 | /// Maximum time to wait for the server to become healthy. 19 | /// Time interval between health check attempts. 20 | /// True if the server is healthy within the timeout; otherwise, false. 21 | public static async Task WaitAsync(string healthCheckUrl, int timeoutSeconds = 30, int retryIntervalSeconds = 1) 22 | { 23 | using var httpClient = new HttpClient(); 24 | httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 25 | 26 | var timeout = TimeSpan.FromSeconds(timeoutSeconds); 27 | var delay = TimeSpan.FromSeconds(retryIntervalSeconds); 28 | var startTime = DateTime.UtcNow; 29 | 30 | while (DateTime.UtcNow - startTime < timeout) 31 | { 32 | try 33 | { 34 | var response = await httpClient.GetAsync(healthCheckUrl); 35 | if (response.IsSuccessStatusCode) 36 | { 37 | return true; 38 | } 39 | } 40 | catch (HttpRequestException ex) 41 | { 42 | Console.WriteLine($"Health check request failed: {ex.Message}. Retrying in {delay.Seconds} seconds..."); 43 | } 44 | 45 | await Task.Delay(delay); 46 | } 47 | 48 | Console.WriteLine("Server health check timed out."); 49 | return false; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/RemoteWebView/IBlazorWebView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | using Microsoft.Web.WebView2.Core; 5 | 6 | namespace PeakSWC.RemoteWebView 7 | { 8 | public interface IBlazorWebView 9 | { 10 | public event EventHandler? Connected; 11 | public event EventHandler? Disconnected; 12 | public event EventHandler? Refreshed; 13 | public event EventHandler? ReadyToConnect; 14 | public void FireConnected(ConnectedEventArgs args); 15 | public void FireDisconnected(DisconnectedEventArgs args); 16 | public void FireRefreshed(RefreshedEventArgs args); 17 | public void FireReadyToConnect(ReadyToConnectEventArgs args); 18 | public Uri? ServerUri { get; set; } 19 | 20 | public Uri? GrpcBaseUri { get; set; } 21 | 22 | public Task GetGrpcBaseUriAsync(Uri? serverUri); 23 | public string Group { get; set; } 24 | public Guid Id { get; set; } 25 | public bool EnableMirrors { get; set; } 26 | 27 | public uint PingIntervalSeconds { get; set; } 28 | 29 | public string Markup { get; set; } 30 | public void Restart(); 31 | 32 | public void NavigateToString(string htmlContent); 33 | 34 | public Task WaitForInitializationComplete(); 35 | 36 | public CoreWebView2CookieManager CookieManager {get;} 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/RemoteWebView/ReadyToConnectEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PeakSWC.RemoteWebView 4 | { 5 | public class ReadyToConnectEventArgs(Guid id, Uri url) : EventArgs 6 | { 7 | public Guid Id { get; } = id; 8 | public Uri Url { get; } = url; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/RemoteWebView/RefreshedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PeakSWC.RemoteWebView 4 | { 5 | public class RefreshedEventArgs(Guid id, Uri url) : EventArgs 6 | { 7 | public Guid Id { get; } = id; 8 | public Uri Url { get; } = url; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/RemoteWebView/_framework/blazor.modules.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /src/RemoteWebViewService/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "microsoft.dotnet-msidentity": { 6 | "version": "2.0.8", 7 | "commands": [ 8 | "dotnet-msidentity" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/RemoteWebViewService/BrowserIPCState.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Threading; 3 | 4 | namespace PeakSWC.RemoteWebView 5 | { 6 | public class BrowserIPCState 7 | { 8 | public ConcurrentDictionary MessageDictionary { get; } = new(); 9 | public uint SequenceNum { get; set; } = 1; 10 | public SemaphoreSlim Semaphore { get; } = new SemaphoreSlim(1, 1); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/BrowserResponseNode.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | 3 | namespace PeakSWC.RemoteWebView 4 | { 5 | public class BrowserResponseNode(IServerStreamWriter streamWriter, string clientId, bool isPrimary) 6 | { 7 | public IServerStreamWriter StreamWriter { get; set; } = streamWriter; 8 | public string ClientId { get; set; } = clientId; 9 | public bool IsPrimary { get; set; } = isPrimary; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/CreateCert.ps1: -------------------------------------------------------------------------------- 1 | # Run this on the server machine as administrator 2 | $IP = "192.168.1.35" 3 | $DnsName = "localhost" 4 | $CertName = "DevCertificate_$IP" 5 | $CertPath = "Cert:\LocalMachine\My" 6 | $PfxPassword = "YourStrongPassword" 7 | $SecurePassword = ConvertTo-SecureString -String $PfxPassword -Force -AsPlainText 8 | $PfxPath = "C:\Certificates\DevCertificate_$IP.pfx" 9 | $CerPath = "C:\Certificates\DevCertificate_$IP.cer" 10 | 11 | # Ensure the certificate directory exists 12 | New-Item -ItemType Directory -Force -Path (Split-Path -Parent $PfxPath) | Out-Null 13 | 14 | # Create the SAN entries 15 | $san = @("IPAddress=$IP", "DNS=$DnsName") 16 | $sanText = [string]::Join("&", $san) 17 | 18 | # Generate certificate with IP address in SAN as IPAddress and localhost as DNS 19 | $cert = New-SelfSignedCertificate ` 20 | -Subject "CN=$IP" ` 21 | -CertStoreLocation $CertPath ` 22 | -FriendlyName $CertName ` 23 | -NotAfter (Get-Date).AddYears(10) ` 24 | -KeyUsage DigitalSignature, KeyEncipherment ` 25 | -KeyExportPolicy Exportable ` 26 | -KeyAlgorithm RSA ` 27 | -KeyLength 2048 ` 28 | -HashAlgorithm SHA256 ` 29 | -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" ` 30 | -TextExtension @("2.5.29.17={text}$sanText") 31 | 32 | # Export PFX 33 | Export-PfxCertificate -Cert $cert -FilePath $PfxPath -Password $SecurePassword 34 | 35 | # Export CER 36 | Export-Certificate -Cert $cert -FilePath $CerPath -Type CERT 37 | 38 | Write-Host "Certificate created with IP ($IP) as primary subject and localhost in SAN" 39 | Write-Host "PFX File: $PfxPath" 40 | Write-Host "CER File: $CerPath" 41 | Write-Host "Please ensure you have the PFX and CER files for further use" 42 | 43 | # Display certificate details 44 | Write-Host "`nCertificate Details:" 45 | $cert | Format-List Subject, DnsNameList, Thumbprint 46 | 47 | Write-Host "`nSubject Alternative Name Extension Details:" 48 | $sanExtension = $cert.Extensions | Where-Object {$_.Oid.FriendlyName -eq "Subject Alternative Name"} 49 | if ($sanExtension) { 50 | $sanASN = New-Object System.Security.Cryptography.AsnEncodedData ($sanExtension.Oid, $sanExtension.RawData) 51 | Write-Host $sanASN.Format($true) 52 | } else { 53 | Write-Host "No Subject Alternative Name extension found." 54 | } 55 | #PFX File: C:\Certificates\DevCertificate_192.168.1.35.pfx 56 | #CER File: C:\Certificates\DevCertificate_192.168.1.35.cer -------------------------------------------------------------------------------- /src/RemoteWebViewService/EndPoints/Contact.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Logging; 4 | using PeakSWC.RemoteWebView.Pages; 5 | using System; 6 | using System.Reflection; 7 | 8 | namespace PeakSWC.RemoteWebView.EndPoints 9 | { 10 | public static partial class Endpoints 11 | { 12 | public static RequestDelegate Contact() 13 | { 14 | return async context => 15 | { 16 | try 17 | { 18 | // Get the version of the currently executing assembly 19 | var assembly = Assembly.GetExecutingAssembly(); 20 | var assemblyVersion = assembly.GetName().Version?.ToString() ?? "Version not found"; 21 | 22 | // Create the version string 23 | string versionString = $"Version {assemblyVersion}"; 24 | 25 | var contact = new ContactInfo 26 | { 27 | Company = "Peak Software Consulting, LLC", 28 | Email = "budcribar@msn.com", 29 | Name = "Bud Cribar", 30 | Url = "https://github.com/budcribar/RemoteBlazorWebView" 31 | }; 32 | 33 | var html = HtmlPageGenerator.GenerateContactPage(contact, versionString); 34 | 35 | context.Response.ContentType = "text/html"; 36 | 37 | context.Response.ContentLength = html.Length; 38 | 39 | await context.Response.WriteAsync(html).ConfigureAwait(false); 40 | } 41 | catch (Exception ex) 42 | { 43 | ILogger logger = context.RequestServices.GetRequiredService>(); 44 | logger.LogError(ex, ex.Message); 45 | context.Response.StatusCode = 500; 46 | await context.Response.WriteAsync("An error occurred while generating the contact page.").ConfigureAwait(false); 47 | } 48 | }; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/EndPoints/Favicon.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System.Reflection; 3 | 4 | namespace PeakSWC.RemoteWebView.EndPoints 5 | { 6 | public static partial class Endpoints 7 | { 8 | public static RequestDelegate Favicon() 9 | { 10 | return async context => 11 | { 12 | // Specify the resource name, typically it is namespace.filename 13 | var resourceName = "PeakSWC.RemoteWebView.Resources.favicon.ico"; 14 | 15 | // Get the assembly where the resource is embedded 16 | var assembly = Assembly.GetExecutingAssembly(); 17 | 18 | // Set the correct content type for favicon.ico 19 | context.Response.ContentType = "image/x-icon"; 20 | 21 | // Find and stream the embedded file 22 | using var stream = assembly.GetManifestResourceStream(resourceName); 23 | if (stream == null) 24 | { 25 | context.Response.StatusCode = 404; 26 | await context.Response.WriteAsync("Favicon not found").ConfigureAwait(false); 27 | return; 28 | } 29 | 30 | context.Response.ContentLength = stream.Length; 31 | context.Response.Headers.CacheControl = "public,max-age=604800"; // Cache for 7 days 32 | 33 | await stream.CopyToAsync(context.Response.Body).ConfigureAwait(false); 34 | }; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/EndPoints/GrpcBaseUri.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | 8 | namespace PeakSWC.RemoteWebView.EndPoints 9 | { 10 | public static partial class Endpoints 11 | { 12 | public static RequestDelegate GrpcBaseUri() 13 | { 14 | return async context => 15 | { 16 | try 17 | { 18 | var baseUri = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.PathBase}/"; 19 | 20 | var response = new GrpcBaseUriResponse 21 | { 22 | GrpcBaseUri = baseUri, 23 | }; 24 | 25 | var jsonResponse = JsonSerializer.Serialize(response, JsonContext.Default.GrpcBaseUriResponse); 26 | 27 | context.Response.ContentType = "application/json"; 28 | context.Response.ContentLength = jsonResponse.Length; 29 | 30 | await context.Response.WriteAsync(jsonResponse).ConfigureAwait(false); 31 | } 32 | catch (Exception ex) 33 | { 34 | ILogger logger = context.RequestServices.GetRequiredService>(); 35 | logger.LogError(ex, ex.Message); 36 | 37 | context.Response.StatusCode = 500; 38 | await context.Response.WriteAsync("An error occurred while processing the request.").ConfigureAwait(false); 39 | } 40 | }; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/EndPoints/Health.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Logging; 4 | using PeakSWC.RemoteWebView.Pages; 5 | using System; 6 | using System.Reflection; 7 | 8 | namespace PeakSWC.RemoteWebView.EndPoints 9 | { 10 | public static partial class Endpoints 11 | { 12 | public static RequestDelegate Health() 13 | { 14 | return async context => 15 | { 16 | context.Response.StatusCode = StatusCodes.Status200OK; 17 | await context.Response.WriteAsync("OK").ConfigureAwait(false); 18 | }; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/EndPoints/ResetStats.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System.Threading.Tasks; 4 | 5 | namespace PeakSWC.RemoteWebView.EndPoints 6 | { 7 | public static partial class Endpoints 8 | { 9 | public static RequestDelegate ResetStats() 10 | { 11 | return context => 12 | { 13 | // Retrieve the ServerStats service from the DI container 14 | var stats = context.RequestServices.GetRequiredService(); 15 | 16 | // Reset the statistics 17 | stats.ResetStats(); 18 | 19 | context.Response.StatusCode = StatusCodes.Status204NoContent; 20 | 21 | return Task.CompletedTask; 22 | }; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/EndPoints/Status.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System.Collections.Concurrent; 4 | using System.Text.Json; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | namespace PeakSWC.RemoteWebView.EndPoints 9 | { 10 | public static partial class Endpoints 11 | { 12 | public static RequestDelegate Status() 13 | { 14 | return async context => 15 | { 16 | // Check if 'id' route value exists and is a valid GUID 17 | if (!context.Request.RouteValues.TryGetValue("id", out var idValue) || idValue == null || !Guid.TryParse(idValue.ToString(), out var guid)) 18 | { 19 | context.Response.StatusCode = StatusCodes.Status400BadRequest; 20 | await context.Response.WriteAsync($"Invalid or missing GUID {idValue}").ConfigureAwait(false); 21 | return; 22 | } 23 | 24 | // Retrieve service state from the service dictionary 25 | var serviceDictionary = context.RequestServices.GetRequiredService>>(); 26 | 27 | var response = new StatusResponse 28 | { 29 | Connected = serviceDictionary.ContainsKey(guid.ToString()) 30 | }; 31 | 32 | // Set content type to JSON and return the response 33 | context.Response.ContentType = "application/json"; 34 | await JsonSerializer.SerializeAsync(context.Response.Body, response).ConfigureAwait(false); 35 | }; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/EndPoints/Wait.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using PeakSWC.RemoteWebView.Pages; 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.Threading.Tasks; 7 | 8 | namespace PeakSWC.RemoteWebView.EndPoints 9 | { 10 | public static partial class Endpoints 11 | { 12 | public static RequestDelegate Wait() 13 | { 14 | return async context => 15 | { 16 | // Check if 'id' route value exists and is a valid GUID 17 | if (!context.Request.RouteValues.TryGetValue("id", out var idValue) || idValue == null || !Guid.TryParse(idValue.ToString(), out var guid)) 18 | { 19 | context.Response.StatusCode = StatusCodes.Status400BadRequest; 20 | await context.Response.WriteAsync($"Invalid or missing GUID {idValue}").ConfigureAwait(false); 21 | return; 22 | } 23 | 24 | // Retrieve the service state from the service dictionary 25 | var serviceDictionary = context.RequestServices.GetRequiredService>>(); 26 | 27 | // Wait for the specified service to appear in the dictionary, up to a 30-second timeout 28 | for (int i = 0; i < 30; i++) 29 | { 30 | if (serviceDictionary.ContainsKey(guid.ToString())) 31 | { 32 | context.Response.StatusCode = StatusCodes.Status200OK; 33 | context.Response.ContentType = "text/plain"; 34 | await context.Response.WriteAsync("Wait completed").ConfigureAwait(false); 35 | return; 36 | } 37 | 38 | // Delay for 1 second before the next check 39 | await Task.Delay(1000).ConfigureAwait(false); 40 | } 41 | 42 | // After 30 seconds, if the condition isn't met, return a timeout response 43 | context.Response.StatusCode = StatusCodes.Status408RequestTimeout; 44 | context.Response.ContentType = "text/html"; 45 | await context.Response.WriteAsync(RestartFailedPage.Fragment(guid.ToString())).ConfigureAwait(false); 46 | }; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/FileEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Pipelines; 3 | using System.Threading; 4 | 5 | namespace PeakSWC.RemoteWebView 6 | { 7 | public class FileEntry 8 | { 9 | public string Path { get; set; } = string.Empty; 10 | public long Length { get; set; } = -1; 11 | public Pipe Pipe { get; set; } = new Pipe(); 12 | public int Instance { get; set; } = 0; 13 | public DateTimeOffset LastModified { get; set; } = DateTimeOffset.MinValue; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/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 | using System.Diagnostics.CodeAnalysis; 7 | [assembly: SuppressMessage("Usage", "ASP0018:Unused route parameter", Justification = "", Scope = "member", Target = "~M:PeakSWC.RemoteWebView.Startup.Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder,Microsoft.AspNetCore.Hosting.IWebHostEnvironment)")] 8 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/IUserService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace PeakSWC.RemoteWebView 5 | { 6 | public interface IUserService 7 | { 8 | Task> GetUserGroups(string oid); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/JsonContext.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace PeakSWC.RemoteWebView 5 | { 6 | [JsonSerializable(typeof(StatusResponse))] 7 | [JsonSerializable(typeof(GrpcBaseUriResponse))] 8 | [JsonSerializable(typeof(Point))] 9 | [JsonSerializable(typeof(Size))] 10 | public partial class JsonContext : JsonSerializerContext 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/MockUserService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace PeakSWC.RemoteWebView 5 | { 6 | public class MockUserService : IUserService 7 | { 8 | public Task> GetUserGroups(string oid) => Task.FromResult>(["hp", "test"]); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/Pages/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Security.Principal 2 | 3 | 4 | @if (User.Identity?.IsAuthenticated == true) 5 | { 6 | Hello @User.Identity?.Name! 7 | 8 | Sign out 9 | 10 | } 11 | else 12 | { 13 | 14 | Sign in 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/Performance.md: -------------------------------------------------------------------------------- 1 | Server Performance Benchmarks 2 | 3 | Version 9.0.rc1 ClientFileReadResponse channel bounded to 1 no file read lock 4 | 5 | MultipleConcurrentFileRequestsWithSameFile_ReturnsCorrectResponses 500x3 = 1500 files 2.7 sec 6 | MultipleConcurrentFileRequests_ReturnsCorrectResponses 100 files 2.2 seconds 7 | 8 | 9 | Version 9.0.rc1 ClientFileReadResponse channel unbounded no file read lock 10 | MultipleConcurrentFileRequestsWithSameFile_ReturnsCorrectResponses 500x3 = 1500 files 2.6 sec 11 | MultipleConcurrentFileRequests_ReturnsCorrectResponses 100 files 2.2 seconds 12 | MultipleConcurrentFileRequestsWithSameFile_ReturnsCorrectResponses 1000x3 = 3000 files 3 sec 13 | MultipleConcurrentFileRequestsWithSameFile_ReturnsCorrectResponses 2000x3 = 6000 files hangs 14 | MultipleConcurrentFileRequestsWithSameFile_ReturnsCorrectResponses 1500x3 = 4500 files 3.2 sec 15 | MultipleConcurrentFileRequestsWithSameFile_ReturnsCorrectResponses 1750x3 = 5250 files hangs 16 | MultipleConcurrentFileRequestsWithSameFile_ReturnsCorrectResponses 1625x3 = 4875 files 3.3 17 | MultipleConcurrentFileRequestsWithSameFile_ReturnsCorrectResponses 1667x3 = 5001 files 5.3 sec 18 | 19 | MultipleConcurrentFileRequestsWithSameFile_BrowserAndNoClientCaching 100x3 = 300 pages 14.6 sec 20 | MultipleConcurrentFileRequestsWithSameFile_BrowserAndClientCaching 100x3 = 300 pages 14.8 sec 21 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Server.Kestrel.Core; 4 | using Microsoft.Extensions.Hosting; 5 | using System.IO; 6 | using System.Net; 7 | using System.Threading; 8 | 9 | namespace PeakSWC.RemoteWebView 10 | { 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | ThreadPool.SetMinThreads(workerThreads: 200, completionPortThreads: 200); 16 | Directory.SetCurrentDirectory(System.AppDomain.CurrentDomain.BaseDirectory); 17 | CreateHostBuilder(args).Build().Run(); 18 | } 19 | 20 | public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => 21 | { 22 | // Note: If appsettings.json does not exist in Azure the following configuration will fail with a certificate error 23 | // if (!File.Exists("appsettings.json")) 24 | webBuilder.ConfigureKestrel(options => 25 | { 26 | options.Listen(IPAddress.Loopback, 5001, listenOptions => 27 | { 28 | listenOptions.UseHttps(); 29 | //listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3; // http3 needs a real certificate 30 | listenOptions.Protocols = HttpProtocols.Http1AndHttp2; 31 | }); 32 | string certPath = "C:\\Certificates\\DevCertificate_192.168.1.35.pfx"; 33 | if (File.Exists(certPath)) 34 | options.Listen(IPAddress.Parse("192.168.1.35"), 5002, listenOptions => 35 | { 36 | listenOptions.UseHttps(certPath, "YourStrongPassword"); 37 | //listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3; // http3 needs a real certificate 38 | listenOptions.Protocols = HttpProtocols.Http1AndHttp2; 39 | }); 40 | }); 41 | 42 | webBuilder.UseStartup(); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/Properties/PublishProfiles/FolderProfileAuth.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Authorization 8 | x64 9 | bin\publishAuth 10 | FileSystem 11 | net9 12 | win-x64 13 | true 14 | false 15 | false 16 | true 17 | 18 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/Properties/PublishProfiles/FolderProfileNoAuth.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/Properties/PublishProfiles/RemoteWebViewServer - Web Deploy1.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | MSDeploy 9 | /subscriptions/1daa07fa-8487-4314-9de9-bba79b33e516/resourceGroups/CAndCResourceGroup/providers/Microsoft.Web/sites/RemoteWebViewServer 10 | True 11 | AzureWebSite 12 | Release 13 | Any CPU 14 | https://remotewebviewserver.azurewebsites.net 15 | true 16 | false 17 | f1cc0c20-31d5-4011-863e-5139b979c75a 18 | waws-prod-dm1-181.publish.azurewebsites.windows.net:443 19 | RemoteWebViewServer 20 | 21 | true 22 | WMSVC 23 | true 24 | true 25 | $RemoteWebViewServer 26 | <_SavePWD>true 27 | <_DestinationType>AzureWebSite 28 | 29 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/Properties/PublishProfiles/RemoteWebViewServer - Zip Deploy.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | ZipDeploy 9 | AzureWebSite 10 | NoAuthorization 11 | x64 12 | https://remotewebviewserver.azurewebsites.net/test 13 | true 14 | f1cc0c20-31d5-4011-863e-5139b979c75a 15 | https://remotewebviewserver.scm.azurewebsites.net/ 16 | $RemoteWebViewServer 17 | <_SavePWD>true 18 | false 19 | net9 20 | false 21 | 22 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/Properties/PublishProfiles/RemoteWebViewServer.PublishSettings: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "RemoteWebViewService": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "launchUrl": "https://localhost:5001/", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | }, 10 | "applicationUrl": "https://localhost:5001", 11 | "nativeDebugging": false 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/RemoteWebViewService/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "identityapp1": { 4 | "type": "identityapp" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/RemoteWebViewService/RemoteStaticFiles/FileStats.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System; 3 | using System.Threading; 4 | 5 | namespace PeakSWC.RemoteWebView 6 | { 7 | public class FileStats 8 | { 9 | public static void Update(ServiceState serviceState, string clientId, FileMetadata metadata) 10 | { 11 | Interlocked.Increment(ref serviceState.TotalFilesRead); 12 | Interlocked.Add(ref serviceState.TotalBytesRead, metadata.Length); 13 | } 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/RemoteStaticFiles/FileStream.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Net; 3 | 4 | namespace PeakSWC.RemoteWebView 5 | { 6 | public class FileStream 7 | { 8 | public required Stream Stream { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/RemoteStaticFiles/IFileProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.FileProviders; 2 | using System.Threading.Tasks; 3 | 4 | namespace PeakSWC.RemoteWebView.RemoteStaticFiles 5 | { 6 | public interface IFileProvider 7 | { 8 | Task GetFileInfo(string subpath); 9 | 10 | Task FileStreamExistsAsync(string subpath); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/RemoteStaticFiles/RemoteFileResolver.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.IO; 4 | using System.Net; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | using System.Threading.Tasks; 8 | 9 | namespace PeakSWC.RemoteWebView 10 | { 11 | public partial class RemoteFileResolver(ILogger logger, ServerFileSyncManager manager) 12 | { 13 | public Task GetFileMetaDataAsync(string clientId, string subpath) 14 | { 15 | if (Path.GetFileName(subpath) == "_framework/blazor.modules.json") 16 | { 17 | return Task.FromResult(new FileMetadata { ETag = "etag", LastModified = DateTimeOffset.MinValue.Ticks, Length=2, StatusCode=(int)HttpStatusCode.OK }); 18 | } 19 | return manager.RequestFileMetadataAsync(clientId, subpath, logger); 20 | } 21 | public async Task GetFileStreamAsync(string clientId, string subpath) 22 | { 23 | if (Path.GetFileName(subpath) == "_framework/blazor.modules.json") 24 | { 25 | return new FileStream { Stream = new MemoryStream(Encoding.ASCII.GetBytes("[]")) }; 26 | } 27 | 28 | DataRequest dataRequest = await manager.RequestFileDataAsync(clientId, subpath,logger).ConfigureAwait(false); 29 | 30 | var htmlHostPath = manager.GetHtmlHostPath(clientId); 31 | 32 | // check to see if we need to edit index.html 33 | if (Path.GetFileName(subpath) == Path.GetFileName(htmlHostPath)) 34 | { 35 | using StreamReader sr = new(dataRequest.Pipe.Reader.AsStream()); 36 | var contents = await sr.ReadToEndAsync().ConfigureAwait(false); 37 | var initialLength = contents.Length; 38 | contents = HrefRegEx().Replace(contents, $"(); 14 | } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/RemoteStaticFiles/RemoteFilesOptions.cs: -------------------------------------------------------------------------------- 1 | namespace PeakSWC.RemoteWebView 2 | { 3 | public class RemoteFilesOptions 4 | { 5 | /// 6 | /// The root directory from which files are served. 7 | /// 8 | public string RootDirectory { get; set; } = "client_cache"; 9 | 10 | /// 11 | /// Determines whether the server should cache files. 12 | /// If false, the server retrieves files directly from the file system on each request. 13 | /// 14 | public bool UseServerCache { get; set; } = false; 15 | 16 | /// 17 | /// Determines whether the server should utilize client-side caching using ETags. 18 | /// If true, the server checks the client's ETag and avoids sending the file if it's unchanged. 19 | /// 20 | public bool UseClientCache { get; set; } = false; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/RenewCert.txt: -------------------------------------------------------------------------------- 1 | 1. login to https://azure.microsoft.com/en-us/account/ as bud@chargeandchange.onmicrosoft.com 2 | 3 | 2. Start WebView VM 4 | 5 | 3. rdp using old MSN password 6 | 7 | 8 | 4. Run the Certify the web application 9 | 10 | Copy the cert to the azure portal for admin.remotewebview.com 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/Services/ShutdownService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Channels; 7 | using System.Threading.Tasks; 8 | 9 | namespace PeakSWC.RemoteWebView.Services 10 | { 11 | public class ShutdownService(ILogger logger, ConcurrentDictionary> serviceStateChannel, ConcurrentDictionary> serviceDictionary) 12 | { 13 | public async Task Shutdown(string id, Exception? exception = null) 14 | { 15 | 16 | if (serviceDictionary.TryRemove(id, out var client)) 17 | { 18 | try 19 | { 20 | if (exception != null) 21 | logger.LogError($"Shutting down {id} Exception:{exception.Message}"); 22 | 23 | var serviceState = await client.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); 24 | await (serviceState.IPC?.ClientResponseStream?.WriteAsync(new WebMessageResponse { Response = "shutdown:" }) ?? Task.CompletedTask).ConfigureAwait(false); 25 | serviceState.InUse = false; 26 | await serviceState.DisposeAsync().ConfigureAwait(false); 27 | } 28 | catch (Exception ex) 29 | { 30 | logger.LogError($"Failed shutdown {id} {ex.Message}."); 31 | } 32 | 33 | try 34 | { 35 | foreach (var channel in serviceStateChannel.Values) 36 | { 37 | if (!channel.Writer.TryWrite($"Shutdown:{id}")) 38 | { 39 | logger.LogError($"Failed to write shutdown notification to channel for {id}."); 40 | } 41 | } 42 | } 43 | catch (Exception ex) 44 | { 45 | logger.LogError($"Failed shutdown {id} {ex.Message}."); 46 | } 47 | 48 | } 49 | 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/StaticMethods.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Builder; 4 | using System; 5 | 6 | namespace PeakSWC.RemoteWebView 7 | { 8 | public static class StaticMethods 9 | { 10 | public static TBuilder ConditionallyRequireAuthorization(this TBuilder builder) where TBuilder : IEndpointConventionBuilder 11 | { 12 | if (builder == null) 13 | { 14 | throw new ArgumentNullException(nameof(builder)); 15 | } 16 | #if AUTHORIZATION 17 | return builder.RequireAuthorization(new AuthorizeAttribute()); 18 | #else 19 | return builder; 20 | #endif 21 | 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/TrustCert.ps1: -------------------------------------------------------------------------------- 1 | # Add certificate to Trusted Root store 2 | $CerPath = "C:\Certificates\MySelfSignedCert.cer" 3 | $CertStore = "Cert:\LocalMachine\Root" 4 | 5 | # Import the certificate to the Trusted Root store 6 | Import-Certificate -FilePath $CerPath -CertStoreLocation $CertStore 7 | 8 | Write-Host "Certificate has been added to the Trusted Root store." 9 | 10 | # Configure PowerShell to trust the certificate for HTTPS connections 11 | Add-Type @" 12 | using System.Net; 13 | using System.Security.Cryptography.X509Certificates; 14 | public class TrustAllCertsPolicy : ICertificatePolicy { 15 | public bool CheckValidationResult( 16 | ServicePoint srvPoint, X509Certificate certificate, 17 | WebRequest request, int certificateProblem) { 18 | return true; 19 | } 20 | } 21 | "@ 22 | [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy 23 | 24 | Write-Host "PowerShell has been configured to trust all certificates for this session." -------------------------------------------------------------------------------- /src/RemoteWebViewService/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "ResourceUrl": "https://graph.windows.net", 3 | 4 | "AzureAdB2C": { 5 | "Instance": "https://RemoteWebView.b2clogin.com", 6 | "ClientId": "29edb278-c0af-4c65-9ab7-5b623a6847dc", 7 | "Domain": "RemoteWebView.onmicrosoft.com", 8 | "SignedOutCallbackPath": "/signout/B2C_1_susi_reset_v2", 9 | "SignUpSignInPolicyId": "B2C_1_SignUpAndIn", 10 | "DirectoryId": "aae96de0-374d-4d41-ae04-6c4488a7e3b7" 11 | }, 12 | 13 | "Logging": { 14 | "EventLog": { 15 | "LogLevel": { 16 | "Default": "Trace", 17 | "System": "Warning", 18 | "Grpc": "Warning", 19 | "Microsoft": "Warning", 20 | "Kestrel": "Trace" 21 | } 22 | }, 23 | "LogLevel": { 24 | "Default": "Warning", 25 | "System": "Warning", 26 | "Grpc": "Warning", 27 | "Microsoft": "Warning" 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/appsettings.json.save: -------------------------------------------------------------------------------- 1 | { 2 | "ResourceUrl": "https://graph.windows.net", 3 | 4 | "AzureAdB2C": { 5 | "Instance": "https://RemoteWebView.b2clogin.com", 6 | "ClientId": "29edb278-c0af-4c65-9ab7-5b623a6847dc", 7 | "Domain": "RemoteWebView.onmicrosoft.com", 8 | "SignedOutCallbackPath": "/signout/B2C_1_susi_reset_v2", 9 | "SignUpSignInPolicyId": "B2C_1_SignUpAndIn", 10 | "DirectoryId": "aae96de0-374d-4d41-ae04-6c4488a7e3b7" 11 | }, 12 | 13 | "Logging": { 14 | "EventLog": { 15 | "LogLevel": { 16 | "Default": "Debug", 17 | "System": "Debug", 18 | "Grpc": "Debug", 19 | "Microsoft": "Debug" 20 | } 21 | }, 22 | "LogLevel": { 23 | "Default": "Debug", 24 | "System": "Information", 25 | "Grpc": "Information", 26 | "Microsoft": "Information" 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/RemoteWebViewService/start.bat: -------------------------------------------------------------------------------- 1 | RemoteWebViewService.exe https://localhost:443 2 | pause -------------------------------------------------------------------------------- /src/SharedSource/AutoCloseOnReadCompleteStream.cs: -------------------------------------------------------------------------------- 1 | #if WEBVIEW2_WINFORMS || WEBVIEW2_WPF 2 | 3 | using System.IO; 4 | 5 | namespace PeakSWC.RemoteBlazorWebView 6 | { 7 | internal class AutoCloseOnReadCompleteStream : Stream 8 | { 9 | private readonly Stream _baseStream; 10 | 11 | public AutoCloseOnReadCompleteStream(Stream baseStream) 12 | { 13 | _baseStream = baseStream; 14 | } 15 | 16 | public override bool CanRead => _baseStream.CanRead; 17 | 18 | public override bool CanSeek => _baseStream.CanSeek; 19 | 20 | public override bool CanWrite => _baseStream.CanWrite; 21 | 22 | public override long Length => _baseStream.Length; 23 | 24 | public override long Position { get => _baseStream.Position; set => _baseStream.Position = value; } 25 | 26 | public override void Flush() => _baseStream.Flush(); 27 | 28 | public override int Read(byte[] buffer, int offset, int count) 29 | { 30 | var bytesRead = _baseStream.Read(buffer, offset, count); 31 | 32 | // Stream.Read only returns 0 when it has reached the end of stream 33 | // and no further bytes are expected. Otherwise it blocks until 34 | // one or more (and at most count) bytes can be read. 35 | if (bytesRead == 0) 36 | { 37 | _baseStream.Close(); 38 | } 39 | 40 | return bytesRead; 41 | } 42 | 43 | public override long Seek(long offset, SeekOrigin origin) => _baseStream.Seek(offset, origin); 44 | 45 | public override void SetLength(long value) => _baseStream.SetLength(value); 46 | 47 | public override void Write(byte[] buffer, int offset, int count) => _baseStream.Write(buffer, offset, count); 48 | } 49 | } 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /src/SharedSource/BlazorWebViewDeveloperTools.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #if WEBVIEW2_WINFORMS 4 | namespace PeakSWC.RemoteBlazorWebView.WindowsForms 5 | #elif WEBVIEW2_WPF 6 | namespace PeakSWC.RemoteBlazorWebView.Wpf 7 | #elif WEBVIEW2_MAUI 8 | namespace PeakSWC.RemoteBlazorWebView.Maui 9 | #else 10 | #error Must define WEBVIEW2_WINFORMS, WEBVIEW2_WPF, WEBVIEW2_MAUI 11 | #endif 12 | { 13 | public class BlazorWebViewDeveloperTools 14 | { 15 | public bool Enabled { get; set; } = false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/SharedSource/BlazorWebViewInitializedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | #if WEBVIEW2_WINFORMS 4 | using Microsoft.Web.WebView2.Core; 5 | using WebView2Control = Microsoft.Web.WebView2.WinForms.WebView2; 6 | #elif WEBVIEW2_WPF 7 | using Microsoft.Web.WebView2.Core; 8 | using WebView2Control = Microsoft.Web.WebView2.Wpf.WebView2; 9 | #elif WINDOWS && WEBVIEW2_MAUI 10 | using Microsoft.Web.WebView2.Core; 11 | using WebView2Control = Microsoft.UI.Xaml.Controls.WebView2; 12 | #elif ANDROID 13 | using AWebView = Android.Webkit.WebView; 14 | #elif IOS || MACCATALYST 15 | using WebKit; 16 | #elif TIZEN 17 | using TWebView = Tizen.NUI.BaseComponents.WebView; 18 | #endif 19 | 20 | namespace PeakSWC.RemoteBlazorWebView 21 | { 22 | /// 23 | /// Allows configuring the underlying web view after it has been initialized. 24 | /// 25 | public class BlazorWebViewInitializedEventArgs : EventArgs 26 | { 27 | #nullable disable 28 | #if WINDOWS 29 | /// 30 | /// Gets the instance that was initialized. 31 | /// 32 | public WebView2Control WebView { get; internal set; } 33 | #elif ANDROID 34 | /// 35 | /// Gets the instance that was initialized. 36 | /// 37 | public AWebView WebView { get; internal set; } 38 | #elif MACCATALYST || IOS 39 | /// 40 | /// Gets the instance that was initialized. 41 | /// the default values to allow further configuring additional options. 42 | /// 43 | public WKWebView WebView { get; internal set; } 44 | #elif TIZEN 45 | /// 46 | /// Gets the instance that was initialized. 47 | /// 48 | public TWebView WebView { get; internal set; } 49 | #endif 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/SharedSource/BlazorWebViewInitializingEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | #if WEBVIEW2_WINFORMS 3 | using Microsoft.Web.WebView2.Core; 4 | using WebView2Control = Microsoft.Web.WebView2.WinForms.WebView2; 5 | #elif WEBVIEW2_WPF 6 | using Microsoft.Web.WebView2.Core; 7 | using WebView2Control = Microsoft.Web.WebView2.Wpf.WebView2; 8 | #elif WINDOWS && WEBVIEW2_MAUI 9 | using Microsoft.Web.WebView2.Core; 10 | using WebView2Control = Microsoft.UI.Xaml.Controls.WebView2; 11 | #elif ANDROID 12 | using AWebView = Android.Webkit.WebView; 13 | #elif IOS || MACCATALYST 14 | using WebKit; 15 | #elif TIZEN 16 | using TWebView = Tizen.WebView.WebView; 17 | #endif 18 | 19 | namespace PeakSWC.RemoteBlazorWebView 20 | { 21 | /// 22 | /// Allows configuring the underlying web view when the application is initializing. 23 | /// 24 | public class BlazorWebViewInitializingEventArgs : EventArgs 25 | { 26 | #nullable disable 27 | #if WINDOWS 28 | /// 29 | /// Gets or sets the browser executable folder path for the . 30 | /// 31 | public string BrowserExecutableFolder { get; set; } 32 | 33 | /// 34 | /// Gets or sets the user data folder path for the . 35 | /// 36 | public string UserDataFolder { get; set; } 37 | 38 | /// 39 | /// Gets or sets the environment options for the . 40 | /// 41 | public CoreWebView2EnvironmentOptions EnvironmentOptions { get; set; } 42 | 43 | #elif MACCATALYST || IOS 44 | /// 45 | /// Gets or sets the web view . 46 | /// 47 | public WKWebViewConfiguration Configuration { get; set; } 48 | #endif 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/SharedSource/HostAddressHelper.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 | 6 | namespace PeakSWC.RemoteBlazorWebView; 7 | 8 | internal static class HostAddressHelper 9 | { 10 | private const string AppHostAddressAlways0000Switch = "BlazorWebView.AppHostAddressAlways0000"; 11 | 12 | private static bool IsAppHostAddressAlways0000Enabled => 13 | AppContext.TryGetSwitch(AppHostAddressAlways0000Switch, out var enabled) && enabled; 14 | 15 | public static string GetAppHostAddress() 16 | => IsAppHostAddressAlways0000Enabled 17 | ? "0.0.0.0" 18 | : "0.0.0.1"; 19 | } 20 | -------------------------------------------------------------------------------- /src/SharedSource/QueryStringHelper.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 | #nullable enable 5 | 6 | using System; 7 | 8 | namespace PeakSWC.RemoteBlazorWebView 9 | { 10 | internal static class QueryStringHelper 11 | { 12 | public static string RemovePossibleQueryString(string? url) 13 | { 14 | if (string.IsNullOrEmpty(url)) 15 | { 16 | return string.Empty; 17 | } 18 | var indexOfQueryString = url.IndexOf('?', StringComparison.Ordinal); 19 | return (indexOfQueryString == -1) 20 | ? url 21 | : url.Substring(0, indexOfQueryString); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/SharedSource/UrlLoadingEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PeakSWC.RemoteBlazorWebView 4 | { 5 | /// 6 | /// Used to provide information about a link (]]>) clicked within a Blazor WebView. 7 | /// 8 | /// Anchor tags with target="_blank" will always open in the default 9 | /// browser and the UrlLoading event won't be called. 10 | /// 11 | /// 12 | public class UrlLoadingEventArgs : EventArgs 13 | { 14 | public static UrlLoadingEventArgs CreateWithDefaultLoadingStrategy(Uri urlToLoad, Uri appOriginUri) 15 | { 16 | var split = urlToLoad.AbsolutePath.Split('/'); 17 | var isMirrorUrl = split.Length == 3 && split[1] == "mirror" && Guid.TryParse(split[2], out Guid _); 18 | var strategy = (appOriginUri.IsBaseOf(urlToLoad) || urlToLoad.Scheme == "data" || isMirrorUrl) ? 19 | UrlLoadingStrategy.OpenInWebView : 20 | UrlLoadingStrategy.OpenExternally; 21 | 22 | return new(urlToLoad, strategy); 23 | } 24 | 25 | private UrlLoadingEventArgs(Uri url, UrlLoadingStrategy urlLoadingStrategy) 26 | { 27 | Url = url; 28 | UrlLoadingStrategy = urlLoadingStrategy; 29 | } 30 | 31 | /// 32 | /// Gets the URL to be loaded. 33 | /// 34 | public Uri Url { get; } 35 | 36 | /// 37 | /// The policy to use when loading links from the webview. 38 | /// Defaults to unless has a host 39 | /// matching the app origin, in which case the default becomes . 40 | /// 41 | /// This value should not be changed to for external links 42 | /// unless you can ensure they are fully trusted. 43 | /// 44 | /// 45 | public UrlLoadingStrategy UrlLoadingStrategy { get; set; } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/SharedSource/UrlLoadingStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace PeakSWC.RemoteBlazorWebView 2 | { 3 | /// 4 | /// URL loading strategy for anchor tags ]]> within a Blazor WebView. 5 | /// 6 | /// Anchor tags with target="_blank" will always open in the default 7 | /// browser and the UrlLoading event won't be called. 8 | /// 9 | public enum UrlLoadingStrategy 10 | { 11 | /// 12 | /// Allows loading URLs using an app determined by the system. 13 | /// This is the default strategy for URLs with an external host. 14 | /// 15 | OpenExternally, 16 | 17 | /// 18 | /// Allows loading URLs within the Blazor WebView. 19 | /// This is the default strategy for URLs with a host matching the app origin. 20 | /// 21 | /// This strategy should not be used for external links unless you can ensure they are fully trusted. 22 | /// 23 | /// 24 | OpenInWebView, 25 | 26 | /// 27 | /// Cancels the current URL loading attempt. 28 | /// 29 | CancelLoad 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/node_modules/.yarn-integrity: -------------------------------------------------------------------------------- 1 | { 2 | "systemParams": "win32-x64-127", 3 | "modulesFolders": [], 4 | "flags": [], 5 | "linkedModules": [], 6 | "topLevelPatterns": [], 7 | "lockfileEntries": {}, 8 | "files": [], 9 | "artifacts": {} 10 | } -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | [assembly: CollectionBehavior(DisableTestParallelization = true)] 4 | -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/ClientCachingCollection.cs: -------------------------------------------------------------------------------- 1 | [CollectionDefinition("Client caching collection", DisableParallelization = true)] 2 | public class ClientCachingCollection : ICollectionFixture, ICollectionFixture 3 | { 4 | // This class has no code, and is never created. Its purpose is simply 5 | // to be the place to apply [CollectionDefinition] and all the 6 | // ICollectionFixture<> interfaces. 7 | } 8 | -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/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 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "", Scope = "member", Target = "~M:FileSyncServer.Utilities.ModifyFilePermissions(System.String,System.String,System.Boolean,System.Boolean)")] 9 | [assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "", Scope = "member", Target = "~F:FileSyncServer.Tests.ClientCachingTests._currentUser")] 10 | [assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "", Scope = "member", Target = "~F:FileSyncServer.Tests.ServerCachingTests._currentUser")] 11 | -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/Home.razor: -------------------------------------------------------------------------------- 1 | @using System.Threading.Tasks 2 | 3 | Hello World 4 | 5 | -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/Home.razor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Components; 5 | using System.Threading.Tasks; 6 | 7 | namespace WebdriverTestProject 8 | { 9 | public partial class Home : ComponentBase 10 | { 11 | protected async override Task OnInitializedAsync() 12 | { 13 | await Task.Delay(11); 14 | } 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/LargeFileSetup.cs: -------------------------------------------------------------------------------- 1 | public class LargeFileSetup 2 | { 3 | public static void EnsureLargeFileExists(string filePath, long sizeInBytes) 4 | { 5 | if (!File.Exists(filePath)) 6 | { 7 | using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None); 8 | fs.SetLength(sizeInBytes); 9 | // Optionally, write random data 10 | // byte[] data = new byte[8192]; 11 | // new Random().NextBytes(data); 12 | // for (long i = 0; i < sizeInBytes; i += data.Length) 13 | // { 14 | // fs.Write(data, 0, (int)Math.Min(data.Length, sizeInBytes - i)); 15 | // } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/PerformanceCollection.cs: -------------------------------------------------------------------------------- 1 | // PerformanceCollection.cs 2 | [CollectionDefinition("Performance collection", DisableParallelization = true)] 3 | public class PerformanceCollection : ICollectionFixture 4 | { 5 | // No additional code needed 6 | } -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/Remote/BaseTestRemote.cs: -------------------------------------------------------------------------------- 1 | // TestRemoteBlazorWpf.cs 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace WebdriverTestProject 7 | { 8 | [Collection("RemoteBlazorWpf Collection")] 9 | public abstract class BaseTestRemote(ITestOutputHelper output) : IAsyncLifetime where T : BaseTestRemoteFixture, new() 10 | { 11 | protected readonly T _fixture = new T(); 12 | protected readonly ITestOutputHelper Output = output; 13 | 14 | [Fact] 15 | public async virtual Task Test2Client5Refresh() 16 | { 17 | await _fixture.TestRefresh(2, 5); 18 | } 19 | 20 | [Fact] 21 | public async virtual Task Test1Client() 22 | { 23 | await _fixture.TestClient(1); 24 | } 25 | 26 | [Fact] 27 | public async virtual Task Test2Client() 28 | { 29 | await _fixture.TestClient(2); 30 | } 31 | 32 | [Fact] 33 | public async virtual Task Test5Client() 34 | { 35 | await _fixture.TestClient(5); 36 | } 37 | 38 | public virtual async Task InitializeAsync() 39 | { 40 | await _fixture.InitializeAsync(); 41 | } 42 | 43 | public async Task DisposeAsync() 44 | { 45 | await _fixture.DisposeAsync(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/Remote/TestServerForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Xunit.Abstractions; 8 | 9 | namespace WebdriverTestProject 10 | { 11 | public class TestServerForm(ITestOutputHelper output) : TestRemoteBlazorForm(output) 12 | { 13 | private readonly int BYTES_READ = 976076; 14 | private readonly int FILES_READ = 25; 15 | 16 | public override async Task Test2Client5Refresh() { await Task.CompletedTask; } 17 | 18 | [Fact] 19 | public override async Task Test1Client() 20 | { 21 | await _fixture.TestClient(1); 22 | await _fixture.VerifyServerStats(1, FILES_READ, BYTES_READ); 23 | await _fixture.VerifyDisconnect(1, false); 24 | } 25 | 26 | [Fact] 27 | public override async Task Test2Client() 28 | { 29 | await _fixture.TestClient(2); 30 | await _fixture.VerifyServerStats(2, FILES_READ, BYTES_READ); 31 | await _fixture.VerifyDisconnect(2, false); 32 | 33 | } 34 | 35 | [Fact] 36 | public override async Task Test5Client() 37 | { 38 | await _fixture.TestClient(5); 39 | await _fixture.VerifyServerStats(5,FILES_READ, BYTES_READ); 40 | await _fixture.VerifyDisconnect(5, false); 41 | 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/Remote/TestServerWpf.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Xunit.Abstractions; 8 | 9 | namespace WebdriverTestProject 10 | { 11 | public class TestServerWpf(ITestOutputHelper output) : TestRemoteBlazorWpf(output) 12 | { 13 | private readonly int BYTES_READ = 976500; 14 | private readonly int FILES_READ = 29; 15 | public override async Task Test2Client5Refresh() { await Task.CompletedTask; } 16 | 17 | [Fact] 18 | public override async Task Test1Client() 19 | { 20 | await _fixture.TestClient(1); 21 | await _fixture.VerifyServerStats(1, FILES_READ, BYTES_READ); 22 | await _fixture.VerifyDisconnect(1, true); 23 | } 24 | 25 | [Fact] 26 | public override async Task Test2Client() 27 | { 28 | await _fixture.TestClient(2); 29 | await _fixture.VerifyServerStats(2, FILES_READ, BYTES_READ); 30 | await _fixture.VerifyDisconnect(2, true); 31 | } 32 | 33 | [Fact] 34 | public override async Task Test5Client() 35 | { 36 | await _fixture.TestClient(5); 37 | await _fixture.VerifyServerStats(5,FILES_READ, BYTES_READ); 38 | await _fixture.VerifyDisconnect(5, true); 39 | 40 | } 41 | 42 | [Fact] 43 | public async Task Test10Client() 44 | { 45 | await _fixture.TestClient(10); 46 | await _fixture.VerifyServerStats(10, FILES_READ, BYTES_READ); 47 | await _fixture.VerifyDisconnect(10, true); 48 | 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/ServerCachingCollection.cs: -------------------------------------------------------------------------------- 1 | [CollectionDefinition("Server caching collection", DisableParallelization = true)] 2 | public class ServerCachingCollection : ICollectionFixture, ICollectionFixture 3 | { 4 | // This class has no code, and is never created. Its purpose is simply 5 | // to be the place to apply [CollectionDefinition] and all the 6 | // ICollectionFixture<> interfaces. 7 | } 8 | -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/ServerCollection.cs: -------------------------------------------------------------------------------- 1 | [CollectionDefinition("Server collection", DisableParallelization = true)] 2 | public class ServerCollection : ICollectionFixture, ICollectionFixture 3 | { 4 | // This class has no code, and is never created. Its purpose is simply 5 | // to be the place to apply [CollectionDefinition] and all the 6 | // ICollectionFixture<> interfaces. 7 | } 8 | -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/TestMisc.cs: -------------------------------------------------------------------------------- 1 | 2 | using Xunit.Abstractions; 3 | using WebdriverTestProject; 4 | 5 | namespace FileSyncServer.Tests 6 | { 7 | public class TestMisc 8 | { 9 | private readonly ITestOutputHelper _output; 10 | 11 | // Constructor to inject ITestOutputHelper 12 | public TestMisc(ITestOutputHelper output) 13 | { 14 | _output = output; 15 | } 16 | 17 | [Fact] 18 | public void TestJavascriptCompiledForRelease() 19 | { 20 | // Assuming Utilities.JavascriptFile is a valid file path 21 | FileInfo fi = new FileInfo(Utilities.JavascriptFile); 22 | var max = 402 * 1024; // 402 KB in bytes 23 | 24 | // Assert that the file size is less than the maximum allowed size 25 | Assert.True(fi.Length < max, $"{Path.GetFileName(Utilities.JavascriptFile)} >= {max} bytes"); 26 | 27 | // Output the file size in kilobytes 28 | _output.WriteLine($"{Path.GetFileName(Utilities.JavascriptFile)} is {fi.Length / 1024} KB long"); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | #blazor-error-ui { 2 | background: lightyellow; 3 | bottom: 0; 4 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 5 | display: none; 6 | left: 0; 7 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 8 | position: fixed; 9 | width: 100%; 10 | z-index: 1000; 11 | } 12 | 13 | #blazor-error-ui .dismiss { 14 | cursor: pointer; 15 | position: absolute; 16 | right: 0.75rem; 17 | top: 0.5rem; 18 | } 19 | -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | /*@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');*/ 2 | 3 | .spinner { 4 | border: 16px solid silver; 5 | border-top: 16px solid #337AB7; 6 | border-radius: 50%; 7 | width: 80px; 8 | height: 80px; 9 | animation: spin 700ms linear infinite; 10 | top: 40%; 11 | left: 55%; 12 | position: absolute; 13 | } 14 | 15 | @keyframes spin { 16 | 0% { 17 | transform: rotate(0deg) 18 | } 19 | 20 | 100% { 21 | transform: rotate(360deg) 22 | } 23 | } 24 | 25 | html, body { 26 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 27 | } 28 | 29 | a, .btn-link { 30 | color: #0366d6; 31 | } 32 | 33 | .btn-primary { 34 | color: #fff; 35 | background-color: #1b6ec2; 36 | border-color: #1861ac; 37 | } 38 | 39 | .content { 40 | padding-top: 1.1rem; 41 | } 42 | 43 | .valid.modified:not([type=checkbox]) { 44 | outline: 1px solid #26b050; 45 | } 46 | 47 | .invalid { 48 | outline: 1px solid red; 49 | } 50 | 51 | .validation-message { 52 | color: red; 53 | } 54 | 55 | #blazor-error-ui { 56 | background: lightyellow; 57 | bottom: 0; 58 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 59 | display: none; 60 | left: 0; 61 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 62 | position: fixed; 63 | width: 100%; 64 | z-index: 1000; 65 | } 66 | 67 | #blazor-error-ui .dismiss { 68 | cursor: pointer; 69 | position: absolute; 70 | right: 0.75rem; 71 | top: 0.5rem; 72 | } 73 | -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/budcribar/RemoteBlazorWebView/599a5226fa1055722c3c6db4ea272a19e850b590/test/FileSyncServer.Tests/wwwroot/favicon.ico -------------------------------------------------------------------------------- /test/FileSyncServer.Tests/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RemoteBlazorWebViewWpfTutorial 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Loading... 17 | 18 | 19 | An unhandled error has occurred. 20 | Reload 21 | 🗙 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | --------------------------------------------------------------------------------