Click here to Skip to main content
15,881,812 members
Articles / Web Development / HTML

Hosted Javascript-WinRT duplex communication

Rate me:
Please Sign up or sign in to vote.
4.86/5 (8 votes)
9 Jul 2015CPOL9 min read 12.7K   101   6   2
In this article I will explain how to create a local WebView hosted Html/Javascript application on Winrt Platform and how to establish the communication between both sides.

Hosted Javascript to WinRT duplex communication

Source:

On GitHub: https://github.com/Pavel-Durov/CodeProject-Hosted-Javascript-WinRT-Duplex
Direct:

 

 

Knowledge Requirements: Html, Javascript, C#, Winrt

Introduction

In this article I will talk about establishing a basic communication between C# and JavaScript through WebView. I'll cover WinRT application package usage and Visual Studio Project configurations that are relevant for our goal.

The examples will be demonstrated using WinRT as a platform, but the same concepts can be used similarly on all other platform as well.

This interaction will work in both ways, on one side of the communication, we're going to use an Internet Explorer web engine (WebVIew Xaml Control) since we are developing for Windows, and JavaScript on the other.

Why bother?

At first glance it seems strange to create this communication, but sometimes you just need to.

Example:

We need to create a cross platform application and we want to create one component that will serve us on all platforms.

Of course, we could use only Html/JavaScript by simply hosting it in the WebView and run it on all platforms, but what if we need to use some device specific functionalities – like creating a local log file or recording a voice message etc….

That's where you'll need to implement this kind of interaction.

Global Overview

We are going create a WinRT project which has a WebView stretched on the Page layout, so that end user will see only the WebView as its content.

When the page finishes loading, we'll navigate to local html file which is stored in our local folder, actually it's like using html cache files and browsing offline – without depending on the internet.

All the application logic will be performed on the JavaScript, C# side is used to call only the device specific functionalities, hence our goal is to create one component that will be reused on all platforms.

Our application architecture can be represented as the following diagram:

Image 1

Visual Studio Solution and  Project specifics

Debugger Type – Application process

Visual Studio projects have several debug modes:

Managed, Native, Mixed, Script:

To select Debugger Type :

Right click on your project -> properties -> Debug

Image 2

As you switch between the Debug modes (In our example Debug/Script) you will be able to debug JavaScript and Managed Code, but only one at a time. You cannot mix those modes (Only Managed and Native can be mixed).

Select Managed Mode to debug C# or Script for JavaScript debugging;

Application's Local folder and package name

In order to create basic integration we'll need to navigate from our web view to local Html file.

This can be done without a local file but it needs a special permissions – this article would not cover this topic.

Build your project once, and navigate to its local folder on your PC:

The local folder path should be as this convention:

C:\Users\<USER NAME>\AppData\Local\Packages\<PACKAGE GUID>\LocalState

If you want to simplify your access to the Local folder you can set your package name manually for ease of use:

Select Package.appxmanifest file from your Solution Explorer and navigate to Packaging Tab:

Image 3

Replace the Package name in some guid value that you could immediately notice in the Packages directory hierarchy.

I set my Package Name to some arbitrary value: 01010101-0101-0101-0101-0101010101212

As you'll deploy your project you will see this folder in the Packages directory:

This is your actual path on your pc to your Local Folder:

C:\Users\<USER NAME>\AppData\Local\Packages\01010101-0101-0101-0101-0101010101212

In order to access this folder from your code, use the following code:

C#
ApplicationData.Current.LocalFolder

// Summary:
//     Gets the root folder in the local app data store.

// Returns:
//     The file system folder that contains the files.

public StorageFolder LocalFolder { get; }

 

Implementing Communication

In order to create a basic communication you'll need to navigate from your web view to local Html file. Build your project once, and navigate to its local folder on your PC:

The local folder path should be of this convention:

C:\Users\<USER NAME>\AppData\Local\Packages\<PACKAGE GUID>\LocalState

Create an HTML in the local folder and give it a name (whatever you call it, remember to change the NotifyScript params accordingly…).

You can do it manually or you can simply copy file from your assets (or whatever you wish) folder to your local folder, in my examples I will use this function in my App class, as the application lunches (in OnLaunched method):

C#
private async void CreateLocalFile()            
{
    string indexName = "index.html";
    string localFolderPath = Path.Combine("Assets\HTML\" + "index.html");
    var item = await ApplicationData.Current.LocalFolder.TryGetItemAsync(localFolderPath);
    if (item == null)
    {
        StorageFolder InstallationFolder = Windows.ApplicationModel.Package.Current.InstalledLocation;
        StorageFile assetfile = await InstallationFolder.GetFileAsync(localFolderPath);
        await assetfile.CopyAsync(ApplicationData.Current.LocalFolder, indexName, NameCollisionOption.ReplaceExisting);
    }
}

 

Example for html/js file:

HTML
<html>
<head>
    <title>This is a title that no one will notice</title>
    <script>
        function Test(message) {//The function that will be called from C#
            if (message) {
                window.external.notify(message + " " + new Date())            }
        }
    </script>
</head>
<body>
<h1>Hello from html</h1>
<input type="button" value="Send message to C#" onclick="Test('Hey')"/>
</body>
</html>

 

  • You can press the button in order to send a message to C# from Js.

 

Navigating to local Html file

To perform a navigation to local HTML file from WebView you will need to use a custom object which implements IUriToStreamResolver.

IUriToStreamResolver has one method which is public. The main goal of object is to convert/resolve a file path into an InputStream.

C#
public interface IUriToStreamResolver
{
    IAsyncOperation<global::Windows.Storage.Streams.IInputStream> UriToStreamAsync(Uri uri);
}

 

Our Implementation:

C#
public class UrlResolver : IUriToStreamResolver
{
    public IAsyncOperation<IInputStream> UriToStreamAsync(Uri fileName)
    {
        IAsyncOperation<IInputStream> result = null;
        result = GetContent(fileName.AbsolutePath).AsAsyncOperation();
        return result;
    }
    private async Task<IInputStream> GetContent(string fileName)
    {
        IRandomAccessStream result = null;
        String path = fileName.Replace("/", "");
        var storageFile = await ApplicationData.Current.LocalFolder.TryGetItemAsync(path);
        if (storageFile != null && storageFile.IsOfType(StorageItemTypes.File))
        {
            StorageFile file = storageFile as StorageFile;
            result = await file.OpenAsync(FileAccessMode.Read);
        }
        return result;
    }
}

Now that we have this Resolver object, we can use it in our View code behind in order to navigate to our local Html file:

C#
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    Uri path = webView.BuildLocalStreamUri("someIdentifier", "index.html");
    var uriResolver = new UrlResolver();

    try
    {
        webView.NavigateToLocalStreamUri(path, uriResolver);
    }
    catch (Exception ex)
    {
        if (ex != null)
            Debug.WriteLine(ex.ToString());
    }
}

 

We are calling here a WebView function BuildLocalStreamUri that accepts a file path and a key, this will generate a local Uri to our file.

One row below, we are instantiating our StreamResolved and pass the generated Uri with the resolver object as parameters to WebView function NavigateToLocalStreamUri.

Local stream uri convention should look as follows:

ms-local-stream://< LOCAL FILE PATH >

Notice the prefix: ms-local-stream

If you'll put a breakpoint in our StreamResolved UriToStreamAsync function your breakpoint will be hit as you call NavigateToLocalStreamUri function. That function is called by the WebView as it's trying to get the stream from the given uri.

Calling C# from JS

window.external.notify function

If you'll open Internet Explorer Console and type in the function in the Console you will get an undefined as a result, since its window.external is native.

Read about it on MSDN:

https://msdn.microsoft.com/en-us/library/ms535246(v=vs.85).aspx

Integration

All calls from Javascipts to C# and vice versa will be implemented through one function  called  window.external.notify, this function except one string parameter and send it to  the WebView event called ScriptNotify as NotifyEventArgs value :

window.external.notify("Hello from JS");

Receive Js message on C#:

In order receive JavaScript call on C# you'll need to subscribe to ScriptNotify event:

C#
webView.ScriptNotify += webView_ScriptNotify;

void webView_ScriptNotify(object sender, NotifyEventArgs e)
{
        if (e != null && !String.IsNullOrEmpty(e.Value))
        {
            Debug.WriteLine("JS Message {0}", e.Value);
        }
    }
}

 

Calling JS method from C#

First we need to implement a function in our JavaScript, something like this:

 

JavaScript
function Test(message){
    if(message){
        window.external.notify("I Recieved your message");
    }
}

 

Now we'll call this method from C# using WebView function called NotifyScript

C#
public async Task NotifyScript()
{                           
    try
    {
        await webView.InvokeScriptAsync("Test", new string[] { "hello from C#!" });
    }
    catch (Exception e)
    {
        //throw;
    }
}

If you followed the steps, check out the Output Window in Visual Studio and JS Console and see if you got the messages printed…

You can use a shortcut: Ctr + Alt + O

 

JavaScript callbacks functions

Usually you wouldn't like to wait synchronically to the C# response for your request since it can take a while, and then you will get a frozen UI or a stuck JavaScript code - which is nasty.

That's why you’d probably like to use callback functions. The concept is pretty simple, we are passing the function as an object, and we’ll store its instance in some collection - in our case in a dictionary, we'll release the JavaScript thread until we'll get the result from the C# code, that's where we'll invoke the method and as a result the past instance would be notified with the callback.

Installing Json Nugget Package

We are sending serialized Json object from JavaScript to the C# as our communication baseline, so obviously we need to deserialize the sent object on the managed code side - so we could use it.

Of course you could do it by your own and extract the data from the string based on Json object format, but why make it difficult for yourself if you can use working libraries that someone else already tested?

In my project I am using Newtonsoft.Json Nugget as Json converter.

In Visual Studio you can install nuggets from the GUI or from the Package Manager Console, I've used the console as follows:

Open the Package Manager Console :

Type in the Quick Lunch: "Package Manager Console", as you hit ENTER you will see a console:

Image 4

Type in the following command (Be sure that you are connected to the internet):

Install-Package Newtonsoft.Json

That's it, you have installed the nugget. You can see now that your project references it:

Image 5

 

 

Calling C# Methods Asynchronously

Json Object as Protocol

In order to establish a normal communication (as always) we need to invent some sort of protocol, which is known and used by both sides.

In our example we will base this protocol on Json object as follows:

{ "data" :"", "guid": "" }

Data – this is the data that we'll send to the C# as our data (obviously)

Guid – this is a unique identifier which helps us to find the callback function when we get the result from C# side.

HTML
<script>
setInterval(function(){console.log("ji");},3000);    
var intervalFunction = function(){
    callbackService.Send( callbackService._formatObject("Hello From JS!"));
}
    var callbackService = {
        callbackService._guid = 0;
          //Native callbacks array, will be invoked as native invoke the NotifyJS event
        callbackService.callbacks = []; 

      //An event theat called from the native side, 
    //picked from the nativeCallBacks array by GUID 
    callbackService.NotifyJS = function (guid, data){
        if(data && guid){
            if(callbackService.callbacks[guid])
            {
                //invoking the found callback
                callbackService.callbacks[guid](data);
                //Deletes the callback from the array
                delete callbackService.callbacks[guid];  
            }
        }
    }
    
    //Sends to the C# side...
    //cmd = the desitred command (log, sql, etc...)
    //content = the content of the message
    //callback that will be invoked as native invokes NotifyJS function
    callbackService.Send = function(data, callback){
        var jObj = callbackService._formatObject( content);
        callbackService.callbacks[jObj.guid] = callback;
        //The actual javacript method that send the event to the webview
        window.external.notify(JSON.stringify(jObj));   
    } 
    
    //Gets the format of the API sent object
    //cmd = the desitred command (log, sql, etc...)
    //content = the content of the message
    //guid = an unique identifier, for determining the callback in callbacks array.
    callbackService._formatObject = function( content){
        var guid = _guid + 1;
        return {
            data   : content,
            'guid'      : guid
        };
    };
</script>

callbackService object got an Array of callbacks which stores the callbacks instances that passed to the Send function, callbackService will invoke a callback as C# will sends back a response to the JavaScript request using guid identification.

Removing the instance after use (delete) is very important, otherwise those objects will continue to exist in our application's memory.

Notice that we are not generating a complex guid value – we’re simply using a running number which will be initialized as zero if we’ll restart the application process or we'll refresh the html.

Catching Javascript call on C# and returning result

In order to catch the JavaScript call on the C# side, we need to subscribe to the ScripNotify event, in this event we will get the Json object, extract the relevant data from it, do some work and send back the response with guid identifier.

Here I am going to use Newtonsoft.Json library (which I installed previously) In order to parse the Json data, but you can do it by yourself or use other Libraries as you like.

       

C#
void webView_ScriptNotify(object sender, NotifyEventArgs e)
 {
    if (e != null && !String.IsNullOrEmpty(e.Value))
    {
     
            var jobj = JsonConvert.DeserializeObject<JObject>(e.Value);
            JToken dataValue;
            JToken guidValue;
            if (jobj.TryGetValue("data", out dataValue) && jobj.TryGetValue("guid", out guidValue)) 
            {
                //DoSomeWork(guidValue.Value<string>());
                await Task NotifyScript("TestNative", guidValue.Value<string>(), "{'data': 'success'}");

            }
    }
}

As we deserialize our Json object we are extracting the data from it, and accordingly to its property – guid, sending back the response. J

 

Summary

That's it, we’ve created an interaction between C# and JavaScript through the WebView engine.

We could expand this implementation much more by creating a special class which identifies the JavaScript intent and such. This may be required when you have a lot of different functionalities involved, however the basics are explained and should work.

Try it out!

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionAwesome! Pin
Lidan Hackmon3-Sep-15 2:55
Lidan Hackmon3-Sep-15 2:55 
GeneralMy vote of 5 Pin
Robert J. Good13-Jul-15 11:39
professionalRobert J. Good13-Jul-15 11:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.