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/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);
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);