Xamarin.Forms Hybrid Webview

MDemirMDemir USMember ✭✭
edited November 2018 in Xamarin.Forms

Hi fellas,

I've a HybridWebview with handle JS Event Register and Evaluate JS fuctions and its works well. But i need some improvment on my hybridview whitch is js alert,promp, etc. support. I already find a way in Android side adding setWebCrome parametters. But still have problem with ios part. I find a solution on xamarin recepit called webview handle with js project but i couldn't understand how can i implament this.

Here is my HybridWebview code on X.Forms Side

using System;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace Paperwork.Mobile.CustomControls
{
public class PWHybridWebView : WebView
{
    public static readonly BindableProperty UriProperty = BindableProperty.Create(
        "Uri",
        typeof(string),
        typeof(PWHybridWebView),
        default(string));

    //public static BindableProperty DialogProperty =
    //    BindableProperty.Create(nameof(Dialog), typeof(IUserDialogs), typeof(PWHybridWebView), null);

    //public IUserDialogs Dialog
    //{
    //    get { return (IUserDialogs)GetValue(DialogProperty); }
    //    set { SetValue(UriProperty, value); }
    //}

    public static BindableProperty EvaluateJavascriptProperty =
        BindableProperty.Create(nameof(EvaluateJavascript), typeof(Func<string, Task<string>>),
            typeof(PWHybridWebView), null, BindingMode.OneWayToSource);

    private Action<string> action;

    public Func<string, Task<string>> EvaluateJavascript
    {
        get => (Func<string, Task<string>>) GetValue(EvaluateJavascriptProperty);
        set => SetValue(EvaluateJavascriptProperty, value);
    }

    public string Uri
    {
        get => (string) GetValue(UriProperty);
        set => SetValue(UriProperty, value);
    }

    public void RegisterAction(Action<string> callback)
    {
        action = callback;
    }

    public void Cleanup()
    {
        action = null;
    }

    public void InvokeAction(string data)
    {
        if (action == null || data == null) return;
        action.Invoke(data);
    }
}
}

And ios Renderer

using System;
using System.Threading;
using System.Threading.Tasks;
using Foundation;
using Paperwork.Mobile.CustomControls;
using Paperwork.Mobile.iOS.CustomRenderers;
using WebKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(PWHybridWebView), typeof(PWHybridWebViewRenderer))]

namespace Paperwork.Mobile.iOS.CustomRenderers
{
    public class PWHybridWebViewRenderer : ViewRenderer<PWHybridWebView, WKWebView>, IWKScriptMessageHandler,
        IWKNavigationDelegate
    {
        private const string JavaScriptFunction =
            "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}";

        private WKUserContentController userController;

        public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
        {
            Element.InvokeAction(message.Body.ToString());
        }

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

            if (Control == null)
            {
                userController = new WKUserContentController();
                var script = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd,
                    false);
                userController.AddUserScript(script);
                userController.AddScriptMessageHandler(this, "invokeAction");

                var config = new WKWebViewConfiguration {UserContentController = userController};
                var webView = new WKWebView(Frame, config);
                SetNativeControl(webView);
            }

            if (e.OldElement != null)
            {
                userController.RemoveAllUserScripts();
                userController.RemoveScriptMessageHandler("invokeAction");
                var hybridWebView = e.OldElement;
                hybridWebView.Cleanup();
            }

            if (e.NewElement != null)
            {
                Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Uri)));
                var webView = e.NewElement;
                //webView.EvaluateJavascript.Invoke("Save()");

                    webView.EvaluateJavascript = async js =>
                            {
                                var reset = new ManualResetEvent(false);
                                Device.BeginInvokeOnMainThread(async () =>
                                {
                                    try
                                    {
                                        await Control.EvaluateJavaScriptAsync(js);
                                    }
                                    catch (Exception exception)
                                    {
                                        //
                                    }
                                });
                                await Task.Run(() => { reset.WaitOne(); });
                                return null;
                            };

            }
        }
    }
}

and also here is a solution from xamarin recepit

using System;

using UIKit;
using WebKit;
using Foundation;
using System.IO;

namespace WKUIDelegateDemo
{
    // We'll use the IWKUIDelegate protocol to conform to WKUIDelegate

    public partial class ViewController : UIViewController, IWKUIDelegate
    {
        WKWebView webView;

        public ViewController (IntPtr handle) : base (handle)
        {
        }

        public override void ViewDidLoad ()
        {
            base.ViewDidLoad ();

            // Create a new WKWebView, assign the delegate and add it to the view
            webView = new WKWebView(View.Frame, new WKWebViewConfiguration());
            webView.UIDelegate = this;
            View.AddSubview (webView);

            // Find the Alerts.html file and load it into the WKWebView
            string htmlPath = NSBundle.MainBundle.PathForResource ("Alerts", "html");
            string htmlContents = File.ReadAllText (htmlPath);
            webView.LoadHtmlString (htmlContents, null);
        }

        // Called when a Javascript alert() is called in the WKWebView 
        // The alert panel should display the message and a single "OK" button
        [Foundation.Export ("webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:")]
        public void RunJavaScriptAlertPanel (WebKit.WKWebView webView, string message, WebKit.WKFrameInfo frame, System.Action completionHandler)
        {
            // Create and present a native UIAlertController with the message
            var alertController = UIAlertController.Create (null, message, UIAlertControllerStyle.Alert);
            alertController.AddAction (UIAlertAction.Create ("Ok", UIAlertActionStyle.Default, null));
            PresentViewController (alertController, true, null);

            // Call the completion handler 
            completionHandler ();
        }

        // Called when a Javascript confirm() alert is called in the WKWebView
        // The alert panel should display the message with two buttons - "OK" and "Cancel"
        [Export ("webView:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:completionHandler:")]
        public void RunJavaScriptConfirmPanel (WKWebView webView, string message, WKFrameInfo frame, Action<bool> completionHandler)
        {
            // Create a native UIAlertController with the message
            var alertController = UIAlertController.Create (null, message, UIAlertControllerStyle.Alert);

            // Add two actions to the alert. Based on the result we call the completion handles and pass either true or false
            alertController.AddAction (UIAlertAction.Create ("Ok", UIAlertActionStyle.Default, okAction => {
                completionHandler(true);
            }));
            alertController.AddAction (UIAlertAction.Create ("Cancel", UIAlertActionStyle.Default, cancelAction => {
                completionHandler (false);
            }));

            // Present the alert
            PresentViewController (alertController, true, null);
        }

        // Called when a Javascript prompt() alert is called in the WKWebView
        // The alert panel should display the prompt, default placeholder text and two buttons - "OK" and "Cancel"
        [Foundation.Export ("webView:runJavaScriptTextInputPanelWithPrompt:defaultText:initiatedByFrame:completionHandler:")]
        public void RunJavaScriptTextInputPanel (WebKit.WKWebView webView, string prompt, string defaultText, WebKit.WKFrameInfo frame, System.Action<string> completionHandler)
        {
            // Create a native UIAlertController with the message
            var alertController = UIAlertController.Create (null, prompt, UIAlertControllerStyle.Alert);

            // Add a text field to the alert, set the placeholder text and keep a refernce to the field
            UITextField alertTextField = null;
            alertController.AddTextField ((textField) => {
                textField.Placeholder = defaultText;
                alertTextField = textField;
            });

            // Pass the text to the completion handler when the "OK" button is tapped
            alertController.AddAction (UIAlertAction.Create ("Ok", UIAlertActionStyle.Default, okAction => {
                completionHandler (alertTextField.Text);
            }));

            // If "Cancel" is tapped, we can just return null
            alertController.AddAction (UIAlertAction.Create ("Cancel", UIAlertActionStyle.Default, cancelAction => {
                completionHandler (null);
            }));

            // Present the alert
            PresentViewController (alertController, true, null);
        }
    }
}

Answers

  • ColeXColeX Member, Xamarin Team Xamurai
    edited November 2018

    You should make the webview implement IWKUIDelegate .

    Add IWKUIDelegate

    public class PWHybridWebViewRenderer : ViewRenderer<PWHybridWebView, WKWebView>, IWKScriptMessageHandler,
        IWKNavigationDelegate ,IWKUIDelegate
    

    Assign the UIDelegate.

     if (e.NewElement != null)
            {
                Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Uri)));
                var webView = e.NewElement;
                webview.UIDelegate = this;
                //xxx
    

    Implement the delegate methods.

        [Foundation.Export ("webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:")]
        public void RunJavaScriptAlertPanel (WebKit.WKWebView webView, string message, WebKit.WKFrameInfo frame, System.Action completionHandler)
        {
            // Create and present a native UIAlertController with the message
            var alertController = UIAlertController.Create (null, message, UIAlertControllerStyle.Alert);
            alertController.AddAction (UIAlertAction.Create ("Ok", UIAlertActionStyle.Default, null));
            PresentViewController (alertController, true, null);
    
            // Call the completion handler 
            completionHandler ();
        }
    
  • SurbhiAroraaSurbhiAroraa Member ✭✭✭

    @MDemir How did you achieve both way communication from hybrid web view i.e. JS to C# and C# to JS. I am also facing issues with HybridWebView. With HybridWebview, JS to C# is possible and in webview C# to JS is possible. How to achieve both via single control. Any leads will be appreciated much. TIA

Sign In or Register to comment.