Jump to content

Xamarin call JS from C# and vice-versa

Posted on:April 10, 2019 at 02:00 AM

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:

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:

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.