| File: BlazorWebView.cs | Web Access |
| Project: src\src\BlazorWebView\src\WindowsForms\Microsoft.AspNetCore.Components.WebView.WindowsForms.csproj (Microsoft.AspNetCore.Components.WebView.WindowsForms) |
// 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.Specialized; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; using System.Windows.Forms; using Microsoft.AspNetCore.Components.WebView.WebView2; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using WebView2Control = Microsoft.Web.WebView2.WinForms.WebView2; namespace Microsoft.AspNetCore.Components.WebView.WindowsForms { /// <summary> /// A Windows Forms control for hosting Razor components locally in Windows desktop applications. /// </summary> public class BlazorWebView : ContainerControl { private readonly WebView2Control _webview; private WebView2WebViewManager? _webviewManager; private string? _hostPage; private IServiceProvider? _services; /// <summary> /// Creates a new instance of <see cref="BlazorWebView"/>. /// </summary> public BlazorWebView() { ComponentsDispatcher = new WindowsFormsDispatcher(this); RootComponents.CollectionChanged += HandleRootComponentsCollectionChanged; _webview = new WebView2Control() { Dock = DockStyle.Fill, }; ((BlazorWebViewControlCollection)Controls).AddInternal(_webview); } /// <summary> /// Returns the inner <see cref="WebView2Control"/> used by this control. /// </summary> /// <remarks> /// Directly using some functionality of the inner web view can cause unexpected results because its behavior /// is controlled by the <see cref="BlazorWebView"/> that is hosting it. /// </remarks> [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public WebView2Control WebView => _webview; private WindowsFormsDispatcher ComponentsDispatcher { get; } /// <inheritdoc cref="Control.OnCreateControl" /> protected override void OnCreateControl() { base.OnCreateControl(); StartWebViewCoreIfPossible(); } /// <summary> /// Path to the host page within the application's static files. For example, <code>wwwroot\index.html</code>. /// This property must be set to a valid value for the Razor components to start. /// </summary> [Category("Behavior")] [DefaultValue(null)] [Description(@"Path to the host page within the application's static files. Example: wwwroot\index.html.")] public string? HostPage { get => _hostPage; set { _hostPage = value; OnHostPagePropertyChanged(); } } /// <summary> /// Path for initial Blazor navigation when the Blazor component is finished loading. /// </summary> [Category("Behavior")] [DefaultValue("/")] [Description(@"Path for initial Blazor navigation when the Blazor component is finished loading.")] public string StartPath { get; set; } = "/"; // Learn more about these methods here: https://docs.microsoft.com/en-us/dotnet/desktop/winforms/controls/defining-default-values-with-the-shouldserialize-and-reset-methods?view=netframeworkdesktop-4.8 private void ResetHostPage() => HostPage = null; private bool ShouldSerializeHostPage() => !string.IsNullOrEmpty(HostPage); /// <summary> /// A collection of <see cref="RootComponent"/> instances that specify the Blazor <see cref="IComponent"/> types /// to be used directly in the specified <see cref="HostPage"/>. /// </summary> [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public RootComponentsCollection RootComponents { get; } = new(); /// <summary> /// Gets or sets an <see cref="IServiceProvider"/> 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. /// </summary> [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] [DisallowNull] public IServiceProvider Services { get => _services!; set { _services = value; OnServicesPropertyChanged(); } } /// <summary> /// Allows customizing how links are opened. /// By default, opens internal links in the webview and external links in an external app. /// </summary> [Category("Action")] [Description("Allows customizing how links are opened. By default, opens internal links in the webview and external links in an external app.")] public EventHandler<UrlLoadingEventArgs>? UrlLoading; /// <summary> /// Allows customizing the web view before it is created. /// </summary> [Category("Action")] [Description("Allows customizing the web view before it is created.")] public EventHandler<BlazorWebViewInitializingEventArgs>? BlazorWebViewInitializing; /// <summary> /// Allows customizing the web view after it is created. /// </summary> [Category("Action")] [Description("Allows customizing the web view after it is created.")] public EventHandler<BlazorWebViewInitializedEventArgs>? BlazorWebViewInitialized; private void OnHostPagePropertyChanged() => StartWebViewCoreIfPossible(); private void OnServicesPropertyChanged() => StartWebViewCoreIfPossible(); private bool RequiredStartupPropertiesSet => Created && _webview != null && HostPage != null && Services != null; private void StartWebViewCoreIfPossible() { // We never start the Blazor code in design time because it doesn't make sense to run // a Razor component in the designer. if (IsAncestorSiteInDesignMode) { return; } // If we don't have all the required properties, or if there's already a WebViewManager, do nothing if (!RequiredStartupPropertiesSet || _webviewManager != null) { return; } var logger = Services.GetService<ILogger<BlazorWebView>>() ?? NullLogger<BlazorWebView>.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. string appRootDir; #pragma warning disable IL3000 // 'System.Reflection.Assembly.Location.get' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'. var entryAssemblyLocation = Assembly.GetEntryAssembly()?.Location; #pragma warning restore IL3000 if (!string.IsNullOrEmpty(entryAssemblyLocation)) { appRootDir = Path.GetDirectoryName(entryAssemblyLocation)!; } else { appRootDir = AppContext.BaseDirectory; } var hostPageFullPath = Path.GetFullPath(Path.Combine(appRootDir, HostPage!)); // HostPage is nonnull because RequiredStartupPropertiesSet is checked above var contentRootDirFullPath = Path.GetDirectoryName(hostPageFullPath)!; var contentRootRelativePath = Path.GetRelativePath(appRootDir, contentRootDirFullPath); var hostPageRelativePath = Path.GetRelativePath(contentRootDirFullPath, hostPageFullPath); logger.CreatingFileProvider(contentRootDirFullPath, hostPageRelativePath); var fileProvider = CreateFileProvider(contentRootDirFullPath); _webviewManager = new WebView2WebViewManager( _webview, Services, ComponentsDispatcher, fileProvider, RootComponents.JSComponents, contentRootRelativePath, 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); // Since the page isn't loaded yet, this will always complete synchronously _ = rootComponent.AddToWebViewManagerAsync(_webviewManager); } logger.StartingInitialNavigation(StartPath); _webviewManager.Navigate(StartPath); } private void HandleRootComponentsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs eventArgs) { // 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 () => { var newItems = (eventArgs.NewItems ?? Array.Empty<object>()).Cast<RootComponent>(); var oldItems = (eventArgs.OldItems ?? Array.Empty<object>()).Cast<RootComponent>(); foreach (var item in newItems.Except(oldItems)) { await item.AddToWebViewManagerAsync(_webviewManager); } foreach (var item in oldItems.Except(newItems)) { await item.RemoveFromWebViewManagerAsync(_webviewManager); } }); } } /// <summary> /// Creates a file provider for static assets used in the <see cref="BlazorWebView"/>. The default implementation /// serves files from disk. Override this method to return a custom <see cref="IFileProvider"/> to serve assets such /// as <c>wwwroot/index.html</c>. Call the base method and combine its return value with a <see cref="CompositeFileProvider"/> /// to use both custom assets and default assets. /// </summary> /// <param name="contentRootDir">The base directory to use for all requested assets, such as <c>wwwroot</c>.</param> /// <returns>Returns a <see cref="IFileProvider"/> for static assets.</returns> public virtual IFileProvider CreateFileProvider(string 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(); } } /// <inheritdoc cref="Control.Dispose(bool)" /> protected override void Dispose(bool disposing) { if (disposing && _webviewManager is not null) { // Await disposal of this component's contents so that user-written disposal logic and // Razor component disposal logic will complete first. Then call base.Dispose(), which will dispose // the WebView2 control. This order is critical because once the WebView2 is disposed it will prevent // Razor component code from working because it requires the WebView to exist. // Dispatch because this is going to be async, and we want to catch any errors. _ = ComponentsDispatcher.InvokeAsync(async () => { await _webviewManager.DisposeAsync(); base.Dispose(disposing); }); return; } base.Dispose(disposing); } /// <inheritdoc cref="Control.CreateControlsInstance" /> protected override ControlCollection CreateControlsInstance() { return new BlazorWebViewControlCollection(this); } /// <summary> /// Calls the specified <paramref name="workItem"/> asynchronously and passes in the scoped services available to Razor components. /// </summary> /// <param name="workItem">The action to call.</param> /// <returns>Returns a <see cref="Task"/> representing <c>true</c> if the <paramref name="workItem"/> was called, or <c>false</c> if it was not called because Blazor is not currently running.</returns> /// <exception cref="ArgumentNullException">Thrown if <paramref name="workItem"/> is <c>null</c>.</exception> public virtual async Task<bool> TryDispatchAsync(Action<IServiceProvider> workItem) { ArgumentNullException.ThrowIfNull(workItem); if (_webviewManager is null) { return false; } return await _webviewManager.TryDispatchAsync(workItem); } /// <summary> /// Custom control collection that ensures that only the owning <see cref="BlazorWebView"/> can add /// controls to it. /// </summary> private sealed class BlazorWebViewControlCollection : ControlCollection { public BlazorWebViewControlCollection(BlazorWebView owner) : base(owner) { } /// <summary> /// This is the only API we use; everything else is blocked. /// </summary> /// <param name="value"></param> internal void AddInternal(Control value) => base.Add(value); // Everything below is overridden to protect the control collection as read-only. public override bool IsReadOnly => true; public override void Add(Control? value) => throw new NotSupportedException(); public override void Clear() => throw new NotSupportedException(); public override void Remove(Control? value) => throw new NotSupportedException(); public override void SetChildIndex(Control child, int newIndex) => throw new NotSupportedException(); } } }