diff --git a/src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs index c8e67f2..eaf074c 100644 --- a/src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs @@ -385,6 +385,16 @@ internal interface IWebViewAdapterWithInputRedirect : IWebViewAdapter event Action Input; } +internal interface IWebViewAdapterWithProcessTermination : IWebViewAdapter +{ + /// + /// WebContentProcessTerminated dispatches after the web content process backing the webview terminates, + /// for example after a crash or after the operating system reclaims its memory. The native control stays + /// alive but renders nothing until the page is reloaded. + /// + event EventHandler? WebContentProcessTerminated; +} + internal interface IWebViewAdapterWithCommands { void Copy(); diff --git a/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKNavigationDelegate.cs b/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKNavigationDelegate.cs index 91b889e..a391ee4 100644 --- a/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKNavigationDelegate.cs +++ b/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKNavigationDelegate.cs @@ -14,6 +14,8 @@ internal unsafe class WKNavigationDelegate : NSManagedObjectBase s_willPresentNotification = &OnDidFinishNavigation; private static readonly delegate* unmanaged[Cdecl] s_decidePolicyForNavigationAction = &OnDecidePolicyForNavigationAction; + private static readonly delegate* unmanaged[Cdecl] + s_webContentProcessDidTerminate = &OnWebContentProcessDidTerminate; static WKNavigationDelegate() { @@ -31,6 +33,10 @@ static WKNavigationDelegate() result = Libobjc.class_addMethod(delegateClass, didReceiveNotificationResponse, s_decidePolicyForNavigationAction, "v@:@@@"); Debug.Assert(result == 1); + var webContentProcessDidTerminateSel = Libobjc.sel_getUid("webViewWebContentProcessDidTerminate:"); + result = Libobjc.class_addMethod(delegateClass, webContentProcessDidTerminateSel, s_webContentProcessDidTerminate, "v@:@"); + Debug.Assert(result == 1); + result = RegisterManagedMembers(delegateClass) ? 1 : 0; Debug.Assert(result == 1); @@ -45,6 +51,7 @@ public WKNavigationDelegate() : base(s_class) public event EventHandler? DidFinishNavigation; public event EventHandler? DecidePolicyNavigation; + public event EventHandler? WebContentProcessDidTerminate; [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] private static void OnDidFinishNavigation(IntPtr self, IntPtr sel, IntPtr webView, IntPtr navigation) @@ -53,6 +60,13 @@ private static void OnDidFinishNavigation(IntPtr self, IntPtr sel, IntPtr webVie managed?.DidFinishNavigation?.Invoke(managed, EventArgs.Empty); } + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static void OnWebContentProcessDidTerminate(IntPtr self, IntPtr sel, IntPtr webView) + { + var managed = ReadManagedSelf(self); + managed?.WebContentProcessDidTerminate?.Invoke(managed, EventArgs.Empty); + } + private static readonly IntPtr s_actionRequest = Libobjc.sel_getUid("request"); private static readonly IntPtr s_navigationType = Libobjc.sel_getUid("navigationType"); private static readonly IntPtr s_targetFrame = Libobjc.sel_getUid("targetFrame"); diff --git a/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs index 133662c..c93637e 100644 --- a/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs @@ -18,7 +18,8 @@ namespace Avalonia.Controls.Macios; [SupportedOSPlatform("macos")] [SupportedOSPlatform("ios")] internal class MaciosWebViewAdapter : IWebViewAdapterWithFocus, IWebViewAdapterWithInputRedirect, - IWebViewAdapterWithCookieManager, IWebViewAdapterWithCommands, IWebViewWithPrint, IAppleWKWebViewPlatformHandle + IWebViewAdapterWithCookieManager, IWebViewAdapterWithCommands, IWebViewAdapterWithProcessTermination, + IWebViewWithPrint, IAppleWKWebViewPlatformHandle { private const string DefaultPostAvWebViewMessageName = "postAvWebViewMessage"; @@ -73,6 +74,7 @@ public MaciosWebViewAdapter(AppleWKWebViewEnvironmentRequestedEventArgs options) _navDelegate = new WKNavigationDelegate(); _navDelegate.DidFinishNavigation += OnDelegateOnDidFinishNavigation; _navDelegate.DecidePolicyNavigation += OnDelegateOnDecidePolicyNavigation; + _navDelegate.WebContentProcessDidTerminate += OnDelegateOnWebContentProcessDidTerminate; _webView = new WKWebView(_config) { NavigationDelegate = _navDelegate }; _webView.Opaque = false; @@ -157,6 +159,7 @@ public Color DefaultBackground public event EventHandler? WebResourceRequested; public event EventHandler? GotFocus; public event EventHandler? LostFocus; + public event EventHandler? WebContentProcessTerminated; public event Action? Input; public bool GoBack() => _webView.GoBack() != default; public bool GoForward() => _webView.GoForward() != default; @@ -203,6 +206,7 @@ public void Dispose() _scriptHandler.DidReceiveScriptMessage -= OnScriptHandlerOnDidReceiveScriptMessage; _navDelegate.DidFinishNavigation -= OnDelegateOnDidFinishNavigation; _navDelegate.DecidePolicyNavigation -= OnDelegateOnDecidePolicyNavigation; + _navDelegate.WebContentProcessDidTerminate -= OnDelegateOnWebContentProcessDidTerminate; _webView.PerformKeyEquivalent -= WebViewOnPerformKeyEquivalent; _webView.BecomeFirstResponder -= OnWebViewOnBecomeFirstResponder; _webView.ResignFirstResponder -= OnWebViewOnResignFirstResponder; @@ -297,6 +301,11 @@ private async void OnDelegateOnDidFinishNavigation(object? sender, EventArgs arg NavigationCompleted?.Invoke(this, new WebViewNavigationCompletedEventArgs { Request = Uri.TryCreate(url!.AbsoluteString, UriKind.Absolute, out var uri) ? uri : null, IsSuccess = true }); } + private void OnDelegateOnWebContentProcessDidTerminate(object? sender, EventArgs args) + { + WebContentProcessTerminated?.Invoke(this, EventArgs.Empty); + } + private void OnWebViewOnResignFirstResponder(object? o, EventArgs eventArgs) { LostFocus?.Invoke(this, IWebViewAdapterWithFocus.LostFocusDirection.Unknown); diff --git a/src/Avalonia.Controls.WebView/NativeWebDialog.cs b/src/Avalonia.Controls.WebView/NativeWebDialog.cs index 56ab3ed..bcfa6c4 100644 --- a/src/Avalonia.Controls.WebView/NativeWebDialog.cs +++ b/src/Avalonia.Controls.WebView/NativeWebDialog.cs @@ -365,6 +365,17 @@ public bool Move(int x, int y) public event EventHandler? AdapterDestroyed; public event EventHandler? EnvironmentRequested; + /// + /// WebContentProcessTerminated dispatches after the web content process backing the webview terminates, + /// for example after a crash or after the operating system reclaims its memory. The dialog stays open + /// but renders nothing until the page is reloaded, so handle this event to again. + /// + /// + /// Raised by WebKit-based adapters (WKWebView on macOS and iOS). Other platforms do not raise this + /// event yet. + /// + public event EventHandler? WebContentProcessTerminated; + /// public Core.NativeWebViewCommandManager? TryGetCommandManager() => TryGetAdapter() switch { @@ -465,6 +476,8 @@ private void DialogImplOnAdapterDestroyed(object? sender, Core.WebViewAdapterEve adapter.WebMessageReceived -= WebViewAdapterOnWebMessageReceived; adapter.WebResourceRequested -= WebViewAdapterOnWebResourceRequested; adapter.NewWindowRequested -= WebViewAdapterOnNewWindowRequested; + if (adapter is Core.IWebViewAdapterWithProcessTermination withProcessTermination) + withProcessTermination.WebContentProcessTerminated -= WebViewAdapterOnWebContentProcessTerminated; _dialogInitialized = false; AdapterDestroyed?.Invoke(this, e); } @@ -492,6 +505,8 @@ private void DialogImplOnAdapterCreated(object? sender, Core.WebViewAdapterEvent adapter.WebResourceRequested += WebViewAdapterOnWebResourceRequested; if (_newWindowRequested is not null) adapter.NewWindowRequested += WebViewAdapterOnNewWindowRequested; + if (adapter is Core.IWebViewAdapterWithProcessTermination withProcessTermination) + withProcessTermination.WebContentProcessTerminated += WebViewAdapterOnWebContentProcessTerminated; if (_initialSource is Uri url) adapter.Source = url; else if (_initialSource is ValueTuple pair) @@ -528,5 +543,10 @@ private void WebViewAdapterOnNewWindowRequested(object? sender, Core.WebViewNewW { _newWindowRequested?.Invoke(this, e); } + + private void WebViewAdapterOnWebContentProcessTerminated(object? sender, EventArgs e) + { + WebContentProcessTerminated?.Invoke(this, e); + } } } diff --git a/src/Avalonia.Controls.WebView/NativeWebView.cs b/src/Avalonia.Controls.WebView/NativeWebView.cs index 31ccf23..2567fa7 100644 --- a/src/Avalonia.Controls.WebView/NativeWebView.cs +++ b/src/Avalonia.Controls.WebView/NativeWebView.cs @@ -247,6 +247,18 @@ public event EventHandler? WebResourceReques /// public event EventHandler? AdapterDestroyed; + /// + /// WebContentProcessTerminated dispatches after the web content process backing the webview terminates, + /// for example after a crash or after the operating system reclaims its memory. The native control stays + /// alive but renders nothing until the page is reloaded, so handle this event to + /// or again. + /// + /// + /// Raised by WebKit-based adapters (WKWebView on macOS and iOS). Other platforms do not raise this + /// event yet. + /// + public event EventHandler? WebContentProcessTerminated; + /// public Uri Source { @@ -428,6 +440,12 @@ private void WebViewAdapterOnNewWindowRequested(object? sender, Core.WebViewNewW _newWindowRequested?.Invoke(this, e); } + private void WebViewAdapterOnWebContentProcessTerminated(object? sender, EventArgs e) + { + Core.WebViewDispatcher.VerifyAccess(); + WebContentProcessTerminated?.Invoke(this, e); + } + #if AVALONIA protected override async void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { @@ -563,6 +581,10 @@ private void ControlHostImplOnAdapterDeinitialized(object? sender, Core.IWebView { withInput.Input -= WithInputOnInput; } + if (adapter is Core.IWebViewAdapterWithProcessTermination withProcessTermination) + { + withProcessTermination.WebContentProcessTerminated -= WebViewAdapterOnWebContentProcessTerminated; + } AdapterDestroyed?.Invoke(this, new Core.WebViewAdapterEventArgs(adapter)); } @@ -595,6 +617,11 @@ private void ControlHostImplOnAdapterCreated(object? sender, Core.IWebViewAdapte withInput.Input += WithInputOnInput; } + if (adapter is Core.IWebViewAdapterWithProcessTermination withProcessTermination) + { + withProcessTermination.WebContentProcessTerminated += WebViewAdapterOnWebContentProcessTerminated; + } + if (_initialSource is Uri url) { adapter.Navigate(url);