Skip to content

Commit 4c51d79

Browse files
authored
Merge pull request #41 from mgnslndh/webview2-movefocus-crash
Fix WebView2 MoveFocus crash on window activation (issue #27)
2 parents 1936efe + eca57f6 commit 4c51d79

2 files changed

Lines changed: 108 additions & 1 deletion

File tree

src/Avalonia.Controls.WebView.Core/Win/WebView2/WebView2BaseAdapter.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,44 @@ private Task<Stream> PrintToPdfStreamAsyncInternal(WebViewPrintSettings? setting
245245

246246
public void Focus()
247247
{
248-
controller.MoveFocus(0 /* Programmatic */);
248+
SafeCallController(() =>
249+
controller.MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON.COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC));
249250
}
250251

251252
public void ResignFocus() { }
252253

254+
private void SafeCallController(Action action)
255+
{
256+
if (Disposed || controller == null)
257+
{
258+
return;
259+
}
260+
try
261+
{
262+
action();
263+
}
264+
catch (ArgumentException ex) when (ex.HResult == HResult.EInvalidArg)
265+
{
266+
// Controller rejected MoveFocus with E_INVALIDARG — transient state during
267+
// async init or window-activation race. Safe to ignore; focus re-establishes
268+
// naturally once the controller reaches a ready state.
269+
}
270+
catch (COMException ex) when (ex.HResult is HResult.EInvalidArg or HResult.EInvalidState)
271+
{
272+
// Same transient rejection surfaced as COMException depending on the
273+
// source-generated interop path (e.g. during controller teardown).
274+
}
275+
}
276+
277+
private static class HResult
278+
{
279+
// E_INVALIDARG (0x80070057): returned by MoveFocus when the controller rejects
280+
// the call during async init or window-activation races.
281+
public const int EInvalidArg = unchecked((int)0x80070057);
282+
// ERROR_INVALID_STATE (0x8007139F): observed in teardown-phase rejections.
283+
public const int EInvalidState = unchecked((int)0x8007139F);
284+
}
285+
253286
internal EventHandler<WebViewNavigationStartingEventArgs>? GetNavigationStarted() => NavigationStarted;
254287
internal EventHandler<WebViewNavigationCompletedEventArgs>? GetNavigationCompleted() => NavigationCompleted;
255288
internal EventHandler<WebMessageReceivedEventArgs>? GetWebMessageReceived() => WebMessageReceived;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#pragma warning disable CA1416 // Platform compatibility — guarded by runtime OS check inside each test
2+
3+
using System;
4+
using System.Runtime.InteropServices;
5+
using Avalonia.Controls.Win;
6+
using Avalonia.Controls.Win.WebView2;
7+
using Avalonia.Controls.Win.WebView2.Interop;
8+
using Xunit;
9+
10+
namespace Avalonia.Controls.WebView.Tests;
11+
12+
public class WebView2BaseAdapterTests
13+
{
14+
// Reproduces https://github.com/AvaloniaUI/Avalonia.Controls.WebView/issues/27
15+
// The controller exists but rejects MoveFocus with E_INVALIDARG during a
16+
// window-activation race (WM_ACTIVATE fires before async init completes).
17+
// This tests that the adapter's Focus method doesn't throw in this scenario, which would cause a crash at runtime.
18+
// NOTE: This is a regression test for a specific issue. We test the implementation by faking the behavior
19+
// of the controller, rather than using a real WebView2 instance, to avoid the complexity of reproducing the exact race
20+
// condition in a reliable way.
21+
[Fact]
22+
public void FocusShouldNotThrowWhenControllerThrowsEInvalidArg()
23+
{
24+
if (!OperatingSystem.IsWindows())
25+
{
26+
Assert.Skip("WebView2 adapter is Windows-only");
27+
}
28+
29+
var adapter = new TestableAdapter(new ThrowingFakeController());
30+
31+
adapter.Focus();
32+
}
33+
34+
private sealed class TestableAdapter(ICoreWebView2Controller controller)
35+
: WebView2BaseAdapter(controller)
36+
{
37+
public override IntPtr Handle => IntPtr.Zero;
38+
public override string? HandleDescriptor => null;
39+
}
40+
41+
private sealed class ThrowingFakeController : ICoreWebView2Controller
42+
{
43+
void ICoreWebView2Controller.MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON reason)
44+
{
45+
// Marshal.GetExceptionForHR produces the exact ArgumentException with HResult
46+
// E_INVALIDARG that the source-generated COM interop layer throws at runtime.
47+
throw (ArgumentException)Marshal.GetExceptionForHR(unchecked((int)0x80070057))!;
48+
}
49+
50+
void ICoreWebView2Controller.Close() { }
51+
52+
int ICoreWebView2Controller.GetIsVisible() => throw new NotImplementedException();
53+
void ICoreWebView2Controller.SetIsVisible(int value) => throw new NotImplementedException();
54+
tagRECT ICoreWebView2Controller.GetBounds() => throw new NotImplementedException();
55+
void ICoreWebView2Controller.SetBounds(tagRECT value) => throw new NotImplementedException();
56+
double ICoreWebView2Controller.GetZoomFactor() => throw new NotImplementedException();
57+
void ICoreWebView2Controller.SetZoomFactor(double value) => throw new NotImplementedException();
58+
void ICoreWebView2Controller.add_ZoomFactorChanged(IntPtr eventHandler, out EventRegistrationToken token) => throw new NotImplementedException();
59+
void ICoreWebView2Controller.remove_ZoomFactorChanged(EventRegistrationToken token) => throw new NotImplementedException();
60+
void ICoreWebView2Controller.SetBoundsAndZoomFactor(tagRECT bounds, double zoomFactor) => throw new NotImplementedException();
61+
void ICoreWebView2Controller.add_MoveFocusRequested(ICoreWebView2MoveFocusRequestedEventHandler eventHandler, out EventRegistrationToken token) => throw new NotImplementedException();
62+
void ICoreWebView2Controller.remove_MoveFocusRequested(EventRegistrationToken token) => throw new NotImplementedException();
63+
void ICoreWebView2Controller.add_GotFocus(ICoreWebView2FocusChangedEventHandler eventHandler, out EventRegistrationToken token) => throw new NotImplementedException();
64+
void ICoreWebView2Controller.remove_GotFocus(EventRegistrationToken token) => throw new NotImplementedException();
65+
void ICoreWebView2Controller.add_LostFocus(ICoreWebView2FocusChangedEventHandler eventHandler, out EventRegistrationToken token) => throw new NotImplementedException();
66+
void ICoreWebView2Controller.remove_LostFocus(EventRegistrationToken token) => throw new NotImplementedException();
67+
void ICoreWebView2Controller.add_AcceleratorKeyPressed(IntPtr eventHandler, out EventRegistrationToken token) => throw new NotImplementedException();
68+
void ICoreWebView2Controller.remove_AcceleratorKeyPressed(EventRegistrationToken token) => throw new NotImplementedException();
69+
IntPtr ICoreWebView2Controller.GetParentWindow() => throw new NotImplementedException();
70+
void ICoreWebView2Controller.SetParentWindow(IntPtr value) => throw new NotImplementedException();
71+
void ICoreWebView2Controller.NotifyParentWindowPositionChanged() => throw new NotImplementedException();
72+
ICoreWebView2 ICoreWebView2Controller.GetCoreWebView2() => throw new NotImplementedException();
73+
}
74+
}

0 commit comments

Comments
 (0)