From cbc2c0046bab670265da3a794ef6e09ecc2c282f Mon Sep 17 00:00:00 2001 From: "Tony \"mobiletonster\" Spencer" Date: Tue, 31 Mar 2026 15:31:18 -0600 Subject: [PATCH 1/5] Add UseCompositionControl property to WPF BlazorWebView Allows opting out of the WebView2CompositionControl (introduced in net10.0 to fix WPF airspace issues) in favor of the standard WebView2 control when airspace layering is not required and lower rendering overhead is preferred. - Add UseCompositionControl dependency property (default: true) to BlazorWebView, preserving existing behavior by default - Type _webview and the WebView property as IWebView2, the interface implemented by both WebView2 and WebView2CompositionControl - CreateWebViewTemplate() selects the correct control type at init time; the property-changed callback swaps the template if set before the control is added to the visual tree - Throw InvalidOperationException if UseCompositionControl is changed after the underlying WebView2 has already been created - Update WebView2WebViewManager and BlazorWebViewInitializedEventArgs shared source to use IWebView2 for the WPF type alias - Update PublicAPI.Shipped.txt to reflect the IWebView2 return types and new UseCompositionControl public API --- .../BlazorWebViewInitializedEventArgs.cs | 2 +- .../SharedSource/WebView2WebViewManager.cs | 2 +- src/BlazorWebView/src/Wpf/BlazorWebView.cs | 71 ++++++++++++++++--- .../src/Wpf/PublicAPI.Shipped.txt | 7 +- 4 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/BlazorWebView/src/SharedSource/BlazorWebViewInitializedEventArgs.cs b/src/BlazorWebView/src/SharedSource/BlazorWebViewInitializedEventArgs.cs index ba8e1e6263bb..c34d8ceaa5bb 100644 --- a/src/BlazorWebView/src/SharedSource/BlazorWebViewInitializedEventArgs.cs +++ b/src/BlazorWebView/src/SharedSource/BlazorWebViewInitializedEventArgs.cs @@ -5,7 +5,7 @@ using WebView2Control = Microsoft.Web.WebView2.WinForms.WebView2; #elif WEBVIEW2_WPF using Microsoft.Web.WebView2.Core; -using WebView2Control = Microsoft.Web.WebView2.Wpf.WebView2CompositionControl; +using WebView2Control = Microsoft.Web.WebView2.Wpf.IWebView2; #elif WINDOWS && WEBVIEW2_MAUI using Microsoft.Web.WebView2.Core; using WebView2Control = Microsoft.UI.Xaml.Controls.WebView2; diff --git a/src/BlazorWebView/src/SharedSource/WebView2WebViewManager.cs b/src/BlazorWebView/src/SharedSource/WebView2WebViewManager.cs index 132f746c02b3..f0087a9e3e68 100644 --- a/src/BlazorWebView/src/SharedSource/WebView2WebViewManager.cs +++ b/src/BlazorWebView/src/SharedSource/WebView2WebViewManager.cs @@ -28,7 +28,7 @@ using Microsoft.AspNetCore.Components.WebView.Wpf; using Microsoft.Extensions.DependencyInjection; using Microsoft.Web.WebView2.Core; -using WebView2Control = Microsoft.Web.WebView2.Wpf.WebView2CompositionControl; +using WebView2Control = Microsoft.Web.WebView2.Wpf.IWebView2; using System.Reflection; #elif WEBVIEW2_MAUI using Microsoft.AspNetCore.Components.WebView.Maui; diff --git a/src/BlazorWebView/src/Wpf/BlazorWebView.cs b/src/BlazorWebView/src/Wpf/BlazorWebView.cs index e811f1dee101..f15ba4a7e67e 100644 --- a/src/BlazorWebView/src/Wpf/BlazorWebView.cs +++ b/src/BlazorWebView/src/Wpf/BlazorWebView.cs @@ -16,7 +16,9 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using WebView2Control = Microsoft.Web.WebView2.Wpf.WebView2CompositionControl; +using IWebView2 = Microsoft.Web.WebView2.Wpf.IWebView2; +using WebView2CompositionControl = Microsoft.Web.WebView2.Wpf.WebView2CompositionControl; +using WebView2Control = Microsoft.Web.WebView2.Wpf.WebView2; namespace Microsoft.AspNetCore.Components.WebView.Wpf { @@ -85,10 +87,19 @@ public class BlazorWebView : Control, IAsyncDisposable propertyType: typeof(EventHandler), ownerType: typeof(BlazorWebView)); + /// + /// The backing store for the property. + /// + public static readonly DependencyProperty UseCompositionControlProperty = DependencyProperty.Register( + name: nameof(UseCompositionControl), + propertyType: typeof(bool), + ownerType: typeof(BlazorWebView), + typeMetadata: new PropertyMetadata(true, OnUseCompositionControlPropertyChanged)); + #endregion private const string WebViewTemplateChildName = "WebView"; - private WebView2Control? _webview; + private IWebView2? _webview; private WebView2WebViewManager? _webviewManager; private bool _isDisposed; @@ -113,23 +124,24 @@ public BlazorWebView() SetValue(RootComponentsProperty, new RootComponentsCollection()); RootComponents.CollectionChanged += HandleRootComponentsCollectionChanged; - Template = new ControlTemplate - { - VisualTree = new FrameworkElementFactory(typeof(WebView2Control), WebViewTemplateChildName) - }; + // Default to the composition control; OnUseCompositionControlPropertyChanged will + // update this if UseCompositionControl is set to false before the control is initialized. + Template = CreateWebViewTemplate(useComposition: true); ApplyTabNavigation(IsTabStop); } /// - /// Returns the inner used by this control. + /// Returns the inner used by this control. + /// When is (the default), this is a + /// ; otherwise it is a . /// /// /// 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!; + public IWebView2 WebView => _webview!; /// /// Path to the host page within the application's static files. For example, wwwroot\index.html. @@ -195,6 +207,23 @@ public IServiceProvider Services set => SetValue(ServicesProperty, value); } + /// + /// Gets or sets a value indicating whether to use the composition-based , + /// which resolves WPF airspace issues at the cost of additional rendering overhead, or the standard + /// for better performance in scenarios where airspace layering is not required. + /// Defaults to . + /// + /// + /// This property must be set before the control is initialized (e.g., in XAML or before adding the control to + /// the visual tree). Changing it after the underlying WebView2 has been created will throw an + /// . + /// + public bool UseCompositionControl + { + get => (bool)GetValue(UseCompositionControlProperty); + set => SetValue(UseCompositionControlProperty, value); + } + private static void OnServicesPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((BlazorWebView)d).OnServicesPropertyChanged(e); private void OnServicesPropertyChanged(DependencyPropertyChangedEventArgs e) => StartWebViewCoreIfPossible(); @@ -203,6 +232,28 @@ public IServiceProvider Services private void OnHostPagePropertyChanged(DependencyPropertyChangedEventArgs e) => StartWebViewCoreIfPossible(); + private static void OnUseCompositionControlPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((BlazorWebView)d).OnUseCompositionControlPropertyChanged(e); + + private void OnUseCompositionControlPropertyChanged(DependencyPropertyChangedEventArgs e) + { + if (_webview != null) + { + throw new InvalidOperationException( + $"The {nameof(UseCompositionControl)} property cannot be changed after the {nameof(BlazorWebView)} has been initialized."); + } + + Template = CreateWebViewTemplate(useComposition: (bool)e.NewValue); + } + + private static ControlTemplate CreateWebViewTemplate(bool useComposition) + { + var controlType = useComposition ? typeof(WebView2CompositionControl) : typeof(WebView2Control); + return new ControlTemplate + { + VisualTree = new FrameworkElementFactory(controlType, WebViewTemplateChildName) + }; + } + private static void OnIsTabStopPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((BlazorWebView)d).OnIsTabStopPropertyChanged(e); private void OnIsTabStopPropertyChanged(DependencyPropertyChangedEventArgs e) => ApplyTabNavigation((bool)e.NewValue); @@ -228,7 +279,7 @@ public override void OnApplyTemplate() if (_webview == null) { - _webview = (WebView2Control)GetTemplateChild(WebViewTemplateChildName); + _webview = (IWebView2)GetTemplateChild(WebViewTemplateChildName); StartWebViewCoreIfPossible(); } } @@ -392,7 +443,7 @@ await _webviewManager.DisposeAsync() _webviewManager = null; } - _webview?.Dispose(); + (_webview as IDisposable)?.Dispose(); _webview = null; } diff --git a/src/BlazorWebView/src/Wpf/PublicAPI.Shipped.txt b/src/BlazorWebView/src/Wpf/PublicAPI.Shipped.txt index dc588e5d4735..2da5face888a 100644 --- a/src/BlazorWebView/src/Wpf/PublicAPI.Shipped.txt +++ b/src/BlazorWebView/src/Wpf/PublicAPI.Shipped.txt @@ -1,5 +1,5 @@ #nullable enable -~Microsoft.AspNetCore.Components.WebView.BlazorWebViewInitializedEventArgs.WebView.get -> Microsoft.Web.WebView2.Wpf.WebView2CompositionControl +~Microsoft.AspNetCore.Components.WebView.BlazorWebViewInitializedEventArgs.WebView.get -> Microsoft.Web.WebView2.Wpf.IWebView2 ~Microsoft.AspNetCore.Components.WebView.BlazorWebViewInitializingEventArgs.BrowserExecutableFolder.get -> string ~Microsoft.AspNetCore.Components.WebView.BlazorWebViewInitializingEventArgs.BrowserExecutableFolder.set -> void ~Microsoft.AspNetCore.Components.WebView.BlazorWebViewInitializingEventArgs.EnvironmentOptions.get -> Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions @@ -34,7 +34,10 @@ Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.StartPath.get -> strin Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.StartPath.set -> void Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UrlLoading.get -> System.EventHandler! Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UrlLoading.set -> void -Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.WebView.get -> Microsoft.Web.WebView2.Wpf.WebView2CompositionControl! +Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UseCompositionControl.get -> bool +Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UseCompositionControl.set -> void +Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.WebView.get -> Microsoft.Web.WebView2.Wpf.IWebView2! +static readonly Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UseCompositionControlProperty -> System.Windows.DependencyProperty! Microsoft.AspNetCore.Components.WebView.Wpf.IWpfBlazorWebViewBuilder Microsoft.AspNetCore.Components.WebView.Wpf.IWpfBlazorWebViewBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! Microsoft.AspNetCore.Components.WebView.Wpf.RootComponent From 487e316728bd94af5b370ca72de1f47430d9c789 Mon Sep 17 00:00:00 2001 From: "Tony \"mobiletonster\" Spencer" Date: Tue, 31 Mar 2026 19:57:24 -0600 Subject: [PATCH 2/5] Corrected the changes in the PublicAPI.Shipped.txt and the PublicAPI.Unshipped.txt I misunderstood how this was used. This should correct it. --- src/BlazorWebView/src/Wpf/PublicAPI.Shipped.txt | 7 ++----- src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt | 5 +++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/BlazorWebView/src/Wpf/PublicAPI.Shipped.txt b/src/BlazorWebView/src/Wpf/PublicAPI.Shipped.txt index 2da5face888a..dc588e5d4735 100644 --- a/src/BlazorWebView/src/Wpf/PublicAPI.Shipped.txt +++ b/src/BlazorWebView/src/Wpf/PublicAPI.Shipped.txt @@ -1,5 +1,5 @@ #nullable enable -~Microsoft.AspNetCore.Components.WebView.BlazorWebViewInitializedEventArgs.WebView.get -> Microsoft.Web.WebView2.Wpf.IWebView2 +~Microsoft.AspNetCore.Components.WebView.BlazorWebViewInitializedEventArgs.WebView.get -> Microsoft.Web.WebView2.Wpf.WebView2CompositionControl ~Microsoft.AspNetCore.Components.WebView.BlazorWebViewInitializingEventArgs.BrowserExecutableFolder.get -> string ~Microsoft.AspNetCore.Components.WebView.BlazorWebViewInitializingEventArgs.BrowserExecutableFolder.set -> void ~Microsoft.AspNetCore.Components.WebView.BlazorWebViewInitializingEventArgs.EnvironmentOptions.get -> Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions @@ -34,10 +34,7 @@ Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.StartPath.get -> strin Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.StartPath.set -> void Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UrlLoading.get -> System.EventHandler! Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UrlLoading.set -> void -Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UseCompositionControl.get -> bool -Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UseCompositionControl.set -> void -Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.WebView.get -> Microsoft.Web.WebView2.Wpf.IWebView2! -static readonly Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UseCompositionControlProperty -> System.Windows.DependencyProperty! +Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.WebView.get -> Microsoft.Web.WebView2.Wpf.WebView2CompositionControl! Microsoft.AspNetCore.Components.WebView.Wpf.IWpfBlazorWebViewBuilder Microsoft.AspNetCore.Components.WebView.Wpf.IWpfBlazorWebViewBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! Microsoft.AspNetCore.Components.WebView.Wpf.RootComponent diff --git a/src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt b/src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt index 7dc5c58110bf..1a1c7ed2144c 100644 --- a/src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt +++ b/src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ #nullable enable +~Microsoft.AspNetCore.Components.WebView.BlazorWebViewInitializedEventArgs.WebView.get -> Microsoft.Web.WebView2.Wpf.IWebView2 +Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UseCompositionControl.get -> bool +Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UseCompositionControl.set -> void +Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.WebView.get -> Microsoft.Web.WebView2.Wpf.IWebView2! +static readonly Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UseCompositionControlProperty -> System.Windows.DependencyProperty! \ No newline at end of file From 94b6add6620ef934dad6cae326621eaf2ff7c2ed Mon Sep 17 00:00:00 2001 From: Tony Spencer Date: Tue, 31 Mar 2026 19:47:19 -0600 Subject: [PATCH 3/5] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/BlazorWebView/src/Wpf/BlazorWebView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BlazorWebView/src/Wpf/BlazorWebView.cs b/src/BlazorWebView/src/Wpf/BlazorWebView.cs index f15ba4a7e67e..02fe3e47f67a 100644 --- a/src/BlazorWebView/src/Wpf/BlazorWebView.cs +++ b/src/BlazorWebView/src/Wpf/BlazorWebView.cs @@ -239,7 +239,7 @@ private void OnUseCompositionControlPropertyChanged(DependencyPropertyChangedEve if (_webview != null) { throw new InvalidOperationException( - $"The {nameof(UseCompositionControl)} property cannot be changed after the {nameof(BlazorWebView)} has been initialized."); + $"The {nameof(UseCompositionControl)} property cannot be changed after the underlying WebView has been created."); } Template = CreateWebViewTemplate(useComposition: (bool)e.NewValue); From 1fa862d60ab406057dc99633a12bdf4eba85629e Mon Sep 17 00:00:00 2001 From: Tony Spencer Date: Tue, 31 Mar 2026 19:48:49 -0600 Subject: [PATCH 4/5] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/BlazorWebView/src/Wpf/BlazorWebView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BlazorWebView/src/Wpf/BlazorWebView.cs b/src/BlazorWebView/src/Wpf/BlazorWebView.cs index 02fe3e47f67a..56c91fc92aee 100644 --- a/src/BlazorWebView/src/Wpf/BlazorWebView.cs +++ b/src/BlazorWebView/src/Wpf/BlazorWebView.cs @@ -126,7 +126,7 @@ public BlazorWebView() // Default to the composition control; OnUseCompositionControlPropertyChanged will // update this if UseCompositionControl is set to false before the control is initialized. - Template = CreateWebViewTemplate(useComposition: true); + Template = CreateWebViewTemplate(useComposition: UseCompositionControl); ApplyTabNavigation(IsTabStop); } From 543def2fdc9ce5a48b15a6d0e626c7a32690048b Mon Sep 17 00:00:00 2001 From: "Tony \"mobiletonster\" Spencer" Date: Tue, 7 Apr 2026 17:30:42 -0600 Subject: [PATCH 5/5] Improve WebView2 template error handling and API type Enhanced error reporting when WebView2 template child is missing or of the wrong type. Updated public API to return IWebView2 instead of WebView2CompositionControl for greater abstraction. --- src/BlazorWebView/src/Wpf/BlazorWebView.cs | 6 +++++- src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/BlazorWebView/src/Wpf/BlazorWebView.cs b/src/BlazorWebView/src/Wpf/BlazorWebView.cs index 56c91fc92aee..bd9cafb318a1 100644 --- a/src/BlazorWebView/src/Wpf/BlazorWebView.cs +++ b/src/BlazorWebView/src/Wpf/BlazorWebView.cs @@ -279,7 +279,11 @@ public override void OnApplyTemplate() if (_webview == null) { - _webview = (IWebView2)GetTemplateChild(WebViewTemplateChildName); + if (GetTemplateChild(WebViewTemplateChildName) is not IWebView2 webView) + { + throw new InvalidOperationException($"Template child '{WebViewTemplateChildName}' was not found or does not implement {nameof(IWebView2)}. Ensure the control template contains a WebView2 or WebView2CompositionControl element named '{WebViewTemplateChildName}'."); + } + _webview = webView; StartWebViewCoreIfPossible(); } } diff --git a/src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt b/src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt index 1a1c7ed2144c..e2e393ae5c9e 100644 --- a/src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt +++ b/src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +*REMOVED*~Microsoft.AspNetCore.Components.WebView.BlazorWebViewInitializedEventArgs.WebView.get -> Microsoft.Web.WebView2.Wpf.WebView2CompositionControl +*REMOVED*Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.WebView.get -> Microsoft.Web.WebView2.Wpf.WebView2CompositionControl! ~Microsoft.AspNetCore.Components.WebView.BlazorWebViewInitializedEventArgs.WebView.get -> Microsoft.Web.WebView2.Wpf.IWebView2 Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UseCompositionControl.get -> bool Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView.UseCompositionControl.set -> void