The Win32 WebViewControl does not support ObjectForScripting
or AddWebAllowedObject
. This sample was designed with the goal of showcasing how communications could be standardized using window.external.notify
and the ScriptNotify
event.
Clone this repository or download the source code. You will need the contents of JavaScriptBridge for this sample.
- Create a new WinForms or WPF project targeting .NET Framework 4.6.2 or later. Samples are given for both WinForms and WPF, and .NET 4.6.2 is required for the WebView NuGet package.
- Install Microsoft.Toolkit.Win32.UI.Controls NuGet package to project.
- Copy JavaScriptBridge project from clone to your solution
- Use Add existing project to add the copied JavaScriptBridge project to your new solution.
- From your WinForms or WPF project, use Add reference to add the JavaScriptBridge project.
When using Visual Studio 15.8, the WebView control shows up automatically in your toolbox. If using an older version of Visual Studio, use the instructions in the documentation to Add the WebView control to the Visual Studio Toolbox.
For WPF we assume a WebView has been added with the name WebView.
MainWindow.xaml.cs
using System.Windows;
namespace AddWebAllowedObject_GettingStarted
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
WebView.DOMContentLoaded += (o, e) => Title = WebView.DocumentTitle;
}
private void WebView_OnLoaded(object sender, RoutedEventArgs e)
{
WebView.NavigateToLocal("/Content.html");
}
}
}
For WinForms we assume a WebView has been added via the designer with the default name webView1.
Form1.cs
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
webView1.DOMContentLoaded += (o, e) => Text = webView1.DocumentTitle;
webView1.NavigateToLocal("/Content.html");
}
}
}
In the previous step we use the method NavigateToLocal(String)
method to navigate to content. To create that content:
- Add a new HTML page to your project called Content.html.
- After adding, ensure the property Copy to Output Directory is set to Copy if newer.
- Replace the default content with the following
Content.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>JavaScript Bridge: Getting Started</title>
</head>
<body>
<button id="hello-world">Hello, World!</button>
<div id="output"></div>
<script>
var ConnectWebViewBridge = function (callback) {
if (window.JavaScriptBridge) {
callback(JavaScriptBridge);
} else {
// Bridge is not yet loaded, wait for event
document.addEventListener(
"JavaScriptBridgeReady",
function () {
callback(JavaScriptBridge);
},
false);
}
};
ConnectWebViewBridge(function (bridge) {
console.log("JavaScript bridge is ready!");
});
</script>
</body>
</html>
The content includes basic HTML elements, and the script
block used to wait for the JavaScript bridge to initialize. You can place it wherever you would like, but it is placed in this sample at the end of the body
tag so as not to block any other scripts, and the DOM has the ability to get ready before parsing and invoking our script.
If you run the sample at this point, there should be an empty window with a title of JavaScript Bridge Getting Started.
During the course of development you may wish to debug the content in the WebView instance. To do that, you will need to use the Microsoft Edge DevTools Preview, available from the Microsoft Store.
- Install Microsoft Edge DevTools Preview from the Microsoft Store.
- Launch your application.
- Launch Microsoft Edge DevTools Preview
- Look for a Local Debug Target with the title JavaScript Bridge Getting Started with the URI ms-local-stream://microsoft.win32webviewhost_cw5n1h2txyewy_4c6f63616c436f6e74656e74/Content.html
- Click that item to attach the debugger. Once attached a new window will pop up to debug your local content within the WebView.
Once the debugger is attached, we will need the URI to configure the JavaScript bridge. You can easily get the URI of the document by following these steps
- With the Microsoft Edge DevTools Preview attached to your WebView instance, click on the Console tab.
- Next to the
>
symbol, typedocument.URL
which will output"ms-local-stream://Microsoft.Win32WebViewHost_cw5n1h2txyewy_4c6f63616c436f6e74656e74/Content.html"
- Copy the returned value. We'll use that in the configuration step.
- After the value has been copied, close the debugger and your application instance.
Injecting the JavaScript bridge into your content requires two items: an instance of IWebView
, which we have added in a previous step, and the expected location of content that we will listen to IWebView.ScriptNotify
events, which we just received from the debugger.
To configure the JavaScript bridge, return to your .NET code-behind and replace the contents with the following.
MainWindow.xaml.cs
using System;
using System.Windows;
using Microsoft.Toolkit.Win32.UI.Controls.WebViewExtensions;
namespace AddWebAllowedObject_GettingStarted
{
public partial class MainWindow : Window
{
private JavaScriptBridge _javaScriptBridge;
public MainWindow()
{
InitializeComponent();
_javaScriptBridge = JavaScriptBridge.CreateAndStart(
WebView,
new Uri("ms-local-stream://microsoft.win32webviewhost_cw5n1h2txyewy_4c6f63616c436f6e74656e74/Content.html"));
WebView.DOMContentLoaded += (o, e) => Title = WebView.DocumentTitle;
}
private void WebView_OnLoaded(object sender, RoutedEventArgs e)
{
WebView.NavigateToLocal("/Content.html");
}
}
}
Form1.cs
using Microsoft.Toolkit.Win32.UI.Controls.WebViewExtensions;
using System;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
private JavaScriptBridge _javaScriptBridge;
public Form1()
{
InitializeComponent();
_javaScriptBridge = JavaScriptBridge.CreateAndStart(
webView1,
new Uri("ms-local-stream://microsoft.win32webviewhost_cw5n1h2txyewy_4c6f63616c436f6e74656e74/Content.html"));
webView1.DOMContentLoaded += (o, e) => Text = webView1.DocumentTitle;
webView1.NavigateToLocal("/Content.html");
}
}
}
After initialization, let's check to make sure everything is working by debugging the WebView:
- Launch your application
- Launch Microsoft Edge DevTools Preview
- Look for a Local Debug Target with the title JavaScript Bridge Getting Started with the URI ms-local-stream://microsoft.win32webviewhost_cw5n1h2txyewy_4c6f63616c436f6e74656e74/Content.html
- Click that item to attach the debugger. Once attached a new window will pop up to debug your local content within the WebView.
- Click on the Console tab of the debugger.
- There should be a message
JavaScript bridge is ready!
If you see the ready message, the JavaScript bridge was successfully injected into the running WebView instance.
Now that everything is configured and the JavaScript bridge is injected into your content page, we can begin using it by configuring a scripting handler. A script handler takes two pieces of information, an identifier, and a delegate to execute with named parameters and an optional return value. To register a script handler from our .NET code, use the JavaScriptBridge.AddScriptingHandler
method.
MainWindow.xaml.cs
using System;
using System.Windows;
using Microsoft.Toolkit.Win32.UI.Controls.WebViewExtensions;
namespace AddWebAllowedObject_GettingStarted
{
public partial class MainWindow : Window
{
private JavaScriptBridge _javaScriptBridge;
public MainWindow()
{
InitializeComponent();
_javaScriptBridge = JavaScriptBridge.CreateAndStart(
WebView,
new Uri("ms-local-stream://microsoft.win32webviewhost_cw5n1h2txyewy_4c6f63616c436f6e74656e74/Content.html"));
_javaScriptBridge.AddScriptingHandler(
"HelloWorld",
@params => "Hello, World!");
WebView.DOMContentLoaded += (o, e) => Title = WebView.DocumentTitle;
}
private void WebView_OnLoaded(object sender, RoutedEventArgs e)
{
WebView.NavigateToLocal("/Content.html");
}
}
}
Form1.cs
using Microsoft.Toolkit.Win32.UI.Controls.WebViewExtensions;
using System;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
private JavaScriptBridge _javaScriptBridge;
public Form1()
{
InitializeComponent();
_javaScriptBridge = JavaScriptBridge.CreateAndStart(
webView1,
new Uri("ms-local-stream://microsoft.win32webviewhost_cw5n1h2txyewy_4c6f63616c436f6e74656e74/Content.html"));
_javaScriptBridge.AddScriptingHandler(
"HelloWorld",
@params => "Hello, World!");
webView1.DOMContentLoaded += (o, e) => Text = webView1.DocumentTitle;
webView1.NavigateToLocal("/Content.html");
}
}
}
A scripting handler is registered with the id HelloWorld, which then specifies a lambda taking the named parameters called @params, which is a Dictionary<string, object>
instance.
To invoke our registered scripting handler HelloWorld, we need to invoke the callNative
method on the JavaScript bridge injected into our page. For our sample we will handle the click event of the Hello, World! button and call the JavaScript bridge.
The signature for callNative
accepts a handlerId, data to pass that handler in the form of named arguments, a success callback function, and an error callback function. The bridge accepts three types of invocations: callbacks, JavaScript Promises, and async/await syntax. Each implementation is shown in that order and you can pick the one that matches your coding style.
This example only passes in the handlerId and a success function.
Content.html
ConnectWebViewBridge(function(bridge) {
console.log("JavaScript bridge is ready!");
const helloWorldButton = document.getElementById("hello-world");
helloWorldButton.onclick = function(e) {
e.preventDefault();
const handlerId = "HelloWorld";
bridge.callNative(handlerId, function(response) {
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(response, null, 2)}</code></pre>`;
});
};
});
The JavaScript bridge also supports Promises. As before, we invoke callNative
, which returns a Promise
(the eventual result) instead of our value.
This example passes in the handlerId as before, and uses .then
to handle success.
Content.html
ConnectWebViewBridge(function(bridge) {
console.log("JavaScript bridge is ready!");
const helloWorldButton = document.getElementById("hello-world");
helloWorldButton.onclick = function(e) {
e.preventDefault();
const handlerId = "HelloWorld";
bridge.callNative(handlerId)
.then(function (response) {
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(response, null, 2)}</code></pre>`;});
};
});
The JavaScript bridge also supports the async/await syntax. As before, we invoke callNative
, which returns a Promise
, that we then await
.
This example passes in the handlerId as before, awaits the response, then outputs to the log as before.
Content.html
ConnectWebViewBridge(function(bridge) {
console.log("JavaScript bridge is ready!");
const helloWorldButton = document.getElementById("hello-world");
helloWorldButton.onclick = async function(e) {
e.preventDefault();
const handlerId = "HelloWorld";
const response = await bridge.callNative(handlerId);
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(response, null, 2)}</code></pre>`;
};
});
Run your project. After clicking the Hello, World! button, you should see the text Hello, World!
below the button.
So far we have used the HelloWorld
script handler, which takes no parameters and returns a string. Let's now refactor that delegate to take a single parameter, name
, and include that in the response.
Let's go back to our .NET code and change the scripting handler from
_javaScriptBridge.AddScriptingHandler(
"HelloWorld",
@params => "Hello, World!");
to the following
_javaScriptBridge.AddScriptingHandler(
"HelloWorld",
@params => $"Hello, {@params["name"]}!");
We also need to change the JavaScript to pass in the data. callNative
includes a parameter for passing the handler data, after passing the handler id. The data is expected to be passed as a set of key/value pairs as a JavaScript object.
const handlerData = { name: "World" };
The data must be passed as a set of key/value pairs. In the above example, a new JavaScript object with the property name
is created, with a string value of World. This is deserialized by the bridge into a Dictionary<string,object>
that is passed in as the @params parameter.
Here is what the inclusion of the data parameter looks like in the various syntaxes in JavaScript.
Content.html
ConnectWebViewBridge(function(bridge) {
console.log("JavaScript bridge is ready!");
const helloWorldButton = document.getElementById("hello-world");
helloWorldButton.onclick = function(e) {
e.preventDefault();
const handlerId = "HelloWorld";
const handlerData = { name: "World" };
bridge.callNative(handlerId, handlerData, function(response) {
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(response, null, 2)}</code></pre>`;
});
};
});
Content.html
ConnectWebViewBridge(function(bridge) {
console.log("JavaScript bridge is ready!");
const helloWorldButton = document.getElementById("hello-world");
helloWorldButton.onclick = function(e) {
e.preventDefault();
const handlerId = "HelloWorld";
const handlerData = { name: "World" };
bridge.callNative(handlerId, handlerData)
.then(function (response) {
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(response, null, 2)}</code></pre>`;
});
};
});
Content.html
ConnectWebViewBridge(function(bridge) {
console.log("JavaScript bridge is ready!");
const helloWorldButton = document.getElementById("hello-world");
helloWorldButton.onclick = async function(e) {
e.preventDefault();
const handlerId = "HelloWorld";
const handlerData = { name: "World" };
const response = await bridge.callNative(handlerId, handlerData);
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(response, null, 2)}</code></pre>`;
};
});
If you run the project you'll see the familiar Hello, World!
text below the button. If you change the handler data to something else (say, your name), you would expect to see something like Hello, Richard!
below the button.
It may be the case that when calling .NET code an exception is encountered. This is handled automatically by the JavaScript bridge and is raised as either a callback, JavaScript Promise reject, or an exception that can be caught with async/await. In each case an object representing the .NET exception is returned via the bridge.
Let's go back to our .NET code and change the scripting handler from
_javaScriptBridge.AddScriptingHandler(
"HelloWorld",
@params => $"Hello, {@params["name"]}!");
to the following
_javaScriptBridge.AddScriptingHandler("HelloWorld", @params =>
{
if (@params == null)
{
throw new ArgumentNullException(nameof(@params));
}
if (@params.Count != 1)
{
throw new ArgumentOutOfRangeException(nameof(@params), "Expected one parameter.");
}
if (!@params.ContainsKey("name"))
{
throw new ArgumentOutOfRangeException(nameof(@params), "Expected parameter 'name'.");
}
return $"Hello, {@params["name"]}!";
});
When implementing this in your project it is always a good idea to check the parameters coming from JavaScript.
To handle the error events, the JavaScript code needs to be altered to provide an error callback or catch the exception from the Promise
. Examples are given below for each syntax.
Content.html
ConnectWebViewBridge(function(bridge) {
console.log("JavaScript bridge is ready!");
const helloWorldButton = document.getElementById("hello-world");
helloWorldButton.onclick = function(e) {
e.preventDefault();
const handlerId = "HelloWorld";
const handlerData = { name: "World" };
bridge.callNative(
handlerId,
handlerData,
function(response) {
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(response, null, 2)}</code></pre>`;
console.info(response);
}, function(err) {
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(err, null, 2)}</code></pre>`;
console.error(err.message);
});
};
});
Content.html
ConnectWebViewBridge(function(bridge) {
console.log("JavaScript bridge is ready!");
const helloWorldButton = document.getElementById("hello-world");
helloWorldButton.onclick = function(e) {
e.preventDefault();
const handlerId = "HelloWorld";
const handlerData = { name: "World" };
bridge.callNative(handlerId, handlerData)
.then(function(response) {
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(response, null, 2)}</code></pre>`;
console.info(response);})
.catch(function(err) {
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(err, null, 2)}</code></pre>`;
console.error(err.message);});
};
});
Content.html
ConnectWebViewBridge(function(bridge) {
console.log("JavaScript bridge is ready!");
const helloWorldButton = document.getElementById("hello-world");
helloWorldButton.onclick = async function(e) {
e.preventDefault();
const handlerId = "HelloWorld";
const handlerData = { name: "World" };
try {
const response = await bridge.callNative(handlerId, handlerData);
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(response, null, 2)}</code></pre>`;
console.info(response);
} catch (err) {
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(err, null, 2)}</code></pre>`;
console.error(err.message);
}
};
});
If you run the project the output should appear as before: Hello, World!
. To see the error handler in action, change the JavaScript code from
const handlerData = { name: "World" };
to
const handlerData = { name2: "World" };
Since the .NET script handler is expecting a dictionary key the name "name", after clicking the button you should see the following on the screen
{
"message": "Expected parameter 'name'\\r\\nParameter name: params",
"data": {},
"source": "JavaScript Bridge",
"hResult": -2146233088
}
The JavaScript bridge is also capable of returning .NET objects and structures, so long as they can be serialized to JSON. Let's return to our .NET code and update the scripting handler.
_javaScriptBridge.AddScriptingHandler("HelloWorld", @params =>
{
if (@params == null)
{
throw new ArgumentNullException(nameof(@params));
}
if (@params.Count == 0 || @params.Count > 3)
{
throw new ArgumentOutOfRangeException(nameof(@params), "Expected three or less parameters");
}
var retval = new AddressBookEntry();
if (@params.ContainsKey("firstName"))
{
retval.FirstName = @params["firstName"]?.ToString();
}
if (@params.ContainsKey("lastName"))
{
retval.LastName = @params["lastName"]?.ToString();
}
if (@params.ContainsKey("company"))
{
retval.Company = @params["company"]?.ToString();
}
if (string.IsNullOrEmpty(retval.FullName))
{
throw new InvalidOperationException("Parameters did not produce a valid name.");
}
return retval;
});
The handler includes checks on the number of parameters as before, and builds up a .NET type AddressBookEntry
based on the keys of the dictionary passed in. To compile the sample, we'll also need to add the AddressBookEntry
class to the project.
public class AddressBookEntry
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Company { get; set; }
public string FullName
{
get
{
var fnEmpty = string.IsNullOrEmpty(FirstName);
var lnEmpty = string.IsNullOrEmpty(LastName);
var cEmpty = string.IsNullOrEmpty(Company);
if (fnEmpty && lnEmpty && !cEmpty)
{
return Company;
}
if (!fnEmpty || !lnEmpty)
{
var sb = new StringBuilder(256);
if (!fnEmpty)
{
sb.Append(FirstName);
sb.Append(" ");
}
if (!lnEmpty)
{
sb.Append(LastName);
sb.Append(" ");
}
if (!cEmpty)
{
sb.Append("(");
sb.Append(Company);
sb.Append(")");
}
return sb.ToString().Trim();
}
return string.Empty;
}
}
public override string ToString()
{
return FullName;
}
}
If we run the sample without changing the JavaScript, the following error is returned
{
"message": "Parameters did not produce a valid name.",
"data": {},
"source": "JavaScript Bridge",
"hResult": -2146233088
}
The new delegate function expects some additional parameters to be sent in order to build the AddressBookEntry
. Since one parameter was sent, the dictionary had one key, but it was not a key that was usable by the delegate. As such, it returned a InvalidOperationException
instead of an empty instance of AddressBookEntry
.
To take advantage of the new AddressBookEntry
object, updates need to be made to the JavaScript. As before, data is passed in as key/value pairs.
const handlerData = {
firstName: "Hello",
lastName: "World"
};
ConnectWebViewBridge(function(bridge) {
console.log("JavaScript bridge is ready!");
const helloWorldButton = document.getElementById("hello-world");
helloWorldButton.onclick = function(e) {
e.preventDefault();
const handlerId = "HelloWorld";
const handlerData = {
firstName: "Hello",
lastName: "World"
};
bridge.callNative(
handlerId,
handlerData,
function(response) {
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(response, null, 2)}</code></pre>`;
console.info(response);
}, function(err) {
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(err, null, 2)}</code></pre>`;
console.error(err.message);
});
};
});
ConnectWebViewBridge(function(bridge) {
console.log("JavaScript bridge is ready!");
const helloWorldButton = document.getElementById("hello-world");
helloWorldButton.onclick = function(e) {
e.preventDefault();
const handlerId = "HelloWorld";
const handlerData = {
firstName: "Hello",
lastName: "World"
};
bridge.callNative(handlerId, handlerData)
.then(function(response) {
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(response, null, 2)}</code></pre>`;
console.info(response);
})
.catch(function (err) {
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(err, null, 2)}</code></pre>`;
console.error(err.message);
});
};
});
ConnectWebViewBridge(function(bridge) {
console.log("JavaScript bridge is ready!");
const helloWorldButton = document.getElementById("hello-world");
helloWorldButton.onclick = async function(e) {
e.preventDefault();
const handlerId = "HelloWorld";
const handlerData = {
firstName: "Hello",
lastName: "World"
};
try {
const response = await bridge.callNative(handlerId, handlerData);
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(response, null, 2)}</code></pre>`;
console.info(response);
} catch (err) {
const log = document.getElementById("output");
log.innerHTML = `<pre><code>${JSON.stringify(err, null, 2)}</code></pre>`;
console.error(err.message);
}
};
});
Running the project will a JSON response of the serialized AddressBookEntry
class.
{
"firstName": "Hello",
"lastName": "World",
"fullName": "Hello World"
}