Top

Interact with Javascript from C# using Xamarin.Forms

Interact with Javascript from C# using Xamarin.Forms

In one of our projects, we needed to make a navigation from a page that only contains a WebView rendering a web page, but only the web page knew when that behavior must happened, there is no way to know from the app point of view when it should happen, so we needed to call a C# method from the Javascript code of a web page.

 

How did we did it?

 

Hopefully we found this: https://docs.microsoft.com/es-es/xamarin/xamarin-forms/app-fundamentals/custom-renderer/hybridwebview

 

The idea is:

  • A Xamarin.Forms View that contains the Url of the web page to load, and the method we want to call from Javascript.
  • A renderer for each platform that will make the magic on every platform.
public class HybridWebView : View
{
    Action action;

    public static readonly BindableProperty UriProperty =
        BindableProperty.Create(propertyName: "Uri", returnType: typeof(string), declaringType: typeof(HybridWebView), defaultValue: default(string));

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

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

    public void Cleanup()
    {
        action = null;
    }

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

        action.Invoke(data);
    }
}

That view can be used around the Xamarin.Forms project. The important thing is the RegisterAction method, that will contain the code to execute from JavaScrip. To set that code, just:

 

hybridWebView.RegisterAction (data => DisplayAlert ("Alert", "Hello " + data, "OK"));

 

This example will display a modal popup with the data that has been passed from the Javascript code.

Now we need a custom renderer for each application project. The point is, the custom renderer will create a native web control and load the HTML page, inject a invokeCSSharpAction Javascript function that will invoke the registered method of the HybridWebView control.

 

iOS Custom Renderer

 

[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))]
namespace CustomRenderer.iOS
{
    public class HybridWebViewRenderer : ViewRenderer<HybridWebView, WKWebView>, IWKScriptMessageHandler
    {
        const string JavaScriptFunction = "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}";
        WKUserContentController userController;

        protected override void OnElementChanged(ElementChangedEventArgs<HybridWebView> 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 as HybridWebView;
                hybridWebView.Cleanup();
            }
            if (e.NewElement != null)
            {
                string fileName = Path.Combine(NSBundle.MainBundle.BundlePath, string.Format("Content/{0}", Element.Uri));
                Control.LoadRequest(new NSUrlRequest(new NSUrl(fileName, false)));
            }
        }

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

This renderer will inject the Javascript function invokeCSharpAction making it available for the code on the web page, if the method is invoked from Javascript, the method DidReceiveScriptMessage  will be executed and the magic show ends.

 

Android Custom Renderer

 

To meet this behavior in Android a little more code is needed.

The renderer looks like:

 

[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))]
namespace CustomRenderer.Droid
{
    public class HybridWebViewRenderer : ViewRenderer<HybridWebView, Android.Webkit.WebView>
    {
        const string JavascriptFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}";
        Context _context;

        public HybridWebViewRenderer(Context context) : base(context)
        {
            _context = context;
        }

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

            if (Control == null)
            {
                var webView = new Android.Webkit.WebView(_context);
                webView.Settings.JavaScriptEnabled = true;
                webView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {JavascriptFunction}"));
                SetNativeControl(webView);
            }
            if (e.OldElement != null)
            {
                Control.RemoveJavascriptInterface("jsBridge");
                var hybridWebView = e.OldElement as HybridWebView;
                hybridWebView.Cleanup();
            }
            if (e.NewElement != null)
            {
                Control.AddJavascriptInterface(new JSBridge(this), "jsBridge");
                Control.LoadUrl($"file:///android_asset/Content/{Element.Uri}");
            }
        }
    }
}

We need a WebViewClient to inject the Javascript function invokeCSharpAction, but we cannot do that after the page is loaded so, the JavascriptWebViewClient we will create for that is:

 

public class JavascriptWebViewClient : WebViewClient
{
    string _javascript;

    public JavascriptWebViewClient(string javascript)
    {
        _javascript = javascript;
    }

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

The AddJavascriptInterface method will inject a “bridge” that allows the methods to be accessed from Javascript. Those methods need the JavaScriptInterface and Export attributes to make it accessible from Javascript:

 

public class JSBridge : Java.Lang.Object
{
  readonly WeakReference<HybridWebViewRenderer> hybridWebViewRenderer;

  public JSBridge (HybridWebViewRenderer hybridRenderer)
  {
    hybridWebViewRenderer = new WeakReference <HybridWebViewRenderer> (hybridRenderer);
  }

  [JavascriptInterface]
  [Export ("invokeAction")]
  public void InvokeAction (string data)
  {
    HybridWebViewRenderer hybridRenderer;

    if (hybridWebViewRenderer != null && hybridWebViewRenderer.TryGetTarget (out hybridRenderer))
    {
      hybridRenderer.Element.InvokeAction (data);
    }
  }
}

And the Android magic show is ended.

 

UWP Custom Renderer

 

The UWP scenario is very similar to iOS:

 

[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))]
namespace CustomRenderer.UWP
{
    public class HybridWebViewRenderer : ViewRenderer<HybridWebView, Windows.UI.Xaml.Controls.WebView>
    {
        const string JavaScriptFunction = "function invokeCSharpAction(data){window.external.notify(data);}";

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

            if (Control == null)
            {
                SetNativeControl(new Windows.UI.Xaml.Controls.WebView());
            }
            if (e.OldElement != null)
            {
                Control.NavigationCompleted -= OnWebViewNavigationCompleted;
                Control.ScriptNotify -= OnWebViewScriptNotify;
            }
            if (e.NewElement != null)
            {
                Control.NavigationCompleted += OnWebViewNavigationCompleted;
                Control.ScriptNotify += OnWebViewScriptNotify;
                Control.Source = new Uri(string.Format("ms-appx-web:///Content//{0}", Element.Uri));
            }
        }

        async void OnWebViewNavigationCompleted(WebView sender, WebViewNavigationCompletedEventArgs args)
        {
            if (args.IsSuccess)
            {
                // Inject JS script
                await Control.InvokeScriptAsync("eval", new[] { JavaScriptFunction });
            }
        }

        void OnWebViewScriptNotify(object sender, NotifyEventArgs e)
        {
            Element.InvokeAction(e.Value);
        }
    }
}

After the web page is loaded, the Javascript function invokeCSharpAction is injected. When it is called from the web page, a notification is sent to the renderer that will execute the registered action.

 

Summary

 

We have learnt a way to interact between Javascript and C#, to make a web page to invoke C# code in our Xamarin.Forms apps.

You can find a full code sample here: https://github.com/xamarin/xamarin-forms-samples/tree/master/CustomRenderers/HybridWebView

 

Hope this helps!

Juan Maria Laó
No Comments

Post a Comment