From 16d911588095ffd6368a0f7435eea2981b2644b7 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 10 Feb 2021 11:17:46 -0800 Subject: [PATCH 01/92] Initial project structure --- AspNetCore.sln | 57 +++++++++++++++++++ src/Components/ComponentsNoDeps.slnf | 5 +- ...NetCore.Components.WebView.Headless.csproj | 23 ++++++++ ...osoft.AspNetCore.Components.WebView.csproj | 19 +++++++ .../BlazorHeadlessSample.csproj | 23 ++++++++ .../Samples/BlazorHeadlessSample/Program.cs | 12 ++++ 6 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 src/Components/WebView/Headless/src/Microsoft.AspNetCore.Components.WebView.Headless.csproj create mode 100644 src/Components/WebView/Hosting/src/Microsoft.AspNetCore.Components.WebView.csproj create mode 100644 src/Components/WebView/Samples/BlazorHeadlessSample/BlazorHeadlessSample.csproj create mode 100644 src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index d6480d77016c..d02f1d6736e6 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1588,6 +1588,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostedBlazorWebassemblyApp. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostedBlazorWebassemblyApp.Shared", "src\Components\WebAssembly\Samples\HostedBlazorWebassemblyApp\Shared\HostedBlazorWebassemblyApp.Shared.csproj", "{E18EF144-9C2C-4366-B54C-09ACF7692A4F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebView", "WebView", "{DDECDC13-D488-4257-BFBD-9DC668E580BE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{A95597AD-E9EF-451B-AB84-E97D83C0F897}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.WebView.Headless", "src\Components\WebView\Headless\src\Microsoft.AspNetCore.Components.WebView.Headless.csproj", "{59A3C129-0A59-4C9D-A841-227F655A49B6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosting", "Hosting", "{C0C49345-5908-48BE-8F82-7A8A62C00088}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.WebView", "src\Components\WebView\Hosting\src\Microsoft.AspNetCore.Components.WebView.csproj", "{ED278AA3-5CBC-4609-A578-2C19BA756C40}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{D71E7638-80CA-46B7-91E1-4E1F502808A8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorHeadlessSample", "src\Components\WebView\Samples\BlazorHeadlessSample\BlazorHeadlessSample.csproj", "{B1B97932-1452-4A73-8122-4F0799C96322}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -7527,6 +7541,42 @@ Global {E18EF144-9C2C-4366-B54C-09ACF7692A4F}.Release|x64.Build.0 = Release|Any CPU {E18EF144-9C2C-4366-B54C-09ACF7692A4F}.Release|x86.ActiveCfg = Release|Any CPU {E18EF144-9C2C-4366-B54C-09ACF7692A4F}.Release|x86.Build.0 = Release|Any CPU + {59A3C129-0A59-4C9D-A841-227F655A49B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59A3C129-0A59-4C9D-A841-227F655A49B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59A3C129-0A59-4C9D-A841-227F655A49B6}.Debug|x64.ActiveCfg = Debug|Any CPU + {59A3C129-0A59-4C9D-A841-227F655A49B6}.Debug|x64.Build.0 = Debug|Any CPU + {59A3C129-0A59-4C9D-A841-227F655A49B6}.Debug|x86.ActiveCfg = Debug|Any CPU + {59A3C129-0A59-4C9D-A841-227F655A49B6}.Debug|x86.Build.0 = Debug|Any CPU + {59A3C129-0A59-4C9D-A841-227F655A49B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59A3C129-0A59-4C9D-A841-227F655A49B6}.Release|Any CPU.Build.0 = Release|Any CPU + {59A3C129-0A59-4C9D-A841-227F655A49B6}.Release|x64.ActiveCfg = Release|Any CPU + {59A3C129-0A59-4C9D-A841-227F655A49B6}.Release|x64.Build.0 = Release|Any CPU + {59A3C129-0A59-4C9D-A841-227F655A49B6}.Release|x86.ActiveCfg = Release|Any CPU + {59A3C129-0A59-4C9D-A841-227F655A49B6}.Release|x86.Build.0 = Release|Any CPU + {ED278AA3-5CBC-4609-A578-2C19BA756C40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED278AA3-5CBC-4609-A578-2C19BA756C40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED278AA3-5CBC-4609-A578-2C19BA756C40}.Debug|x64.ActiveCfg = Debug|Any CPU + {ED278AA3-5CBC-4609-A578-2C19BA756C40}.Debug|x64.Build.0 = Debug|Any CPU + {ED278AA3-5CBC-4609-A578-2C19BA756C40}.Debug|x86.ActiveCfg = Debug|Any CPU + {ED278AA3-5CBC-4609-A578-2C19BA756C40}.Debug|x86.Build.0 = Debug|Any CPU + {ED278AA3-5CBC-4609-A578-2C19BA756C40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED278AA3-5CBC-4609-A578-2C19BA756C40}.Release|Any CPU.Build.0 = Release|Any CPU + {ED278AA3-5CBC-4609-A578-2C19BA756C40}.Release|x64.ActiveCfg = Release|Any CPU + {ED278AA3-5CBC-4609-A578-2C19BA756C40}.Release|x64.Build.0 = Release|Any CPU + {ED278AA3-5CBC-4609-A578-2C19BA756C40}.Release|x86.ActiveCfg = Release|Any CPU + {ED278AA3-5CBC-4609-A578-2C19BA756C40}.Release|x86.Build.0 = Release|Any CPU + {B1B97932-1452-4A73-8122-4F0799C96322}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1B97932-1452-4A73-8122-4F0799C96322}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1B97932-1452-4A73-8122-4F0799C96322}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1B97932-1452-4A73-8122-4F0799C96322}.Debug|x64.Build.0 = Debug|Any CPU + {B1B97932-1452-4A73-8122-4F0799C96322}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1B97932-1452-4A73-8122-4F0799C96322}.Debug|x86.Build.0 = Debug|Any CPU + {B1B97932-1452-4A73-8122-4F0799C96322}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1B97932-1452-4A73-8122-4F0799C96322}.Release|Any CPU.Build.0 = Release|Any CPU + {B1B97932-1452-4A73-8122-4F0799C96322}.Release|x64.ActiveCfg = Release|Any CPU + {B1B97932-1452-4A73-8122-4F0799C96322}.Release|x64.Build.0 = Release|Any CPU + {B1B97932-1452-4A73-8122-4F0799C96322}.Release|x86.ActiveCfg = Release|Any CPU + {B1B97932-1452-4A73-8122-4F0799C96322}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -8311,6 +8361,13 @@ Global {8F6F73F7-0DDA-4AA3-9887-2FB0141786AC} = {B4226BE2-DCB7-40C5-93F2-94C9BD6F4394} {1A5582DD-06F4-4427-BFDC-B021A84A01BC} = {B4226BE2-DCB7-40C5-93F2-94C9BD6F4394} {E18EF144-9C2C-4366-B54C-09ACF7692A4F} = {B4226BE2-DCB7-40C5-93F2-94C9BD6F4394} + {DDECDC13-D488-4257-BFBD-9DC668E580BE} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF} + {A95597AD-E9EF-451B-AB84-E97D83C0F897} = {DDECDC13-D488-4257-BFBD-9DC668E580BE} + {59A3C129-0A59-4C9D-A841-227F655A49B6} = {A95597AD-E9EF-451B-AB84-E97D83C0F897} + {C0C49345-5908-48BE-8F82-7A8A62C00088} = {DDECDC13-D488-4257-BFBD-9DC668E580BE} + {ED278AA3-5CBC-4609-A578-2C19BA756C40} = {C0C49345-5908-48BE-8F82-7A8A62C00088} + {D71E7638-80CA-46B7-91E1-4E1F502808A8} = {DDECDC13-D488-4257-BFBD-9DC668E580BE} + {B1B97932-1452-4A73-8122-4F0799C96322} = {D71E7638-80CA-46B7-91E1-4E1F502808A8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/src/Components/ComponentsNoDeps.slnf b/src/Components/ComponentsNoDeps.slnf index b89704c487d0..0cc6c0538e65 100644 --- a/src/Components/ComponentsNoDeps.slnf +++ b/src/Components/ComponentsNoDeps.slnf @@ -37,6 +37,9 @@ "src\\Components\\WebAssembly\\testassets\\Wasm.Authentication.Server\\Wasm.Authentication.Server.csproj", "src\\Components\\WebAssembly\\testassets\\Wasm.Authentication.Shared\\Wasm.Authentication.Shared.csproj", "src\\Components\\WebAssembly\\testassets\\WasmLinkerTest\\WasmLinkerTest.csproj", + "src\\Components\\WebView\\Headless\\src\\Microsoft.AspNetCore.Components.WebView.Headless.csproj", + "src\\Components\\WebView\\Hosting\\src\\Microsoft.AspNetCore.Components.WebView.csproj", + "src\\Components\\WebView\\Samples\\BlazorHeadlessSample\\BlazorHeadlessSample.csproj", "src\\Components\\Web\\src\\Microsoft.AspNetCore.Components.Web.csproj", "src\\Components\\Web\\test\\Microsoft.AspNetCore.Components.Web.Tests.csproj", "src\\Components\\benchmarkapps\\Wasm.Performance\\ConsoleHost\\Wasm.Performance.ConsoleHost.csproj", @@ -50,4 +53,4 @@ "src\\Components\\test\\testassets\\TestServer\\Components.TestServer.csproj" ] } -} \ No newline at end of file +} diff --git a/src/Components/WebView/Headless/src/Microsoft.AspNetCore.Components.WebView.Headless.csproj b/src/Components/WebView/Headless/src/Microsoft.AspNetCore.Components.WebView.Headless.csproj new file mode 100644 index 000000000000..297eac62d27b --- /dev/null +++ b/src/Components/WebView/Headless/src/Microsoft.AspNetCore.Components.WebView.Headless.csproj @@ -0,0 +1,23 @@ + + + + $(DefaultNetCoreTargetFramework) + Test host for Blazor desktop applications. + false + $(NoWarn);BL0006 + true + enable + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Components/WebView/Hosting/src/Microsoft.AspNetCore.Components.WebView.csproj b/src/Components/WebView/Hosting/src/Microsoft.AspNetCore.Components.WebView.csproj new file mode 100644 index 000000000000..d3669e48ef1f --- /dev/null +++ b/src/Components/WebView/Hosting/src/Microsoft.AspNetCore.Components.WebView.csproj @@ -0,0 +1,19 @@ + + + + $(DefaultNetCoreTargetFramework) + Build desktop applications with WebView and Blazor. + true + $(NoWarn);BL0006 + true + enable + + + + + + + + + + \ No newline at end of file diff --git a/src/Components/WebView/Samples/BlazorHeadlessSample/BlazorHeadlessSample.csproj b/src/Components/WebView/Samples/BlazorHeadlessSample/BlazorHeadlessSample.csproj new file mode 100644 index 000000000000..276e1b4ca408 --- /dev/null +++ b/src/Components/WebView/Samples/BlazorHeadlessSample/BlazorHeadlessSample.csproj @@ -0,0 +1,23 @@ + + + + $(DefaultNetCoreTargetFramework) + Sample blazor headless application. + false + $(NoWarn);BL0006 + true + enable + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs b/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs new file mode 100644 index 000000000000..91064fdda66e --- /dev/null +++ b/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace BlazorHeadlessSample +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} From b7c3420286b63c5e84f2cc2e577fe2b30fac4db1 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Fri, 12 Feb 2021 03:32:23 -0800 Subject: [PATCH 02/92] tmp --- .../Samples/BlazorHeadlessSample/Program.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs b/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs index 91064fdda66e..a0244281c3aa 100644 --- a/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs +++ b/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs @@ -1,12 +1,14 @@ -using System; - -namespace BlazorHeadlessSample -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello World!"); - } - } -} + +using Microsoft.AspNetCore.Components.WebView.Hosting; + +// var builder = WebViewHostBuilder.CreateDefault(); + +var host = builder.Build(); + +var renderClient = new ConsoleRenderClient(); + +host.AttachRenderClient(renderClient); + +// renderClient.AddComponent("#app"); + +host.StartAsync() From 869718a2b0f512a966f36303f43c10b9a77c7949 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Mon, 22 Feb 2021 03:56:31 -0800 Subject: [PATCH 03/92] tmp --- eng/ProjectReferences.props | 2 ++ .../WebView/Hosting/src/PublicAPI.Shipped.txt | 1 + .../Hosting/src/PublicAPI.Unshipped.txt | 4 +++ .../WebView/Hosting/src/WebViewHost.cs | 9 +++++++ .../WebView/Hosting/src/WebViewHostBuilder.cs | 27 +++++++++++++++++++ .../Samples/BlazorHeadlessSample/Program.cs | 4 +-- 6 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 src/Components/WebView/Hosting/src/PublicAPI.Shipped.txt create mode 100644 src/Components/WebView/Hosting/src/PublicAPI.Unshipped.txt create mode 100644 src/Components/WebView/Hosting/src/WebViewHost.cs create mode 100644 src/Components/WebView/Hosting/src/WebViewHostBuilder.cs diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 541a8559fb60..328255d25935 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -147,6 +147,8 @@ + + diff --git a/src/Components/WebView/Hosting/src/PublicAPI.Shipped.txt b/src/Components/WebView/Hosting/src/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..ab058de62d44 --- /dev/null +++ b/src/Components/WebView/Hosting/src/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Components/WebView/Hosting/src/PublicAPI.Unshipped.txt b/src/Components/WebView/Hosting/src/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..42a3f9a81d47 --- /dev/null +++ b/src/Components/WebView/Hosting/src/PublicAPI.Unshipped.txt @@ -0,0 +1,4 @@ +Microsoft.AspNetCore.Components.WebView.Hosting.WebViewHostBuilder +Microsoft.AspNetCore.Components.WebView.Hosting.WebViewHostBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +Microsoft.AspNetCore.Components.WebView.Hosting.WebViewHostBuilder.WebViewHostBuilder() -> void +static Microsoft.AspNetCore.Components.WebView.Hosting.WebViewHostBuilder.CreateDefault(string![]? args = null) -> Microsoft.AspNetCore.Components.WebView.Hosting.WebViewHostBuilder! \ No newline at end of file diff --git a/src/Components/WebView/Hosting/src/WebViewHost.cs b/src/Components/WebView/Hosting/src/WebViewHost.cs new file mode 100644 index 000000000000..f0d8ac55d190 --- /dev/null +++ b/src/Components/WebView/Hosting/src/WebViewHost.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Components.WebView.Hosting +{ + public class WebViewHost + { + } +} diff --git a/src/Components/WebView/Hosting/src/WebViewHostBuilder.cs b/src/Components/WebView/Hosting/src/WebViewHostBuilder.cs new file mode 100644 index 000000000000..b1a019f6874e --- /dev/null +++ b/src/Components/WebView/Hosting/src/WebViewHostBuilder.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Components.WebView.Hosting +{ + public class WebViewHostBuilder + { + public IServiceCollection Services { get; } = new ServiceCollection(); + + public static WebViewHostBuilder CreateDefault(string[]? args = null) + { + return new WebViewHostBuilder(); + } + + public WebViewHost Build() + { + return new WebViewHost(); + } + } +} diff --git a/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs b/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs index a0244281c3aa..a4f6e4af9876 100644 --- a/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs +++ b/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Components.WebView.Hosting; -// var builder = WebViewHostBuilder.CreateDefault(); +var builder = WebViewHostBuilder.CreateDefault(); var host = builder.Build(); @@ -9,6 +9,6 @@ host.AttachRenderClient(renderClient); -// renderClient.AddComponent("#app"); +renderClient.AddComponent("#app"); host.StartAsync() From 4a0705e78b095ceed968f9d168dcd1b05aec960b Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Tue, 23 Feb 2021 04:01:30 -0800 Subject: [PATCH 04/92] tmp --- AspNetCore.sln | 40 ++++----- .../WebView/Headless/src/ConsoleJsRuntime.cs | 21 +++++ .../WebView/Headless/src/ConsoleRenderer.cs | 47 ++++++++++ .../WebView/Headless/src/ConsoleWindow.cs | 89 +++++++++++++++++++ .../WebView/Headless/src/IContentProvider.cs | 6 ++ ...NetCore.Components.WebView.Headless.csproj | 4 +- .../WebView/Hosting/src/IRenderPort.cs | 17 ++++ ...osoft.AspNetCore.Components.WebView.csproj | 6 +- .../WebView/Hosting/src/WebViewHost.cs | 16 ++++ .../WebView/Hosting/src/WebViewHostBuilder.cs | 2 +- .../BlazorHeadlessSample.csproj | 10 ++- .../Samples/BlazorHeadlessSample/Program.cs | 10 +-- .../Properties/launchSettings.json | 27 ++++++ 13 files changed, 260 insertions(+), 35 deletions(-) create mode 100644 src/Components/WebView/Headless/src/ConsoleJsRuntime.cs create mode 100644 src/Components/WebView/Headless/src/ConsoleRenderer.cs create mode 100644 src/Components/WebView/Headless/src/ConsoleWindow.cs create mode 100644 src/Components/WebView/Headless/src/IContentProvider.cs create mode 100644 src/Components/WebView/Hosting/src/IRenderPort.cs create mode 100644 src/Components/WebView/Samples/BlazorHeadlessSample/Properties/launchSettings.json diff --git a/AspNetCore.sln b/AspNetCore.sln index d02f1d6736e6..eb2abbe1fd67 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1574,7 +1574,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Browse EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{722E5A66-D84A-4689-AA87-7197FF5D7070}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WasmLinkerTest", "src\Components\WebAssembly\testassets\WasmLinkerTest\WasmLinkerTest.csproj", "{3B375FFC-1E38-453E-A26D-A510CCD3339E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WasmLinkerTest", "src\Components\WebAssembly\testassets\WasmLinkerTest\WasmLinkerTest.csproj", "{3B375FFC-1E38-453E-A26D-A510CCD3339E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapActionSample", "src\Http\Routing\samples\MapActionSample\MapActionSample.csproj", "{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}" EndProject @@ -1582,25 +1582,25 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{7128 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HostedBlazorWebassemblyApp", "HostedBlazorWebassemblyApp", "{B4226BE2-DCB7-40C5-93F2-94C9BD6F4394}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostedBlazorWebassemblyApp.Client", "src\Components\WebAssembly\Samples\HostedBlazorWebassemblyApp\Client\HostedBlazorWebassemblyApp.Client.csproj", "{8F6F73F7-0DDA-4AA3-9887-2FB0141786AC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostedBlazorWebassemblyApp.Client", "src\Components\WebAssembly\Samples\HostedBlazorWebassemblyApp\Client\HostedBlazorWebassemblyApp.Client.csproj", "{8F6F73F7-0DDA-4AA3-9887-2FB0141786AC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostedBlazorWebassemblyApp.Server", "src\Components\WebAssembly\Samples\HostedBlazorWebassemblyApp\Server\HostedBlazorWebassemblyApp.Server.csproj", "{1A5582DD-06F4-4427-BFDC-B021A84A01BC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostedBlazorWebassemblyApp.Server", "src\Components\WebAssembly\Samples\HostedBlazorWebassemblyApp\Server\HostedBlazorWebassemblyApp.Server.csproj", "{1A5582DD-06F4-4427-BFDC-B021A84A01BC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostedBlazorWebassemblyApp.Shared", "src\Components\WebAssembly\Samples\HostedBlazorWebassemblyApp\Shared\HostedBlazorWebassemblyApp.Shared.csproj", "{E18EF144-9C2C-4366-B54C-09ACF7692A4F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostedBlazorWebassemblyApp.Shared", "src\Components\WebAssembly\Samples\HostedBlazorWebassemblyApp\Shared\HostedBlazorWebassemblyApp.Shared.csproj", "{E18EF144-9C2C-4366-B54C-09ACF7692A4F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebView", "WebView", "{DDECDC13-D488-4257-BFBD-9DC668E580BE}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{A95597AD-E9EF-451B-AB84-E97D83C0F897}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.WebView.Headless", "src\Components\WebView\Headless\src\Microsoft.AspNetCore.Components.WebView.Headless.csproj", "{59A3C129-0A59-4C9D-A841-227F655A49B6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.WebView.Headless", "src\Components\WebView\Headless\src\Microsoft.AspNetCore.Components.WebView.Headless.csproj", "{59A3C129-0A59-4C9D-A841-227F655A49B6}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosting", "Hosting", "{C0C49345-5908-48BE-8F82-7A8A62C00088}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.WebView", "src\Components\WebView\Hosting\src\Microsoft.AspNetCore.Components.WebView.csproj", "{ED278AA3-5CBC-4609-A578-2C19BA756C40}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.WebView", "src\Components\WebView\Hosting\src\Microsoft.AspNetCore.Components.WebView.csproj", "{ED278AA3-5CBC-4609-A578-2C19BA756C40}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{D71E7638-80CA-46B7-91E1-4E1F502808A8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorHeadlessSample", "src\Components\WebView\Samples\BlazorHeadlessSample\BlazorHeadlessSample.csproj", "{B1B97932-1452-4A73-8122-4F0799C96322}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorHeadlessSample", "src\Components\WebView\Samples\BlazorHeadlessSample\BlazorHeadlessSample.csproj", "{B1B97932-1452-4A73-8122-4F0799C96322}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -7481,6 +7481,18 @@ Global {B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|x64.Build.0 = Release|Any CPU {B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|x86.ActiveCfg = Release|Any CPU {B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|x86.Build.0 = Release|Any CPU + {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Debug|x64.ActiveCfg = Debug|Any CPU + {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Debug|x64.Build.0 = Debug|Any CPU + {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Debug|x86.ActiveCfg = Debug|Any CPU + {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Debug|x86.Build.0 = Debug|Any CPU + {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|Any CPU.Build.0 = Release|Any CPU + {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|x64.ActiveCfg = Release|Any CPU + {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|x64.Build.0 = Release|Any CPU + {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|x86.ActiveCfg = Release|Any CPU + {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|x86.Build.0 = Release|Any CPU {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|Any CPU.Build.0 = Debug|Any CPU {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -7505,18 +7517,6 @@ Global {8F6F73F7-0DDA-4AA3-9887-2FB0141786AC}.Release|x64.Build.0 = Release|Any CPU {8F6F73F7-0DDA-4AA3-9887-2FB0141786AC}.Release|x86.ActiveCfg = Release|Any CPU {8F6F73F7-0DDA-4AA3-9887-2FB0141786AC}.Release|x86.Build.0 = Release|Any CPU - {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Debug|x64.ActiveCfg = Debug|Any CPU - {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Debug|x64.Build.0 = Debug|Any CPU - {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Debug|x86.ActiveCfg = Debug|Any CPU - {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Debug|x86.Build.0 = Debug|Any CPU - {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|Any CPU.Build.0 = Release|Any CPU - {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|x64.ActiveCfg = Release|Any CPU - {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|x64.Build.0 = Release|Any CPU - {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|x86.ActiveCfg = Release|Any CPU - {3B375FFC-1E38-453E-A26D-A510CCD3339E}.Release|x86.Build.0 = Release|Any CPU {1A5582DD-06F4-4427-BFDC-B021A84A01BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A5582DD-06F4-4427-BFDC-B021A84A01BC}.Debug|Any CPU.Build.0 = Debug|Any CPU {1A5582DD-06F4-4427-BFDC-B021A84A01BC}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -8354,8 +8354,8 @@ Global {8F33439F-5532-45D6-8A44-20EF9104AA9D} = {5F0044F2-4C66-46A8-BD79-075F001AA034} {B739074E-6652-4F5B-B37E-775DC2245FEC} = {8F33439F-5532-45D6-8A44-20EF9104AA9D} {722E5A66-D84A-4689-AA87-7197FF5D7070} = {54C42F57-5447-4C21-9812-4AF665567566} - {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2} = {722E5A66-D84A-4689-AA87-7197FF5D7070} {3B375FFC-1E38-453E-A26D-A510CCD3339E} = {7D2B0799-A634-42AC-AE77-5D167BA51389} + {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2} = {722E5A66-D84A-4689-AA87-7197FF5D7070} {71287382-95EF-490D-A285-87196E29E88A} = {562D5067-8CD8-4F19-BCBB-873204932C61} {B4226BE2-DCB7-40C5-93F2-94C9BD6F4394} = {71287382-95EF-490D-A285-87196E29E88A} {8F6F73F7-0DDA-4AA3-9887-2FB0141786AC} = {B4226BE2-DCB7-40C5-93F2-94C9BD6F4394} diff --git a/src/Components/WebView/Headless/src/ConsoleJsRuntime.cs b/src/Components/WebView/Headless/src/ConsoleJsRuntime.cs new file mode 100644 index 000000000000..893106456a3b --- /dev/null +++ b/src/Components/WebView/Headless/src/ConsoleJsRuntime.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.JSInterop; +using Microsoft.JSInterop.Infrastructure; + +namespace Microsoft.AspNetCore.Components.WebView.Headless +{ + internal class ConsoleJsRuntime : JSRuntime + { + protected override void BeginInvokeJS(long taskId, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId) + { + throw new System.NotImplementedException(); + } + + protected override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/src/Components/WebView/Headless/src/ConsoleRenderer.cs b/src/Components/WebView/Headless/src/ConsoleRenderer.cs new file mode 100644 index 000000000000..3184e0258fe7 --- /dev/null +++ b/src/Components/WebView/Headless/src/ConsoleRenderer.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.WebView.Hosting; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Components.WebView.Headless +{ + internal class ConsoleRenderer : Renderer + { + private Dispatcher _dispatcher; + private IRenderPort _renderPort; + + public ConsoleRenderer( + IServiceProvider serviceProvider, + IRenderPort renderPort, + ILoggerFactory loggerFactory, + IComponentActivator componentActivator, + Dispatcher dispatcher) : base(serviceProvider, loggerFactory, componentActivator) + { + _dispatcher = dispatcher; + _renderPort = renderPort; + } + + public override Dispatcher Dispatcher => _dispatcher; + + protected override void HandleException(Exception exception) + { + _renderPort.OnException(exception); + } + + protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) + { + return _renderPort.ApplyBatchAsync(renderBatch); + } + + public Task AddComponent(string selector, in ParameterView initialParameters = default) + { + var component = InstantiateComponent(typeof(TComponent)); + var componentId = AssignRootComponentId(component); + + _renderPort.AttachRootComponent(componentId, selector); + + return RenderRootComponentAsync(componentId, initialParameters); + } + } +} diff --git a/src/Components/WebView/Headless/src/ConsoleWindow.cs b/src/Components/WebView/Headless/src/ConsoleWindow.cs new file mode 100644 index 000000000000..5f64b7c1ef57 --- /dev/null +++ b/src/Components/WebView/Headless/src/ConsoleWindow.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.WebView.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.JSInterop; +using Microsoft.JSInterop.Infrastructure; + +namespace Microsoft.AspNetCore.Components.WebView.Headless +{ + public class ConsoleWindow : IRenderPort + { + private Dictionary _rootComponents = new(); + private IServiceProvider _scope; + private IContentProvider _contentProvider; + private ConsoleRenderer _renderer; + private IJSRuntime _jsRuntime; + + public void Attach(IServiceProvider scope) + { + _scope = scope; + _contentProvider = _scope.GetRequiredService(); + _renderer = ActivatorUtilities.CreateInstance(_scope, this); + _jsRuntime = new ConsoleJsRuntime(); + } + + public void AddComponent(string selector) + { + _renderer.Dispatcher.InvokeAsync(() => + { + _renderer.AddComponent(selector); + }); + } + + public Task ApplyBatchAsync(RenderBatch renderBatch) + { + throw new NotImplementedException(); + } + + public void OnException(Exception exception) + { + Console.WriteLine(exception.ToString()); + } + + public void AttachRootComponent(int componentId, string selector) + { + _rootComponents.Add(componentId, selector); + } + + public Task DispatchEventAsync(WebEventDescriptor descriptor, string eventArguments) + { + throw new InvalidOperationException(); + } + } + + public interface MessageChannels + { + public void Attach(); + + // Rendering + // (Outbound) + public Task ApplyRenderBatch(RenderBatch renderBatch); + + // (Inbound) + public Task RenderBatchCompleted(long batchId, string renderErrors); + + // Event handling + // (Inbound) + public Task DispatchEventAsync(WebEventDescriptor descriptor, string eventArguments); + + // Error handling + // (Outbound) + public Task OnError(Exception ex); + + // .NET interop (this is typically handled via JS interop, should it be its own thing?) + // (Inbound) + public void BeginInvokeDotNetFromJS(string assembly, string methodIdentifier, int dotnetObjectId, string argsJson); + // (Outbound) + public void EndInvokeDotNetFromJS(int callbackId, bool success, string resultOrError); + + // JS interop + // (Outbound) + public void BeginInvokeJS(long taskId, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId); + // (Inbound) + public void EndInvokeJSFromDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult); + } +} diff --git a/src/Components/WebView/Headless/src/IContentProvider.cs b/src/Components/WebView/Headless/src/IContentProvider.cs new file mode 100644 index 000000000000..e755906b9603 --- /dev/null +++ b/src/Components/WebView/Headless/src/IContentProvider.cs @@ -0,0 +1,6 @@ +namespace Microsoft.AspNetCore.Components.WebView.Headless +{ + internal interface IContentProvider + { + } +} \ No newline at end of file diff --git a/src/Components/WebView/Headless/src/Microsoft.AspNetCore.Components.WebView.Headless.csproj b/src/Components/WebView/Headless/src/Microsoft.AspNetCore.Components.WebView.Headless.csproj index 297eac62d27b..9b011af6b249 100644 --- a/src/Components/WebView/Headless/src/Microsoft.AspNetCore.Components.WebView.Headless.csproj +++ b/src/Components/WebView/Headless/src/Microsoft.AspNetCore.Components.WebView.Headless.csproj @@ -5,8 +5,8 @@ Test host for Blazor desktop applications. false $(NoWarn);BL0006 - true - enable + + false diff --git a/src/Components/WebView/Hosting/src/IRenderPort.cs b/src/Components/WebView/Hosting/src/IRenderPort.cs new file mode 100644 index 000000000000..c568440d4664 --- /dev/null +++ b/src/Components/WebView/Hosting/src/IRenderPort.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.RenderTree; + +namespace Microsoft.AspNetCore.Components.WebView.Hosting +{ + public interface IRenderPort + { + void Attach(IServiceProvider scope); + Task ApplyBatchAsync(RenderBatch renderBatch); + void OnException(Exception exception); + void AttachRootComponent(int componentId, string selector); + } +} diff --git a/src/Components/WebView/Hosting/src/Microsoft.AspNetCore.Components.WebView.csproj b/src/Components/WebView/Hosting/src/Microsoft.AspNetCore.Components.WebView.csproj index d3669e48ef1f..f226e6614cc8 100644 --- a/src/Components/WebView/Hosting/src/Microsoft.AspNetCore.Components.WebView.csproj +++ b/src/Components/WebView/Hosting/src/Microsoft.AspNetCore.Components.WebView.csproj @@ -1,12 +1,12 @@ - + $(DefaultNetCoreTargetFramework) Build desktop applications with WebView and Blazor. true $(NoWarn);BL0006 - true - enable + + false diff --git a/src/Components/WebView/Hosting/src/WebViewHost.cs b/src/Components/WebView/Hosting/src/WebViewHost.cs index f0d8ac55d190..a2247bd5d99f 100644 --- a/src/Components/WebView/Hosting/src/WebViewHost.cs +++ b/src/Components/WebView/Hosting/src/WebViewHost.cs @@ -1,9 +1,25 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + namespace Microsoft.AspNetCore.Components.WebView.Hosting { public class WebViewHost { + private Dictionary _clientRenderers = new(); + + public IServiceProvider Services { get; set; } + + public void AttachRenderClient(IRenderPort renderClient) + { + var factory = Services.GetRequiredService(); + var scope = factory.CreateScope(); + _clientRenderers.Add(renderClient, scope); + renderClient.Attach(scope.ServiceProvider); + } } } diff --git a/src/Components/WebView/Hosting/src/WebViewHostBuilder.cs b/src/Components/WebView/Hosting/src/WebViewHostBuilder.cs index b1a019f6874e..27dbbb939a9b 100644 --- a/src/Components/WebView/Hosting/src/WebViewHostBuilder.cs +++ b/src/Components/WebView/Hosting/src/WebViewHostBuilder.cs @@ -14,7 +14,7 @@ public class WebViewHostBuilder { public IServiceCollection Services { get; } = new ServiceCollection(); - public static WebViewHostBuilder CreateDefault(string[]? args = null) + public static WebViewHostBuilder CreateDefault(string[] args = null) { return new WebViewHostBuilder(); } diff --git a/src/Components/WebView/Samples/BlazorHeadlessSample/BlazorHeadlessSample.csproj b/src/Components/WebView/Samples/BlazorHeadlessSample/BlazorHeadlessSample.csproj index 276e1b4ca408..2ad3415b00e0 100644 --- a/src/Components/WebView/Samples/BlazorHeadlessSample/BlazorHeadlessSample.csproj +++ b/src/Components/WebView/Samples/BlazorHeadlessSample/BlazorHeadlessSample.csproj @@ -1,14 +1,18 @@ - + $(DefaultNetCoreTargetFramework) Sample blazor headless application. false $(NoWarn);BL0006 - true - enable + + false + + + + diff --git a/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs b/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs index a4f6e4af9876..979f2b2e09da 100644 --- a/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs +++ b/src/Components/WebView/Samples/BlazorHeadlessSample/Program.cs @@ -1,14 +1,12 @@ - +using Microsoft.AspNetCore.Components.WebView.Headless; using Microsoft.AspNetCore.Components.WebView.Hosting; var builder = WebViewHostBuilder.CreateDefault(); var host = builder.Build(); -var renderClient = new ConsoleRenderClient(); - -host.AttachRenderClient(renderClient); +var renderPort = new ConsoleWindow(); -renderClient.AddComponent("#app"); +host.AttachRenderClient(renderPort); -host.StartAsync() +renderPort.AddComponent("#app"); diff --git a/src/Components/WebView/Samples/BlazorHeadlessSample/Properties/launchSettings.json b/src/Components/WebView/Samples/BlazorHeadlessSample/Properties/launchSettings.json new file mode 100644 index 000000000000..8b4f750c7bf8 --- /dev/null +++ b/src/Components/WebView/Samples/BlazorHeadlessSample/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:51974/", + "sslPort": 44319 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "BlazorHeadlessSample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} \ No newline at end of file From 2ca6888bc51ac90140ffa4bc670235db596ec8a7 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 24 Feb 2021 06:53:49 -0800 Subject: [PATCH 05/92] Second checkpoint --- .../WebView/Headless/src/ConsoleJsRuntime.cs | 21 - .../WebView/Headless/src/ConsoleRenderer.cs | 47 - .../WebView/Headless/src/ConsoleWindow.cs | 89 -- .../WebView/Headless/src/HeadlessWebView.cs | 993 ++++++++++++++++++ .../WebView/Headless/src/IContentProvider.cs | 6 - ...lazorWebViewServiceCollectionExtensions.cs | 18 + .../WebView/Hosting/src/IRenderPort.cs | 17 - .../WebView/Hosting/src/WebViewHost.cs | 25 - .../WebView/Hosting/src/WebViewHostBuilder.cs | 27 - .../BlazorHeadlessSample.csproj | 7 +- .../Samples/BlazorHeadlessSample/Program.cs | 16 +- .../Properties/launchSettings.json | 27 - 12 files changed, 1027 insertions(+), 266 deletions(-) delete mode 100644 src/Components/WebView/Headless/src/ConsoleJsRuntime.cs delete mode 100644 src/Components/WebView/Headless/src/ConsoleRenderer.cs delete mode 100644 src/Components/WebView/Headless/src/ConsoleWindow.cs create mode 100644 src/Components/WebView/Headless/src/HeadlessWebView.cs delete mode 100644 src/Components/WebView/Headless/src/IContentProvider.cs create mode 100644 src/Components/WebView/Hosting/src/BlazorWebViewServiceCollectionExtensions.cs delete mode 100644 src/Components/WebView/Hosting/src/IRenderPort.cs delete mode 100644 src/Components/WebView/Hosting/src/WebViewHost.cs delete mode 100644 src/Components/WebView/Hosting/src/WebViewHostBuilder.cs delete mode 100644 src/Components/WebView/Samples/BlazorHeadlessSample/Properties/launchSettings.json diff --git a/src/Components/WebView/Headless/src/ConsoleJsRuntime.cs b/src/Components/WebView/Headless/src/ConsoleJsRuntime.cs deleted file mode 100644 index 893106456a3b..000000000000 --- a/src/Components/WebView/Headless/src/ConsoleJsRuntime.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.JSInterop; -using Microsoft.JSInterop.Infrastructure; - -namespace Microsoft.AspNetCore.Components.WebView.Headless -{ - internal class ConsoleJsRuntime : JSRuntime - { - protected override void BeginInvokeJS(long taskId, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId) - { - throw new System.NotImplementedException(); - } - - protected override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) - { - throw new System.NotImplementedException(); - } - } -} diff --git a/src/Components/WebView/Headless/src/ConsoleRenderer.cs b/src/Components/WebView/Headless/src/ConsoleRenderer.cs deleted file mode 100644 index 3184e0258fe7..000000000000 --- a/src/Components/WebView/Headless/src/ConsoleRenderer.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.RenderTree; -using Microsoft.AspNetCore.Components.WebView.Hosting; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Components.WebView.Headless -{ - internal class ConsoleRenderer : Renderer - { - private Dispatcher _dispatcher; - private IRenderPort _renderPort; - - public ConsoleRenderer( - IServiceProvider serviceProvider, - IRenderPort renderPort, - ILoggerFactory loggerFactory, - IComponentActivator componentActivator, - Dispatcher dispatcher) : base(serviceProvider, loggerFactory, componentActivator) - { - _dispatcher = dispatcher; - _renderPort = renderPort; - } - - public override Dispatcher Dispatcher => _dispatcher; - - protected override void HandleException(Exception exception) - { - _renderPort.OnException(exception); - } - - protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) - { - return _renderPort.ApplyBatchAsync(renderBatch); - } - - public Task AddComponent(string selector, in ParameterView initialParameters = default) - { - var component = InstantiateComponent(typeof(TComponent)); - var componentId = AssignRootComponentId(component); - - _renderPort.AttachRootComponent(componentId, selector); - - return RenderRootComponentAsync(componentId, initialParameters); - } - } -} diff --git a/src/Components/WebView/Headless/src/ConsoleWindow.cs b/src/Components/WebView/Headless/src/ConsoleWindow.cs deleted file mode 100644 index 5f64b7c1ef57..000000000000 --- a/src/Components/WebView/Headless/src/ConsoleWindow.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.RenderTree; -using Microsoft.AspNetCore.Components.WebView.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.JSInterop; -using Microsoft.JSInterop.Infrastructure; - -namespace Microsoft.AspNetCore.Components.WebView.Headless -{ - public class ConsoleWindow : IRenderPort - { - private Dictionary _rootComponents = new(); - private IServiceProvider _scope; - private IContentProvider _contentProvider; - private ConsoleRenderer _renderer; - private IJSRuntime _jsRuntime; - - public void Attach(IServiceProvider scope) - { - _scope = scope; - _contentProvider = _scope.GetRequiredService(); - _renderer = ActivatorUtilities.CreateInstance(_scope, this); - _jsRuntime = new ConsoleJsRuntime(); - } - - public void AddComponent(string selector) - { - _renderer.Dispatcher.InvokeAsync(() => - { - _renderer.AddComponent(selector); - }); - } - - public Task ApplyBatchAsync(RenderBatch renderBatch) - { - throw new NotImplementedException(); - } - - public void OnException(Exception exception) - { - Console.WriteLine(exception.ToString()); - } - - public void AttachRootComponent(int componentId, string selector) - { - _rootComponents.Add(componentId, selector); - } - - public Task DispatchEventAsync(WebEventDescriptor descriptor, string eventArguments) - { - throw new InvalidOperationException(); - } - } - - public interface MessageChannels - { - public void Attach(); - - // Rendering - // (Outbound) - public Task ApplyRenderBatch(RenderBatch renderBatch); - - // (Inbound) - public Task RenderBatchCompleted(long batchId, string renderErrors); - - // Event handling - // (Inbound) - public Task DispatchEventAsync(WebEventDescriptor descriptor, string eventArguments); - - // Error handling - // (Outbound) - public Task OnError(Exception ex); - - // .NET interop (this is typically handled via JS interop, should it be its own thing?) - // (Inbound) - public void BeginInvokeDotNetFromJS(string assembly, string methodIdentifier, int dotnetObjectId, string argsJson); - // (Outbound) - public void EndInvokeDotNetFromJS(int callbackId, bool success, string resultOrError); - - // JS interop - // (Outbound) - public void BeginInvokeJS(long taskId, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId); - // (Inbound) - public void EndInvokeJSFromDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult); - } -} diff --git a/src/Components/WebView/Headless/src/HeadlessWebView.cs b/src/Components/WebView/Headless/src/HeadlessWebView.cs new file mode 100644 index 000000000000..a13604ca722e --- /dev/null +++ b/src/Components/WebView/Headless/src/HeadlessWebView.cs @@ -0,0 +1,993 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.JSInterop; +using Microsoft.JSInterop.Infrastructure; + +namespace Microsoft.AspNetCore.Components.WebView.Headless +{ + public static class HeadlessWebViewServiceCollectionExtensions + { + public static IServiceCollection AddHeadlessWebView(this IServiceCollection services) + { + services.AddBlazorWebView(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } + } + + public class InteractionOptions + { + public List Interactions { get; } = Enum.GetValues().ToList(); + } + + public enum InteractionType + { + Exit, + Navigate + } + + public record Interaction(InteractionType Kind, string Description); + + public record ExitInteraction() : Interaction(InteractionType.Exit, "Stops the application."); + + public record NavigateToUrlInteraction(string Url, bool Intercepted) : Interaction(InteractionType.Navigate, "Navigate to the given url."); + + public class HeadlessWebView : IDisposable + { + private readonly IServiceProvider _provider; + private readonly List _registeredComponents = new(); + private IServiceScope _scope; + private HeadlessHost _host; + private Dispatcher _dispatcher; + private HeadlessRenderer _renderer; + + public HeadlessWebView(IServiceProvider provider) + { + _provider = provider; + } + + public void AddComponent(string selector) where TComponent : IComponent + { + _registeredComponents.Add(new RootComponent(typeof(TComponent), selector)); + } + + public async Task StartAsync( + Action displayData, + Func selectInteractionOption) + { + _scope = _provider.CreateScope(); + var services = _scope.ServiceProvider; + _host = services.GetRequiredService(); + var baseUrl = _host.BaseUrl; + var url = _host.CurrentUrl; + + var jsRuntime = (HeadlessJSRuntime)services.GetRequiredService(); + var manager = (HeadlessNavigationManager)services.GetRequiredService(); + + var navigationInterception = (HeadlessNavigationInterception)services.GetRequiredService(); + await navigationInterception.EnableNavigationInterceptionAsync(); + + _dispatcher = Dispatcher.CreateDefault(); + _renderer = ActivatorUtilities.CreateInstance(services, _dispatcher); + + // Initial render + try + { + await _renderer.Dispatcher.InvokeAsync(async () => + { + foreach (var (component, selector) in _registeredComponents) + { + await _renderer.RenderRootComponentAsync(component, selector); + } + displayData(_host.GetHtml()); + }); + await StartEventLoop(selectInteractionOption); + } + catch (Exception ex) + { + displayData(ex); + } + } + + private Task StartEventLoop( + Action displayData, + Func selectInteractionOption) + { + _host.BatchCompleted += (_,successOrError,error) => { + if (successOrError) { + displayData(_host.GetHtml()); + } else { + displayData(error); + } + }; + + do + { + var interaction = selectInteractionOption(new InteractionOptions()); + switch (interaction.Kind) + { + case InteractionType.Exit: + return Task.CompletedTask; + case InteractionType.Navigate: + var (url, intercepted) = (NavigateToUrlInteraction)interaction; + _host.UpdateLocation(url, intercepted); + break; + default: + break; + } + } while (true); + } + + public void Dispose() + { + _scope.Dispose(); + } + + private record RootComponent(Type ComponentType, string Selector); + } + + internal class HeadlessRenderer : Renderer + { + private Dispatcher _dispatcher; + private HeadlessHost _host; + + public HeadlessRenderer( + IServiceProvider serviceProvider, + Dispatcher dispatcher, + HeadlessHost host, + ILoggerFactory loggerFactory) : + base(serviceProvider, loggerFactory) + { + _dispatcher = dispatcher; + _host = host; + } + + public override Dispatcher Dispatcher => _dispatcher; + + protected override void HandleException(Exception exception) + { + _host.NotifyUnhandledException(exception); + } + + protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) + { + return _host.ApplyRenderBatch(renderBatch); + } + + internal async Task RenderRootComponentAsync(Type componentType, string selector) + { + var component = InstantiateComponent(componentType); + var componentId = AssignRootComponentId(component); + + await _host.AttachToDocumentAsync(componentId, selector); + + await RenderRootComponentAsync(componentId); + } + } + + internal class HeadlessJSRuntime : JSRuntime + { + private readonly HeadlessHost _host; + + public HeadlessJSRuntime(HeadlessHost host) + { + _host = host; + } + + protected override void BeginInvokeJS(long taskId, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId) + { + _host.BeginInvokeJS(taskId, identifier, argsJson, resultType, targetInstanceId); + } + + protected override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) + { + if (!invocationResult.Success) + { + string errorMessage; + + errorMessage = $"There was an exception invoking '{invocationInfo.MethodIdentifier}'"; + if (invocationInfo.AssemblyName != null) + { + errorMessage += $" on assembly '{invocationInfo.AssemblyName}'"; + } + + EndInvokeDotNetCore(invocationInfo.CallId, success: false, errorMessage); + } + else + { + EndInvokeDotNetCore(invocationInfo.CallId, success: true, invocationResult.Result); + } + + void EndInvokeDotNetCore(string callId, bool success, object resultOrError) + { + _host.EndInvokeDotNet(JsonSerializer.Serialize(new[] { callId, success, resultOrError }, JsonSerializerOptions)); + } + } + } + + internal class HeadlessNavigationInterception : INavigationInterception + { + public Task EnableNavigationInterceptionAsync() + { + // NO:OP + return Task.CompletedTask; + } + } + + internal class HeadlessNavigationManager : NavigationManager + { + private readonly HeadlessHost _host; + + public HeadlessNavigationManager(HeadlessHost host) + { + _host = host; + _host.LocationUpdated += LocationUpdated; + Initialize(_host.BaseUrl, _host.CurrentUrl); + } + + protected override void NavigateToCore(string uri, bool forceLoad) + { + _host.Navigate(uri, forceLoad); + } + + public void LocationUpdated(string newUrl, bool intercepted) + { + Uri = newUrl; + NotifyLocationChanged(intercepted); + } + } + + internal class HeadlessHost + { + public int _batchNumber = 1; + private readonly HeadlessDocument _headlessDocument = new(); + + public event Action LocationUpdated; + public event Action BatchCompleted; + + public string BaseUrl { get; set; } = "https://localhost:5001/"; + public string CurrentUrl { get; set; } = "https://localhost:5001/"; + + internal Task ApplyRenderBatch(RenderBatch renderBatch) + { + _headlessDocument.ApplyChanges(renderBatch); + BatchCompleted(1, true, null); + return Task.CompletedTask; + } + + internal void UpdateLocation(string newUrl, bool intercepted) + { + LocationUpdated?.Invoke(newUrl, intercepted); + } + + internal Task AttachToDocumentAsync(int componentId, string selector) + { + _headlessDocument.AddRootComponent(componentId, selector); + + return Task.CompletedTask; + } + + internal void Navigate(string uri, bool forceLoad) + { + throw new NotImplementedException(); + } + + internal void BeginInvokeJS(long taskId, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId) + { + throw new NotImplementedException(); + } + + internal void EndInvokeDotNet(string v) + { + throw new NotImplementedException(); + } + + internal void NotifyUnhandledException(Exception exception) + { + Console.WriteLine(exception); + } + + internal string GetHtml() + { + return _headlessDocument.GetHtml(); + } + } + + internal class HeadlessDocument + { + private const string SelectValuePropname = "_blazorSelectValue"; + + private readonly Dictionary _componentsById = new(); + + internal void AddRootComponent(int componentId, string selector) + { + if (_componentsById.ContainsKey(componentId)) + { + throw new InvalidOperationException($"Component with Id '{componentId}' already exists."); + } + + _componentsById.Add(componentId, new RootComponentNode(componentId, selector)); + } + + internal void ApplyChanges(RenderBatch batch) + { + for (var i = 0; i < batch.UpdatedComponents.Count; i++) + { + var diff = batch.UpdatedComponents.Array[i]; + var componentId = diff.ComponentId; + var edits = diff.Edits; + UpdateComponent(batch, componentId, edits); + } + + for (var i = 0; i < batch.DisposedComponentIDs.Count; i++) + { + DisposeComponent(batch.DisposedComponentIDs.Array[i]); + } + + for (var i = 0; i < batch.DisposedEventHandlerIDs.Count; i++) + { + DisposeEventHandler(batch.DisposedEventHandlerIDs.Array[i]); + } + } + + private void UpdateComponent(RenderBatch batch, int componentId, ArrayBuilderSegment edits) + { + if (!_componentsById.TryGetValue(componentId, out var component)) + { + component = new ComponentNode(componentId); + _componentsById.Add(componentId, component); + } + + ApplyEdits(batch, component, 0, edits); + } + + private void DisposeComponent(int componentId) + { + + } + + private void DisposeEventHandler(ulong eventHandlerId) + { + + } + + private void ApplyEdits(RenderBatch batch, ContainerNode parent, int childIndex, ArrayBuilderSegment edits) + { + var currentDepth = 0; + var childIndexAtCurrentDepth = childIndex; + var permutations = new List(); + + for (var editIndex = edits.Offset; editIndex < edits.Offset + edits.Count; editIndex++) + { + var edit = edits.Array[editIndex]; + switch (edit.Type) + { + case RenderTreeEditType.PrependFrame: + { + var frame = batch.ReferenceFrames.Array[edit.ReferenceFrameIndex]; + var siblingIndex = edit.SiblingIndex; + InsertFrame(batch, parent, childIndexAtCurrentDepth + siblingIndex, batch.ReferenceFrames.Array, frame, edit.ReferenceFrameIndex); + break; + } + + case RenderTreeEditType.RemoveFrame: + { + var siblingIndex = edit.SiblingIndex; + parent.RemoveLogicalChild(childIndexAtCurrentDepth + siblingIndex); + break; + } + + case RenderTreeEditType.SetAttribute: + { + var frame = batch.ReferenceFrames.Array[edit.ReferenceFrameIndex]; + var siblingIndex = edit.SiblingIndex; + var node = parent.Children[childIndexAtCurrentDepth + siblingIndex]; + if (node is ElementNode element) + { + ApplyAttribute(batch, element, frame); + } + else + { + throw new Exception("Cannot set attribute on non-element child"); + } + break; + } + + case RenderTreeEditType.RemoveAttribute: + { + // Note that we don't have to dispose the info we track about event handlers here, because the + // disposed event handler IDs are delivered separately (in the 'disposedEventHandlerIds' array) + var siblingIndex = edit.SiblingIndex; + var node = parent.Children[childIndexAtCurrentDepth + siblingIndex]; + if (node is ElementNode element) + { + var attributeName = edit.RemovedAttributeName; + + // First try to remove any special property we use for this attribute + if (!TryApplySpecialProperty(batch, element, attributeName, default)) + { + // If that's not applicable, it's a regular DOM attribute so remove that + element.RemoveAttribute(attributeName); + } + } + else + { + throw new Exception("Cannot remove attribute from non-element child"); + } + break; + } + + case RenderTreeEditType.UpdateText: + { + var frame = batch.ReferenceFrames.Array[edit.ReferenceFrameIndex]; + var siblingIndex = edit.SiblingIndex; + var node = parent.Children[childIndexAtCurrentDepth + siblingIndex]; + if (node is TextNode textNode) + { + textNode.Text = frame.TextContent; + } + else + { + throw new Exception("Cannot set text content on non-text child"); + } + break; + } + + + case RenderTreeEditType.UpdateMarkup: + { + var frame = batch.ReferenceFrames.Array[edit.ReferenceFrameIndex]; + var siblingIndex = edit.SiblingIndex; + parent.RemoveLogicalChild(childIndexAtCurrentDepth + siblingIndex); + InsertMarkup(parent, childIndexAtCurrentDepth + siblingIndex, frame); + break; + } + + case RenderTreeEditType.StepIn: + { + var siblingIndex = edit.SiblingIndex; + parent = (ContainerNode)parent.Children[childIndexAtCurrentDepth + siblingIndex]; + currentDepth++; + childIndexAtCurrentDepth = 0; + break; + } + + case RenderTreeEditType.StepOut: + { + parent = parent.Parent ?? throw new InvalidOperationException($"Cannot step out of {parent}"); + currentDepth--; + childIndexAtCurrentDepth = currentDepth == 0 ? childIndex : 0; // The childIndex is only ever nonzero at zero depth + break; + } + + case RenderTreeEditType.PermutationListEntry: + { + permutations.Add(new PermutationListEntry(childIndexAtCurrentDepth + edit.SiblingIndex, childIndexAtCurrentDepth + edit.MoveToSiblingIndex)); + break; + } + + case RenderTreeEditType.PermutationListEnd: + { + throw new NotSupportedException(); + //permuteLogicalChildren(parent, permutations!); + //permutations.Clear(); + //break; + } + + default: + { + throw new Exception($"Unknown edit type: '{edit.Type}'"); + } + } + } + } + + private int InsertFrame(RenderBatch batch, ContainerNode parent, int childIndex, ArraySegment frames, RenderTreeFrame frame, int frameIndex) + { + switch (frame.FrameType) + { + case RenderTreeFrameType.Element: + { + InsertElement(batch, parent, childIndex, frames, frame, frameIndex); + return 1; + } + + case RenderTreeFrameType.Text: + { + InsertText(parent, childIndex, frame); + return 1; + } + + case RenderTreeFrameType.Attribute: + { + throw new Exception("Attribute frames should only be present as leading children of element frames."); + } + + case RenderTreeFrameType.Component: + { + InsertComponent(parent, childIndex, frame); + return 1; + } + + case RenderTreeFrameType.Region: + { + return InsertFrameRange(batch, parent, childIndex, frames, frameIndex + 1, frameIndex + frame.RegionSubtreeLength); + } + + case RenderTreeFrameType.ElementReferenceCapture: + { + if (parent is ElementNode) + { + return 0; // A "capture" is a child in the diff, but has no node in the DOM + } + else + { + throw new Exception("Reference capture frames can only be children of element frames."); + } + } + + case RenderTreeFrameType.Markup: + { + InsertMarkup(parent, childIndex, frame); + return 1; + } + + } + + throw new Exception($"Unknown frame type: {frame.FrameType}"); + } + + private void InsertText(ContainerNode parent, int childIndex, RenderTreeFrame frame) + { + var textContent = frame.TextContent; + var newTextNode = new TextNode(textContent); + parent.InsertLogicalChild(newTextNode, childIndex); + } + + private void InsertComponent(ContainerNode parent, int childIndex, RenderTreeFrame frame) + { + // All we have to do is associate the child component ID with its location. We don't actually + // do any rendering here, because the diff for the child will appear later in the render batch. + var childComponentId = frame.ComponentId; + var containerElement = parent.CreateAndInsertComponent(childComponentId, childIndex); + + _componentsById[childComponentId] = containerElement; + } + + private int InsertFrameRange(RenderBatch batch, ContainerNode parent, int childIndex, ArraySegment frames, int startIndex, int endIndexExcl) + { + var origChildIndex = childIndex; + for (var index = startIndex; index < endIndexExcl; index++) + { + var frame = batch.ReferenceFrames.Array[index]; + var numChildrenInserted = InsertFrame(batch, parent, childIndex, frames, frame, index); + childIndex += numChildrenInserted; + + // Skip over any descendants, since they are already dealt with recursively + index += CountDescendantFrames(frame); + } + + return (childIndex - origChildIndex); // Total number of children inserted + } + + private void InsertElement(RenderBatch batch, ContainerNode parent, int childIndex, ArraySegment frames, RenderTreeFrame frame, int frameIndex) + { + // Note: we don't handle SVG here + var newElement = new ElementNode(frame.ElementName); + + var inserted = false; + + // Apply attributes + for (var i = frameIndex + 1; i < frameIndex + frame.ElementSubtreeLength; i++) + { + var descendantFrame = batch.ReferenceFrames.Array[i]; + if (descendantFrame.FrameType == RenderTreeFrameType.Attribute) + { + ApplyAttribute(batch, newElement, descendantFrame); + } + else + { + parent.InsertLogicalChild(newElement, childIndex); + inserted = true; + + // As soon as we see a non-attribute child, all the subsequent child frames are + // not attributes, so bail out and insert the remnants recursively + InsertFrameRange(batch, newElement, 0, frames, i, frameIndex + frame.ElementSubtreeLength); + break; + } + } + + // this element did not have any children, so it's not inserted yet. + if (!inserted) + { + parent.InsertLogicalChild(newElement, childIndex); + } + } + + private void ApplyAttribute(RenderBatch batch, ElementNode elementNode, RenderTreeFrame attributeFrame) + { + var attributeName = attributeFrame.AttributeName; + var eventHandlerId = attributeFrame.AttributeEventHandlerId; + + if (eventHandlerId != 0) + { + var firstTwoChars = attributeName.Substring(0, 2); + var eventName = attributeName.Substring(2); + if (firstTwoChars != "on" || string.IsNullOrEmpty(eventName)) + { + throw new InvalidOperationException($"Attribute has nonzero event handler ID, but attribute name '${attributeName}' does not start with 'on'."); + } + var descriptor = new ElementNode.ElementEventDescriptor(eventName, eventHandlerId); + elementNode.SetEvent(eventName, descriptor); + + return; + } + + // First see if we have special handling for this attribute + if (!TryApplySpecialProperty(batch, elementNode, attributeName, attributeFrame)) + { + // If not, treat it as a regular string-valued attribute + elementNode.SetAttribute( + attributeName, + attributeFrame.AttributeValue); + } + } + + private bool TryApplySpecialProperty(RenderBatch batch, ElementNode element, string attributeName, RenderTreeFrame attributeFrame) + { + switch (attributeName) + { + case "value": + return TryApplyValueProperty(element, attributeFrame); + case "checked": + return TryApplyCheckedProperty(element, attributeFrame); + default: + return false; + } + } + + private bool TryApplyValueProperty(ElementNode element, RenderTreeFrame attributeFrame) + { + // Certain elements have built-in behaviour for their 'value' property + switch (element.TagName) + { + case "INPUT": + case "SELECT": + case "TEXTAREA": + { + var value = attributeFrame.AttributeValue; + element.SetProperty("value", value); + + if (element.TagName == "SELECT") + { + // is special, in that anything we write to .value will be lost if there + // isn't yet a matching