Hosted Javascript to WinRT duplex communication
Source:
Direct:
Knowledge Requirements: Html, Javascript, C#, Winrt
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.
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.
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:
Visual Studio projects have several debug modes:
Managed, Native, Mixed, Script:
To select Debugger Type :
Right click on your project -> properties -> Debug
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;
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:
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:
ApplicationData.Current.LocalFolder
public StorageFolder LocalFolder { get; }
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):
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>
<head>
<title>This is a title that no one will notice</title>
<script>
function Test(message) {
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.
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
.
public interface IUriToStreamResolver
{
IAsyncOperation<global::Windows.Storage.Streams.IInputStream> UriToStreamAsync(Uri uri);
}
Our Implementation:
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:
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:
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.
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
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:
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);
}
}
}
First we need to implement a function in our JavaScript, something like this:
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
public async Task NotifyScript()
{
try
{
await webView.InvokeScriptAsync("Test", new string[] { "hello from C#!" });
}
catch (Exception e)
{
}
}
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
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.
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:
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:
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.
<script>
setInterval(function(){console.log("ji");},3000);
var intervalFunction = function(){
callbackService.Send( callbackService._formatObject("Hello From JS!"));
}
var callbackService = {
callbackService._guid = 0;
callbackService.callbacks = [];
callbackService.NotifyJS = function (guid, data){
if(data && guid){
if(callbackService.callbacks[guid])
{
callbackService.callbacks[guid](data);
delete callbackService.callbacks[guid];
}
}
}
callbackService.Send = function(data, callback){
var jObj = callbackService._formatObject( content);
callbackService.callbacks[jObj.guid] = callback;
window.external.notify(JSON.stringify(jObj));
}
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.
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.
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))
{
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
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!