Implementing a bi-directional communication between C# and Javascript in Xamarin.iOS

Xamarin is a cross-platform mobile application development framework which uses .NET ecosystem.

The key advantages of Xamarin are,

  • Ability to create fully native user interfaces, as well as a cross-platform UI toolkit called Xamarin.Forms
  • Application logic can be reused by making a shared .NET project.
  • There are C# class mappings for all native components and required classes of Android and iOS.

Webview components in iOS

There are two webview components in iOS, UIWebView and WKWebView. The first one is now deprecated (Starting in iOS 8.0 and OS X 10.10 Apple recommends not to use UIWebView). Therefore, we are able to use WKWebView in order to display rich HTML content inside our iOS applications.


Bi-directional communication

There are situations that developers need to invoke native code from Javascript and vice versa. In other words, it is like implementing a bridge between native code and web scripts.


Getting started with WkWebview sample project

It is possible to load web content to web view from a string, remote URL and local file path. We will be using content from a local file in this tutorial. Thus, our goal is to display a text message in webview via a native button and display a text message in native via a button which resides inside the web view. (See the figure below)

xamarin 1

At the top of the screen, there is a webview which has rendered version of index.html file. The HTML file defines invokeWeb function which will be triggered from native ‘Invoke Web’ button. ‘Invoke Native’ HTML button calls InvokeNative function which triggers a native function.

Resources/www/index.html source

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
    <body style="background-color: #bbbbbb;">
        <br />
        <br />
        <br />
        <input
           type="button"
           value="Invoke Native"
           style="width: 100%; font-size: 18px; padding: 6px;"
           onclick="invokeNative('Hello!');">
        <br />
        <br />
        <center>
            <span id="msg" style="font-size: 18px;">Ready.</span>
        </center>


     <script>
        function invokeWeb(param) {
            document.getElementById('msg').innerHTML = param;
            setTimeout(function() {
                document.getElementById('msg').innerHTML = 'Ready';
            }, 1000);
        }
    </script>
    </body>
</html>

ViewController.cs source

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

namespace WebviewCom.iOS
{
    public partial class ViewController : UIViewController
    {

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

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

            webView.Configuration.UserContentController.AddScriptMessageHandler(new MessageHandler(this), "native");

            webView.Configuration.UserContentController.AddUserScript(
            new WKUserScript(
                new NSString("window.invokeNative=function (param) {window.webkit.messageHandlers.native.postMessage(param);}"),
                WKUserScriptInjectionTime.AtDocumentStart, false));

            string index = Path.Combine(NSBundle.MainBundle.ResourcePath, "www/index.html");
            string webAppFolder = Path.Combine(NSBundle.MainBundle.ResourcePath, "www");
            webView.LoadFileUrl(new NSUrl("file://" + index),  new NSUrl("file://" + webAppFolder));
        }

        public override void DidReceiveMemoryWarning()
        {
            base.DidReceiveMemoryWarning();
            // Release any cached data, images, etc that aren't in use.
        }

        partial void Btn_TouchUpInside(UIButton sender)
        {
            webView.EvaluateJavaScript("invokeWeb('Hello!')", null);
        }

        public async Task ChangeTextAsync(int delay, string msg)
        {
            await Task.Delay(delay);
            lbl.Text = msg;
        }
    }

    class MessageHandler : NSObject, IWKScriptMessageHandler
    {
        ViewController ctx;

        public MessageHandler(ViewController context)
        {
            ctx = context;
        }

        public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
        {
            InvokeOnMainThread(() => {
                ctx.ChangeTextAsync(0, message.Body.ToString());
                ctx.ChangeTextAsync(1000, "Ready.");
            });
        }
    }
}

Source code explanation

C# to Javascript communication

When the native button is clicked it will execute this code snippet

webView.EvaluateJavaScript("invokeWeb('Hello!')", null);

Thereafter, invokeWeb Javascript function will be executed and**tag will display Hello! String as expected.

Javascript to C# communication

At ViewDidLoad following code snippet will be executed

webView.Configuration.UserContentController.AddScriptMessageHandler(new MessageHandler(this), "native");

Thereafter MessageHandler will be listening to Javascript method invocations which are coming from window.webkit.messageHandlers.native

invokeNativewrapper method is injected to WkWebView’s document by using the following code since we need to use invokeNative globally.

webView.Configuration.UserContentController.AddUserScript(
  new WKUserScript(
    new NSString("window.invokeNative=function (param) {window.webkit.messageHandlers.native.postMessage(param);}"),
WKUserScriptInjectionTime.AtDocumentStart, false));

Eventually, when ‘Invoke Native’ button clicked it will trigger DidReceiveScriptMessageofMessageHandler. Importantly, it has*‘ Hello!’* as the string parameter. Thereafter ctx.ChangeTextAsync is invoked and lbl native label is updated with ‘Hello!’ as Text.


Conclusion

If the requirement is to expose multiple native functions to the window object the function/action name can be passed via wrapper function and native function selection logic can be implemented inside DidReceiveScriptMessage of the MessageHandler.

Download tutorialhttps://github.com/shalithasuranga/xamarin-ios-jsnative-com


Find Shalitha Suranga on Medium for more tutorials and articles