Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
75 changes: 65 additions & 10 deletions src/BlazorWebView/src/Wpf/BlazorWebView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -85,10 +87,19 @@ public class BlazorWebView : Control, IAsyncDisposable
propertyType: typeof(EventHandler<BlazorWebViewInitializedEventArgs>),
ownerType: typeof(BlazorWebView));

/// <summary>
/// The backing store for the <see cref="UseCompositionControl"/> property.
/// </summary>
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;

Expand All @@ -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: UseCompositionControl);

ApplyTabNavigation(IsTabStop);
}

/// <summary>
/// Returns the inner <see cref="WebView2Control"/> used by this control.
/// Returns the inner <see cref="IWebView2"/> used by this control.
/// When <see cref="UseCompositionControl"/> is <see langword="true"/> (the default), this is a
/// <see cref="WebView2CompositionControl"/>; otherwise it is a <see cref="WebView2Control"/>.
/// </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)]
public WebView2Control WebView => _webview!;
public IWebView2 WebView => _webview!;

/// <summary>
/// Path to the host page within the application's static files. For example, <code>wwwroot\index.html</code>.
Expand Down Expand Up @@ -195,6 +207,23 @@ public IServiceProvider Services
set => SetValue(ServicesProperty, value);
}

/// <summary>
/// Gets or sets a value indicating whether to use the composition-based <see cref="WebView2CompositionControl"/>,
/// which resolves WPF airspace issues at the cost of additional rendering overhead, or the standard
/// <see cref="WebView2Control"/> for better performance in scenarios where airspace layering is not required.
/// Defaults to <see langword="true"/>.
/// </summary>
/// <remarks>
/// 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
/// <see cref="InvalidOperationException"/>.
/// </remarks>
public bool UseCompositionControl

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[moderate] Regression Prevention and Test Coverage — This new public WPF behavior has no detected regression coverage. Please add targeted coverage for the default UseCompositionControl=true path, the false path creating a standard Microsoft.Web.WebView2.Wpf.WebView2, the initialized event/control property type exposed in each mode, and the documented late-change exception after the template child has been created.

{
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();
Expand All @@ -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 underlying WebView has been created.");
}

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);
Expand All @@ -228,7 +279,11 @@ public override void OnApplyTemplate()

if (_webview == null)
{
_webview = (WebView2Control)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();
}
}
Expand Down Expand Up @@ -392,7 +447,7 @@ await _webviewManager.DisposeAsync()
_webviewManager = null;
}

_webview?.Dispose();
(_webview as IDisposable)?.Dispose();
_webview = null;
}

Expand Down
7 changes: 7 additions & 0 deletions src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
#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!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] Public API Surface — This removes the shipped BlazorWebView.WebView getter returning WebView2CompositionControl and replaces it with an IWebView2 return type. Return-type widening is still a metadata/signature break for existing compiled WPF consumers, and the same compatibility issue applies to BlazorWebViewInitializedEventArgs.WebView. Prefer an additive API shape that preserves the existing concrete WebView2CompositionControl-facing API for compatibility and adds a neutral IWebView2/active-control property for the new standard WebView2 option.

~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!
Loading