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:
scripts.js
javascript file included (more about it below)- 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
andJavascriptInterface
- 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.