From a6271275dbbc6d00197b20f1042ef4be8eb55ea5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 13 Aug 2026 20:34:32 -0700 Subject: [PATCH 1/2] Somewhat unify "invokeCSharpAction" callback definition --- .../Android/AndroidWebViewAdapter.cs | 12 ++------- .../Gtk/GtkWebViewAdapter.cs | 14 +++-------- .../Macios/MaciosWebViewAdapter.cs | 6 ++--- .../WebViewHelper.cs | 25 +++++++++++++++++++ .../Win/WebView1/WebViewCallbacks.cs | 10 ++------ .../Win/WebView2/WebView2BaseAdapter.cs | 2 +- 6 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/Avalonia.Controls.WebView.Core/Android/AndroidWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Android/AndroidWebViewAdapter.cs index 7548e6d..b59ee4d 100644 --- a/src/Avalonia.Controls.WebView.Core/Android/AndroidWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Android/AndroidWebViewAdapter.cs @@ -30,7 +30,6 @@ namespace Avalonia.Controls.Android; internal class AndroidWebViewAdapter : IWebViewAdapterWithFocus, IWebViewAdapterWithInputRedirect, IWebViewAdapterWithCookieManager, IAndroidWebViewPlatformHandle, IWebViewWithPrintWithOptions { - private const string PostAvWebViewMessageName = "postAvWebViewMessage"; private static bool s_canSetDataDirectorySuffix = true; private readonly JavaScriptInterface _jsInterface; private WebView? _webView; @@ -93,7 +92,7 @@ public AndroidWebViewAdapter(global::Android.Content.Context parentContext, Andr _webView.Settings.LoadWithOverviewMode = true; _webView.Settings.UseWideViewPort = true; } - _webView.AddJavascriptInterface(_jsInterface, PostAvWebViewMessageName); + _webView.AddJavascriptInterface(_jsInterface, WebViewHelper.PostAvWebViewMessageName); _webView.SetWebViewClient(new AvaloniaWebViewClient(this)); _webView.SetWebChromeClient(new WebChromeClient()); @@ -661,14 +660,7 @@ public override void OnPageFinished(WebView? view, string? url) return; adapter._webView.EvaluateJavascript( - """ - function invokeCSharpAction(data) - { - var message = typeof data === 'object' ? JSON.stringify(data) : data; - postAvWebViewMessage.postMessage(message); - } - """ - , null); + WebViewHelper.BuildInvokeCSharpActionScript(WebViewHelper.PostAvWebViewMessageName, stringify: true), null); if (!_lastNavigationCompleted) { diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs index 34e0bef..c1958e3 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs @@ -16,8 +16,6 @@ namespace Avalonia.Controls.Gtk; internal abstract class GtkWebViewAdapter : IWebViewAdapterWithFocus, IGtkWebViewPlatformHandle, IWebViewWithPrintWithOptions { - private const string PostAvWebViewMessageName = "postAvWebViewMessage"; - internal enum WebKitLoadEvent { Started, @@ -111,17 +109,11 @@ protected GtkWebViewAdapter(GtkWebViewEnvironmentRequestedEventArgs args) _webViewHandle = webkit_web_view_new_with_context(context); var contentManager = webkit_web_view_get_user_content_manager(WebViewHandle); - _scriptMessageReceivedSignal = new GtkSignal(contentManager, $"script-message-received::{PostAvWebViewMessageName}", s_scriptMessageReceivedCallback, this); - webkit_user_content_manager_register_script_message_handler(contentManager, PostAvWebViewMessageName); + _scriptMessageReceivedSignal = new GtkSignal(contentManager, $"script-message-received::{WebViewHelper.PostAvWebViewMessageName}", s_scriptMessageReceivedCallback, this); + webkit_user_content_manager_register_script_message_handler(contentManager, WebViewHelper.PostAvWebViewMessageName); var script = webkit_user_script_new( - $$""" - function invokeCSharpAction(data) - { - var message = typeof data === 'object' ? JSON.stringify(data) : data; - window.webkit.messageHandlers.{{PostAvWebViewMessageName}}.postMessage(message); - } - """, + WebViewHelper.BuildWebKitInvokeCSharpActionScript(), 0, 0, IntPtr.Zero, IntPtr.Zero); webkit_user_content_manager_add_script(contentManager, script); diff --git a/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs index 700e150..5976f53 100644 --- a/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs @@ -21,8 +21,6 @@ namespace Avalonia.Controls.Macios; internal class MaciosWebViewAdapter : IWebViewAdapterWithFocus, IWebViewAdapterWithInputRedirect, IWebViewAdapterWithCookieManager, IWebViewAdapterWithCommands, IWebViewWithPrint, IAppleWKWebViewPlatformHandle { - private const string DefaultPostAvWebViewMessageName = "postAvWebViewMessage"; - private readonly string _scriptHandlerMessageName; private readonly NSString _scriptHandlerMessageNameNative; private readonly WKWebViewConfiguration _config; @@ -40,7 +38,7 @@ public MaciosWebViewAdapter(AppleWKWebViewEnvironmentRequestedEventArgs options) _scriptHandler = new WKScriptMessageHandler(); _scriptHandler.DidReceiveScriptMessage += OnScriptHandlerOnDidReceiveScriptMessage; - _scriptHandlerMessageName = options.ScriptHandlerMessageName ?? DefaultPostAvWebViewMessageName; + _scriptHandlerMessageName = options.ScriptHandlerMessageName ?? WebViewHelper.PostAvWebViewMessageName; _scriptHandlerMessageNameNative = NSString.Create(_scriptHandlerMessageName); _config = new WKWebViewConfiguration { JavaScriptEnabled = true }; _config.AddScriptMessageHandler(_scriptHandler, _scriptHandlerMessageNameNative); @@ -297,7 +295,7 @@ private void OnDelegateOnDecidePolicyNavigation(object? _, WKNavigationDelegate. private async void OnDelegateOnDidFinishNavigation(object? sender, EventArgs args) { - _ = await InvokeScript($"function invokeCSharpAction(data){{window.webkit.messageHandlers.{_scriptHandlerMessageName}.postMessage(data);}}"); + _ = await InvokeScript(WebViewHelper.BuildWebKitInvokeCSharpActionScript(_scriptHandlerMessageName, stringify: true)); using var url = _webView.Url; NavigationCompleted?.Invoke(this, new WebViewNavigationCompletedEventArgs { Request = Uri.TryCreate(url!.AbsoluteString, UriKind.Absolute, out var uri) ? uri : null, IsSuccess = true }); diff --git a/src/Avalonia.Controls.WebView.Core/WebViewHelper.cs b/src/Avalonia.Controls.WebView.Core/WebViewHelper.cs index 4fcfdeb..276edf0 100644 --- a/src/Avalonia.Controls.WebView.Core/WebViewHelper.cs +++ b/src/Avalonia.Controls.WebView.Core/WebViewHelper.cs @@ -4,8 +4,33 @@ namespace Avalonia.Controls; internal static class WebViewHelper { + internal const string PostAvWebViewMessageName = "postAvWebViewMessage"; + public static Uri EmptyPage { get; } = new("about:blank"); + internal static string BuildWebKitInvokeCSharpActionScript( + string messageName = PostAvWebViewMessageName, bool stringify = true) => + BuildInvokeCSharpActionScript("window.webkit.messageHandlers." + messageName, stringify: stringify); + + /// Target object to send message to. + /// Method on the that should be invoked to pass the message. + /// + /// Defines if post data should be JSON serialized, + /// some backends do that automatically when marshall objects to the C# handlers. + /// + internal static string BuildInvokeCSharpActionScript(string postObject, + string postMethod = "postMessage", bool stringify = true) + { + return stringify ? + "function invokeCSharpAction(data){" + + "var message = typeof data === 'object' ? JSON.stringify(data) : data;" + + $"{postObject}.{postMethod}(message);" + + "}" : + "function invokeCSharpAction(data){" + + $"{postObject}.{postMethod}(data);" + + "}"; + } + internal static bool IsAnchorNavigation(Uri? currentUrl, Uri? newUrl) { if (currentUrl is null || newUrl is null) diff --git a/src/Avalonia.Controls.WebView.Core/Win/WebView1/WebViewCallbacks.cs b/src/Avalonia.Controls.WebView.Core/Win/WebView1/WebViewCallbacks.cs index e7e0191..79eea96 100644 --- a/src/Avalonia.Controls.WebView.Core/Win/WebView1/WebViewCallbacks.cs +++ b/src/Avalonia.Controls.WebView.Core/Win/WebView1/WebViewCallbacks.cs @@ -45,14 +45,8 @@ async void InitScript() { try { - var initScript = - """ - window.invokeCSharpAction = function(data) { - var message = typeof data === 'object' ? JSON.stringify(data) : data; - window.external.notify(message); - }; - """; - await adapter.InvokeScript(initScript); + await adapter.InvokeScript(WebViewHelper.BuildInvokeCSharpActionScript( + "window.external", postMethod: "notify", stringify: true)); } catch (Exception ex) { diff --git a/src/Avalonia.Controls.WebView.Core/Win/WebView2/WebView2BaseAdapter.cs b/src/Avalonia.Controls.WebView.Core/Win/WebView2/WebView2BaseAdapter.cs index 7ad2612..2c707b7 100644 --- a/src/Avalonia.Controls.WebView.Core/Win/WebView2/WebView2BaseAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Win/WebView2/WebView2BaseAdapter.cs @@ -297,7 +297,7 @@ public async Task InitializeAsync(WindowsWebView2EnvironmentRequestedEventArgs e var addScriptCompletion = new AddScriptToExecuteOnDocumentCreatedCompletedHandler(); var webView = TryGetWebView2() ?? throw new InvalidOperationException("WebView2 is not initialized."); webView.AddScriptToExecuteOnDocumentCreated( - "function invokeCSharpAction(data){window.chrome.webview.postMessage(data);}", addScriptCompletion); + WebViewHelper.BuildInvokeCSharpActionScript("window.chrome.webview", stringify: false), addScriptCompletion); _ = await addScriptCompletion.Result.Task; controller.SetIsVisible(1); From 9fc9e48b90efc8597a76b0f83999f7171fff2fa6 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 13 Aug 2026 20:35:35 -0700 Subject: [PATCH 2/2] Fix WPE backend script callbacks --- .../Linux/Interop/WpeInterop.cs | 16 +++++++ .../Linux/WpeWebViewAdapter.cs | 44 ++++++++++++++++--- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls.WebView.Core/Linux/Interop/WpeInterop.cs b/src/Avalonia.Controls.WebView.Core/Linux/Interop/WpeInterop.cs index 0aeba21..53c63de 100644 --- a/src/Avalonia.Controls.WebView.Core/Linux/Interop/WpeInterop.cs +++ b/src/Avalonia.Controls.WebView.Core/Linux/Interop/WpeInterop.cs @@ -137,11 +137,27 @@ public static partial void webkit_web_view_set_background_color( [LibraryImport(LibWpeWebKit)] public static partial IntPtr webkit_web_view_get_user_content_manager(IntPtr webView); + [LibraryImport(LibWpeWebKit)] + public static partial IntPtr webkit_user_content_manager_get_type(); + + [LibraryImport(LibWpeWebKit)] + public static partial IntPtr webkit_user_content_manager_new(); + [LibraryImport(LibWpeWebKit, StringMarshalling = StringMarshalling.Utf8)] [return: MarshalAs(UnmanagedType.Bool)] public static partial bool webkit_user_content_manager_register_script_message_handler( IntPtr manager, string name, string? worldName); + [LibraryImport(LibWpeWebKit)] + public static partial void webkit_user_content_manager_add_script(IntPtr manager, IntPtr userScript); + + [LibraryImport(LibWpeWebKit, StringMarshalling = StringMarshalling.Utf8)] + public static partial IntPtr webkit_user_script_new( + string source, int injectedFrames, int injectionTime, IntPtr allowList, IntPtr blockList); + + [LibraryImport(LibWpeWebKit)] + public static partial void webkit_user_script_unref(IntPtr userScript); + [LibraryImport(LibWpeWebKit, StringMarshalling = StringMarshalling.Utf8)] public static partial void webkit_settings_set_user_agent(IntPtr settings, string? userAgent); diff --git a/src/Avalonia.Controls.WebView.Core/Linux/WpeWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Linux/WpeWebViewAdapter.cs index e04e9ef..39575f9 100644 --- a/src/Avalonia.Controls.WebView.Core/Linux/WpeWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Linux/WpeWebViewAdapter.cs @@ -33,6 +33,7 @@ internal sealed unsafe class WpeWebViewAdapter private IntPtr _exportable; private IntPtr _viewBackend; private IntPtr _networkSession; + private IntPtr _userContentManager; private IntPtr _cookieManager; private bool _exportableOwnedByWebKit; // true when webkit_web_view_backend_new took ownership private PixelSize _currentSize; @@ -231,15 +232,32 @@ private void Initialize(LinuxWpeWebViewEnvironmentRequestedEventArgs args, Contr else _networkSession = WpeInterop.webkit_network_session_get_default(); + _userContentManager = WpeInterop.webkit_user_content_manager_new(); + if (_userContentManager == IntPtr.Zero) + throw new InvalidOperationException("webkit_user_content_manager_new failed."); + var webViewType = WpeInterop.webkit_web_view_get_type(); var wkBackendType = WpeInterop.webkit_web_view_backend_get_type(); var networkSessionType = WpeInterop.webkit_network_session_get_type(); - var keys = new[] { "backend", "network-session" }; - var values = new[] { new GValue(wkBackendType, wkBackend), new GValue(networkSessionType, _networkSession) }; - _webView = WpeInterop.g_object_new_with_properties(webViewType, 2, keys, values); + var contentManagerType = WpeInterop.webkit_user_content_manager_get_type(); + var keys = new[] { "backend", "network-session", "user-content-manager" }; + var values = new[] + { + new GValue(wkBackendType, wkBackend), + new GValue(networkSessionType, _networkSession), + new GValue(contentManagerType, _userContentManager) + }; + _webView = WpeInterop.g_object_new_with_properties(webViewType, 3, keys, values); if (_webView == IntPtr.Zero) throw new InvalidOperationException("webkit_web_view_new failed."); + var viewContentManager = WpeInterop.webkit_web_view_get_user_content_manager(_webView); + if (viewContentManager != IntPtr.Zero && viewContentManager != _userContentManager) + { + WpeInterop.g_object_unref(_userContentManager); + _userContentManager = WpeInterop.g_object_ref(viewContentManager); + } + // 5. Start GLib pump (WebKit needs it for internal IPC) WpeGLibIntegration.Start(); @@ -258,11 +276,17 @@ private void Initialize(LinuxWpeWebViewEnvironmentRequestedEventArgs args, Contr ConnectSignal(_webView, "decide-policy", Marshal.GetFunctionPointerForDelegate(_decidePolicyCallback), selfPtr); ConnectSignal(_webView, "create", Marshal.GetFunctionPointerForDelegate(_createCallback), selfPtr); - // 8. Register invokeCSharpAction message handler - var contentManager = WpeInterop.webkit_web_view_get_user_content_manager(_webView); - WpeInterop.webkit_user_content_manager_register_script_message_handler(contentManager, "invokeCSharpAction", null); - ConnectSignal(contentManager, "script-message-received::invokeCSharpAction", + // 8. Register the message handler, connecting before registering so that no message can arrive unhandled. + ConnectSignal(_userContentManager, $"script-message-received::{WebViewHelper.PostAvWebViewMessageName}", Marshal.GetFunctionPointerForDelegate(_scriptMessageCallback), selfPtr); + WpeInterop.webkit_user_content_manager_register_script_message_handler( + _userContentManager, WebViewHelper.PostAvWebViewMessageName, null); + + // Inject the invokeCSharpAction wrapper into all frames at document start. + var bridgeScript = WpeInterop.webkit_user_script_new( + WebViewHelper.BuildWebKitInvokeCSharpActionScript(), 0, 0, IntPtr.Zero, IntPtr.Zero); + WpeInterop.webkit_user_content_manager_add_script(_userContentManager, bridgeScript); + WpeInterop.webkit_user_script_unref(bridgeScript); // 9. Apply settings var settings = WpeInterop.webkit_web_view_get_settings(_webView); @@ -1019,6 +1043,12 @@ public void Dispose() _webView = IntPtr.Zero; } + if (_userContentManager != IntPtr.Zero) + { + WpeInterop.g_object_unref(_userContentManager); + _userContentManager = IntPtr.Zero; + } + if (!_exportableOwnedByWebKit && _exportable != IntPtr.Zero) { WpeInterop.wpe_view_backend_exportable_fdo_destroy(_exportable);