Xamarin call JS from C# and vice-versa

Published on 10 April 2019

In one of the project in GEMOTIAL we work on an Angular app that will work on mobile devices.

We had several requirements. Here are the most important ones:

  • one codebase (no need for Java/Swift knowledge, so no extra developers needed)
  • highly customizable UI (also dynamically generated)
  • leverage device-specific capabilities (some devices may have bar-code scanners, etc.)

For GEMOTIAL, as primary .NET focused software studio, Xamarin was the obvious choice. We already had some Xamarin development experience and feel pretty positive about it. The ecosystem is mature and all analysis tells us that the platform is good enough to bet on it.

C# and JS calls in Xamarin (both ways)

It this post I will cover how Xamarin helps us to leverage the device's capabilities.

Out app architecture when it comes to Xamarin part is fairly simple. We use WebView that loads static index.html page. The flow is simple: executing js function will trigger a bound C# method to execute. In order to achieve that we can start with the UI part:

Sample index.html:

<!doctype html>
<html lang="en">
    <head>
    <meta charset="utf-8">
    <title>App</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/x-icon" href="favicon.ico">
    </head>
    <body>
        <script type="text/javascript" src="scripts.js"></script>
        <a href="#" onclick="Run();">Call C#</a>
    </body>
</html>

Calling C# from javascript

Notice 2 things here:

  1. scripts.js javascript file included (more about it below)
  2. javascript function Run(); called when the user presses on the link.

The Run() function is defined as following:

function Run() {
    try {
        Device.CallCSharp(); //<-- executes C# code
    }
    catch(err) {
        alert(err);
    }
}

So we call Device.CallCSharp(). This is a wrapper to use a shim in the future.

The index.html can contain the following JS code:

In Xamarin the view is defined as following (here we use Android sample and MvvmCross):

[Activity(Label = "Web view container", MainLauncher = true)]
public class ContainerView : MvxActivity<ContainerViewModel>
{
    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);
        SetContentView(Resource.Layout.ContainerView);

        WebView view = FindViewById<WebView>(Resource.Id.web);
        view.Settings.JavaScriptEnabled = true;

        view.SetWebChromeClient(new WebChromeClient());
        view.LoadUrl("file:///android_asset/index.html");
        view.AddJavascriptInterface(new DeviceFunctions(this, view), "Device");
    }
}

The most interesting part is here view.AddJavascriptInterface(new DeviceFunctions(this, view), "Device");. We tell the WebView to expose functions defined in DeviceFunctions a Device in javascript. So that is how they are accessible in the browser.

The DeviceFunctions are defined like so:

public class DeviceFunctions : Java.Lang.Object
{
    private readonly Context _context;
    private readonly WebView _view;

    public DeviceFunctions(Context context, WebView view)
    {
        _context = context;
        _view = view;
    }

    [Export]
    [JavascriptInterface]
    public void CallCSharp()
    {
        Toast.MakeText(_context, $"This is a Toast from C#! ({DateTime.Now.ToLongTimeString()})", ToastLength.Short).Show();
    }

    [Export]
    [JavascriptInterface]
    public void TakePicture()
    {
        Toast.MakeText(_context, $"Starting camera (TakePicture)! ({DateTime.Now.ToLongTimeString()})", ToastLength.Short).Show();
    }

    [Export]
    [JavascriptInterface]
    public void Validate()
    {
        _view.Post(async () =>
        {
            JavascriptResult jr = new JavascriptResult();
            _view.EvaluateJavascript("validate()", jr);

            var result = await jr.JsResult;
            Toast.MakeText(_context, $"Validation result {result}", ToastLength.Short)
                .Show();
        });
    }
}

A couple of interesting parts in DeviceFunctions:

  • the exposed function has to be marked with Export and JavascriptInterface
  • the view is passed in the constructor if we want to make interactions with javascript (so actually call javascript from C#).

In this simple way, we can call C# from javascript.

Calling javascript from C#

Another piece of the puzzle is calling javascript from C#

As you might notices in DeviceFunctions there is a Validate method that calls validate() javascript function.

Executing javascipt is this case is pretty straight forward:

_view.EvaluateJavascript("validate()", jr);

What is passed in jr parameter is a placeholder for the javascript result. The JavascriptResult is defined as the following:

public class JavascriptResult : Java.Lang.Object, IValueCallback
{
    private TaskCompletionSource<string> source;

    public Task<string> JsResult => source.Task;

    public JavascriptResult()
    {
        source = new TaskCompletionSource<string>();
    }

    public void OnReceiveValue(Java.Lang.Object result)
    {
        try
        {
            string res = ((Java.Lang.String)result).ToString();

            source.SetResult(res);
        }
        catch (Exception ex)
        {
            source.SetException(ex);
        }
    }
}

As you can see here a Task and TaskCompletionSource are used. This is necessary due to the asynchronous nature of the events.

Summary

As you can see the implementation is easy and simple. There are no hacks or walkarounds. Future work and project extension are achievable because MvvmCross supports plugins.

We think that such a framework will be a very good and stable technical foundation for the customer.

comments powered by Disqus