【问题标题】:JS/.NET interact on MAUI WebViewJS/.NET 在 MAUI WebView 上交互
【发布时间】:2022-09-29 12:12:32
【问题描述】:

我正在将我们的应用程序从 Xamarin 迁移到 MAUI,并且在迁移 Android 和 iOS 上的 WebView 中处理 JS/.NET 交互的代码时我有点挣扎。让我们专注于Android。它特别是关于从 WebView 中的 JS 调用 .NET 代码。

在 Xamarin 中,我们可以这样做(基本上根据本教程 https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/custom-renderer/hybridwebview):

protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
    base.OnElementChanged(e);

    if (e.OldElement != null)
    {
        Control.RemoveJavascriptInterface(\"jsBridge\");
    }

    if (e.NewElement != null)
    {
        Control.SetWebViewClient(new JavascriptWebViewClient(this, $\"javascript: {JavascriptFunction}\"));
        Control.AddJavascriptInterface(new JsBridge(this), \"jsBridge\");
    }
}

public class JavascriptWebViewClient : FormsWebViewClient
{
    private readonly string javascript;

    public JavascriptWebViewClient(HybridWebViewRenderer renderer, string javascript) : base(renderer)
    {
        this.javascript = javascript;
    }

    public override void OnPageFinished(WebView view, string url)
    {
        base.OnPageFinished(view, url);
        view.EvaluateJavascript(javascript, null);
    }
}

在带有 MAUI 的 .NET 6 中,不推荐使用此功能。我尝试使用处理程序来构建它,但从未调用过 OnPageFinished。缺乏例子让我很难弄清楚我错过了什么。

Microsoft.Maui.Handlers.WebViewHandler.Mapper.AppendToMapping(\"MyCustomization\", (handler, view) =>
  {
#if ANDROID
    handler.PlatformView.SetWebViewClient(new JavascriptWebViewClient($\"javascript: {JavascriptFunction}\"));
    handler.PlatformView.AddJavascriptInterface(new JsBridge(this), \"jsBridge\");
#endif
  });

public class JavascriptWebViewClient : WebViewClient
{
  private readonly string javascript;

  public JavascriptWebViewClient(string javascript) : base()
  {
    this.javascript = javascript;
  }

  public override void OnPageFinished(WebView view, string url)
  {
    base.OnPageFinished(view, url);
    view.EvaluateJavascript(javascript, null);
  }
}

我应该把这段代码放在哪里?这是正确的方法吗?我错过了什么?我现在把它放在一个子类 WebView 中,但可能这不是正确的方法。

    标签: c# .net-core maui


    【解决方案1】:

    更新:我为 Windows 开发了一个解决方法。见下文。

    TL;博士- https://github.com/nmoschkin/MAUIWebViewExample

    我提出了一个适用于 iOS 和 Android 的 MAUI 解决方案,使用新的 Handler 模式,如下所述:

    Porting Custom Renderers To Handlers

    上面的文档有些差,并且没有针对 iOS 版本的实现。我在这里提供。

    这种改编也使 Source 属性成为可绑定属性.与上面链接中的示例不同,我愿意不是实际上将属性添加到属性映射器以传统方式在平台处理程序中。相反,我们将监听由可绑定属性的属性更改通知方法触发的事件。

    这个例子实现了一个 100% 自定义的 WebView。如果您想从本机组件移植其他属性和方法,则必须自己添加这些附加功能。

    共享代码:

    在共享代码文件中,您希望通过以下方式实现上述链接中描述的类和接口来创建自定义视图(为我们将提供给消费者的事件提供额外的类):

    public class SourceChangedEventArgs : EventArgs
    {
        public WebViewSource Source
        {
            get;
            private set;
        }
    
        public SourceChangedEventArgs(WebViewSource source)
        {
            Source = source;
        }
    }
    
    public class JavaScriptActionEventArgs : EventArgs
    {
        public string Payload { get; private set; }
    
        public JavaScriptActionEventArgs(string payload)
        {
            Payload = payload;
        }
    }
    
    public interface IHybridWebView : IView
    {
        event EventHandler<SourceChangedEventArgs> SourceChanged;
        event EventHandler<JavaScriptActionEventArgs> JavaScriptAction;
        
        void Refresh();
    
        WebViewSource Source { get; set; }
    
        void Cleanup();
    
        void InvokeAction(string data);
    
    }
        
    
    public class HybridWebView : View, IHybridWebView
    {
        public event EventHandler<SourceChangedEventArgs> SourceChanged;
        public event EventHandler<JavaScriptActionEventArgs> JavaScriptAction;
    
        public HybridWebView()
        {
    
        }
    
        public void Refresh()
        {
            if (Source == null) return;
            var s = Source;
            Source = null;
            Source = s;
        }
    
        public WebViewSource Source
        {
            get { return (WebViewSource)GetValue(SourceProperty); }
            set { SetValue(SourceProperty, value); }
        }
    
        public static readonly BindableProperty SourceProperty = BindableProperty.Create(
          propertyName: "Source",
          returnType: typeof(WebViewSource),
          declaringType: typeof(HybridWebView),
          defaultValue: new UrlWebViewSource() { Url = "about:blank" },
          propertyChanged: OnSourceChanged);
    
        private static void OnSourceChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var view = bindable as HybridWebView;
    
            bindable.Dispatcher.Dispatch(() =>
            {
                view.SourceChanged?.Invoke(view, new SourceChangedEventArgs(newValue as WebViewSource));
    
            });
        }
    
        public void Cleanup()
        {
            JavaScriptAction = null;
        }
    
        public void InvokeAction(string data)
        {
            JavaScriptAction?.Invoke(this, new JavaScriptActionEventArgs(data));
        }
    }
    

    然后您必须为每个平台声明处理程序,如下所示:

    安卓实现:

    public class HybridWebViewHandler : ViewHandler<IHybridWebView, Android.Webkit.WebView>
    {
        public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);
    
        const string JavascriptFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}";
    
        private JSBridge jsBridgeHandler;
    
        public HybridWebViewHandler() : base(HybridWebViewMapper)
        {
        }
    
        private void VirtualView_SourceChanged(object sender, SourceChangedEventArgs e)
        {
            LoadSource(e.Source, PlatformView);
        }
    
        protected override Android.Webkit.WebView CreatePlatformView()
        {
            var webView = new Android.Webkit.WebView(Context);
            jsBridgeHandler = new JSBridge(this);
    
            webView.Settings.JavaScriptEnabled = true;
    
            webView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {JavascriptFunction}"));
            webView.AddJavascriptInterface(jsBridgeHandler, "jsBridge");
    
            return webView;
        }
    
        protected override void ConnectHandler(Android.Webkit.WebView platformView)
        {
            base.ConnectHandler(platformView);
    
            if (VirtualView.Source != null)
            {
                LoadSource(VirtualView.Source, PlatformView);
            }
    
            VirtualView.SourceChanged += VirtualView_SourceChanged;
        }
    
        protected override void DisconnectHandler(Android.Webkit.WebView platformView)
        {
            base.DisconnectHandler(platformView);
    
            VirtualView.SourceChanged -= VirtualView_SourceChanged;
            VirtualView.Cleanup();
    
            jsBridgeHandler?.Dispose();
            jsBridgeHandler = null;
        }
    
        private static void LoadSource(WebViewSource source, Android.Webkit.WebView control)
        {
            try
            {
                if (source is HtmlWebViewSource html)
                {
                    control.LoadDataWithBaseURL(html.BaseUrl, html.Html, null, "charset=UTF-8", null);
                }
                else if (source is UrlWebViewSource url)
                {
                    control.LoadUrl(url.Url);
                }
            }
            catch { }
        }
    }
    
    public class JavascriptWebViewClient : WebViewClient
    {
        string _javascript;
    
        public JavascriptWebViewClient(string javascript)
        {
            _javascript = javascript;
        }
    
        public override void OnPageStarted(Android.Webkit.WebView view, string url, Bitmap favicon)
        {
            base.OnPageStarted(view, url, favicon);
            view.EvaluateJavascript(_javascript, null);
        }
    }
    
    public class JSBridge : Java.Lang.Object
    {
        readonly WeakReference<HybridWebViewHandler> hybridWebViewRenderer;
    
        internal JSBridge(HybridWebViewHandler hybridRenderer)
        {
            hybridWebViewRenderer = new WeakReference<HybridWebViewHandler>(hybridRenderer);
        }
    
        [JavascriptInterface]
        [Export("invokeAction")]
        public void InvokeAction(string data)
        {
            HybridWebViewHandler hybridRenderer;
    
            if (hybridWebViewRenderer != null && hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
            {
                hybridRenderer.VirtualView.InvokeAction(data);
            }
        }
    }
    

    iOS 实现:

    public class HybridWebViewHandler : ViewHandler<IHybridWebView, WKWebView>
    {
        public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);
    
        const string JavaScriptFunction = "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}";
    
        private WKUserContentController userController;
        private JSBridge jsBridgeHandler;
    
        public HybridWebViewHandler() : base(HybridWebViewMapper)
        {
        }
    
        private void VirtualView_SourceChanged(object sender, SourceChangedEventArgs e)
        {
            LoadSource(e.Source, 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);
    
            if (VirtualView.Source != null)
            {
                LoadSource(VirtualView.Source, PlatformView);
            }
    
            VirtualView.SourceChanged += VirtualView_SourceChanged;
        }
    
        protected override void DisconnectHandler(WKWebView platformView)
        {
            base.DisconnectHandler(platformView);
    
            VirtualView.SourceChanged -= VirtualView_SourceChanged;
    
            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<HybridWebViewHandler> hybridWebViewRenderer;
    
        internal JSBridge(HybridWebViewHandler hybridRenderer)
        {
            hybridWebViewRenderer = new WeakReference<HybridWebViewHandler>(hybridRenderer);
        }
    
        public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
        {
            HybridWebViewHandler hybridRenderer;
    
            if (hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
            {
                hybridRenderer.VirtualView?.InvokeAction(message.Body.ToString());
            }
        }
    }
    

    如您所见,我正在监听事件以更改源,然后它将执行更改它所需的特定于平台的步骤。

    另请注意,在这两种实现中JSBridge我正在使用一个弱引用跟踪控制。我不确定处置可能会陷入僵局的任何情况,但我这样做是出于非常谨慎的考虑。

    窗口实现

    所以。根据我阅读的各种文章,当前的 WebView2 for MAUI 的 WinUI3 迭代还不允许我们调用AddHostObjectToScript.他们计划在将来的版本中进行此操作。

    但是,后来我记得它是 Windows,所以我创建了一个变通方案,可以肯定地模拟相同的行为并达到相同的结果,但使用了一个有点非正统的解决方案:通过使用HttpListener.

    public class HybridSocket
    {
        private HttpListener listener;
        private HybridWebViewHandler handler;
        bool token = false;
        
        public HybridSocket(HybridWebViewHandler handler)
        {
            this.handler = handler;
            CreateSocket();
        }
    
        private void CreateSocket()
        {
            listener = new HttpListener();
            listener.Prefixes.Add("http://localhost:32000/");
        }
    
        public void StopListening()
        {
            token = false;
        }
    
        private void Fling(string json)
        {
            handler.VirtualView.InvokeAction(json);
        }
    
        public void Listen()
        {
            var s = listener;
            try
            {
                token = true;
                s.Start();
                
                while (token)
                {                
                    HttpListenerContext ctx = listener.GetContext();
                    
                    using HttpListenerResponse resp = ctx.Response;
    
                    resp.AddHeader("Access-Control-Allow-Origin", "null");
                    resp.AppendHeader("Access-Control-Allow-Headers", "content-type");
                    
                    var req = ctx.Request;
    
                    Stream body = req.InputStream;
                    Encoding encoding = req.ContentEncoding;
    
                    using (StreamReader reader = new StreamReader(body, encoding))
                    {
                        if (req.ContentType != null)
                        {
                            Console.WriteLine("Client data content type {0}", req.ContentType);
                        }
    
                        var json = reader.ReadToEnd();
                        if (!string.IsNullOrEmpty(json))
                        {
                            Fling(json);
                        }
                    }
    
    
                    resp.StatusCode = (int)HttpStatusCode.OK;
                    resp.StatusDescription = "Status OK";
                }
                           
                CreateSocket();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }
    }
    
    public class HybridWebViewHandler : ViewHandler<IHybridWebView, WebView2>
    {
        public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);
    
        const string JavascriptFunction = @"function invokeCSharpAction(data)
                    {
                        var http = new XMLHttpRequest();
                        var url = 'http://localhost:32000';
                        http.open('POST', url, true);
                        http.setRequestHeader('Content-type', 'application/json');
                        http.send(JSON.stringify(data));
                    }";
    
        static SynchronizationContext sync;
        private HybridSocket socketer;
    
        public HybridWebViewHandler() : base(HybridWebViewMapper)
        {
            sync = SynchronizationContext.Current;
            socketer = new HybridSocket(this);
    
            Task.Run(() => socketer.Listen());
        }
    
        ~HybridWebViewHandler()
        {
            socketer.StopListening();
        }
    
        private void OnWebSourceChanged(object sender, SourceChangedEventArgs e)
        {
            LoadSource(e.Source, PlatformView);
        }
    
        protected override WebView2 CreatePlatformView()
        { 
            sync = sync ?? SynchronizationContext.Current;
            var webView = new WebView2();
    
            webView.NavigationCompleted += WebView_NavigationCompleted;
    
            return webView;
        }
    
        private void WebView_NavigationCompleted(WebView2 sender, CoreWebView2NavigationCompletedEventArgs args)
        {
            var req = new EvaluateJavaScriptAsyncRequest(JavascriptFunction);
            PlatformView.EvaluateJavaScript(req);
        }
    
        protected override void ConnectHandler(WebView2 platformView)
        {
            base.ConnectHandler(platformView);
    
            if (VirtualView.Source != null)
            {
                LoadSource(VirtualView.Source, PlatformView);
            }
    
            VirtualView.SourceChanged += OnWebSourceChanged;
        }
    
        protected override void DisconnectHandler(WebView2 platformView)
        {
            base.DisconnectHandler(platformView);
    
            VirtualView.SourceChanged -= OnWebSourceChanged;
            VirtualView.Cleanup();
        }
    
        private static void LoadSource(WebViewSource source, WebView2 control)
        {
            try
            {
                if (control.CoreWebView2 == null)
                {
                    control.EnsureCoreWebView2Async().AsTask().ContinueWith((t) =>
                    {
                       
                        sync.Post((o) =>
                        {
                            if (source is HtmlWebViewSource html)
                            {
                                control.CoreWebView2.NavigateToString(html.Html);
                            }
                            else if (source is UrlWebViewSource url)
                            {
                                control.CoreWebView2.Navigate(url.Url);
                            }
                        }, null);
                    });
                }
                else
                {
                    if (source is HtmlWebViewSource html)
                    {
                        control.CoreWebView2.NavigateToString(html.Html);
                    }
                    else if (source is UrlWebViewSource url)
                    {
                        control.CoreWebView2.Navigate(url.Url);
                    }
                }
            }
            catch { }
        }
    }
    

    最后,您需要通过添加初始化 MAUI 应用程序配置MauiHandlers应用程序生成器:

    在 MauiProgram.cs 中初始化 MAUI 应用程序

    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(HybridWebView), typeof(HybridWebViewHandler));
            });
    
    
        return builder.Build();
    }
    

    将控件添加到 XAML

    <controls:HybridWebView
                x:Name="MyWebView"
                HeightRequest="128"
                HorizontalOptions="Fill"
                Source="{Binding Source}"
                VerticalOptions="FillAndExpand"
                WidthRequest="512"
                />
    

    最后,我将以上所有内容添加到 GitHub 存储库中的完整示例 MAUI 项目中:

    https://github.com/nmoschkin/MAUIWebViewExample

    GitHub 存储库示例还包括一个 ViewModel,其中包含WebViewSource控件在标记中绑定到的。

    【讨论】:

    • 干得好——干得好!!!我对 Windows 进行了同样的尝试,但我找不到像 AddUserScript() 和 CoreWebView2.AddHostObjectToScript(...) 这样的函数会引发错误:“组或资源未处于执行请求操作的正确状态。”您能否为 Windows 扩展您的示例?
    • 当然!我可以尽力而为。
    • @Markus - 你能把我链接到你已经为 Windows 做的事情吗?我不想重复努力。
    • Nathan:我在你的 github 项目上开始了一个问题......
    • @Markus 好的,我已经对 GitHub 问题进行了分类。我可能要到星期二才能开始。在为我的公司进行重大部署的过程中。
    【解决方案2】:

    好的,想通了。为那些寻找相同问题的人添加信息。

    你需要做什么:

    1. 覆盖 WebView 客户端。

      通用的:

      public partial class CustomWebView : WebView
      {
          partial void ChangedHandler(object sender);
          partial void ChangingHandler(object sender, HandlerChangingEventArgs e);
      
          protected override void OnHandlerChanging(HandlerChangingEventArgs args)
          {
              base.OnHandlerChanging(args);
              ChangingHandler(this, args);
          }
      
          protected override void OnHandlerChanged()
          {
              base.OnHandlerChanged();
              ChangedHandler(this);
          }
      
          public void InvokeAction(string data)
          {
              // your custom code
          }
      }
      
      

      安卓:

      public partial class CustomWebView
      {
          const string JavascriptFunction = "function invokeActionInCS(data){jsBridge.invokeAction(data);}";
      
          partial void ChangedHandler(object sender)
          {
              if (sender is not WebView { Handler: { PlatformView: Android.Webkit.WebView nativeWebView } }) return;
      
              nativeWebView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {JavascriptFunction}"));
              nativeWebView.AddJavascriptInterface(new JsBridge(this), "jsBridge");
          }
      
          partial void ChangingHandler(object sender, HandlerChangingEventArgs e)
          {
              if (e.OldHandler != null)
              {
                  if (sender is not WebView { Handler: { PlatformView: Android.Webkit.WebView nativeWebView } }) return;
                  nativeWebView.RemoveJavascriptInterface("jsBridge");
              }
          }
      }
      
      1. 将此自定义视图添加到您的 XAML
      <views:CustomWebView x:Name="CustomWebViewName"/>
      
      1. 修改 JS 桥
      public class JsBridge : Java.Lang.Object
          {
              private readonly HarmonyWebView webView;
      
              public JsBridge(HarmonyWebView webView)
              {
                  this.webView = webView;
              }
      
              [JavascriptInterface]
              [Export("invokeAction")]
              public void InvokeAction(string data)
              {
                  webView.InvokeAction(data);
              }
          }
      

    【讨论】:

    • 实际上,我一直在为您准备一个适用于 iOS 和 Android 的答案,因为我需要在我的一个项目中完全覆盖控件。我刚刚在测试最后一点(昨天新版本的 MAUI 下降了,我不得不重新安排一切)我实际上只是将 ContentView 子类化并从头开始创建整个 Web 视图,以提供更大的灵活性。
    • 感谢您的努力,非常感谢!我仍然期待看到您的解决方案,很高兴看到它是否比我的更好。
    • 我还在努力。完成后,我会在这里发布。
    • 完毕!经验证可在 Android 和 iOS 上运行
    猜你喜欢
    • 2022-12-12
    • 2022-10-25
    • 2022-11-08
    • 2022-08-23
    • 1970-01-01
    • 2022-12-24
    • 1970-01-01
    • 1970-01-01
    • 2023-02-02
    相关资源
    最近更新 更多