From b0852cb22b1032ba518c81971a38190fdf9f377d Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Mon, 1 Jul 2024 14:46:19 +0500 Subject: [PATCH] code refactor --- README.md | 4 +- .../AutoCloseOnReadCompleteStream.cs | 67 +- .../BlazorWebView.WinUI.csproj | 2 +- src/BlazorWebView.WinUI/BlazorWebView.cs | 579 ++++++++-------- .../BlazorWebViewDeveloperTools.cs | 9 +- .../BlazorWebViewInitializedEventArgs.cs | 21 +- .../BlazorWebViewInitializingEventArgs.cs | 37 +- ...lazorWebViewServiceCollectionExtensions.cs | 55 +- .../IWinUIBlazorWebViewBuilder.cs | 24 +- src/BlazorWebView.WinUI/Log.cs | 76 +-- src/BlazorWebView.WinUI/QueryStringHelper.cs | 34 +- src/BlazorWebView.WinUI/RootComponent.cs | 96 ++- .../RootComponentsCollection.cs | 22 +- .../StaticContentHotReloadManager.cs | 273 ++++---- .../StaticContentProvider.cs | 474 +++++++++++++ .../UrlLoadingEventArgs.cs | 73 +-- src/BlazorWebView.WinUI/UrlLoadingStrategy.cs | 53 +- .../WebView2WebViewManager.cs | 620 ++++++++++-------- .../WinUIBlazorMarkerService.cs | 8 +- .../WinUIBlazorWebViewBuilder.cs | 20 +- src/BlazorWebView.WinUI/WinUIDispatcher.cs | 123 ++-- 21 files changed, 1585 insertions(+), 1085 deletions(-) create mode 100644 src/BlazorWebView.WinUI/StaticContentProvider.cs diff --git a/README.md b/README.md index fa08772..3f711b8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# BlazorWebView.WinUI3 -A clone of WPF BlazorWebView to use in WinUI3 +# BlazorWebView.WinUI + Build WinUI apps with Blazor and WebView2. https://github.com/w-ahmad/BlazorWebView.WinUI3/assets/17172092/ee581dab-cd60-49e8-b11e-ff7ca23784c2 diff --git a/src/BlazorWebView.WinUI/AutoCloseOnReadCompleteStream.cs b/src/BlazorWebView.WinUI/AutoCloseOnReadCompleteStream.cs index 363bbf7..ce0cda3 100644 --- a/src/BlazorWebView.WinUI/AutoCloseOnReadCompleteStream.cs +++ b/src/BlazorWebView.WinUI/AutoCloseOnReadCompleteStream.cs @@ -1,47 +1,58 @@ using System.IO; -namespace Microsoft.AspNetCore.Components.WebView.WebView2 +namespace Microsoft.AspNetCore.Components.WebView.WebView2; + +internal class AutoCloseOnReadCompleteStream : Stream { - internal class AutoCloseOnReadCompleteStream : Stream + private readonly Stream _baseStream; + + public AutoCloseOnReadCompleteStream(Stream baseStream) { - private readonly Stream _baseStream; + _baseStream = baseStream; + } - public AutoCloseOnReadCompleteStream(Stream baseStream) - { - _baseStream = baseStream; - } + public override bool CanRead => _baseStream.CanRead; - public override bool CanRead => _baseStream.CanRead; + public override bool CanSeek => _baseStream.CanSeek; - public override bool CanSeek => _baseStream.CanSeek; + public override bool CanWrite => _baseStream.CanWrite; - public override bool CanWrite => _baseStream.CanWrite; + public override long Length => _baseStream.Length; - public override long Length => _baseStream.Length; + public override long Position { get => _baseStream.Position; set => _baseStream.Position = value; } - public override long Position { get => _baseStream.Position; set => _baseStream.Position = value; } + public override void Flush() + { + _baseStream.Flush(); + } - public override void Flush() => _baseStream.Flush(); + public override int Read(byte[] buffer, int offset, int count) + { + var bytesRead = _baseStream.Read(buffer, offset, count); - public override int Read(byte[] buffer, int offset, int count) + // Stream.Read only returns 0 when it has reached the end of stream + // and no further bytes are expected. Otherwise it blocks until + // one or more (and at most count) bytes can be read. + if (bytesRead == 0) { - var bytesRead = _baseStream.Read(buffer, offset, count); - - // Stream.Read only returns 0 when it has reached the end of stream - // and no further bytes are expected. Otherwise it blocks until - // one or more (and at most count) bytes can be read. - if (bytesRead == 0) - { - _baseStream.Close(); - } - - return bytesRead; + _baseStream.Close(); } - public override long Seek(long offset, SeekOrigin origin) => _baseStream.Seek(offset, origin); + return bytesRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _baseStream.Seek(offset, origin); + } - public override void SetLength(long value) => _baseStream.SetLength(value); + public override void SetLength(long value) + { + _baseStream.SetLength(value); + } - public override void Write(byte[] buffer, int offset, int count) => _baseStream.Write(buffer, offset, count); + public override void Write(byte[] buffer, int offset, int count) + { + _baseStream.Write(buffer, offset, count); } } diff --git a/src/BlazorWebView.WinUI/BlazorWebView.WinUI.csproj b/src/BlazorWebView.WinUI/BlazorWebView.WinUI.csproj index 184a04d..387ad8f 100644 --- a/src/BlazorWebView.WinUI/BlazorWebView.WinUI.csproj +++ b/src/BlazorWebView.WinUI/BlazorWebView.WinUI.csproj @@ -1,6 +1,6 @@  - net8.0-windows10.0.19041.0 + net6.0-windows10.0.19041.0 10.0.17763.0 Microsoft.AspNetCore.Components.WebView.WinUI win-x86;win-x64;win-arm64 diff --git a/src/BlazorWebView.WinUI/BlazorWebView.cs b/src/BlazorWebView.WinUI/BlazorWebView.cs index 06f54e8..48e5b4a 100644 --- a/src/BlazorWebView.WinUI/BlazorWebView.cs +++ b/src/BlazorWebView.WinUI/BlazorWebView.cs @@ -1,7 +1,4 @@ -// 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; using System.Collections.Specialized; using System.ComponentModel; using System.IO; @@ -17,364 +14,322 @@ using Microsoft.UI.Xaml.Controls; using WebView2Control = Microsoft.UI.Xaml.Controls.WebView2; -namespace Microsoft.AspNetCore.Components.WebView.WinUI +namespace Microsoft.AspNetCore.Components.WebView.WinUI; + +/// +/// A WinUI control for hosting Razor components locally in Windows desktop applications. +/// +public class BlazorWebView : Control, IAsyncDisposable { + #region Dependency property definitions /// - /// A Windows Presentation Foundation (WPF) control for hosting Razor components locally in Windows desktop applications. + /// The backing store for the property. /// - public class BlazorWebView : Control, IAsyncDisposable - { - #region Dependency property definitions - /// - /// The backing store for the property. - /// - public static readonly DependencyProperty HostPageProperty = DependencyProperty.Register( - name: nameof(HostPage), - propertyType: typeof(string), - ownerType: typeof(BlazorWebView), - typeMetadata: new PropertyMetadata(default, OnHostPagePropertyChanged)); - - /// - /// The backing store for the property. - /// - public static readonly DependencyProperty StartPathProperty = DependencyProperty.Register( - name: nameof(StartPath), - propertyType: typeof(string), - ownerType: typeof(BlazorWebView), - typeMetadata: new PropertyMetadata("/")); - - /// - /// The backing store for the property. - /// - public static readonly DependencyProperty RootComponentsProperty = DependencyProperty.Register( - name: nameof(RootComponents), - propertyType: typeof(RootComponentsCollection), - ownerType: typeof(BlazorWebView), - typeMetadata: new PropertyMetadata(default)); - - /// - /// The backing store for the property. - /// - public static readonly DependencyProperty ServicesProperty = DependencyProperty.Register( - name: nameof(Services), - propertyType: typeof(IServiceProvider), - ownerType: typeof(BlazorWebView), - typeMetadata: new PropertyMetadata(default, OnServicesPropertyChanged)); - - /// - /// The backing store for the property. - /// - public static readonly DependencyProperty UrlLoadingProperty = DependencyProperty.Register( - name: nameof(UrlLoading), - propertyType: typeof(EventHandler), - ownerType: typeof(BlazorWebView), - typeMetadata: new PropertyMetadata(default)); - - /// - /// The backing store for the event. - /// - public static readonly DependencyProperty BlazorWebViewInitializingProperty = DependencyProperty.Register( - name: nameof(BlazorWebViewInitializing), - propertyType: typeof(EventHandler), - ownerType: typeof(BlazorWebView), - typeMetadata: new PropertyMetadata(default)); - - /// - /// The backing store for the event. - /// - public static readonly DependencyProperty BlazorWebViewInitializedProperty = DependencyProperty.Register( - name: nameof(BlazorWebViewInitialized), - propertyType: typeof(EventHandler), - ownerType: typeof(BlazorWebView), - typeMetadata: new PropertyMetadata(default)); - - #endregion - - private const string WebViewTemplateChildName = "WebView"; - private WebView2Control? _webview; - private WebView2WebViewManager? _webviewManager; - private bool _isDisposed; - - /// - /// Creates a new instance of . - /// - public BlazorWebView() - { - DefaultStyleKey = typeof(BlazorWebView); + public static readonly DependencyProperty HostPageProperty = DependencyProperty.Register( + name: nameof(HostPage), + propertyType: typeof(string), + ownerType: typeof(BlazorWebView), + typeMetadata: new PropertyMetadata(default, OnHostPagePropertyChanged)); - ComponentsDispatcher = new WinUIDispatcher(DispatcherQueue); + /// + /// The backing store for the property. + /// + public static readonly DependencyProperty StartPathProperty = DependencyProperty.Register( + name: nameof(StartPath), + propertyType: typeof(string), + ownerType: typeof(BlazorWebView), + typeMetadata: new PropertyMetadata("/")); - SetValue(RootComponentsProperty, new RootComponentsCollection()); - RootComponents.CollectionChanged += HandleRootComponentsCollectionChanged; - } + /// + /// The backing store for the property. + /// + public static readonly DependencyProperty RootComponentsProperty = DependencyProperty.Register( + name: nameof(RootComponents), + propertyType: typeof(RootComponentsCollection), + ownerType: typeof(BlazorWebView), + typeMetadata: new PropertyMetadata(default)); - /// - /// Returns the inner used by this control. - /// - /// - /// Directly using some functionality of the inner web view can cause unexpected results because its behavior - /// is controlled by the that is hosting it. - /// - [Browsable(false)] - public WebView2Control WebView => _webview!; - - /// - /// Path to the host page within the application's static files. For example, wwwroot\index.html. - /// This property must be set to a valid value for the Razor components to start. - /// - public string HostPage - { - get => (string)GetValue(HostPageProperty); - set => SetValue(HostPageProperty, value); - } + /// + /// The backing store for the property. + /// + public static readonly DependencyProperty ServicesProperty = DependencyProperty.Register( + name: nameof(Services), + propertyType: typeof(IServiceProvider), + ownerType: typeof(BlazorWebView), + typeMetadata: new PropertyMetadata(default, OnServicesPropertyChanged)); + #endregion + + private const string WebViewTemplateChildName = "WebView"; + private WebView2Control? _webview; + private WebView2WebViewManager? _webviewManager; + private bool _isDisposed; - /// - /// Path for initial Blazor navigation when the Blazor component is finished loading. - /// - public string StartPath - { - get => (string)GetValue(StartPathProperty); - set => SetValue(StartPathProperty, value); - } + /// + /// Creates a new instance of . + /// + public BlazorWebView() + { + DefaultStyleKey = typeof(BlazorWebView); - /// - /// A collection of instances that specify the Blazor types - /// to be used directly in the specified . - /// - public RootComponentsCollection RootComponents => - (RootComponentsCollection)GetValue(RootComponentsProperty); - - /// - /// Allows customizing how links are opened. - /// By default, opens internal links in the webview and external links in an external app. - /// - public EventHandler UrlLoading - { - get => (EventHandler)GetValue(UrlLoadingProperty); - set => SetValue(UrlLoadingProperty, value); - } + ComponentsDispatcher = new WinUIDispatcher(DispatcherQueue); - /// - /// Allows customizing the web view before it is created. - /// - public EventHandler BlazorWebViewInitializing - { - get => (EventHandler)GetValue(BlazorWebViewInitializingProperty); - set => SetValue(BlazorWebViewInitializingProperty, value); - } + SetValue(RootComponentsProperty, new RootComponentsCollection()); + RootComponents.CollectionChanged += HandleRootComponentsCollectionChanged; + } - /// - /// Allows customizing the web view after it is created. - /// - public EventHandler BlazorWebViewInitialized - { - get => (EventHandler)GetValue(BlazorWebViewInitializedProperty); - set => SetValue(BlazorWebViewInitializedProperty, value); - } + /// + /// Returns the inner used by this control. + /// + /// + /// Directly using some functionality of the inner web view can cause unexpected results because its behavior + /// is controlled by the that is hosting it. + /// + [Browsable(false)] + public WebView2Control WebView => _webview!; - /// - /// Gets or sets an containing services to be used by this control and also by application code. - /// This property must be set to a valid value for the Razor components to start. - /// - public IServiceProvider Services - { - get => (IServiceProvider)GetValue(ServicesProperty); - set => SetValue(ServicesProperty, value); - } + /// + /// Path to the host page within the application's static files. For example, wwwroot\index.html. + /// This property must be set to a valid value for the Razor components to start. + /// + public string HostPage + { + get => (string)GetValue(HostPageProperty); + set => SetValue(HostPageProperty, value); + } - private static void OnServicesPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((BlazorWebView)d).OnServicesPropertyChanged(e); + /// + /// Path for initial Blazor navigation when the Blazor component is finished loading. + /// + public string StartPath + { + get => (string)GetValue(StartPathProperty); + set => SetValue(StartPathProperty, value); + } - private void OnServicesPropertyChanged(DependencyPropertyChangedEventArgs e) => StartWebViewCoreIfPossible(); + /// + /// A collection of instances that specify the Blazor types + /// to be used directly in the specified . + /// + public RootComponentsCollection RootComponents => (RootComponentsCollection)GetValue(RootComponentsProperty); - private static void OnHostPagePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((BlazorWebView)d).OnHostPagePropertyChanged(e); + /// + /// Allows customizing how links are opened. + /// By default, opens internal links in the webview and external links in an external app. + /// + public event EventHandler? UrlLoading; - private void OnHostPagePropertyChanged(DependencyPropertyChangedEventArgs e) => StartWebViewCoreIfPossible(); + /// + /// Allows customizing the web view before it is created. + /// + public event EventHandler? BlazorWebViewInitializing; + /// + /// Allows customizing the web view after it is created. + /// + public event EventHandler? BlazorWebViewInitialized; + /// + /// Gets or sets an containing services to be used by this control and also by application code. + /// This property must be set to a valid value for the Razor components to start. + /// + public IServiceProvider Services + { + get => (IServiceProvider)GetValue(ServicesProperty); + set => SetValue(ServicesProperty, value); + } - private bool RequiredStartupPropertiesSet => _webview != null && HostPage != null && Services != null; + private static void OnServicesPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((BlazorWebView)d).OnServicesPropertyChanged(e); + } - /// - protected override void OnApplyTemplate() - { - CheckDisposed(); + private void OnServicesPropertyChanged(DependencyPropertyChangedEventArgs e) + { + StartWebViewCoreIfPossible(); + } - // Called when the control is created after its child control (the WebView2) is created from the Template property - base.OnApplyTemplate(); + private static void OnHostPagePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((BlazorWebView)d).OnHostPagePropertyChanged(e); + } - if (_webview == null) - { - _webview = (WebView2Control)GetTemplateChild(WebViewTemplateChildName); - StartWebViewCoreIfPossible(); - } - } + private void OnHostPagePropertyChanged(DependencyPropertyChangedEventArgs e) + { + StartWebViewCoreIfPossible(); + } - private void StartWebViewCoreIfPossible() - { - CheckDisposed(); + private bool RequiredStartupPropertiesSet => _webview != null && HostPage != null && Services != null; - if (!RequiredStartupPropertiesSet || _webviewManager != null) - { - return; - } + /// + protected override void OnApplyTemplate() + { + CheckDisposed(); - var logger = Services.GetService>() ?? NullLogger.Instance; + // Called when the control is created after its child control (the WebView2) is created from the Template property + base.OnApplyTemplate(); - // 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. - string appRootDir; - var entryAssemblyLocation = Assembly.GetEntryAssembly()?.Location; - if (!string.IsNullOrEmpty(entryAssemblyLocation)) - { - appRootDir = Path.GetDirectoryName(entryAssemblyLocation)!; - } - else - { - appRootDir = Environment.CurrentDirectory; - } - var hostPageFullPath = Path.GetFullPath(Path.Combine(appRootDir, HostPage)); - var contentRootDirFullPath = Path.GetDirectoryName(hostPageFullPath)!; - var hostPageRelativePath = Path.GetRelativePath(contentRootDirFullPath, hostPageFullPath); - var contentRootDirRelativePath = Path.GetRelativePath(appRootDir, contentRootDirFullPath); - - logger.CreatingFileProvider(contentRootDirFullPath, hostPageRelativePath); - var fileProvider = CreateFileProvider(contentRootDirFullPath); - - _webviewManager = new WebView2WebViewManager( - _webview!, - Services, - ComponentsDispatcher, - fileProvider, - RootComponents.JSComponents, - contentRootDirRelativePath, - hostPageRelativePath, - (args) => UrlLoading?.Invoke(this, args), - (args) => BlazorWebViewInitializing?.Invoke(this, args), - (args) => BlazorWebViewInitialized?.Invoke(this, args), - logger); - - StaticContentHotReloadManager.AttachToWebViewManagerIfEnabled(_webviewManager); - - foreach (var rootComponent in RootComponents) - { - logger.AddingRootComponent(rootComponent.ComponentType.FullName ?? string.Empty, rootComponent.Selector, rootComponent.Parameters?.Count ?? 0); + if (_webview == null) + { + _webview = (WebView2Control)GetTemplateChild(WebViewTemplateChildName); + StartWebViewCoreIfPossible(); + } + } - // Since the page isn't loaded yet, this will always complete synchronously - _ = rootComponent.AddToWebViewManagerAsync(_webviewManager); - } + private void StartWebViewCoreIfPossible() + { + CheckDisposed(); - logger.StartingInitialNavigation(StartPath); - _webviewManager.Navigate(StartPath); + if (!RequiredStartupPropertiesSet || _webviewManager != null) + { + return; } - public void Navigate(string path) + var logger = Services.GetService>() ?? NullLogger.Instance; + + // 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 entryAssemblyLocation = Assembly.GetEntryAssembly()?.Location; + var appRootDir = !string.IsNullOrEmpty(entryAssemblyLocation) ? Path.GetDirectoryName(entryAssemblyLocation)! : AppContext.BaseDirectory; + var hostPageFullPath = Path.GetFullPath(Path.Combine(appRootDir, HostPage)); + var contentRootDirFullPath = Path.GetDirectoryName(hostPageFullPath)!; + var hostPageRelativePath = Path.GetRelativePath(contentRootDirFullPath, hostPageFullPath); + var contentRootDirRelativePath = Path.GetRelativePath(appRootDir, contentRootDirFullPath); + + logger.CreatingFileProvider(contentRootDirFullPath, hostPageRelativePath); + var fileProvider = CreateFileProvider(contentRootDirFullPath); + + _webviewManager = new WebView2WebViewManager( + _webview!, + Services, + ComponentsDispatcher, + fileProvider, + RootComponents.JSComponents, + contentRootDirRelativePath, + hostPageRelativePath, + (args) => UrlLoading?.Invoke(this, args), + (args) => BlazorWebViewInitializing?.Invoke(this, args), + (args) => BlazorWebViewInitialized?.Invoke(this, args), + logger); + + StaticContentHotReloadManager.AttachToWebViewManagerIfEnabled(_webviewManager); + + foreach (var rootComponent in RootComponents) { - _webviewManager?.Navigate(path); + logger.AddingRootComponent(rootComponent.ComponentType.FullName ?? string.Empty, rootComponent.Selector, rootComponent.Parameters?.Count ?? 0); + + // Since the page isn't loaded yet, this will always complete synchronously + _ = rootComponent.AddToWebViewManagerAsync(_webviewManager); } - private WinUIDispatcher ComponentsDispatcher { get; } + logger.StartingInitialNavigation(StartPath); + _webviewManager.Navigate(StartPath); + } - private void HandleRootComponentsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs eventArgs) - { - CheckDisposed(); + public void Navigate(string path) + { + _webviewManager?.Navigate(path); + } - // If we haven't initialized yet, this is a no-op - if (_webviewManager != null) + private WinUIDispatcher ComponentsDispatcher { get; } + + private void HandleRootComponentsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs eventArgs) + { + CheckDisposed(); + + // If we haven't initialized yet, this is a no-op + if (_webviewManager != null) + { + // Dispatch because this is going to be async, and we want to catch any errors + _ = ComponentsDispatcher.InvokeAsync(async () => { - // Dispatch because this is going to be async, and we want to catch any errors - _ = ComponentsDispatcher.InvokeAsync(async () => + var newItems = (eventArgs.NewItems ?? Array.Empty()).Cast(); + var oldItems = (eventArgs.OldItems ?? Array.Empty()).Cast(); + + foreach (var item in newItems.Except(oldItems)) + { + await item.AddToWebViewManagerAsync(_webviewManager); + } + + foreach (var item in oldItems.Except(newItems)) { - var newItems = (eventArgs.NewItems ?? Array.Empty()).Cast(); - var oldItems = (eventArgs.OldItems ?? Array.Empty()).Cast(); - - foreach (var item in newItems.Except(oldItems)) - { - await item.AddToWebViewManagerAsync(_webviewManager); - } - - foreach (var item in oldItems.Except(newItems)) - { - await item.RemoveFromWebViewManagerAsync(_webviewManager); - } - }); - } + await item.RemoveFromWebViewManagerAsync(_webviewManager); + } + }); } + } - /// - /// Creates a file provider for static assets used in the . The default implementation - /// serves files from disk. Override this method to return a custom to serve assets such - /// as wwwroot/index.html. Call the base method and combine its return value with a - /// to use both custom assets and default assets. - /// - /// The base directory to use for all requested assets, such as wwwroot. - /// Returns a for static assets. - public virtual IFileProvider CreateFileProvider(string contentRootDir) + /// + /// Creates a file provider for static assets used in the . The default implementation + /// serves files from disk. Override this method to return a custom to serve assets such + /// as wwwroot/index.html. Call the base method and combine its return value with a + /// to use both custom assets and default assets. + /// + /// The base directory to use for all requested assets, such as wwwroot. + /// Returns a for static assets. + public virtual IFileProvider CreateFileProvider(string contentRootDir) + { + if (Directory.Exists(contentRootDir)) { - if (Directory.Exists(contentRootDir)) - { - // Typical case after publishing, or if you're copying content to the bin dir in development for some nonstandard reason - return new PhysicalFileProvider(contentRootDir); - } - else - { - // Typical case in development, as the files come from Microsoft.AspNetCore.Components.WebView.StaticContentProvider - // instead and aren't copied to the bin dir - return new NullFileProvider(); - } + // Typical case after publishing, or if you're copying content to the bin dir in development for some nonstandard reason + return new PhysicalFileProvider(contentRootDir); } - - /// - /// Calls the specified asynchronously and passes in the scoped services available to Razor components. - /// - /// The action to call. - /// Returns a representing true if the was called, or false if it was not called because Blazor is not currently running. - /// Thrown if is null. - public virtual Task TryDispatchAsync(Action workItem) + else { - throw new NotImplementedException(); + // Typical case in development, as the files come from Microsoft.AspNetCore.Components.WebView.StaticContentProvider + // instead and aren't copied to the bin dir + return new NullFileProvider(); } + } - private void CheckDisposed() + /// + /// Calls the specified asynchronously and passes in the scoped services available to Razor components. + /// + /// The action to call. + /// Returns a representing true if the was called, or false if it was not called because Blazor is not currently running. + /// Thrown if is null. + public virtual Task TryDispatchAsync(Action workItem) + { + throw new NotImplementedException(); + } + + private void CheckDisposed() + { + if (_isDisposed) { - if (_isDisposed) - { - throw new ObjectDisposedException(GetType().Name); - } + throw new ObjectDisposedException(GetType().Name); } + } - /// - /// Allows asynchronous disposal of the . - /// - protected virtual async ValueTask DisposeAsyncCore() + /// + /// Allows asynchronous disposal of the . + /// + protected virtual async ValueTask DisposeAsyncCore() + { + // Dispose this component's contents that user-written disposal logic and Razor component disposal logic will + // complete first. Then dispose the WebView2 control. This order is critical because once the WebView2 is + // disposed it will prevent and Razor component code from working because it requires the WebView to exist. + if (_webviewManager != null) { - // Dispose this component's contents that user-written disposal logic and Razor component disposal logic will - // complete first. Then dispose the WebView2 control. This order is critical because once the WebView2 is - // disposed it will prevent and Razor component code from working because it requires the WebView to exist. - if (_webviewManager != null) - { - await _webviewManager.DisposeAsync() - .ConfigureAwait(false); - _webviewManager = null; - } - - _webview = null; + await _webviewManager.DisposeAsync() + .ConfigureAwait(false); + _webviewManager = null; } - /// - public async ValueTask DisposeAsync() + _webview = null; + } + + /// + public async ValueTask DisposeAsync() + { + if (_isDisposed) { - if (_isDisposed) - { - return; - } - _isDisposed = true; + return; + } + _isDisposed = true; - // Perform async cleanup. - await DisposeAsyncCore(); + // Perform async cleanup. + await DisposeAsyncCore(); -#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize - // Suppress finalization. - GC.SuppressFinalize(this); -#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize - } + // Suppress finalization. + GC.SuppressFinalize(this); } } diff --git a/src/BlazorWebView.WinUI/BlazorWebViewDeveloperTools.cs b/src/BlazorWebView.WinUI/BlazorWebViewDeveloperTools.cs index 22c65c7..9efa34e 100644 --- a/src/BlazorWebView.WinUI/BlazorWebViewDeveloperTools.cs +++ b/src/BlazorWebView.WinUI/BlazorWebViewDeveloperTools.cs @@ -1,7 +1,6 @@ -namespace Microsoft.AspNetCore.Components.WebView.WinUI +namespace Microsoft.AspNetCore.Components.WebView.WinUI; + +internal class BlazorWebViewDeveloperTools { - internal class BlazorWebViewDeveloperTools - { - public bool Enabled { get; set; } = false; - } + public bool Enabled { get; set; } = false; } diff --git a/src/BlazorWebView.WinUI/BlazorWebViewInitializedEventArgs.cs b/src/BlazorWebView.WinUI/BlazorWebViewInitializedEventArgs.cs index 66cab71..fef725a 100644 --- a/src/BlazorWebView.WinUI/BlazorWebViewInitializedEventArgs.cs +++ b/src/BlazorWebView.WinUI/BlazorWebViewInitializedEventArgs.cs @@ -1,17 +1,16 @@ using System; using WebView2Control = Microsoft.UI.Xaml.Controls.WebView2; -namespace Microsoft.AspNetCore.Components.WebView +namespace Microsoft.AspNetCore.Components.WebView; + +/// +/// Allows configuring the underlying web view after it has been initialized. +/// +public class BlazorWebViewInitializedEventArgs : EventArgs { - /// - /// Allows configuring the underlying web view after it has been initialized. - /// - public class BlazorWebViewInitializedEventArgs : EventArgs - { #nullable disable - /// - /// Gets the instance that was initialized. - /// - public WebView2Control WebView { get; internal set; } - } + /// + /// Gets the instance that was initialized. + /// + public WebView2Control WebView { get; internal set; } } diff --git a/src/BlazorWebView.WinUI/BlazorWebViewInitializingEventArgs.cs b/src/BlazorWebView.WinUI/BlazorWebViewInitializingEventArgs.cs index 8e458c1..6fb7fef 100644 --- a/src/BlazorWebView.WinUI/BlazorWebViewInitializingEventArgs.cs +++ b/src/BlazorWebView.WinUI/BlazorWebViewInitializingEventArgs.cs @@ -1,27 +1,26 @@ using System; using Microsoft.Web.WebView2.Core; -namespace Microsoft.AspNetCore.Components.WebView +namespace Microsoft.AspNetCore.Components.WebView; + +/// +/// Allows configuring the underlying web view when the application is initializing. +/// +public class BlazorWebViewInitializingEventArgs : EventArgs { - /// - /// Allows configuring the underlying web view when the application is initializing. - /// - public class BlazorWebViewInitializingEventArgs : EventArgs - { #nullable disable - /// - /// Gets or sets the browser executable folder path for the . - /// - public string BrowserExecutableFolder { get; set; } + /// + /// Gets or sets the browser executable folder path for the . + /// + public string BrowserExecutableFolder { get; set; } - /// - /// Gets or sets the user data folder path for the . - /// - public string UserDataFolder { get; set; } + /// + /// Gets or sets the user data folder path for the . + /// + public string UserDataFolder { get; set; } - /// - /// Gets or sets the environment options for the . - /// - public CoreWebView2EnvironmentOptions EnvironmentOptions { get; set; } - } + /// + /// Gets or sets the environment options for the . + /// + public CoreWebView2EnvironmentOptions EnvironmentOptions { get; set; } } diff --git a/src/BlazorWebView.WinUI/BlazorWebViewServiceCollectionExtensions.cs b/src/BlazorWebView.WinUI/BlazorWebViewServiceCollectionExtensions.cs index 84cfd31..d9139dc 100644 --- a/src/BlazorWebView.WinUI/BlazorWebViewServiceCollectionExtensions.cs +++ b/src/BlazorWebView.WinUI/BlazorWebViewServiceCollectionExtensions.cs @@ -1,34 +1,33 @@ using Microsoft.AspNetCore.Components.WebView.WinUI; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods to . +/// +public static class BlazorWebViewServiceCollectionExtensions { - /// - /// Extension methods to . - /// - public static class BlazorWebViewServiceCollectionExtensions - { - /// - /// Configures to add support for . - /// - /// The . - /// The . - public static IWinUIBlazorWebViewBuilder AddWpfBlazorWebView(this IServiceCollection services) - { - services.AddBlazorWebView(); - services.TryAddSingleton(new BlazorWebViewDeveloperTools { Enabled = false }); - services.TryAddSingleton(_ => new WinUIBlazorMarkerService()); - return new WinUIBlazorWebViewBuilder(services); - } + /// + /// Configures to add support for . + /// + /// The . + /// The . + public static IWinUIBlazorWebViewBuilder AddWinUIBlazorWebView(this IServiceCollection services) + { + services.AddBlazorWebView(); + services.TryAddSingleton(new BlazorWebViewDeveloperTools { Enabled = false }); + services.TryAddSingleton(_ => new WinUIBlazorMarkerService()); + return new WinUIBlazorWebViewBuilder(services); + } - /// - /// Enables Developer tools on the underlying WebView controls. - /// - /// The . - /// The . - public static IServiceCollection AddBlazorWebViewDeveloperTools(this IServiceCollection services) - { - return services.AddSingleton(new BlazorWebViewDeveloperTools { Enabled = true }); - } - } + /// + /// Enables Developer tools on the underlying WebView controls. + /// + /// The . + /// The . + public static IServiceCollection AddBlazorWebViewDeveloperTools(this IServiceCollection services) + { + return services.AddSingleton(new BlazorWebViewDeveloperTools { Enabled = true }); + } } diff --git a/src/BlazorWebView.WinUI/IWinUIBlazorWebViewBuilder.cs b/src/BlazorWebView.WinUI/IWinUIBlazorWebViewBuilder.cs index f0c07e3..5a6a0ab 100644 --- a/src/BlazorWebView.WinUI/IWinUIBlazorWebViewBuilder.cs +++ b/src/BlazorWebView.WinUI/IWinUIBlazorWebViewBuilder.cs @@ -1,18 +1,14 @@ -// 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.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection; +namespace Microsoft.AspNetCore.Components.WebView.WinUI; -namespace Microsoft.AspNetCore.Components.WebView.WinUI +/// +/// A builder for WinUI Blazor WebViews. +/// +public interface IWinUIBlazorWebViewBuilder { - /// - /// A builder for WPF Blazor WebViews. - /// - public interface IWinUIBlazorWebViewBuilder - { - /// - /// Gets the builder service collection. - /// - IServiceCollection Services { get; } - } + /// + /// Gets the builder service collection. + /// + IServiceCollection Services { get; } } diff --git a/src/BlazorWebView.WinUI/Log.cs b/src/BlazorWebView.WinUI/Log.cs index 290a105..41cf752 100644 --- a/src/BlazorWebView.WinUI/Log.cs +++ b/src/BlazorWebView.WinUI/Log.cs @@ -5,60 +5,60 @@ namespace Microsoft.AspNetCore.Components.WebView; internal static partial class Log { - [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = "Navigating to {uri}.")] - public static partial void NavigatingToUri(this ILogger logger, Uri uri); + [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = "Navigating to {uri}.")] + public static partial void NavigatingToUri(this ILogger logger, Uri uri); - [LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = "Failed to create WebView2 environment. This could mean that WebView2 is not installed.")] - public static partial void FailedToCreateWebView2Environment(this ILogger logger); + [LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = "Failed to create WebView2 environment. This could mean that WebView2 is not installed.")] + public static partial void FailedToCreateWebView2Environment(this ILogger logger); - [LoggerMessage(EventId = 2, Level = LogLevel.Debug, Message = "Starting WebView2...")] - public static partial void StartingWebView2(this ILogger logger); + [LoggerMessage(EventId = 2, Level = LogLevel.Debug, Message = "Starting WebView2...")] + public static partial void StartingWebView2(this ILogger logger); - [LoggerMessage(EventId = 3, Level = LogLevel.Debug, Message = "WebView2 is started.")] - public static partial void StartedWebView2(this ILogger logger); + [LoggerMessage(EventId = 3, Level = LogLevel.Debug, Message = "WebView2 is started.")] + public static partial void StartedWebView2(this ILogger logger); - [LoggerMessage(EventId = 4, Level = LogLevel.Debug, Message = "Handling web request to URI '{requestUri}'.")] - public static partial void HandlingWebRequest(this ILogger logger, string requestUri); + [LoggerMessage(EventId = 4, Level = LogLevel.Debug, Message = "Handling web request to URI '{requestUri}'.")] + public static partial void HandlingWebRequest(this ILogger logger, string requestUri); - [LoggerMessage(EventId = 5, Level = LogLevel.Debug, Message = "Response content being sent for web request to URI '{requestUri}' with HTTP status code {statusCode}.")] - public static partial void ResponseContentBeingSent(this ILogger logger, string requestUri, int statusCode); + [LoggerMessage(EventId = 5, Level = LogLevel.Debug, Message = "Response content being sent for web request to URI '{requestUri}' with HTTP status code {statusCode}.")] + public static partial void ResponseContentBeingSent(this ILogger logger, string requestUri, int statusCode); - [LoggerMessage(EventId = 6, Level = LogLevel.Debug, Message = "Response content was not found for web request to URI '{requestUri}'.")] - public static partial void ReponseContentNotFound(this ILogger logger, string requestUri); + [LoggerMessage(EventId = 6, Level = LogLevel.Debug, Message = "Response content was not found for web request to URI '{requestUri}'.")] + public static partial void ResponseContentNotFound(this ILogger logger, string requestUri); - [LoggerMessage(EventId = 7, Level = LogLevel.Debug, Message = "Navigation event for URI '{uri}' with URL loading strategy '{urlLoadingStrategy}'.")] - public static partial void NavigationEvent(this ILogger logger, Uri uri, UrlLoadingStrategy urlLoadingStrategy); + [LoggerMessage(EventId = 7, Level = LogLevel.Debug, Message = "Navigation event for URI '{uri}' with URL loading strategy '{urlLoadingStrategy}'.")] + public static partial void NavigationEvent(this ILogger logger, Uri uri, UrlLoadingStrategy urlLoadingStrategy); - [LoggerMessage(EventId = 8, Level = LogLevel.Debug, Message = "Launching external browser for URI '{uri}'.")] - public static partial void LaunchExternalBrowser(this ILogger logger, Uri uri); + [LoggerMessage(EventId = 8, Level = LogLevel.Debug, Message = "Launching external browser for URI '{uri}'.")] + public static partial void LaunchExternalBrowser(this ILogger logger, Uri uri); - [LoggerMessage(EventId = 9, Level = LogLevel.Debug, Message = "Calling Blazor.start() in the WebView2.")] - public static partial void CallingBlazorStart(this ILogger logger); + [LoggerMessage(EventId = 9, Level = LogLevel.Debug, Message = "Calling Blazor.start() in the WebView2.")] + public static partial void CallingBlazorStart(this ILogger logger); - [LoggerMessage(EventId = 10, Level = LogLevel.Debug, Message = "Creating file provider at content root '{contentRootDir}', using host page relative path '{hostPageRelativePath}'.")] - public static partial void CreatingFileProvider(this ILogger logger, string contentRootDir, string hostPageRelativePath); + [LoggerMessage(EventId = 10, Level = LogLevel.Debug, Message = "Creating file provider at content root '{contentRootDir}', using host page relative path '{hostPageRelativePath}'.")] + public static partial void CreatingFileProvider(this ILogger logger, string contentRootDir, string hostPageRelativePath); - [LoggerMessage(EventId = 11, Level = LogLevel.Debug, Message = "Adding root component '{componentTypeName}' with selector '{componentSelector}'. Number of parameters: {parameterCount}")] - public static partial void AddingRootComponent(this ILogger logger, string componentTypeName, string componentSelector, int parameterCount); + [LoggerMessage(EventId = 11, Level = LogLevel.Debug, Message = "Adding root component '{componentTypeName}' with selector '{componentSelector}'. Number of parameters: {parameterCount}")] + public static partial void AddingRootComponent(this ILogger logger, string componentTypeName, string componentSelector, int parameterCount); - [LoggerMessage(EventId = 12, Level = LogLevel.Debug, Message = "Starting initial navigation to '{startPath}'.")] - public static partial void StartingInitialNavigation(this ILogger logger, string startPath); + [LoggerMessage(EventId = 12, Level = LogLevel.Debug, Message = "Starting initial navigation to '{startPath}'.")] + public static partial void StartingInitialNavigation(this ILogger logger, string startPath); - [LoggerMessage(EventId = 13, Level = LogLevel.Debug, Message = "Creating Android.Webkit.WebView...")] - public static partial void CreatingAndroidWebkitWebView(this ILogger logger); + [LoggerMessage(EventId = 13, Level = LogLevel.Debug, Message = "Creating Android.Webkit.WebView...")] + public static partial void CreatingAndroidWebkitWebView(this ILogger logger); - [LoggerMessage(EventId = 14, Level = LogLevel.Debug, Message = "Created Android.Webkit.WebView.")] - public static partial void CreatedAndroidWebkitWebView(this ILogger logger); + [LoggerMessage(EventId = 14, Level = LogLevel.Debug, Message = "Created Android.Webkit.WebView.")] + public static partial void CreatedAndroidWebkitWebView(this ILogger logger); - [LoggerMessage(EventId = 15, Level = LogLevel.Debug, Message = "Running Blazor startup scripts.")] - public static partial void RunningBlazorStartupScripts(this ILogger logger); + [LoggerMessage(EventId = 15, Level = LogLevel.Debug, Message = "Running Blazor startup scripts.")] + public static partial void RunningBlazorStartupScripts(this ILogger logger); - [LoggerMessage(EventId = 16, Level = LogLevel.Debug, Message = "Blazor startup scripts finished.")] - public static partial void BlazorStartupScriptsFinished(this ILogger logger); + [LoggerMessage(EventId = 16, Level = LogLevel.Debug, Message = "Blazor startup scripts finished.")] + public static partial void BlazorStartupScriptsFinished(this ILogger logger); - [LoggerMessage(EventId = 17, Level = LogLevel.Debug, Message = "Creating WebKit WKWebView...")] - public static partial void CreatingWebKitWKWebView(this ILogger logger); + [LoggerMessage(EventId = 17, Level = LogLevel.Debug, Message = "Creating WebKit WKWebView...")] + public static partial void CreatingWebKitWKWebView(this ILogger logger); - [LoggerMessage(EventId = 18, Level = LogLevel.Debug, Message = "Created WebKit WKWebView.")] - public static partial void CreatedWebKitWKWebView(this ILogger logger); + [LoggerMessage(EventId = 18, Level = LogLevel.Debug, Message = "Created WebKit WKWebView.")] + public static partial void CreatedWebKitWKWebView(this ILogger logger); } diff --git a/src/BlazorWebView.WinUI/QueryStringHelper.cs b/src/BlazorWebView.WinUI/QueryStringHelper.cs index 92f2f75..59c7cf4 100644 --- a/src/BlazorWebView.WinUI/QueryStringHelper.cs +++ b/src/BlazorWebView.WinUI/QueryStringHelper.cs @@ -1,24 +1,18 @@ -// 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; -#nullable enable +namespace Microsoft.AspNetCore.Components.WebView; -using System; - -namespace Microsoft.AspNetCore.Components.WebView +internal static class QueryStringHelper { - internal static class QueryStringHelper - { - public static string RemovePossibleQueryString(string? url) - { - if (string.IsNullOrEmpty(url)) - { - return string.Empty; - } - var indexOfQueryString = url.IndexOf('?', StringComparison.Ordinal); - return (indexOfQueryString == -1) - ? url - : url.Substring(0, indexOfQueryString); - } - } + public static string RemovePossibleQueryString(string? url) + { + if (string.IsNullOrEmpty(url)) + { + return string.Empty; + } + var indexOfQueryString = url.IndexOf('?', StringComparison.Ordinal); + return (indexOfQueryString == -1) + ? url + : url[..indexOfQueryString]; + } } diff --git a/src/BlazorWebView.WinUI/RootComponent.cs b/src/BlazorWebView.WinUI/RootComponent.cs index fe3644b..e671406 100644 --- a/src/BlazorWebView.WinUI/RootComponent.cs +++ b/src/BlazorWebView.WinUI/RootComponent.cs @@ -1,57 +1,53 @@ -// 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.AspNetCore.Components.WebView.WebView2; -namespace Microsoft.AspNetCore.Components.WebView.WinUI +namespace Microsoft.AspNetCore.Components.WebView.WinUI; + +/// +/// Describes a root component that can be added to a . +/// +public class RootComponent { - /// - /// Describes a root component that can be added to a . - /// - public class RootComponent - { - /// - /// Gets or sets the CSS selector string that specifies where in the document the component should be placed. - /// This must be unique among the root components within the . - /// - public string Selector { get; set; } = default!; - - /// - /// Gets or sets the type of the root component. This type must implement . - /// - public Type ComponentType { get; set; } = default!; - - /// - /// Gets or sets an optional dictionary of parameters to pass to the root component. - /// - public IDictionary? Parameters { get; set; } - - internal Task AddToWebViewManagerAsync(WebViewManager webViewManager) - { - // As a characteristic of XAML,we can't rely on non-default constructors. So we have to - // validate that the required properties were set. We could skip validating this and allow - // the lower-level renderer code to throw, but that would be harder for developers to understand. - - if (string.IsNullOrWhiteSpace(Selector)) - { - throw new InvalidOperationException($"{nameof(RootComponent)} requires a value for its {nameof(Selector)} property, but no value was set."); - } - - if (ComponentType is null) - { - throw new InvalidOperationException($"{nameof(RootComponent)} requires a value for its {nameof(ComponentType)} property, but no value was set."); - } - - var parameterView = Parameters == null ? ParameterView.Empty : ParameterView.FromDictionary(Parameters); - return webViewManager.AddRootComponentAsync(ComponentType, Selector, parameterView); - } - - internal Task RemoveFromWebViewManagerAsync(WebView2WebViewManager webviewManager) - { - return webviewManager.RemoveRootComponentAsync(Selector); - } - } + /// + /// Gets or sets the CSS selector string that specifies where in the document the component should be placed. + /// This must be unique among the root components within the . + /// + public string Selector { get; set; } = default!; + + /// + /// Gets or sets the type of the root component. This type must implement . + /// + public Type ComponentType { get; set; } = default!; + + /// + /// Gets or sets an optional dictionary of parameters to pass to the root component. + /// + public IDictionary? Parameters { get; set; } + + internal Task AddToWebViewManagerAsync(WebViewManager webViewManager) + { + // As a characteristic of XAML,we can't rely on non-default constructors. So we have to + // validate that the required properties were set. We could skip validating this and allow + // the lower-level renderer code to throw, but that would be harder for developers to understand. + + if (string.IsNullOrWhiteSpace(Selector)) + { + throw new InvalidOperationException($"{nameof(RootComponent)} requires a value for its {nameof(Selector)} property, but no value was set."); + } + + if (ComponentType is null) + { + throw new InvalidOperationException($"{nameof(RootComponent)} requires a value for its {nameof(ComponentType)} property, but no value was set."); + } + + var parameterView = Parameters == null ? ParameterView.Empty : ParameterView.FromDictionary(Parameters); + return webViewManager.AddRootComponentAsync(ComponentType, Selector, parameterView); + } + + internal Task RemoveFromWebViewManagerAsync(WebView2WebViewManager webviewManager) + { + return webviewManager.RemoveRootComponentAsync(Selector); + } } diff --git a/src/BlazorWebView.WinUI/RootComponentsCollection.cs b/src/BlazorWebView.WinUI/RootComponentsCollection.cs index 4ca7959..ecb3752 100644 --- a/src/BlazorWebView.WinUI/RootComponentsCollection.cs +++ b/src/BlazorWebView.WinUI/RootComponentsCollection.cs @@ -1,17 +1,13 @@ -// 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.Collections.ObjectModel; +using System.Collections.ObjectModel; using Microsoft.AspNetCore.Components.Web; -namespace Microsoft.AspNetCore.Components.WebView.WinUI +namespace Microsoft.AspNetCore.Components.WebView.WinUI; + +/// +/// A collection of items. +/// +public class RootComponentsCollection : ObservableCollection, IJSComponentConfiguration { - /// - /// A collection of items. - /// - public class RootComponentsCollection : ObservableCollection, IJSComponentConfiguration - { - /// - public JSComponentConfigurationStore JSComponents { get; } = new(); - } + /// + public JSComponentConfigurationStore JSComponents { get; } = new(); } diff --git a/src/BlazorWebView.WinUI/StaticContentHotReloadManager.cs b/src/BlazorWebView.WinUI/StaticContentHotReloadManager.cs index a2a7d81..8987aff 100644 --- a/src/BlazorWebView.WinUI/StaticContentHotReloadManager.cs +++ b/src/BlazorWebView.WinUI/StaticContentHotReloadManager.cs @@ -11,149 +11,150 @@ [assembly: MetadataUpdateHandler(typeof(Microsoft.AspNetCore.Components.WebView.StaticContentHotReloadManager))] -namespace Microsoft.AspNetCore.Components.WebView +namespace Microsoft.AspNetCore.Components.WebView; + +internal static class StaticContentHotReloadManager { - internal static class StaticContentHotReloadManager - { - private delegate void ContentUpdatedHandler(string assemblyName, string relativePath); + private delegate void ContentUpdatedHandler(string assemblyName, string relativePath); - private readonly static Regex ContentUrlRegex = new Regex("^_content/(?[^/]+)/(?.*)"); - private static event ContentUpdatedHandler? OnContentUpdated; + private readonly static Regex ContentUrlRegex = new("^_content/(?[^/]+)/(?.*)"); + private static event ContentUpdatedHandler? OnContentUpdated; - // If the current platform can't tell us the application entry assembly name, we can use a placeholder name - private static string ApplicationAssemblyName { get; } = Assembly.GetEntryAssembly()?.GetName().Name - ?? "__application_assembly__"; + // If the current platform can't tell us the application entry assembly name, we can use a placeholder name + private static string ApplicationAssemblyName { get; } = Assembly.GetEntryAssembly()?.GetName().Name + ?? "__application_assembly__"; - private static readonly Dictionary<(string AssemblyName, string RelativePath), (string? ContentType, byte[] Content)> _updatedContent = new() - { - { (ApplicationAssemblyName, "_framework/static-content-hot-reload.js"), ("text/javascript", Encoding.UTF8.GetBytes(@" + private static readonly Dictionary<(string AssemblyName, string RelativePath), (string? ContentType, byte[] Content)> _updatedContent = new() + { + { (ApplicationAssemblyName, "_framework/static-content-hot-reload.js"), ("text/javascript", Encoding.UTF8.GetBytes(@" export function notifyCssUpdated() { const allLinkElems = Array.from(document.querySelectorAll('link[rel=stylesheet]')); allLinkElems.forEach(elem => elem.href += ''); } ")) } - }; - - /// - /// MetadataUpdateHandler event. This is invoked by the hot reload host via reflection. - /// - public static void UpdateContent(string assemblyName, bool isApplicationProject, string relativePath, byte[] contents) - { - if (isApplicationProject) - { - // Some platforms don't know the name of the application entry assembly (e.g., Android) so in - // those cases we have a placeholder name for it. The tooling does know the real name, but we - // need to use our placeholder so the lookups work later. - assemblyName = ApplicationAssemblyName; - } - - _updatedContent[(assemblyName, relativePath)] = (ContentType: null, Content: contents); - OnContentUpdated?.Invoke(assemblyName, relativePath); - } - - public static void AttachToWebViewManagerIfEnabled(WebViewManager manager) - { - if (MetadataUpdater.IsSupported) - { - manager.AddRootComponentAsync(typeof(StaticContentChangeNotifier), "body::after", ParameterView.Empty); - } - } - - public static bool TryReplaceResponseContent(string contentRootRelativePath, string requestAbsoluteUri, ref int responseStatusCode, ref Stream responseContent, IDictionary responseHeaders) - { - if (MetadataUpdater.IsSupported) - { - var (assemblyName, relativePath) = GetAssemblyNameAndRelativePath(requestAbsoluteUri, contentRootRelativePath); - if (_updatedContent.TryGetValue((assemblyName, relativePath), out var values)) - { - responseStatusCode = 200; - responseContent.Close(); - responseContent = new MemoryStream(values.Content); - if (!string.IsNullOrEmpty(values.ContentType)) - { - responseHeaders["Content-Type"] = values.ContentType; - } - - return true; - } - } - - return false; - } - - private static (string AssemblyName, string RelativePath) GetAssemblyNameAndRelativePath(string requestAbsoluteUri, string appContentRoot) - { - var requestPath = new Uri(requestAbsoluteUri).AbsolutePath.Substring(1); - if (ContentUrlRegex.Match(requestPath) is { Success: true } match) - { - // For RCLs (i.e., URLs of the form _content/assembly/path), we assume the content root within the - // RCL to be "wwwroot" since we have no other information. If this is not the case, content within - // that RCL will not be hot-reloadable. - return (match.Groups["AssemblyName"].Value, $"wwwroot/{match.Groups["RelativePath"].Value}"); - } - else if (requestPath.StartsWith("_framework/", StringComparison.Ordinal)) - { - return (ApplicationAssemblyName, requestPath); - } - else - { - return (ApplicationAssemblyName, Path.Combine(appContentRoot, requestPath).Replace('\\', '/')); - } - } - - // To provide a consistent way of transporting the data across all platforms, - // we can use the existing IJSRuntime. In turn we can get an instance of this - // that's always attached to the currently-loaded page (if it's a Blazor page) - // by injecting this headless root component. - private sealed class StaticContentChangeNotifier : IComponent, IDisposable - { - private ILogger _logger = default!; - - [Inject] private IJSRuntime JSRuntime { get; set; } = default!; - [Inject] private ILoggerFactory LoggerFactory { get; set; } = default!; - - public void Attach(RenderHandle renderHandle) - { - _logger = LoggerFactory.CreateLogger(); - OnContentUpdated += NotifyContentUpdated; - } - - public void Dispose() - { - OnContentUpdated -= NotifyContentUpdated; - } - - private void NotifyContentUpdated(string assemblyName, string relativePath) - { - // It handles its own errors - _ = NotifyContentUpdatedAsync(assemblyName, relativePath); - } - - private async Task NotifyContentUpdatedAsync(string assemblyName, string relativePath) - { - try - { - await using var module = await JSRuntime.InvokeAsync("import", "./_framework/static-content-hot-reload.js"); - - // In the future we might want to hot-reload other content types such as images, but currently the tooling is - // only expected to notify about CSS files. If it notifies us about something else, we'd need different JS logic. - if (string.Equals(".css", Path.GetExtension(relativePath), StringComparison.Ordinal)) - { - // We could try to supply the URL of the modified file, so the JS-side logic could only update the affected - // stylesheet. This would reduce flicker. However, this involves hardcoding further details about URL conventions - // (e.g., _content/AssemblyName/Path) and accounting for configurable content roots. To reduce the chances of - // CSS hot reload being broken by customizations, we'll have the JS-side code refresh all stylesheets. - await module.InvokeVoidAsync("notifyCssUpdated"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, $"Failed to notify about static content update to {relativePath}."); - } - } - - public Task SetParametersAsync(ParameterView parameters) - => Task.CompletedTask; - } - } + }; + + /// + /// MetadataUpdateHandler event. This is invoked by the hot reload host via reflection. + /// + public static void UpdateContent(string assemblyName, bool isApplicationProject, string relativePath, byte[] contents) + { + if (isApplicationProject) + { + // Some platforms don't know the name of the application entry assembly (e.g., Android) so in + // those cases we have a placeholder name for it. The tooling does know the real name, but we + // need to use our placeholder so the lookups work later. + assemblyName = ApplicationAssemblyName; + } + + _updatedContent[(assemblyName, relativePath)] = (ContentType: null, Content: contents); + OnContentUpdated?.Invoke(assemblyName, relativePath); + } + + public static void AttachToWebViewManagerIfEnabled(WebViewManager manager) + { + if (MetadataUpdater.IsSupported) + { + manager.AddRootComponentAsync(typeof(StaticContentChangeNotifier), "body::after", ParameterView.Empty); + } + } + + public static bool TryReplaceResponseContent(string contentRootRelativePath, string requestAbsoluteUri, ref int responseStatusCode, ref Stream responseContent, IDictionary responseHeaders) + { + if (MetadataUpdater.IsSupported) + { + var (assemblyName, relativePath) = GetAssemblyNameAndRelativePath(requestAbsoluteUri, contentRootRelativePath); + if (_updatedContent.TryGetValue((assemblyName, relativePath), out var values)) + { + responseStatusCode = 200; + responseContent.Close(); + responseContent = new MemoryStream(values.Content); + if (!string.IsNullOrEmpty(values.ContentType)) + { + responseHeaders["Content-Type"] = values.ContentType; + } + + return true; + } + } + + return false; + } + + private static (string AssemblyName, string RelativePath) GetAssemblyNameAndRelativePath(string requestAbsoluteUri, string appContentRoot) + { + var requestPath = new Uri(requestAbsoluteUri).AbsolutePath[1..]; + if (ContentUrlRegex.Match(requestPath) is { Success: true } match) + { + // For RCLs (i.e., URLs of the form _content/assembly/path), we assume the content root within the + // RCL to be "wwwroot" since we have no other information. If this is not the case, content within + // that RCL will not be hot-reloadable. + return (match.Groups["AssemblyName"].Value, $"wwwroot/{match.Groups["RelativePath"].Value}"); + } + else if (requestPath.StartsWith("_framework/", StringComparison.Ordinal)) + { + return (ApplicationAssemblyName, requestPath); + } + else + { + return (ApplicationAssemblyName, Path.Combine(appContentRoot, requestPath).Replace('\\', '/')); + } + } + + // To provide a consistent way of transporting the data across all platforms, + // we can use the existing IJSRuntime. In turn we can get an instance of this + // that's always attached to the currently-loaded page (if it's a Blazor page) + // by injecting this headless root component. + private sealed class StaticContentChangeNotifier : IComponent, IDisposable + { + private ILogger _logger = default!; + + [Inject] private IJSRuntime JSRuntime { get; set; } = default!; + [Inject] private ILoggerFactory LoggerFactory { get; set; } = default!; + + public void Attach(RenderHandle renderHandle) + { + _logger = LoggerFactory.CreateLogger(); + OnContentUpdated += NotifyContentUpdated; + } + + public void Dispose() + { + OnContentUpdated -= NotifyContentUpdated; + } + + private void NotifyContentUpdated(string assemblyName, string relativePath) + { + // It handles its own errors + _ = NotifyContentUpdatedAsync(assemblyName, relativePath); + } + + private async Task NotifyContentUpdatedAsync(string assemblyName, string relativePath) + { + try + { + await using var module = await JSRuntime.InvokeAsync("import", "./_framework/static-content-hot-reload.js"); + + // In the future we might want to hot-reload other content types such as images, but currently the tooling is + // only expected to notify about CSS files. If it notifies us about something else, we'd need different JS logic. + if (string.Equals(".css", Path.GetExtension(relativePath), StringComparison.Ordinal)) + { + // We could try to supply the URL of the modified file, so the JS-side logic could only update the affected + // stylesheet. This would reduce flicker. However, this involves hardcoding further details about URL conventions + // (e.g., _content/AssemblyName/Path) and accounting for configurable content roots. To reduce the chances of + // CSS hot reload being broken by customizations, we'll have the JS-side code refresh all stylesheets. + await module.InvokeVoidAsync("notifyCssUpdated"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to notify about static content update to {relativePath}.", relativePath); + } + } + + public Task SetParametersAsync(ParameterView parameters) + { + return Task.CompletedTask; + } + } } diff --git a/src/BlazorWebView.WinUI/StaticContentProvider.cs b/src/BlazorWebView.WinUI/StaticContentProvider.cs new file mode 100644 index 0000000..5baaa2e --- /dev/null +++ b/src/BlazorWebView.WinUI/StaticContentProvider.cs @@ -0,0 +1,474 @@ +#nullable disable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNetCore.Components.WebView.WinUI; + +internal class StaticContentProvider +{ + private static readonly FileExtensionContentTypeProvider ContentTypeProvider = new(); + + internal static string GetResponseContentTypeOrDefault(string path) + { + return ContentTypeProvider.TryGetContentType(path, out var matchedContentType) + ? matchedContentType + : "application/octet-stream"; + } + + internal static IDictionary GetResponseHeaders(string contentType) + { + return new Dictionary(StringComparer.Ordinal) + { + { "Content-Type", contentType }, + { "Cache-Control", "no-cache, max-age=0, must-revalidate, no-store" }, + }; + } + + internal class FileExtensionContentTypeProvider + { + // Notes: + // - This table was initially copied from IIS and has many legacy entries we will maintain for backwards compatibility. + // - We only plan to add new entries where we expect them to be applicable to a majority of developers such as being + // used in the project templates. + #region Extension mapping table + /// + /// Creates a new provider with a set of default mappings. + /// + public FileExtensionContentTypeProvider() + : this(new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { ".323", "text/h323" }, + { ".3g2", "video/3gpp2" }, + { ".3gp2", "video/3gpp2" }, + { ".3gp", "video/3gpp" }, + { ".3gpp", "video/3gpp" }, + { ".aac", "audio/aac" }, + { ".aaf", "application/octet-stream" }, + { ".aca", "application/octet-stream" }, + { ".accdb", "application/msaccess" }, + { ".accde", "application/msaccess" }, + { ".accdt", "application/msaccess" }, + { ".acx", "application/internet-property-stream" }, + { ".adt", "audio/vnd.dlna.adts" }, + { ".adts", "audio/vnd.dlna.adts" }, + { ".afm", "application/octet-stream" }, + { ".ai", "application/postscript" }, + { ".aif", "audio/x-aiff" }, + { ".aifc", "audio/aiff" }, + { ".aiff", "audio/aiff" }, + { ".appcache", "text/cache-manifest" }, + { ".application", "application/x-ms-application" }, + { ".art", "image/x-jg" }, + { ".asd", "application/octet-stream" }, + { ".asf", "video/x-ms-asf" }, + { ".asi", "application/octet-stream" }, + { ".asm", "text/plain" }, + { ".asr", "video/x-ms-asf" }, + { ".asx", "video/x-ms-asf" }, + { ".atom", "application/atom+xml" }, + { ".au", "audio/basic" }, + { ".avi", "video/x-msvideo" }, + { ".axs", "application/olescript" }, + { ".bas", "text/plain" }, + { ".bcpio", "application/x-bcpio" }, + { ".bin", "application/octet-stream" }, + { ".bmp", "image/bmp" }, + { ".c", "text/plain" }, + { ".cab", "application/vnd.ms-cab-compressed" }, + { ".calx", "application/vnd.ms-office.calx" }, + { ".cat", "application/vnd.ms-pki.seccat" }, + { ".cdf", "application/x-cdf" }, + { ".chm", "application/octet-stream" }, + { ".class", "application/x-java-applet" }, + { ".clp", "application/x-msclip" }, + { ".cmx", "image/x-cmx" }, + { ".cnf", "text/plain" }, + { ".cod", "image/cis-cod" }, + { ".cpio", "application/x-cpio" }, + { ".cpp", "text/plain" }, + { ".crd", "application/x-mscardfile" }, + { ".crl", "application/pkix-crl" }, + { ".crt", "application/x-x509-ca-cert" }, + { ".csh", "application/x-csh" }, + { ".css", "text/css" }, + { ".csv", "text/csv" }, // https://tools.ietf.org/html/rfc7111#section-5.1 + { ".cur", "application/octet-stream" }, + { ".dcr", "application/x-director" }, + { ".deploy", "application/octet-stream" }, + { ".der", "application/x-x509-ca-cert" }, + { ".dib", "image/bmp" }, + { ".dir", "application/x-director" }, + { ".disco", "text/xml" }, + { ".dlm", "text/dlm" }, + { ".doc", "application/msword" }, + { ".docm", "application/vnd.ms-word.document.macroEnabled.12" }, + { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { ".dot", "application/msword" }, + { ".dotm", "application/vnd.ms-word.template.macroEnabled.12" }, + { ".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" }, + { ".dsp", "application/octet-stream" }, + { ".dtd", "text/xml" }, + { ".dvi", "application/x-dvi" }, + { ".dvr-ms", "video/x-ms-dvr" }, + { ".dwf", "drawing/x-dwf" }, + { ".dwp", "application/octet-stream" }, + { ".dxr", "application/x-director" }, + { ".eml", "message/rfc822" }, + { ".emz", "application/octet-stream" }, + { ".eot", "application/vnd.ms-fontobject" }, + { ".eps", "application/postscript" }, + { ".etx", "text/x-setext" }, + { ".evy", "application/envoy" }, + { ".exe", "application/vnd.microsoft.portable-executable" }, // https://www.iana.org/assignments/media-types/application/vnd.microsoft.portable-executable + { ".fdf", "application/vnd.fdf" }, + { ".fif", "application/fractals" }, + { ".fla", "application/octet-stream" }, + { ".flr", "x-world/x-vrml" }, + { ".flv", "video/x-flv" }, + { ".gif", "image/gif" }, + { ".gtar", "application/x-gtar" }, + { ".gz", "application/x-gzip" }, + { ".h", "text/plain" }, + { ".hdf", "application/x-hdf" }, + { ".hdml", "text/x-hdml" }, + { ".hhc", "application/x-oleobject" }, + { ".hhk", "application/octet-stream" }, + { ".hhp", "application/octet-stream" }, + { ".hlp", "application/winhlp" }, + { ".hqx", "application/mac-binhex40" }, + { ".hta", "application/hta" }, + { ".htc", "text/x-component" }, + { ".htm", "text/html" }, + { ".html", "text/html" }, + { ".htt", "text/webviewhtml" }, + { ".hxt", "text/html" }, + { ".ical", "text/calendar" }, + { ".icalendar", "text/calendar" }, + { ".ico", "image/x-icon" }, + { ".ics", "text/calendar" }, + { ".ief", "image/ief" }, + { ".ifb", "text/calendar" }, + { ".iii", "application/x-iphone" }, + { ".inf", "application/octet-stream" }, + { ".ins", "application/x-internet-signup" }, + { ".isp", "application/x-internet-signup" }, + { ".IVF", "video/x-ivf" }, + { ".jar", "application/java-archive" }, + { ".java", "application/octet-stream" }, + { ".jck", "application/liquidmotion" }, + { ".jcz", "application/liquidmotion" }, + { ".jfif", "image/pjpeg" }, + { ".jpb", "application/octet-stream" }, + { ".jpe", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".jpg", "image/jpeg" }, + { ".js", "application/javascript" }, + { ".json", "application/json" }, + { ".jsx", "text/jscript" }, + { ".latex", "application/x-latex" }, + { ".lit", "application/x-ms-reader" }, + { ".lpk", "application/octet-stream" }, + { ".lsf", "video/x-la-asf" }, + { ".lsx", "video/x-la-asf" }, + { ".lzh", "application/octet-stream" }, + { ".m13", "application/x-msmediaview" }, + { ".m14", "application/x-msmediaview" }, + { ".m1v", "video/mpeg" }, + { ".m2ts", "video/vnd.dlna.mpeg-tts" }, + { ".m3u", "audio/x-mpegurl" }, + { ".m4a", "audio/mp4" }, + { ".m4v", "video/mp4" }, + { ".man", "application/x-troff-man" }, + { ".manifest", "application/x-ms-manifest" }, + { ".map", "text/plain" }, + { ".markdown", "text/markdown" }, + { ".md", "text/markdown" }, + { ".mdb", "application/x-msaccess" }, + { ".mdp", "application/octet-stream" }, + { ".me", "application/x-troff-me" }, + { ".mht", "message/rfc822" }, + { ".mhtml", "message/rfc822" }, + { ".mid", "audio/mid" }, + { ".midi", "audio/mid" }, + { ".mix", "application/octet-stream" }, + { ".mmf", "application/x-smaf" }, + { ".mno", "text/xml" }, + { ".mny", "application/x-msmoney" }, + { ".mov", "video/quicktime" }, + { ".movie", "video/x-sgi-movie" }, + { ".mp2", "video/mpeg" }, + { ".mp3", "audio/mpeg" }, + { ".mp4", "video/mp4" }, + { ".mp4v", "video/mp4" }, + { ".mpa", "video/mpeg" }, + { ".mpe", "video/mpeg" }, + { ".mpeg", "video/mpeg" }, + { ".mpg", "video/mpeg" }, + { ".mpp", "application/vnd.ms-project" }, + { ".mpv2", "video/mpeg" }, + { ".ms", "application/x-troff-ms" }, + { ".msi", "application/octet-stream" }, + { ".mso", "application/octet-stream" }, + { ".mvb", "application/x-msmediaview" }, + { ".mvc", "application/x-miva-compiled" }, + { ".nc", "application/x-netcdf" }, + { ".nsc", "video/x-ms-asf" }, + { ".nws", "message/rfc822" }, + { ".ocx", "application/octet-stream" }, + { ".oda", "application/oda" }, + { ".odc", "text/x-ms-odc" }, + { ".ods", "application/oleobject" }, + { ".oga", "audio/ogg" }, + { ".ogg", "video/ogg" }, + { ".ogv", "video/ogg" }, + { ".ogx", "application/ogg" }, + { ".one", "application/onenote" }, + { ".onea", "application/onenote" }, + { ".onetoc", "application/onenote" }, + { ".onetoc2", "application/onenote" }, + { ".onetmp", "application/onenote" }, + { ".onepkg", "application/onenote" }, + { ".osdx", "application/opensearchdescription+xml" }, + { ".otf", "font/otf" }, + { ".p10", "application/pkcs10" }, + { ".p12", "application/x-pkcs12" }, + { ".p7b", "application/x-pkcs7-certificates" }, + { ".p7c", "application/pkcs7-mime" }, + { ".p7m", "application/pkcs7-mime" }, + { ".p7r", "application/x-pkcs7-certreqresp" }, + { ".p7s", "application/pkcs7-signature" }, + { ".pbm", "image/x-portable-bitmap" }, + { ".pcx", "application/octet-stream" }, + { ".pcz", "application/octet-stream" }, + { ".pdf", "application/pdf" }, + { ".pfb", "application/octet-stream" }, + { ".pfm", "application/octet-stream" }, + { ".pfx", "application/x-pkcs12" }, + { ".pgm", "image/x-portable-graymap" }, + { ".pko", "application/vnd.ms-pki.pko" }, + { ".pma", "application/x-perfmon" }, + { ".pmc", "application/x-perfmon" }, + { ".pml", "application/x-perfmon" }, + { ".pmr", "application/x-perfmon" }, + { ".pmw", "application/x-perfmon" }, + { ".png", "image/png" }, + { ".pnm", "image/x-portable-anymap" }, + { ".pnz", "image/png" }, + { ".pot", "application/vnd.ms-powerpoint" }, + { ".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" }, + { ".potx", "application/vnd.openxmlformats-officedocument.presentationml.template" }, + { ".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" }, + { ".ppm", "image/x-portable-pixmap" }, + { ".pps", "application/vnd.ms-powerpoint" }, + { ".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" }, + { ".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" }, + { ".ppt", "application/vnd.ms-powerpoint" }, + { ".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" }, + { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { ".prf", "application/pics-rules" }, + { ".prm", "application/octet-stream" }, + { ".prx", "application/octet-stream" }, + { ".ps", "application/postscript" }, + { ".psd", "application/octet-stream" }, + { ".psm", "application/octet-stream" }, + { ".psp", "application/octet-stream" }, + { ".pub", "application/x-mspublisher" }, + { ".qt", "video/quicktime" }, + { ".qtl", "application/x-quicktimeplayer" }, + { ".qxd", "application/octet-stream" }, + { ".ra", "audio/x-pn-realaudio" }, + { ".ram", "audio/x-pn-realaudio" }, + { ".rar", "application/octet-stream" }, + { ".ras", "image/x-cmu-raster" }, + { ".rf", "image/vnd.rn-realflash" }, + { ".rgb", "image/x-rgb" }, + { ".rm", "application/vnd.rn-realmedia" }, + { ".rmi", "audio/mid" }, + { ".roff", "application/x-troff" }, + { ".rpm", "audio/x-pn-realaudio-plugin" }, + { ".rtf", "application/rtf" }, + { ".rtx", "text/richtext" }, + { ".scd", "application/x-msschedule" }, + { ".sct", "text/scriptlet" }, + { ".sea", "application/octet-stream" }, + { ".setpay", "application/set-payment-initiation" }, + { ".setreg", "application/set-registration-initiation" }, + { ".sgml", "text/sgml" }, + { ".sh", "application/x-sh" }, + { ".shar", "application/x-shar" }, + { ".sit", "application/x-stuffit" }, + { ".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12" }, + { ".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide" }, + { ".smd", "audio/x-smd" }, + { ".smi", "application/octet-stream" }, + { ".smx", "audio/x-smd" }, + { ".smz", "audio/x-smd" }, + { ".snd", "audio/basic" }, + { ".snp", "application/octet-stream" }, + { ".spc", "application/x-pkcs7-certificates" }, + { ".spl", "application/futuresplash" }, + { ".spx", "audio/ogg" }, + { ".src", "application/x-wais-source" }, + { ".ssm", "application/streamingmedia" }, + { ".sst", "application/vnd.ms-pki.certstore" }, + { ".stl", "application/vnd.ms-pki.stl" }, + { ".sv4cpio", "application/x-sv4cpio" }, + { ".sv4crc", "application/x-sv4crc" }, + { ".svg", "image/svg+xml" }, + { ".svgz", "image/svg+xml" }, + { ".swf", "application/x-shockwave-flash" }, + { ".t", "application/x-troff" }, + { ".tar", "application/x-tar" }, + { ".tcl", "application/x-tcl" }, + { ".tex", "application/x-tex" }, + { ".texi", "application/x-texinfo" }, + { ".texinfo", "application/x-texinfo" }, + { ".tgz", "application/x-compressed" }, + { ".thmx", "application/vnd.ms-officetheme" }, + { ".thn", "application/octet-stream" }, + { ".tif", "image/tiff" }, + { ".tiff", "image/tiff" }, + { ".toc", "application/octet-stream" }, + { ".tr", "application/x-troff" }, + { ".trm", "application/x-msterminal" }, + { ".ts", "video/vnd.dlna.mpeg-tts" }, + { ".tsv", "text/tab-separated-values" }, + { ".ttc", "application/x-font-ttf" }, + { ".ttf", "application/x-font-ttf" }, + { ".tts", "video/vnd.dlna.mpeg-tts" }, + { ".txt", "text/plain" }, + { ".u32", "application/octet-stream" }, + { ".uls", "text/iuls" }, + { ".ustar", "application/x-ustar" }, + { ".vbs", "text/vbscript" }, + { ".vcf", "text/x-vcard" }, + { ".vcs", "text/plain" }, + { ".vdx", "application/vnd.ms-visio.viewer" }, + { ".vml", "text/xml" }, + { ".vsd", "application/vnd.visio" }, + { ".vss", "application/vnd.visio" }, + { ".vst", "application/vnd.visio" }, + { ".vsto", "application/x-ms-vsto" }, + { ".vsw", "application/vnd.visio" }, + { ".vsx", "application/vnd.visio" }, + { ".vtx", "application/vnd.visio" }, + { ".wasm", "application/wasm" }, + { ".wav", "audio/wav" }, + { ".wax", "audio/x-ms-wax" }, + { ".wbmp", "image/vnd.wap.wbmp" }, + { ".wcm", "application/vnd.ms-works" }, + { ".wdb", "application/vnd.ms-works" }, + { ".webm", "video/webm" }, + { ".webmanifest", "application/manifest+json" }, // https://w3c.github.io/manifest/#media-type-registration + { ".webp", "image/webp" }, + { ".wks", "application/vnd.ms-works" }, + { ".wm", "video/x-ms-wm" }, + { ".wma", "audio/x-ms-wma" }, + { ".wmd", "application/x-ms-wmd" }, + { ".wmf", "application/x-msmetafile" }, + { ".wml", "text/vnd.wap.wml" }, + { ".wmlc", "application/vnd.wap.wmlc" }, + { ".wmls", "text/vnd.wap.wmlscript" }, + { ".wmlsc", "application/vnd.wap.wmlscriptc" }, + { ".wmp", "video/x-ms-wmp" }, + { ".wmv", "video/x-ms-wmv" }, + { ".wmx", "video/x-ms-wmx" }, + { ".wmz", "application/x-ms-wmz" }, + { ".woff", "application/font-woff" }, // https://www.w3.org/TR/WOFF/#appendix-b + { ".woff2", "font/woff2" }, // https://www.w3.org/TR/WOFF2/#IMT + { ".wps", "application/vnd.ms-works" }, + { ".wri", "application/x-mswrite" }, + { ".wrl", "x-world/x-vrml" }, + { ".wrz", "x-world/x-vrml" }, + { ".wsdl", "text/xml" }, + { ".wtv", "video/x-ms-wtv" }, + { ".wvx", "video/x-ms-wvx" }, + { ".x", "application/directx" }, + { ".xaf", "x-world/x-vrml" }, + { ".xaml", "application/xaml+xml" }, + { ".xap", "application/x-silverlight-app" }, + { ".xbap", "application/x-ms-xbap" }, + { ".xbm", "image/x-xbitmap" }, + { ".xdr", "text/plain" }, + { ".xht", "application/xhtml+xml" }, + { ".xhtml", "application/xhtml+xml" }, + { ".xla", "application/vnd.ms-excel" }, + { ".xlam", "application/vnd.ms-excel.addin.macroEnabled.12" }, + { ".xlc", "application/vnd.ms-excel" }, + { ".xlm", "application/vnd.ms-excel" }, + { ".xls", "application/vnd.ms-excel" }, + { ".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" }, + { ".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" }, + { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { ".xlt", "application/vnd.ms-excel" }, + { ".xltm", "application/vnd.ms-excel.template.macroEnabled.12" }, + { ".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" }, + { ".xlw", "application/vnd.ms-excel" }, + { ".xml", "text/xml" }, + { ".xof", "x-world/x-vrml" }, + { ".xpm", "image/x-xpixmap" }, + { ".xps", "application/vnd.ms-xpsdocument" }, + { ".xsd", "text/xml" }, + { ".xsf", "text/xml" }, + { ".xsl", "text/xml" }, + { ".xslt", "text/xml" }, + { ".xsn", "application/octet-stream" }, + { ".xtp", "application/octet-stream" }, + { ".xwd", "image/x-xwindowdump" }, + { ".z", "application/x-compress" }, + { ".zip", "application/x-zip-compressed" }, + }) + { + } + #endregion + + /// + /// Creates a lookup engine using the provided mapping. + /// It is recommended that the IDictionary instance use StringComparer.OrdinalIgnoreCase. + /// + /// + public FileExtensionContentTypeProvider(IDictionary mapping) + { + Mappings = mapping ?? throw new ArgumentNullException(nameof(mapping)); + } + + /// + /// The cross reference table of file extensions and content-types. + /// + public IDictionary Mappings { get; private set; } + + /// + /// Given a file path, determine the MIME type + /// + /// A file path + /// The resulting MIME type + /// True if MIME type could be determined + public bool TryGetContentType(string subPath, [MaybeNullWhen(false)] out string contentType) + { + var extension = GetExtension(subPath); + if (extension == null) + { + contentType = null; + return false; + } + return Mappings.TryGetValue(extension, out contentType); + } + + private static string GetExtension(string path) + { + // Don't use Path.GetExtension as that may throw an exception if there are + // invalid characters in the path. Invalid characters should be handled + // by the FileProviders + + if (string.IsNullOrWhiteSpace(path)) + { + return null; + } + + var index = path.LastIndexOf('.'); + return index < 0 ? null : path[index..]; + } + } +} \ No newline at end of file diff --git a/src/BlazorWebView.WinUI/UrlLoadingEventArgs.cs b/src/BlazorWebView.WinUI/UrlLoadingEventArgs.cs index 5bb0141..43f983c 100644 --- a/src/BlazorWebView.WinUI/UrlLoadingEventArgs.cs +++ b/src/BlazorWebView.WinUI/UrlLoadingEventArgs.cs @@ -1,45 +1,44 @@ using System; -namespace Microsoft.AspNetCore.Components.WebView +namespace Microsoft.AspNetCore.Components.WebView; + +/// +/// Used to provide information about a link (]]>) clicked within a Blazor WebView. +/// +/// Anchor tags with target="_blank" will always open in the default +/// browser and the UrlLoading event won't be called. +/// +/// +public class UrlLoadingEventArgs : EventArgs { - /// - /// Used to provide information about a link (]]>) clicked within a Blazor WebView. - /// - /// Anchor tags with target="_blank" will always open in the default - /// browser and the UrlLoading event won't be called. - /// - /// - public class UrlLoadingEventArgs : EventArgs - { - internal static UrlLoadingEventArgs CreateWithDefaultLoadingStrategy(Uri urlToLoad, Uri appOriginUri) - { - var strategy = appOriginUri.IsBaseOf(urlToLoad) ? - UrlLoadingStrategy.OpenInWebView : - UrlLoadingStrategy.OpenExternally; + internal static UrlLoadingEventArgs CreateWithDefaultLoadingStrategy(Uri urlToLoad, Uri appOriginUri) + { + var strategy = appOriginUri.IsBaseOf(urlToLoad) ? + UrlLoadingStrategy.OpenInWebView : + UrlLoadingStrategy.OpenExternally; - return new(urlToLoad, strategy); - } + return new(urlToLoad, strategy); + } - private UrlLoadingEventArgs(Uri url, UrlLoadingStrategy urlLoadingStrategy) - { - Url = url; - UrlLoadingStrategy = urlLoadingStrategy; - } + private UrlLoadingEventArgs(Uri url, UrlLoadingStrategy urlLoadingStrategy) + { + Url = url; + UrlLoadingStrategy = urlLoadingStrategy; + } - /// - /// Gets the URL to be loaded. - /// - public Uri Url { get; } + /// + /// Gets the URL to be loaded. + /// + public Uri Url { get; } - /// - /// The policy to use when loading links from the webview. - /// Defaults to unless has a host - /// matching the app origin, in which case the default becomes . - /// - /// This value should not be changed to for external links - /// unless you can ensure they are fully trusted. - /// - /// - public UrlLoadingStrategy UrlLoadingStrategy { get; set; } - } + /// + /// The policy to use when loading links from the webview. + /// Defaults to unless has a host + /// matching the app origin, in which case the default becomes . + /// + /// This value should not be changed to for external links + /// unless you can ensure they are fully trusted. + /// + /// + public UrlLoadingStrategy UrlLoadingStrategy { get; set; } } diff --git a/src/BlazorWebView.WinUI/UrlLoadingStrategy.cs b/src/BlazorWebView.WinUI/UrlLoadingStrategy.cs index ded4d60..3f8e9a5 100644 --- a/src/BlazorWebView.WinUI/UrlLoadingStrategy.cs +++ b/src/BlazorWebView.WinUI/UrlLoadingStrategy.cs @@ -1,31 +1,30 @@ -namespace Microsoft.AspNetCore.Components.WebView +namespace Microsoft.AspNetCore.Components.WebView; + +/// +/// URL loading strategy for anchor tags ]]> within a Blazor WebView. +/// +/// Anchor tags with target="_blank" will always open in the default +/// browser and the UrlLoading event won't be called. +/// +public enum UrlLoadingStrategy { - /// - /// URL loading strategy for anchor tags ]]> within a Blazor WebView. - /// - /// Anchor tags with target="_blank" will always open in the default - /// browser and the UrlLoading event won't be called. - /// - public enum UrlLoadingStrategy - { - /// - /// Allows loading URLs using an app determined by the system. - /// This is the default strategy for URLs with an external host. - /// - OpenExternally, + /// + /// Allows loading URLs using an app determined by the system. + /// This is the default strategy for URLs with an external host. + /// + OpenExternally, - /// - /// Allows loading URLs within the Blazor WebView. - /// This is the default strategy for URLs with a host matching the app origin. - /// - /// This strategy should not be used for external links unless you can ensure they are fully trusted. - /// - /// - OpenInWebView, + /// + /// Allows loading URLs within the Blazor WebView. + /// This is the default strategy for URLs with a host matching the app origin. + /// + /// This strategy should not be used for external links unless you can ensure they are fully trusted. + /// + /// + OpenInWebView, - /// - /// Cancels the current URL loading attempt. - /// - CancelLoad - } + /// + /// Cancels the current URL loading attempt. + /// + CancelLoad } diff --git a/src/BlazorWebView.WinUI/WebView2WebViewManager.cs b/src/BlazorWebView.WinUI/WebView2WebViewManager.cs index a18bf8a..f7761c7 100644 --- a/src/BlazorWebView.WinUI/WebView2WebViewManager.cs +++ b/src/BlazorWebView.WinUI/WebView2WebViewManager.cs @@ -1,6 +1,3 @@ -// 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; @@ -15,142 +12,159 @@ using Microsoft.Web.WebView2.Core; using WebView2Control = Microsoft.UI.Xaml.Controls.WebView2; using System.Reflection; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.ApplicationModel; +using Windows.Storage.Streams; + +namespace Microsoft.AspNetCore.Components.WebView.WebView2; -namespace Microsoft.AspNetCore.Components.WebView.WebView2 +/// +/// An implementation of that uses the Edge WebView2 browser control +/// to render web content. +/// +internal class WebView2WebViewManager : WebViewManager { - /// - /// An implementation of that uses the Edge WebView2 browser control - /// to render web content. - /// - internal class WebView2WebViewManager : WebViewManager - { - // Using an IP address means that WebView2 doesn't wait for any DNS resolution, - // making it substantially faster. Note that this isn't real HTTP traffic, since - // we intercept all the requests within this origin. - internal static readonly string AppHostAddress = "0.0.0.0"; - - /// - /// Gets the application's base URI. Defaults to https://0.0.0.0/ - /// - protected static readonly string AppOrigin = $"https://{AppHostAddress}/"; - - internal static readonly Uri AppOriginUri = new(AppOrigin); - private readonly ILogger _logger; - private readonly WebView2Control _webview; - private readonly Task _webviewReadyTask; - private readonly string _contentRootRelativeToAppRoot; - private protected CoreWebView2Environment? _coreWebView2Environment; - private readonly Action _urlLoading; - private readonly Action _blazorWebViewInitializing; - private readonly Action _blazorWebViewInitialized; - private readonly BlazorWebViewDeveloperTools _developerTools; - - /// - /// Constructs an instance of . - /// - /// A to access platform-specific WebView2 APIs. - /// A service provider containing services to be used by this class and also by application code. - /// A instance that can marshal calls to the required thread or sync context. - /// Provides static content to the webview. - /// Describes configuration for adding, removing, and updating root components from JavaScript code. - /// Path to the app's content root relative to the application root directory. - /// Path to the host page within the . - /// Callback invoked when a url is about to load. - /// Callback invoked before the webview is initialized. - /// Callback invoked after the webview is initialized. - /// Logger to send log messages to. - internal WebView2WebViewManager( - WebView2Control webview, - IServiceProvider services, - Dispatcher dispatcher, - IFileProvider fileProvider, - JSComponentConfigurationStore jsComponents, - string contentRootRelativeToAppRoot, - string hostPagePathWithinFileProvider, - Action urlLoading, - Action blazorWebViewInitializing, - Action blazorWebViewInitialized, - ILogger logger) - : base(services, dispatcher, AppOriginUri, fileProvider, jsComponents, hostPagePathWithinFileProvider) - - { - ArgumentNullException.ThrowIfNull(webview); - - if (services.GetService() is null) - { - throw new InvalidOperationException( - "Unable to find the required services. " + - $"Please add all the required services by calling '{nameof(IServiceCollection)}.{nameof(BlazorWebViewServiceCollectionExtensions.AddWpfBlazorWebView)}' in the application startup code."); - } - - _logger = logger; - _webview = webview; - _urlLoading = urlLoading; - _blazorWebViewInitializing = blazorWebViewInitializing; - _blazorWebViewInitialized = blazorWebViewInitialized; - _developerTools = services.GetRequiredService(); - _contentRootRelativeToAppRoot = contentRootRelativeToAppRoot; - - // Unfortunately the CoreWebView2 can only be instantiated asynchronously. - // We want the external API to behave as if initalization is synchronous, - // so keep track of a task we can await during LoadUri. - _webviewReadyTask = TryInitializeWebView2(); - } - - /// - protected override void NavigateCore(Uri absoluteUri) - { - _ = Dispatcher.InvokeAsync(async () => - { - var isWebviewInitialized = await _webviewReadyTask; - - if (isWebviewInitialized) - { - _logger.NavigatingToUri(absoluteUri); - _webview.Source = absoluteUri; - } - }); - } - - /// - protected override void SendMessage(string message) - => _webview.CoreWebView2.PostWebMessageAsString(message); - - private async Task TryInitializeWebView2() - { - var args = new BlazorWebViewInitializingEventArgs(); - _blazorWebViewInitializing?.Invoke(args); - var userDataFolder = args.UserDataFolder ?? GetWebView2UserDataFolder(); - _coreWebView2Environment = await CoreWebView2Environment.CreateWithOptionsAsync( - browserExecutableFolder: args.BrowserExecutableFolder, - userDataFolder: userDataFolder, - options: args.EnvironmentOptions); - - _logger.StartingWebView2(); - await _webview.EnsureCoreWebView2Async(); - _logger.StartedWebView2(); - - var developerTools = _developerTools; - - ApplyDefaultWebViewSettings(developerTools); - _blazorWebViewInitialized?.Invoke(new BlazorWebViewInitializedEventArgs - { - WebView = _webview, - }); - - _webview.CoreWebView2.AddWebResourceRequestedFilter($"{AppOrigin}*", CoreWebView2WebResourceContext.All); - - _webview.CoreWebView2.WebResourceRequested += async (s, eventArgs) => - { - await HandleWebResourceRequest(eventArgs); - }; - - _webview.CoreWebView2.NavigationStarting += CoreWebView2_NavigationStarting; - _webview.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested; - - // The code inside blazor.webview.js is meant to be agnostic to specific webview technologies, - // so the following is an adaptor from blazor.webview.js conventions to WebView2 APIs - await _webview.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(@" + // Using an IP address means that WebView2 doesn't wait for any DNS resolution, + // making it substantially faster. Note that this isn't real HTTP traffic, since + // we intercept all the requests within this origin. + internal static readonly string AppHostAddress = "0.0.0.0"; + + /// + /// Gets the application's base URI. Defaults to https://0.0.0.0/ + /// + protected static readonly string AppOrigin = $"https://{AppHostAddress}/"; + + internal static readonly Uri AppOriginUri = new(AppOrigin); + private readonly ILogger _logger; + private readonly WebView2Control _webview; + private readonly string _hostPageRelativePath; + private readonly Task _webviewReadyTask; + private readonly string _contentRootRelativeToAppRoot; + private protected CoreWebView2Environment? _coreWebView2Environment; + private readonly Action _urlLoading; + private readonly Action _blazorWebViewInitializing; + private readonly Action _blazorWebViewInitialized; + private readonly BlazorWebViewDeveloperTools _developerTools; + private static readonly bool _isPackagedApp; + + static WebView2WebViewManager() + { + try + { + _isPackagedApp = Package.Current != null; + } + catch + { + _isPackagedApp = false; + } + } + + /// + /// Constructs an instance of . + /// + /// A to access platform-specific WebView2 APIs. + /// A service provider containing services to be used by this class and also by application code. + /// A instance that can marshal calls to the required thread or sync context. + /// Provides static content to the webview. + /// Describes configuration for adding, removing, and updating root components from JavaScript code. + /// Path to the app's content root relative to the application root directory. + /// Path to the host page within the . + /// Callback invoked when a url is about to load. + /// Callback invoked before the webview is initialized. + /// Callback invoked after the webview is initialized. + /// Logger to send log messages to. + internal WebView2WebViewManager( + WebView2Control webview, + IServiceProvider services, + Dispatcher dispatcher, + IFileProvider fileProvider, + JSComponentConfigurationStore jsComponents, + string contentRootRelativeToAppRoot, + string hostPagePathWithinFileProvider, + Action urlLoading, + Action blazorWebViewInitializing, + Action blazorWebViewInitialized, + ILogger logger) + : base(services, dispatcher, AppOriginUri, fileProvider, jsComponents, hostPagePathWithinFileProvider) + + { + ArgumentNullException.ThrowIfNull(webview); + + if (services.GetService() is null) + { + throw new InvalidOperationException( + "Unable to find the required services. " + + $"Please add all the required services by calling '{nameof(IServiceCollection)}.{nameof(BlazorWebViewServiceCollectionExtensions.AddWinUIBlazorWebView)}' in the application startup code."); + } + + _logger = logger; + _webview = webview; + _hostPageRelativePath = hostPagePathWithinFileProvider; + _urlLoading = urlLoading; + _blazorWebViewInitializing = blazorWebViewInitializing; + _blazorWebViewInitialized = blazorWebViewInitialized; + _developerTools = services.GetRequiredService(); + _contentRootRelativeToAppRoot = contentRootRelativeToAppRoot; + + // Unfortunately the CoreWebView2 can only be instantiated asynchronously. + // We want the external API to behave as if initialization is synchronous, + // so keep track of a task we can await during LoadUri. + _webviewReadyTask = TryInitializeWebView2(); + } + + /// + protected override void NavigateCore(Uri absoluteUri) + { + _ = Dispatcher.InvokeAsync(async () => + { + var isWebviewInitialized = await _webviewReadyTask; + + if (isWebviewInitialized) + { + _logger.NavigatingToUri(absoluteUri); + _webview.Source = absoluteUri; + } + }); + } + + /// + protected override void SendMessage(string message) + { + _webview.CoreWebView2.PostWebMessageAsString(message); + } + + private async Task TryInitializeWebView2() + { + var args = new BlazorWebViewInitializingEventArgs(); + _blazorWebViewInitializing?.Invoke(args); + var userDataFolder = args.UserDataFolder ?? GetWebView2UserDataFolder(); + _coreWebView2Environment = await CoreWebView2Environment.CreateWithOptionsAsync( + browserExecutableFolder: args.BrowserExecutableFolder, + userDataFolder: userDataFolder, + options: args.EnvironmentOptions); + + _logger.StartingWebView2(); + await _webview.EnsureCoreWebView2Async(); + _logger.StartedWebView2(); + + var developerTools = _developerTools; + + ApplyDefaultWebViewSettings(developerTools); + _blazorWebViewInitialized?.Invoke(new BlazorWebViewInitializedEventArgs + { + WebView = _webview, + }); + + _webview.CoreWebView2.AddWebResourceRequestedFilter($"{AppOrigin}*", CoreWebView2WebResourceContext.All); + + _webview.CoreWebView2.WebResourceRequested += async (s, eventArgs) => await HandleWebResourceRequest(eventArgs); + + _webview.CoreWebView2.NavigationStarting += CoreWebView2_NavigationStarting; + _webview.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested; + + // The code inside blazor.webview.js is meant to be agnostic to specific webview technologies, + // so the following is an adaptor from blazor.webview.js conventions to WebView2 APIs + await _webview.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(@" window.external = { sendMessage: message => { window.chrome.webview.postMessage(message); @@ -161,124 +175,216 @@ await _webview.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(@" }; "); - QueueBlazorStart(); - - _webview.CoreWebView2.WebMessageReceived += (s, e) => MessageReceived(new Uri(e.Source), e.TryGetWebMessageAsString()); - - return true; - } - - /// - /// Handles outbound URL requests. - /// - /// The . - protected virtual Task HandleWebResourceRequest(CoreWebView2WebResourceRequestedEventArgs eventArgs) - { - // Unlike server-side code, we get told exactly why the browser is making the request, - // so we can be smarter about fallback. We can ensure that 'fetch' requests never result - // in fallback, for example. - var allowFallbackOnHostPage = - eventArgs.ResourceContext == CoreWebView2WebResourceContext.Document || - eventArgs.ResourceContext == CoreWebView2WebResourceContext.Other; // e.g., dev tools requesting page source - - var requestUri = QueryStringHelper.RemovePossibleQueryString(eventArgs.Request.Uri); - - _logger.HandlingWebRequest(requestUri); - - if (TryGetResponseContent(requestUri, allowFallbackOnHostPage, out var statusCode, out var statusMessage, out var content, out var headers)) - { - StaticContentHotReloadManager.TryReplaceResponseContent(_contentRootRelativeToAppRoot, requestUri, ref statusCode, ref content, headers); - - var headerString = GetHeaderString(headers); - - var autoCloseStream = new AutoCloseOnReadCompleteStream(content); - - _logger.ResponseContentBeingSent(requestUri, statusCode); - - eventArgs.Response = _coreWebView2Environment!.CreateWebResourceResponse(autoCloseStream.AsRandomAccessStream(), statusCode, statusMessage, headerString); - } - else - { - _logger.ReponseContentNotFound(requestUri); - } - return Task.CompletedTask; - } - - /// - /// Override this method to queue a call to Blazor.start(). Not all platforms require this. - /// - protected virtual void QueueBlazorStart() - { - } - - private void CoreWebView2_NavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs args) - { - if (Uri.TryCreate(args.Uri, UriKind.RelativeOrAbsolute, out var uri)) - { - var callbackArgs = UrlLoadingEventArgs.CreateWithDefaultLoadingStrategy(uri, AppOriginUri); - _urlLoading?.Invoke(callbackArgs); - _logger.NavigationEvent(uri, callbackArgs.UrlLoadingStrategy); - - if (callbackArgs.UrlLoadingStrategy == UrlLoadingStrategy.OpenExternally) - { - LaunchUriInExternalBrowser(uri); - } - - args.Cancel = callbackArgs.UrlLoadingStrategy != UrlLoadingStrategy.OpenInWebView; - } - } - - private void CoreWebView2_NewWindowRequested(object? sender, CoreWebView2NewWindowRequestedEventArgs args) - { - // Intercept _blank target tags to always open in device browser. - // The ExternalLinkCallback is not invoked. - if (Uri.TryCreate(args.Uri, UriKind.RelativeOrAbsolute, out var uri)) - { - LaunchUriInExternalBrowser(uri); - args.Handled = true; - } - } - - private void LaunchUriInExternalBrowser(Uri uri) - { - _logger.LaunchExternalBrowser(uri); - using (var launchBrowser = new Process()) - { - launchBrowser.StartInfo.UseShellExecute = true; - launchBrowser.StartInfo.FileName = uri.ToString(); - launchBrowser.Start(); - } - } - - private protected static string GetHeaderString(IDictionary headers) => - string.Join(Environment.NewLine, headers.Select(kvp => $"{kvp.Key}: {kvp.Value}")); - - private void ApplyDefaultWebViewSettings(BlazorWebViewDeveloperTools devTools) - { - _webview.CoreWebView2.Settings.AreDevToolsEnabled = devTools.Enabled; - - // Desktop applications typically don't want the default web browser context menu - _webview.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false; - - // Desktop applications almost never want to show a URL preview when hovering over a link - _webview.CoreWebView2.Settings.IsStatusBarEnabled = false; - } - private static string? GetWebView2UserDataFolder() - { - if (Assembly.GetEntryAssembly() is { } mainAssembly) - { - // In case the application is running from a non-writable location (e.g., program files if you're not running - // elevated), use our own convention of %LocalAppData%\YourApplicationName.WebView2. - // We may be able to remove this if https://github.com/MicrosoftEdge/WebView2Feedback/issues/297 is fixed. - var applicationName = mainAssembly.GetName().Name; - var result = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - $"{applicationName}.WebView2"); - - return result; - } - - return null; - } - } + QueueBlazorStart(); + + _webview.CoreWebView2.WebMessageReceived += (s, e) => MessageReceived(new Uri(e.Source), e.TryGetWebMessageAsString()); + + return true; + } + + /// + /// Handles outbound URL requests. + /// + /// The . + protected virtual async Task HandleWebResourceRequest(CoreWebView2WebResourceRequestedEventArgs eventArgs) + { + // Unlike server-side code, we get told exactly why the browser is making the request, + // so we can be smarter about fallback. We can ensure that 'fetch' requests never result + // in fallback, for example. + var allowFallbackOnHostPage = + eventArgs.ResourceContext is CoreWebView2WebResourceContext.Document or + CoreWebView2WebResourceContext.Other; // e.g., dev tools requesting page source + + // Get a deferral object so that WebView2 knows there's some async stuff going on. We call Complete() at the end of this method. + using var deferral = eventArgs.GetDeferral(); + + var requestUri = QueryStringHelper.RemovePossibleQueryString(eventArgs.Request.Uri); + + _logger.HandlingWebRequest(requestUri); + + var uri = new Uri(requestUri); + var relativePath = AppOriginUri.IsBaseOf(uri) ? AppOriginUri.MakeRelativeUri(uri).ToString() : null; + + // Check if the uri is _framework/blazor.modules.json is a special case as the built-in file provider + // brings in a default implementation. + if (relativePath != null && + string.Equals(relativePath, "_framework/blazor.modules.json", StringComparison.Ordinal) && + await TryServeFromFolderAsync(eventArgs, allowFallbackOnHostPage: false, requestUri, relativePath)) + { + _logger.ResponseContentBeingSent(requestUri, 200); + } + else if (TryGetResponseContent(requestUri, allowFallbackOnHostPage, out var statusCode, out var statusMessage, out var content, out var headers) + && statusCode != 404) + { + // First, call into WebViewManager to see if it has a framework file for this request. It will + // fall back to an IFileProvider, but on WinUI it's always a NullFileProvider, so that will never + // return a file. + var headerString = GetHeaderString(headers); + _logger.ResponseContentBeingSent(requestUri, statusCode); + eventArgs.Response = _coreWebView2Environment!.CreateWebResourceResponse(content.AsRandomAccessStream(), statusCode, statusMessage, headerString); + } + else if (relativePath != null) + { + await TryServeFromFolderAsync( + eventArgs, + allowFallbackOnHostPage, + requestUri, + relativePath); + } + + // Notify WebView2 that the deferred (async) operation is complete and we set a response. + deferral.Complete(); + } + + private async Task TryServeFromFolderAsync( + CoreWebView2WebResourceRequestedEventArgs eventArgs, + bool allowFallbackOnHostPage, + string requestUri, + string relativePath) + { + // If the path does not end in a file extension (or is empty), it's most likely referring to a page, + // in which case we should allow falling back on the host page. + if (allowFallbackOnHostPage && !Path.HasExtension(relativePath)) + { + relativePath = _hostPageRelativePath; + } + relativePath = Path.Combine(_contentRootRelativeToAppRoot, relativePath.Replace('/', '\\')); + var statusCode = 200; + var statusMessage = "OK"; + var contentType = StaticContentProvider.GetResponseContentTypeOrDefault(relativePath); + var headers = StaticContentProvider.GetResponseHeaders(contentType); + IRandomAccessStream? stream = null; + if (_isPackagedApp) + { + var winUIItem = await Package.Current.InstalledLocation.TryGetItemAsync(relativePath); + if (winUIItem != null) + { + using var contentStream = await Package.Current.InstalledLocation.OpenStreamForReadAsync(relativePath); + stream = await CopyContentToRandomAccessStreamAsync(contentStream); + } + } + else + { + var path = Path.Combine(AppContext.BaseDirectory, relativePath); + if (File.Exists(path)) + { + using var contentStream = File.OpenRead(path); + stream = await CopyContentToRandomAccessStreamAsync(contentStream); + } + } + + var hotReloadedContent = Stream.Null; + if (StaticContentHotReloadManager.TryReplaceResponseContent(_contentRootRelativeToAppRoot, requestUri, ref statusCode, ref hotReloadedContent, headers)) + { + stream = await CopyContentToRandomAccessStreamAsync(hotReloadedContent); + } + + if (stream != null) + { + var headerString = GetHeaderString(headers); + + _logger.ResponseContentBeingSent(requestUri, statusCode); + + eventArgs.Response = _coreWebView2Environment!.CreateWebResourceResponse( + stream, + statusCode, + statusMessage, + headerString); + + return true; + } + else + { + _logger.ResponseContentNotFound(requestUri); + } + + return false; + + async Task CopyContentToRandomAccessStreamAsync(Stream content) + { + using var memStream = new MemoryStream(); + await content.CopyToAsync(memStream); + var randomAccessStream = new InMemoryRandomAccessStream(); + await randomAccessStream.WriteAsync(memStream.GetWindowsRuntimeBuffer()); + return randomAccessStream; + } + } + + /// + /// Override this method to queue a call to Blazor.start(). Not all platforms require this. + /// + protected virtual void QueueBlazorStart() + { + } + + private void CoreWebView2_NavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs args) + { + if (Uri.TryCreate(args.Uri, UriKind.RelativeOrAbsolute, out var uri)) + { + var callbackArgs = UrlLoadingEventArgs.CreateWithDefaultLoadingStrategy(uri, AppOriginUri); + _urlLoading?.Invoke(callbackArgs); + _logger.NavigationEvent(uri, callbackArgs.UrlLoadingStrategy); + + if (callbackArgs.UrlLoadingStrategy == UrlLoadingStrategy.OpenExternally) + { + LaunchUriInExternalBrowser(uri); + } + + args.Cancel = callbackArgs.UrlLoadingStrategy != UrlLoadingStrategy.OpenInWebView; + } + } + + private void CoreWebView2_NewWindowRequested(object? sender, CoreWebView2NewWindowRequestedEventArgs args) + { + // Intercept _blank target tags to always open in device browser. + // The ExternalLinkCallback is not invoked. + if (Uri.TryCreate(args.Uri, UriKind.RelativeOrAbsolute, out var uri)) + { + LaunchUriInExternalBrowser(uri); + args.Handled = true; + } + } + + private void LaunchUriInExternalBrowser(Uri uri) + { + _logger.LaunchExternalBrowser(uri); + using var launchBrowser = new Process(); + launchBrowser.StartInfo.UseShellExecute = true; + launchBrowser.StartInfo.FileName = uri.ToString(); + launchBrowser.Start(); + } + + private protected static string GetHeaderString(IDictionary headers) + { + return string.Join(Environment.NewLine, headers.Select(kvp => $"{kvp.Key}: {kvp.Value}")); + } + + private void ApplyDefaultWebViewSettings(BlazorWebViewDeveloperTools devTools) + { + _webview.CoreWebView2.Settings.AreDevToolsEnabled = devTools.Enabled; + + // Desktop applications typically don't want the default web browser context menu + _webview.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false; + + // Desktop applications almost never want to show a URL preview when hovering over a link + _webview.CoreWebView2.Settings.IsStatusBarEnabled = false; + } + + private static string? GetWebView2UserDataFolder() + { + if (Assembly.GetEntryAssembly() is { } mainAssembly) + { + // In case the application is running from a non-writable location (e.g., program files if you're not running + // elevated), use our own convention of %LocalAppData%\YourApplicationName.WebView2. + // We may be able to remove this if https://github.com/MicrosoftEdge/WebView2Feedback/issues/297 is fixed. + var applicationName = mainAssembly.GetName().Name; + var result = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + $"{applicationName}.WebView2"); + + return result; + } + + return null; + } } diff --git a/src/BlazorWebView.WinUI/WinUIBlazorMarkerService.cs b/src/BlazorWebView.WinUI/WinUIBlazorMarkerService.cs index eeb6496..a99a91b 100644 --- a/src/BlazorWebView.WinUI/WinUIBlazorMarkerService.cs +++ b/src/BlazorWebView.WinUI/WinUIBlazorMarkerService.cs @@ -1,9 +1,5 @@ -// 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.WinUI; -namespace Microsoft.AspNetCore.Components.WebView.WinUI +internal class WinUIBlazorMarkerService { - internal class WinUIBlazorMarkerService - { - } } diff --git a/src/BlazorWebView.WinUI/WinUIBlazorWebViewBuilder.cs b/src/BlazorWebView.WinUI/WinUIBlazorWebViewBuilder.cs index ecdf341..a0cd1cf 100644 --- a/src/BlazorWebView.WinUI/WinUIBlazorWebViewBuilder.cs +++ b/src/BlazorWebView.WinUI/WinUIBlazorWebViewBuilder.cs @@ -1,17 +1,13 @@ -// 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.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection; +namespace Microsoft.AspNetCore.Components.WebView.WinUI; -namespace Microsoft.AspNetCore.Components.WebView.WinUI +internal class WinUIBlazorWebViewBuilder : IWinUIBlazorWebViewBuilder { - internal class WinUIBlazorWebViewBuilder : IWinUIBlazorWebViewBuilder - { - public IServiceCollection Services { get; } + public IServiceCollection Services { get; } - public WinUIBlazorWebViewBuilder(IServiceCollection services) - { - Services = services; - } - } + public WinUIBlazorWebViewBuilder(IServiceCollection services) + { + Services = services; + } } diff --git a/src/BlazorWebView.WinUI/WinUIDispatcher.cs b/src/BlazorWebView.WinUI/WinUIDispatcher.cs index 17468c7..26aed23 100644 --- a/src/BlazorWebView.WinUI/WinUIDispatcher.cs +++ b/src/BlazorWebView.WinUI/WinUIDispatcher.cs @@ -1,98 +1,83 @@ -// 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 CommunityToolkit.WinUI; +using CommunityToolkit.WinUI; using Microsoft.UI.Dispatching; using System; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Components.WebView.WinUI +namespace Microsoft.AspNetCore.Components.WebView.WinUI; + +internal sealed class WinUIDispatcher : Dispatcher { - internal sealed class WinUIDispatcher : Dispatcher - { - private readonly DispatcherQueue _dispatcherQueue; + private readonly DispatcherQueue _dispatcherQueue; - public WinUIDispatcher(DispatcherQueue windowsDispatcher) - { - _dispatcherQueue = windowsDispatcher ?? throw new ArgumentNullException(nameof(windowsDispatcher)); - } + public WinUIDispatcher(DispatcherQueue windowsDispatcher) + { + _dispatcherQueue = windowsDispatcher ?? throw new ArgumentNullException(nameof(windowsDispatcher)); + } - public override bool CheckAccess() => _dispatcherQueue.HasThreadAccess; + public override bool CheckAccess() + { + return _dispatcherQueue.HasThreadAccess; + } - public override async Task InvokeAsync(Action workItem) + public override async Task InvokeAsync(Action workItem) + { + try { - try + if (_dispatcherQueue.HasThreadAccess) { - if (_dispatcherQueue.HasThreadAccess) - { - workItem(); - } - else - { - await _dispatcherQueue.EnqueueAsync(workItem); - } + workItem(); } - catch (Exception) + else { - throw; + await _dispatcherQueue.EnqueueAsync(workItem); } } + catch (Exception) + { + throw; + } + } - public override async Task InvokeAsync(Func workItem) + public override async Task InvokeAsync(Func workItem) + { + try { - try + if (_dispatcherQueue.HasThreadAccess) { - if (_dispatcherQueue.HasThreadAccess) - { - await workItem(); - } - else - { - await _dispatcherQueue.EnqueueAsync(workItem); - } + await workItem(); } - catch (Exception) + else { - throw; + await _dispatcherQueue.EnqueueAsync(workItem); } } + catch (Exception) + { + throw; + } + } - public override async Task InvokeAsync(Func workItem) + public override async Task InvokeAsync(Func workItem) + { + try { - try - { - if (_dispatcherQueue.HasThreadAccess) - { - return workItem(); - } - else - { - return await _dispatcherQueue.EnqueueAsync(workItem); - } - } - catch (Exception) - { - throw; - } + return _dispatcherQueue.HasThreadAccess ? workItem() : await _dispatcherQueue.EnqueueAsync(workItem); + } + catch (Exception) + { + throw; } + } - public override async Task InvokeAsync(Func> workItem) + public override async Task InvokeAsync(Func> workItem) + { + try { - try - { - if (_dispatcherQueue.HasThreadAccess) - { - return await workItem(); - } - else - { - return await _dispatcherQueue.EnqueueAsync(workItem); - } - } - catch (Exception) - { - throw; - } + return _dispatcherQueue.HasThreadAccess ? await workItem() : await _dispatcherQueue.EnqueueAsync(workItem); + } + catch (Exception) + { + throw; } } }