WebViewコントロール(MAUI)とWebページ(JavaScript)の連携

WebViewコントロール(MAUI)とWebページ(JavaScript)の連携

アプリケーションとWebViewコントロール(MAUI)のWebコンテンツとの間で、データのやり取りを行うことができます。

アプリからJavaScriptの処理を実行するEvaluateJavaScriptAsyncメソッドや、
JavaScriptコードにてアプリに文字を送ることが可能です。

C#からWebページのJavaScriptを実行する方法

WebViewコントロール(MAUI)には、表示しているページに対してJavaScriptを実行するEvaluateJavaScriptAsyncメソッドがあります。
このEvaluateJavaScriptAsyncメソッドの引数にJavaScriptのコードを渡すことで実行されます。
実行結果を戻り値で取得することも可能です。

下記コードでは、アプリでボタン押下するとWeb画面でalertを表示し、JavaScriptからの値をアプリで取得してデバッグに出力ます。

							//Xamlファイルにボタンを追加し、xaml.csファイルに以下のコードを追加します。
							private async void OnJsClicked(object sender, EventArgs e)
							{
								var result = await MyWebView.EvaluateJavaScriptAsync("function test(){alert('test');return 'ok'}test();");
								Debug.WriteLine($"return val = {result}");
							}
						

注意

JavaScriptの記載が誤っている場合は、Web側で何も処理されないか、例外が発生します。
JavaScriptのコードを複数行の文字列(StringBuilderで連結したり、@を使った場合)で構成すると、なぜかは分かりませんがJavaScriptが動作しませんでした。
これを回避する為に、各行の末尾にバックスラッシュを入れるか、改行の削除(「js.Replace("\r\n", "")」)を行っています。

参考

JavaScriptからメッセージを送り、C#で受け取る方法

WebViewコントロール(MAUI)にはJavaScriptからのメッセージを受け取るイベントがありませんので、
受け取れるようにWebViewコントロールを拡張します。

MauiProgram.csに拡張WebViewの定義を行います。CreateMauiAppメソッドを以下のように変更します。

							public static MauiApp CreateMauiApp()
							{
								var builder = MauiApp.CreateBuilder();
								builder
									.UseMauiApp<App>()
									.ConfigureFonts(fonts =>
									{
										fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
										fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
									})
									.ConfigureMauiHandlers(handlers =>
									{
										handlers.AddHandler(typeof(WebViewEx), typeof(WebViewExHandler));
									});
						
								return builder.Build();
							}
						

拡張WebViewのクラスとしてWebViewEx.csを作成して、以下のコードにします。
InvokeActionメソッド
							namespace MauiWebView
							{
								public class JavaScriptActionEventArgs : EventArgs
								{
									public string Payload { get; private set; }
							
									public JavaScriptActionEventArgs(string payload)
									{
										Payload = payload;
									}
								}
							
								public class WebViewEx : WebView
								{
									public event EventHandler<JavaScriptActionEventArgs> JavaScriptAction;
							
									public WebViewEx()
									{
									}
							
									public void InvokeAction(string data)
									{
										JavaScriptAction?.Invoke(this, new JavaScriptActionEventArgs(data));
									}
								}
							}
						
MainPage.xamlに拡張WebViewのコントロールを追加します。
※変更する部分を抜粋して記載してます。
							・・・
							<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
								xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
								xmlns:controls="clr-namespace:MauiWebView"
								x:Class="MauiWebView.MainPage">
							・・・
							<controls:WebViewEx
								x:Name="MyWebView"
								HorizontalOptions="Center"
								HeightRequest="300"
								WidthRequest="500"
							/>
							<Button
								x:Name="NavigateBtn"
								Text="ローカルhtmlを表示"
								Clicked="OnNavigateClicked"
								HorizontalOptions="Center"
							/>
						
MainPage.xaml.csに処理を追加します。
							namespace MauiWebView;

							public partial class MainPage : ContentPage
							{
								public MainPage()
								{
									InitializeComponent();
							
									MyWebView.JavaScriptAction += WebViewEx_JavaScriptAction;
								}
							
								private void OnNavigateClicked(object sender, EventArgs e)
								{
									string htmlSource = @"
							<html>
							<head></head>
							<body>
							
							<script>
								var counter = 1;
								function buttonClicked(e) {
									invokeCSharpAction(String(counter++));
								}
								function invokeCSharpAction(data){window.chrome.webview.postMessage(data);}
							</script>
							
							<h1>サンプル</h1>
							<button id='firebtn' onclick='javascript:buttonClicked(event)'>クリック</button>
							</html>
							";
							
									MyWebView.Source = new HtmlWebViewSource() { Html = htmlSource };
							
								}
							
								private void WebViewEx_JavaScriptAction(object sender, JavaScriptActionEventArgs e)
								{
									Dispatcher.Dispatch(() =>
									{
										ChangeLabel.Text = "html内のクリック数: " + e.Payload;
									});
								}
							}
						
各プラットフォーム向けにWebViewExHandler.csを以下の内容で作成します。
							using Microsoft.Maui.Handlers;
							using Microsoft.UI.Xaml.Controls;
							
							namespace MauiWebView.Platforms.Windows
							{
								public class WebViewExHandler : WebViewHandler
								{
									const string JavaScriptFunction = "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}";
							
									public static IPropertyMapper<WebViewEx, WebViewExHandler> PropertyMapper = new PropertyMapper<WebViewEx, WebViewExHandler>(WebViewHandler.Mapper)
									{
										[nameof(WebViewEx.Source)] = MapSource,
									};
							
									public WebViewExHandler() : base(PropertyMapper, CommandMapper)
									{
									}
							
									protected override WebView2 CreatePlatformView()
									{
										var webView = new WebView2();
							
										webView.WebMessageReceived += MessageReceived;
							
										return webView;
									}
							
									protected async override void ConnectHandler(WebView2 platformView)
									{
										base.ConnectHandler(platformView);
										await platformView.EnsureCoreWebView2Async();
									}
							
									protected override void DisconnectHandler(WebView2 platformView)
									{
										base.DisconnectHandler(platformView);
									}
							
									//JavaScriptからメッセージを受信したときに実行
									private void MessageReceived(object sender, Microsoft.Web.WebView2.Core.CoreWebView2WebMessageReceivedEventArgs args)
									{
										//JavaScriptから通知された値を取得
										//String text = args.TryGetWebMessageAsString();  //例外になる
										var text = args.WebMessageAsJson;
							
										//WebViewクラスに通知
										((WebViewEx)VirtualView)?.InvokeAction(text);
									}
							
									public static void MapSource(WebViewExHandler handler, WebViewEx wv)
									{
										if (wv.Source != null)
										{
											if(wv.Source is UrlWebViewSource url)
											{
												(handler?.PlatformView).Source = new Uri(url.Url);
											}
											else if(wv.Source is HtmlWebViewSource html)
											{
												(handler?.PlatformView).NavigateToString(html.Html);
											}
										}
									}
								}
							}
						
							using CoreGraphics;

							using Foundation;
							using Microsoft.Maui.Controls;
							using Microsoft.Maui.Handlers;
							using Microsoft.Maui.Platform;
							using WebKit;
							
							using MauiWebView;
							using System;
							
							namespace MauiWebView.Platforms.iOS
							{
								public class WebViewExHandler : WebViewHandler
								{
									const string JavaScriptFunction = "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}";
							
									private WKUserContentController userController;
									private JSBridge jsBridgeHandler;
							
									public static IPropertyMapper<WebViewEx, WebViewExHandler> PropertyMapper = new PropertyMapper<WebViewEx, WebViewExHandler>(WebViewHandler.Mapper)
									{
										[nameof(WebViewEx.Source)] = MapSource,
									};
							
									public WebViewExHandler() : base(PropertyMapper, CommandMapper)
									{
									}
							
									public static void MapSource(WebViewExHandler handler, WebViewEx wv)
									{
										LoadSource(wv.Source, handler?.PlatformView);
									}
							
									protected override WKWebView CreatePlatformView()
									{
										jsBridgeHandler = new JSBridge(this);
										userController = new WKUserContentController();
							
										var script = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);
							
										userController.AddUserScript(script);
										userController.AddScriptMessageHandler(jsBridgeHandler, "invokeAction");
							
										var config = new WKWebViewConfiguration { UserContentController = userController };
										var webView = new WKWebView(CGRect.Empty, config);
							
										return webView;
									}
							
									protected override void ConnectHandler(WKWebView platformView)
									{
										base.ConnectHandler(platformView);
									}
							
									protected override void DisconnectHandler(WKWebView platformView)
									{
										base.DisconnectHandler(platformView);
							
										userController.RemoveAllUserScripts();
										userController.RemoveScriptMessageHandler("invokeAction");
							
										jsBridgeHandler?.Dispose();
										jsBridgeHandler = null;
									}
							
									private static void LoadSource(WebViewSource source, WKWebView control)
									{
										if (source is HtmlWebViewSource html)
										{
											control.LoadHtmlString(html.Html, new NSUrl(html.BaseUrl ?? "http://localhost", true));
										}
										else if (source is UrlWebViewSource url)
										{
											control.LoadRequest(new NSUrlRequest(new NSUrl(url.Url)));
										}
									}
								}
							
								public class JSBridge : NSObject, IWKScriptMessageHandler
								{
									readonly WeakReference<WebViewExHandler> hybridWebViewRenderer;
							
									internal JSBridge(WebViewExHandler hybridRenderer)
									{
										hybridWebViewRenderer = new WeakReference<WebViewExHandler>(hybridRenderer);
									}
							
									public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
									{
										WebViewExHandler hybridRenderer;
							
										if (hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
										{
											((WebViewEx)hybridRenderer.VirtualView)?.InvokeAction(message.Body.ToString());
										}
									}
								}
							}
						

参考