Using JavaScript frameworks from your C#/UWP application

JavaScript has, without doubt, the most vibrant ecosystem out there. There are gazillions of new frameworks released every month (https://www.javascripting.com/).

As a C# developer—even with a great, active C# community—you may sometimes find yourself a little bit jealous.

What if we could bring the JavaScript language and ecosystem also into the C# world? What if a C# developer could use JavaScript inside C#?

Fret not! I’m thrilled to announce a new WinRT project I’ve created—ChakraBridge—which will allow you to get invited to the party like any web developers.

Indeed, thanks to Chakra (the JavaScript engine used by Microsoft Edge), it is now possible to host one of the fastest JavaScript engines (and also the one with the highest support of ECMAScript 6) inside any Universal Windows Platform application. ChakraBridge embeds Chakra engine in a WinRT application and provides all required high level tools to use it seamlessly in a C# / UWP application.

People developing a HTML/JS/CSS based UWP App (WWA or hosted app in the old world) don’t need to host Chakra separately as it’s already a part of the sandbox.

How to use it?

This is pretty simple: just head to https://github.com/deltakosh/JsBridge and clone the project to your hard drive.

Now you have two options: you can either add the ChakraBridge project (which is a WinRT library) in your solution or you can reference **ChakraBridge.winmd** from /dist folder.

Initializing Chakra

Once referenced, you can call these lines of code to get Chakra ready to use:

host = new ChakraHost();

The variable named host is your JavaScript context.

You may also want to be able to trace the messages sent to the JavaScript console. To do so, please add this code:

Console.OnLog += Console_OnLog;

Once connected, this event handler will be called everytime the JavaScript code executes “console.log()”.

Which JavaScript framework can I use?

Before defining what you can do, you have to understand that Chakra is a JavaScript engine which means that you can execute JavaScript code in your app but there is nothing related to HTML or CSS.

You can then pick any framework not related to HTML (DOM operations) or CSS. Here are some examples (but there are MANY MANY more):

Once you’ve picked the framework that you want to use, you have to inject it into your Chakra context. In my case I wanted to use CDC (CloudDataConnector) because I needed a way to seamlessly connect to various cloud data providers (Amazon, Azure, CouchDB, etc..).You can either download the .js files and embed them in your project or download them every time you launch your application:

await ReadAndExecute("cdc.js");
await ReadAndExecute("azuremobileservices.js");
await ReadAndExecute("cdc-azuremobileservices.js");
await ReadAndExecute("sample.js");

You can replace ReadAndExecute by DownloadAndExecute if you prefer referencing live .js files

Now your JavaScript context has compiled and executed the referenced files.

Please note that “sample.js” is a custom JavaScript file which contains the client code for my application:

var CDCAzureMobileService = new CloudDataConnector.AzureDataService();

var CDCService = new CloudDataConnector.DataService(new CloudDataConnector.OfflineService(), new CloudDataConnector.ConnectivityService());
CDCAzureMobileService.addSource('https://angularpeoplev2.azure-mobile.net/', 'xxxxxxx', ['people']);

CDCService.addSource(CDCAzureMobileService);

var dataContext = {};

var onUpdateDataContext = function (data) {
    if (data && data.length) {
        syncPeople(data);
    }
}

var syncPeople = function (data) {
    sendToHost(JSON.stringify(data), "People[]");
}

CDCService.connect(function (results) {
    if (results === false) {
        console.log("CDCService must first be successfully initialized");
    } else {
        console.log("CDCService is good to go!");
    }
}, dataContext, onUpdateDataContext, 3);

Nothing fancy here, I’m just using CDC to connect to an Azure mobile service in order to get a list of people.

Getting data back from JavaScript world

Next, I’ll get my data back from the JavaScript context. As you may have seen in the “sample.js” file, when the data context is updated, I’m calling a global function called sendToHost. This function is provided by ChakraBridge to allow you to communicate with the C# host.

To get it working, you have to define what types can be sent from JavaScript:

CommunicationManager.RegisterType(typeof(People[]));

So now when sendToHost is called from JavaScript context, a specific event will be raised on C# side:

CommunicationManager.OnObjectReceived = (data) =>
{
    var peopleList = (People[])data;
    peopleCollection = new ObservableCollection<People>(peopleList);

    peopleCollection.CollectionChanged += PeopleCollection_CollectionChanged;

    GridView.ItemsSource = peopleCollection;
    WaitGrid.Visibility = Visibility.Collapsed;
};

Obviously you are responsible for the mapping between your JavaScript object and your C# type (same properties names)

Calling JavaScript functions

On the other hand you may want to call specific functions in your JavaScript context from your C# code. Think, for instance, about committing a transaction or adding a new object.

So first let’s create a function for a specific task in our “sample.js” file:

commitFunction = function () {
    CDCService.commit(function () {
        console.log('Commit successful');
    }, function (e) {
        console.log('Error during commit');
    });
}

To call this function from C#, you can use this code:

host.CallFunction("commitFunction");

If your function accepts parameters, you can pass them as well:

host.CallFunction("deleteFunction", people.Id);

The current version of ChakraBridge accepts int, double, bool and string types.

Debugging in the JavaScript context

Thanks to Visual Studio, it is still possible to debug your JavaScript code even if you are now in a C# application. To do so, you first have to enable script debugging in the project properties:

Then, you can set a breakpoint in your JavaScript code. **

But there is a trick to know: You cannot set this breakpoint in the files in your project as they are here just as a source. You have to reach the executed code through the Script Documents part of the Solution Explorer when running in debug mode:

How does it work?

Interop

Let’s now discuss how things work under the hood.

Basically, Chakra is based on a Win32 library located at “C:WindowsSystem32Chakra.dll” on every Windows 10 desktop devices.

So the idea here is to provide a internal C# class that will embed all entry points to the DLL through DllImport attributes:

 internal static class Native
    {
        [DllImport("Chakra.dll")]
        internal static extern JavaScriptErrorCode JsCreateRuntime(JavaScriptRuntimeAttributes attributes, 
            JavaScriptThreadServiceCallback threadService, out JavaScriptRuntime runtime);

        [DllImport("Chakra.dll")]
        internal static extern JavaScriptErrorCode JsCollectGarbage(JavaScriptRuntime handle);

        [DllImport("Chakra.dll")]
        internal static extern JavaScriptErrorCode JsDisposeRuntime(JavaScriptRuntime handle);

The list of available functions is pretty long. ChakraBridge is here to encapsulate these functions and provide a higher level abstraction.

Other option to consider here: you can also use Rob Paveza’s great wrapper called js-rt winrt: https://github.com/robpaveza/jsrt-winrt. It’s higher-level than the pure Chakra engine and it avoids needing P/Invoke.

Providing missing pieces

One important point to understand is that Chakra only provides the JavaScript engine. But you, as the host, have to provide tools used alongside JavaScript. These tools are usually provided by browsers (think about C# without .NET).

For instance, XmlHttpRequest object or setTimeout function are not part of JavaScript language. They are tools used BY the JavaScript language in the context of your browser.

To allow you to use JavaScript frameworks, ChakraBridge provides some of these tools.

This is an ongoing process and more tools will be added to ChakraBridge in the future by me or the community

Let’s now have a look at the implementation of XmlHttpRequest:

using System;
using System.Collections.Generic;
using System.Net.Http;

namespace ChakraBridge
{
    public delegate void XHREventHandler();

    public sealed class XMLHttpRequest
    {
        readonly Dictionary<string, string> headers = new Dictionary<string, string>();
        Uri uri;
        string httpMethod;
        private int _readyState;

        public int readyState
        {
            get { return _readyState; }
            private set
            {
                _readyState = value;

                try
                {
                    onreadystatechange?.Invoke();
                }
                catch
                {
                }
            }
        }

        public string response => responseText;

        public string responseText
        {
            get; private set;
        }

        public string responseType
        {
            get; private set;
        }

        public bool withCredentials { get; set; }

        public XHREventHandler onreadystatechange { get; set; }

        public void setRequestHeader(string key, string value)
        {
            headers[key] = value;
        }

        public string getResponseHeader(string key)
        {
            if (headers.ContainsKey(key))
            {
                return headers[key];
            }

            return null;
        }

        public void open(string method, string url)
        {
            httpMethod = method;
            uri = new Uri(url);

            readyState = 1;
        }

        public void send(string data)
        {
            SendAsync(data);
        }

        async void SendAsync(string data)
        {
            using (var httpClient = new HttpClient())
            {
                foreach (var header in headers)
                {
                    if (header.Key.StartsWith("Content"))
                    {
                        continue;
                    }
                    httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
                }

                readyState = 2;

                HttpResponseMessage responseMessage = null;

                switch (httpMethod)
                {
                    case "DELETE":
                        responseMessage = await httpClient.DeleteAsync(uri);
                        break;
                    case "PATCH":
                    case "POST":
                        responseMessage = await httpClient.PostAsync(uri, new StringContent(data));
                        break;
                    case "GET":
                        responseMessage = await httpClient.GetAsync(uri);
                        break;
                }

                if (responseMessage != null)
                {
                    using (responseMessage)
                    {
                        using (var content = responseMessage.Content)
                        {
                            responseType = "text";
                            responseText = await content.ReadAsStringAsync();
                            readyState = 4;
                        }
                    }
                }
            }
        }
    }
}

 

As you can see, the XmlHttpRequest class uses internally a HttpClient and uses it to mimic the XmlHttpRequest object that you can find in a browser or in node.js.

This class is then projected (literally) to the JavaScript context:

Native.JsProjectWinRTNamespace("ChakraBridge");

Actually, the entire namespace is projected as there is no way to project only a single class. So a JavaScript is then executed to move the XmlHttpRequest object to the global object:

RunScript("XMLHttpRequest = ChakraBridge.XMLHttpRequest;");

Handling garbage collections

One of the pitfalls you may face if you decide to extend ChakraBridge is garbage collection. Indeed, the JavaScript garbage collector has no idea of what is happening outside of its own context.

So for instance, let’s see how the setTimeout function is developed:

internal static class SetTimeout
    {
        public static JavaScriptValue SetTimeoutJavaScriptNativeFunction(JavaScriptValue callee, bool isConstructCall, 
[
MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] JavaScriptValue[] arguments,
ushort argumentCount, IntPtr callbackData) { // setTimeout signature is (callback, after) JavaScriptValue callbackValue = arguments[1]; JavaScriptValue afterValue = arguments[2].ConvertToNumber(); var after = Math.Max(afterValue.ToDouble(), 1); uint refCount; Native.JsAddRef(callbackValue, out refCount); Native.JsAddRef(callee, out refCount); ExecuteAsync((int)after, callbackValue, callee); return JavaScriptValue.True; } static async void ExecuteAsync(int delay, JavaScriptValue callbackValue, JavaScriptValue callee) { await Task.Delay(delay); callbackValue.CallFunction(callee); uint refCount; Native.JsRelease(callbackValue, out refCount); Native.JsRelease(callee, out refCount); } }

SetTimeoutJavaScriptNativeFunction is the method that will be projected inside JavaScript context. You can note that every parameter is gathered as a JavaScriptValue and then cast to the expected value. For the callback function (callbackValue), we have to indicate to JavaScript garbage collector that we hold a reference so it could not free this variable even if no one is holding it inside JavaScript context:

Native.JsAddRef(callbackValue, out refCount);

The reference has to be released once the callback is called:

Native.JsRelease(callbackValue, out refCount);

On the other hand, C# garbage collector has no idea of what is happening inside the Chakra black box. So you have to take care of keeping reference to objects or functions that you project into the JavaScript context. In the specific case of setTimeout implementation, you first have to create a static field that point to your C# method just to keep a reference on it.

Why not use a Webview?

This is a valid question that you may ask. Using only Chakra provides some great advantages:

  • Memory footprint: No need to embed HTML and CSS engines as we already have XAML.
  • Performance: We can directly control JavaScript context and, for instance, call JavaScript function without having to go through a complex process like with the webview.
  • Simplicity: The webview needs to navigate to a page to execute JavaScript. There is no straightforward way to just execute JavaScript code.
  • Control: By providing our own tools (like XHR or setTimeout), we have a high level of granularity to control what JavaScript can do.

Going further

Thanks to Chakra engine, this is the beginning of a great collaboration between C#, XAML and JavaScript. Depending on the community response, I plan to add more features in the ChakraBridge project to be able to handle more JavaScript frameworks (for instance, it could be great to add support for canvas drawing in order to be able to use all awesome charting frameworks available for JavaScript).

If you are interested in reading more about Chakra itself you can go to the official Chakra samples repository: https://github.com/Microsoft/Chakra-Samples.

You may also find these links interesting: