diff --git a/AspNetCore.sln b/AspNetCore.sln index 7599ccad65e6..52704941e80e 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1612,6 +1612,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Compon EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.WebView.Test", "src\Components\WebView\WebView\test\Microsoft.AspNetCore.Components.WebView.Test.csproj", "{4BD6F0DB-BE9C-4C54-B52A-D20B88855ED5}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{B730328F-D9E9-4EAA-B28E-4631A14095F9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PhotinoPlatform", "PhotinoPlatform", "{44963D50-8B58-44E6-918D-788BCB406695}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{3EC71A0E-6515-4A5A-B759-F0BCF1BCFC56}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhotinoTestApp", "src\Components\WebView\Samples\PhotinoPlatform\testassets\PhotinoTestApp\PhotinoTestApp.csproj", "{558C46DE-DE16-41D5-8DB7-D6D748E32977}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.WebView.Photino", "src\Components\WebView\Samples\PhotinoPlatform\src\Microsoft.AspNetCore.Components.WebView.Photino.csproj", "{B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -7683,6 +7693,30 @@ Global {4BD6F0DB-BE9C-4C54-B52A-D20B88855ED5}.Release|x64.Build.0 = Release|Any CPU {4BD6F0DB-BE9C-4C54-B52A-D20B88855ED5}.Release|x86.ActiveCfg = Release|Any CPU {4BD6F0DB-BE9C-4C54-B52A-D20B88855ED5}.Release|x86.Build.0 = Release|Any CPU + {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Debug|Any CPU.Build.0 = Debug|Any CPU + {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Debug|x64.ActiveCfg = Debug|Any CPU + {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Debug|x64.Build.0 = Debug|Any CPU + {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Debug|x86.ActiveCfg = Debug|Any CPU + {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Debug|x86.Build.0 = Debug|Any CPU + {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Release|Any CPU.ActiveCfg = Release|Any CPU + {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Release|Any CPU.Build.0 = Release|Any CPU + {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Release|x64.ActiveCfg = Release|Any CPU + {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Release|x64.Build.0 = Release|Any CPU + {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Release|x86.ActiveCfg = Release|Any CPU + {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Release|x86.Build.0 = Release|Any CPU + {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Debug|x64.Build.0 = Debug|Any CPU + {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Debug|x86.Build.0 = Debug|Any CPU + {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Release|Any CPU.Build.0 = Release|Any CPU + {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Release|x64.ActiveCfg = Release|Any CPU + {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Release|x64.Build.0 = Release|Any CPU + {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Release|x86.ActiveCfg = Release|Any CPU + {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -8481,6 +8515,11 @@ Global {A1D02CE6-1077-410A-81CB-D4BD500FD765} = {0508E463-0269-40C9-B5C2-3B600FB2A28B} {3044DFA5-DE4F-44D8-8DD8-EDF547BE513E} = {C445B129-0A4D-41F5-8347-6534B6B12303} {4BD6F0DB-BE9C-4C54-B52A-D20B88855ED5} = {C445B129-0A4D-41F5-8347-6534B6B12303} + {B730328F-D9E9-4EAA-B28E-4631A14095F9} = {C445B129-0A4D-41F5-8347-6534B6B12303} + {44963D50-8B58-44E6-918D-788BCB406695} = {B730328F-D9E9-4EAA-B28E-4631A14095F9} + {3EC71A0E-6515-4A5A-B759-F0BCF1BCFC56} = {44963D50-8B58-44E6-918D-788BCB406695} + {558C46DE-DE16-41D5-8DB7-D6D748E32977} = {3EC71A0E-6515-4A5A-B759-F0BCF1BCFC56} + {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD} = {44963D50-8B58-44E6-918D-788BCB406695} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/eng/Dependencies.props b/eng/Dependencies.props index b17675fe495d..8b6592c5d9c1 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -175,6 +175,7 @@ and are generated based on the last package release. + diff --git a/eng/Versions.props b/eng/Versions.props index 20ab38193236..5b03d862e892 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -242,6 +242,7 @@ 1.0.2 12.0.2 13.0.4 + 1.1.6 0.192.0 3.0.0 7.2.2 diff --git a/src/Components/ComponentsNoDeps.slnf b/src/Components/ComponentsNoDeps.slnf index 7f1c9a961788..1ff7bd787cda 100644 --- a/src/Components/ComponentsNoDeps.slnf +++ b/src/Components/ComponentsNoDeps.slnf @@ -37,6 +37,8 @@ "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\\Samples\\PhotinoPlatform\\src\\Microsoft.AspNetCore.Components.WebView.Photino.csproj", + "src\\Components\\WebView\\Samples\\PhotinoPlatform\\testassets\\PhotinoTestApp\\PhotinoTestApp.csproj", "src\\Components\\WebView\\WebView\\src\\Microsoft.AspNetCore.Components.WebView.csproj", "src\\Components\\WebView\\WebView\\test\\Microsoft.AspNetCore.Components.WebView.Test.csproj", "src\\Components\\Web\\src\\Microsoft.AspNetCore.Components.Web.csproj", diff --git a/src/Components/WebView/Samples/PhotinoPlatform/src/BlazorWindow.cs b/src/Components/WebView/Samples/PhotinoPlatform/src/BlazorWindow.cs new file mode 100644 index 000000000000..9c17b99c2bce --- /dev/null +++ b/src/Components/WebView/Samples/PhotinoPlatform/src/BlazorWindow.cs @@ -0,0 +1,85 @@ +// 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.IO; +using Microsoft.Extensions.FileProviders; +using PhotinoNET; + +namespace Microsoft.AspNetCore.Components.WebView.Photino +{ + /// + /// A window containing a Blazor web view. + /// + public class BlazorWindow + { + private readonly PhotinoWindow _window; + private readonly PhotinoWebViewManager _manager; + + /// + /// Constructs an instance of . + /// + /// The window title. + /// The path to the host page. + /// The service provider. + /// A callback that configures the window. + public BlazorWindow( + string title, + string hostPage, + IServiceProvider services, + Action? configureWindow = null) + { + _window = new PhotinoWindow(title, options => + { + options.CustomSchemeHandlers.Add(PhotinoWebViewManager.BlazorAppScheme, HandleWebRequest); + configureWindow?.Invoke(options); + }, width: 1600, height: 1200, left: 300, top: 300); + + // We assume the host page is always in the root of the content directory, because it's + // unclear there's any other use case. We can add more options later if so. + var contentRootDir = Path.GetDirectoryName(Path.GetFullPath(hostPage))!; + var hostPageRelativePath = Path.GetRelativePath(contentRootDir, hostPage); + var fileProvider = new PhysicalFileProvider(contentRootDir); + + var dispatcher = new PhotinoDispatcher(_window); + _manager = new PhotinoWebViewManager(_window, services, dispatcher, new Uri(PhotinoWebViewManager.AppBaseUri), fileProvider, hostPageRelativePath); + } + + /// + /// Gets the underlying . + /// + public PhotinoWindow Photino => _window; + + /// + /// Adds a root component to the window. + /// + /// The component type. + /// A CSS selector describing where the component should be added in the host page. + /// An optional dictionary of parameters to pass to the component. + public void AddRootComponent(string selector, IDictionary? parameters = null) where TComponent: IComponent + { + var parameterView = parameters == null + ? ParameterView.Empty + : ParameterView.FromDictionary(parameters); + + // Dispatch because this is going to be async, and we want to catch any errors + _ = _manager.Dispatcher.InvokeAsync(async () => + { + await _manager.AddRootComponentAsync(typeof(TComponent), selector, parameterView); + }); + } + + /// + /// Shows the window and waits for it to be closed. + /// + public void Run() + { + _manager.Navigate("/"); + _window.WaitForClose(); + } + + private Stream HandleWebRequest(string url, out string contentType) + => _manager.HandleWebRequest(url, out contentType!)!; + } +} diff --git a/src/Components/WebView/Samples/PhotinoPlatform/src/Microsoft.AspNetCore.Components.WebView.Photino.csproj b/src/Components/WebView/Samples/PhotinoPlatform/src/Microsoft.AspNetCore.Components.WebView.Photino.csproj new file mode 100644 index 000000000000..e1875e893510 --- /dev/null +++ b/src/Components/WebView/Samples/PhotinoPlatform/src/Microsoft.AspNetCore.Components.WebView.Photino.csproj @@ -0,0 +1,16 @@ + + + + $(DefaultNetCoreTargetFramework) + Intended for internal testing use only. + false + false + enable + + + + + + + + diff --git a/src/Components/WebView/Samples/PhotinoPlatform/src/PhotinoDispatcher.cs b/src/Components/WebView/Samples/PhotinoPlatform/src/PhotinoDispatcher.cs new file mode 100644 index 000000000000..592330054dfe --- /dev/null +++ b/src/Components/WebView/Samples/PhotinoPlatform/src/PhotinoDispatcher.cs @@ -0,0 +1,67 @@ +// 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; +using System.Threading.Tasks; +using PhotinoNET; + +namespace Microsoft.AspNetCore.Components.WebView.Photino +{ + internal class PhotinoDispatcher : Dispatcher + { + private readonly PhotinoSynchronizationContext _context; + + public PhotinoDispatcher(PhotinoWindow window) + { + _context = new PhotinoSynchronizationContext(window); + _context.UnhandledException += (sender, e) => + { + OnUnhandledException(e); + }; + } + + public override bool CheckAccess() => SynchronizationContext.Current == _context; + + public override Task InvokeAsync(Action workItem) + { + if (CheckAccess()) + { + workItem(); + return Task.CompletedTask; + } + + return _context.InvokeAsync(workItem); + } + + public override Task InvokeAsync(Func workItem) + { + if (CheckAccess()) + { + return workItem(); + } + + return _context.InvokeAsync(workItem); + } + + public override Task InvokeAsync(Func workItem) + { + if (CheckAccess()) + { + return Task.FromResult(workItem()); + } + + return _context.InvokeAsync(workItem); + } + + public override Task InvokeAsync(Func> workItem) + { + if (CheckAccess()) + { + return workItem(); + } + + return _context.InvokeAsync(workItem); + } + } +} diff --git a/src/Components/WebView/Samples/PhotinoPlatform/src/PhotinoSynchronizationContext.cs b/src/Components/WebView/Samples/PhotinoPlatform/src/PhotinoSynchronizationContext.cs new file mode 100644 index 000000000000..b757da730df7 --- /dev/null +++ b/src/Components/WebView/Samples/PhotinoPlatform/src/PhotinoSynchronizationContext.cs @@ -0,0 +1,342 @@ +// 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.Reflection; +using System.Threading; +using System.Threading.Tasks; +using PhotinoNET; + +#nullable disable warnings + +namespace Microsoft.AspNetCore.Components.WebView.Photino +{ + // Most UI platforms have a built-in SyncContext/Dispatcher, e.g., Windows Forms and WPF, which WebView + // can normally use directly. However, Photino currently doesn't. + // + // This is a duplicate of Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContextDispatcher, + // except that it also uses Photino's "Invoke" to ensure we're running on the correct thread to be able to + // interact with the unmanaged resources (the window and WebView). + // + // It might be that a simpler variant of this would work, for example purely using Photino's "Invoke" and + // relying on that for single-threadedness. Maybe also in the future Photino could consider having its own + // built-in SyncContext/Dispatcher like other UI platforms. + + internal class PhotinoSynchronizationContext : SynchronizationContext + { + private static readonly ContextCallback ExecutionContextThunk = (object state) => + { + var item = (WorkItem)state; + item.SynchronizationContext.ExecuteSynchronously(null, item.Callback, item.State); + }; + + private static readonly Action BackgroundWorkThunk = (Task task, object state) => + { + var item = (WorkItem)state; + item.SynchronizationContext.ExecuteBackground(item); + }; + + private readonly PhotinoWindow _window; + private readonly int _uiThreadId; + private readonly MethodInfo _invokeMethodInfo; + + public PhotinoSynchronizationContext(PhotinoWindow window) + : this(window, new State()) + { + } + + private PhotinoSynchronizationContext(PhotinoWindow window, State state) + { + _state = state; + + _window = window ?? throw new ArgumentNullException(nameof(window)); + + _uiThreadId = (int)_window.GetType() + .GetField("_managedThreadId", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(_window)!; + + _invokeMethodInfo = _window.GetType() + .GetMethod("Invoke", BindingFlags.NonPublic | BindingFlags.Instance)!; + } + + private readonly State _state; + + public event UnhandledExceptionEventHandler? UnhandledException; + + public Task InvokeAsync(Action action) + { + var completion = new PhotinoSynchronizationTaskCompletionSource(action); + ExecuteSynchronouslyIfPossible((state) => + { + var completion = (PhotinoSynchronizationTaskCompletionSource)state; + try + { + completion.Callback(); + completion.SetResult(null); + } + catch (OperationCanceledException) + { + completion.SetCanceled(); + } + catch (Exception exception) + { + completion.SetException(exception); + } + }, completion); + + return completion.Task; + } + + public Task InvokeAsync(Func asyncAction) + { + var completion = new PhotinoSynchronizationTaskCompletionSource, object>(asyncAction); + ExecuteSynchronouslyIfPossible(async (state) => + { + var completion = (PhotinoSynchronizationTaskCompletionSource, object>)state; + try + { + await completion.Callback(); + completion.SetResult(null); + } + catch (OperationCanceledException) + { + completion.SetCanceled(); + } + catch (Exception exception) + { + completion.SetException(exception); + } + }, completion); + + return completion.Task; + } + + public Task InvokeAsync(Func function) + { + var completion = new PhotinoSynchronizationTaskCompletionSource, TResult>(function); + ExecuteSynchronouslyIfPossible((state) => + { + var completion = (PhotinoSynchronizationTaskCompletionSource, TResult>)state; + try + { + var result = completion.Callback(); + completion.SetResult(result); + } + catch (OperationCanceledException) + { + completion.SetCanceled(); + } + catch (Exception exception) + { + completion.SetException(exception); + } + }, completion); + + return completion.Task; + } + + public Task InvokeAsync(Func> asyncFunction) + { + var completion = new PhotinoSynchronizationTaskCompletionSource>, TResult>(asyncFunction); + ExecuteSynchronouslyIfPossible(async (state) => + { + var completion = (PhotinoSynchronizationTaskCompletionSource>, TResult>)state; + try + { + var result = await completion.Callback(); + completion.SetResult(result); + } + catch (OperationCanceledException) + { + completion.SetCanceled(); + } + catch (Exception exception) + { + completion.SetException(exception); + } + }, completion); + + return completion.Task; + } + + // asynchronously runs the callback + // + // NOTE: this must always run async. It's not legal here to execute the work item synchronously. + public override void Post(SendOrPostCallback d, object state) + { + lock (_state.Lock) + { + _state.Task = Enqueue(_state.Task, d, state, forceAsync: true); + } + } + + // synchronously runs the callback + public override void Send(SendOrPostCallback d, object state) + { + Task antecedent; + var completion = new TaskCompletionSource(); + + lock (_state.Lock) + { + antecedent = _state.Task; + _state.Task = completion.Task; + } + + // We have to block. That's the contract of Send - we don't expect this to be used + // in many scenarios in Components. + // + // Using Wait here is ok because the antecedent task will never throw. + antecedent.Wait(); + + ExecuteSynchronously(completion, d, state); + } + + // shallow copy + public override SynchronizationContext CreateCopy() + { + return new PhotinoSynchronizationContext(_window, _state); + } + + // Similar to Post, but it can runs the work item synchronously if the context is not busy. + // + // This is the main code path used by components, we want to be able to run async work but only dispatch + // if necessary. + private void ExecuteSynchronouslyIfPossible(SendOrPostCallback d, object state) + { + TaskCompletionSource completion; + lock (_state.Lock) + { + if (!_state.Task.IsCompleted) + { + _state.Task = Enqueue(_state.Task, d, state); + return; + } + + // We can execute this synchronously because nothing is currently running + // or queued. + completion = new TaskCompletionSource(); + _state.Task = completion.Task; + } + + ExecuteSynchronously(completion, d, state); + } + + private Task Enqueue(Task antecedent, SendOrPostCallback d, object state, bool forceAsync = false) + { + // If we get here is means that a callback is being explicitly queued. Let's instead add it to the queue and yield. + // + // We use our own queue here to maintain the execution order of the callbacks scheduled here. Also + // we need a queue rather than just scheduling an item in the thread pool - those items would immediately + // block and hurt scalability. + // + // We need to capture the execution context so we can restore it later. This code is similar to + // the call path of ThreadPool.QueueUserWorkItem and System.Threading.QueueUserWorkItemCallback. + ExecutionContext executionContext = null; + if (!ExecutionContext.IsFlowSuppressed()) + { + executionContext = ExecutionContext.Capture(); + } + + var flags = forceAsync ? TaskContinuationOptions.RunContinuationsAsynchronously : TaskContinuationOptions.None; + return antecedent.ContinueWith(BackgroundWorkThunk, new WorkItem() + { + SynchronizationContext = this, + ExecutionContext = executionContext, + Callback = d, + State = state, + }, CancellationToken.None, flags, TaskScheduler.Current); + } + + private void ExecuteSynchronously( + TaskCompletionSource completion, + SendOrPostCallback d, + object state) + { + // Anything run on the sync context should actually be dispatched as far as Photino + // is concerned, so that it's safe to interact with the native window/WebView. + _invokeMethodInfo.Invoke(_window, new Action[] { () => + { + var original = Current; + try + { + _state.IsBusy = true; + SetSynchronizationContext(this); + d(state); + } + finally + { + _state.IsBusy = false; + SetSynchronizationContext(original); + + completion?.SetResult(null); + } + }}); + } + + private void ExecuteBackground(WorkItem item) + { + if (item.ExecutionContext == null) + { + try + { + ExecuteSynchronously(null, item.Callback, item.State); + } + catch (Exception ex) + { + DispatchException(ex); + } + + return; + } + + // Perf - using a static thunk here to avoid a delegate allocation. + try + { + ExecutionContext.Run(item.ExecutionContext, ExecutionContextThunk, item); + } + catch (Exception ex) + { + DispatchException(ex); + } + } + + private void DispatchException(Exception ex) + { + var handler = UnhandledException; + if (handler != null) + { + handler(this, new UnhandledExceptionEventArgs(ex, isTerminating: false)); + } + } + + private class State + { + public bool IsBusy; // Just for debugging + public object Lock = new object(); + public Task Task = Task.CompletedTask; + + public override string ToString() + { + return $"{{ Busy: {IsBusy}, Pending Task: {Task} }}"; + } + } + + private class WorkItem + { + public PhotinoSynchronizationContext SynchronizationContext; + public ExecutionContext ExecutionContext; + public SendOrPostCallback Callback; + public object State; + } + + private class PhotinoSynchronizationTaskCompletionSource : TaskCompletionSource + { + public PhotinoSynchronizationTaskCompletionSource(TCallback callback) + { + Callback = callback; + } + + public TCallback Callback { get; } + } + } +} diff --git a/src/Components/WebView/Samples/PhotinoPlatform/src/PhotinoWebViewManager.cs b/src/Components/WebView/Samples/PhotinoPlatform/src/PhotinoWebViewManager.cs new file mode 100644 index 000000000000..88e2a375392c --- /dev/null +++ b/src/Components/WebView/Samples/PhotinoPlatform/src/PhotinoWebViewManager.cs @@ -0,0 +1,76 @@ +// 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.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.Extensions.FileProviders; +using PhotinoNET; + +namespace Microsoft.AspNetCore.Components.WebView.Photino +{ + internal class PhotinoWebViewManager : WebViewManager + { + private readonly PhotinoWindow _window; + + // On Windows, we can't use a custom scheme to host the initial HTML, + // because webview2 won't let you do top-level navigation to such a URL. + // On Linux/Mac, we must use a custom scheme, because their webviews + // don't have a way to intercept http:// scheme requests. + internal readonly static string BlazorAppScheme = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? "http" + : "app"; + + internal readonly static string AppBaseUri + = $"{BlazorAppScheme}://0.0.0.0/"; + + public PhotinoWebViewManager(PhotinoWindow window, IServiceProvider provider, Dispatcher dispatcher, Uri appBaseUri, IFileProvider fileProvider, string hostPageRelativePath) + : base(provider, dispatcher, appBaseUri, fileProvider, hostPageRelativePath) + { + _window = window ?? throw new ArgumentNullException(nameof(window)); + _window.WebMessageReceived += (sender, message) => + { + // On some platforms, we need to move off the browser UI thread + Task.Factory.StartNew(message => + { + // TODO: Fix this. Photino should ideally tell us the URL that the message comes from so we + // know whether to trust it. Currently it's hardcoded to trust messages from any source, including + // if the webview is somehow navigated to an external URL. + var messageOriginUrl = new Uri(AppBaseUri); + + MessageReceived(messageOriginUrl, (string)message!); + }, message); + }; + } + + public Stream? HandleWebRequest(string url, out string? contentType) + { + // It would be better if we were told whether or not this is a navigation request, but + // since we're not, guess. + var hasFileExtension = url.LastIndexOf('.') > url.LastIndexOf('/'); + + if (url.StartsWith(AppBaseUri, StringComparison.Ordinal) + && TryGetResponseContent(url, !hasFileExtension, out var statusCode, out var statusMessage, out var content, out var headers)) + { + headers.TryGetValue("Content-Type", out contentType); + return content; + } + else + { + contentType = default; + return null; + } + } + + protected override void NavigateCore(Uri absoluteUri) + { + _window.Load(absoluteUri); + } + + protected override void SendMessage(string message) + { + _window.SendWebMessage(message); + } + } +} diff --git a/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/PhotinoTestApp.csproj b/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/PhotinoTestApp.csproj new file mode 100644 index 000000000000..b9bdce6a5ea5 --- /dev/null +++ b/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/PhotinoTestApp.csproj @@ -0,0 +1,21 @@ + + + + $(DefaultNetCoreTargetFramework) + WinExe + false + false + + + + + + + + + + PreserveNewest + + + + diff --git a/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/Program.cs b/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/Program.cs new file mode 100644 index 000000000000..6d81b6ca2890 --- /dev/null +++ b/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/Program.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Components.WebView.Photino; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Net.Http; + +namespace PhotinoTestApp +{ + class Program + { + [STAThread] + static void Main(string[] args) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddBlazorWebView(); + serviceCollection.AddSingleton(); + + var mainWindow = new BlazorWindow( + title: "Hello, world!", + hostPage: "wwwroot/webviewhost.html", + services: serviceCollection.BuildServiceProvider()); + + AppDomain.CurrentDomain.UnhandledException += (sender, error) => + { + mainWindow.Photino.OpenAlertWindow("Fatal exception", error.ExceptionObject.ToString()); + }; + + mainWindow.AddRootComponent("root"); + + mainWindow.Run(); + } + } +} diff --git a/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/wwwroot/css/app.css b/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/wwwroot/css/app.css new file mode 100644 index 000000000000..ec67cdd4e2d6 --- /dev/null +++ b/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/wwwroot/css/app.css @@ -0,0 +1,22 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} + +html { + font-family: Arial, Helvetica, sans-serif; +} diff --git a/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/wwwroot/webviewhost.html b/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/wwwroot/webviewhost.html new file mode 100644 index 000000000000..93c81beeba2a --- /dev/null +++ b/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/wwwroot/webviewhost.html @@ -0,0 +1,56 @@ + + + + + + + PhotinoTestApp + + + + + + + + + + + + + Loading... + + + + + + + + + + + + + + + + + +