Invoke C# code via Javascript using Xamarin.Android WebView

I’m working on a Xamarin.Forms experiment involving some use of Xamarin.Forms.WebView. I had some issues with XLabs.HybridWebView  which I found too slow for my porpouses (pages renders in many milliseconds and javascript animation doesn’t run as smooth as I expected).

In spite of those lack, I wrote a native Activity to solve my porpouse.

The complete solution

I needed those 2 functionalities the Xamarin.Forms doesn’t offer:

  • I needed to invoke some C# methods from Javascript
  • I needed to load an external URL and inject some javascript to customize the site
using Android.App;
using Android.Widget;
using Android.OS;
using Android.Webkit;
using Android.Content.Res;
using System.IO;
using Android.Content;
using Java.Interop;

namespace PocXamAndr
{
    [Activity(Label = "PocXamAndr", MainLauncher = true, Icon = "@drawable/icon")]
    public class MainActivity : Activity
    {
        public static string jsInjection;
        static JavaScriptInterface jsInterface;
        WebView webView { get; set; }

        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            jsInterface = new JavaScriptInterface(this);
            AssetManager assets = this.Assets;
            using (StreamReader sr = new StreamReader(assets.Open("script.js")))
            {
                jsInjection = sr.ReadToEnd();
            }
            WebView webView = new WebView(this);
            webView.Settings.JavaScriptEnabled = true;
            webView.Settings.AllowUniversalAccessFromFileURLs = true;
            webView.AddJavascriptInterface(jsInterface, "JSInterface");
            webView.SetWebViewClient(new CustomWebClient());
            webView.LoadUrl("http://yoururl.com");
            SetContentView(webView);
        }
    }

    public class JavaScriptInterface : Java.Lang.Object
    {
        private Context activity;
        public JavaScriptInterface(Context activiy)
        {
            this.activity = activiy;
        }

        [Export]
        [JavascriptInterface]
        public void doThings()
        {
            Toast.MakeText(activity, "Hello from C#", ToastLength.Short).Show();
        }
    }

    public class CustomWebClient : WebViewClient
    {
        public override void OnPageFinished(WebView view, string url)
        {
            base.OnPageFinished(view, url);
            injectScriptFile(view);

        }
        private void injectScriptFile(WebView view)
        {
            string encoded = Android.Util.Base64.EncodeToString(System.Text.Encoding.ASCII.GetBytes(MainActivity.jsInjection), Android.Util.Base64Flags.NoWrap);
            view.LoadUrl("javascript:(function() {" +
                    "var parent = document.getElementsByTagName('head').item(0);" +
                    "var script = document.createElement('script');" +
                    "script.type = 'text/javascript';" +
                    "script.innerHTML = window.atob('" + encoded + "');" +
                    "parent.appendChild(script)" +
                    "})()");
        }
    }
}

To use the Export decorator add a reference to Mono.Android.Export

The interesting parts

This code let me implement a custom WebClient (look at CustomWebClient inheritance model) which overrides the OnPageFinished (aka OnNavigationCompleted) default behaviout:

webView.Settings.JavaScriptEnabled = true;
            webView.Settings.AllowUniversalAccessFromFileURLs = true;
            webView.AddJavascriptInterface(jsInterface, "JSInterface");
            webView.SetWebViewClient(new CustomWebClient());

The AddJavascriptInterface creates a fake javascript object which links to the C# JavaScriptInterface instance.

The injectScriptFile method is pretty awesome: the problem I found with

webView.LoadUrl("javascript:...");

is that the javascript code was executed outside the DOM context and because of this it was useless for my purpose.

The javascript resources

I preferred place my javascript inside an external folder despite the I had to add a reference to System.Io. By the way, the app is only 4MB and loading the js outside made me free from the frustration of escaping every line of javascript.
Here my Assets/script.js

(function () {
	var node = document.createElement('LI');
	var anode = document.createElement('A');
	var textnode = document.createTextNode('Invoke C# code...');
	node.appendChild(anode);
	anode.appendChild(textnode);
	document.querySelector('.someStyle').appendChild(node); //Must match some DOM element
	node.addEventListener('click', function () {
		JSInterface.doThings(); //Will invoke the C# equivalent
	});
})();

Last lines

I wrote this code in Java before because after seing the XLabs.HybridWebView I opted for that solution. This post borns after I wrote the equivalent native code with Xamarin, just to have some fun and to compare performance.
I’m glad to say performance are equivalent using Xamarin.Android… I guess there still are some optimization problems with Xamarin.Forms 😐

Leave a Reply

Your email address will not be published. Required fields are marked *