JavaScript interop

The WebView2 control offers three complementary bridges between twinBASIC and the JavaScript running in the page:

  1. Host objects — publish a BASIC COM object to the page so JavaScript can call its methods and read its properties as if it were any other JS object.
  2. Messages — push a value (string, number, array, …) in either direction and listen for it on the other side.
  3. Scripted calls — call a named JavaScript function from BASIC and (optionally) wait for its return value.

This tutorial covers all three, with the matching JavaScript side shown next to each BASIC side. The worked code comes from Sample 0 — WebView2 Examples (form Example 2).

Bridge 1 — Host objects

AddObject publishes a BASIC class instance under chrome.webview.hostObjects.<Name>. Define a small class with public methods or properties:

Class MyCalculator
    Public Function MultiplyByTen(ByVal Value As Long) As Long
        Return Value * 10
    End Function
End Class

Register it once the control is ready:

Private Sub WebView_Ready() Handles WebView.Ready
    WebView.AddObject "myCalculator", New MyCalculator
End Sub

JavaScript can now call into it — but the proxy is asynchronous, so the call must be awaited inside an async function:

async function testHostCalculator() {
    let value = Math.floor(Math.random() * 100000);
    let result = await chrome.webview.hostObjects.myCalculator.MultiplyByTen(value);
    alert(`BASIC said ${value} × 10 = ${result}`);
}

To trigger the JS function from BASIC, call ExecuteScript:

Private Sub btnTest_Click() Handles btnTest.Click
    WebView.ExecuteScript("testHostCalculator()")
End Sub

Requires AreHostObjectsAllowed (default True). See Re-entrancy for the trade-off between synchronous calls (default) and the UseDeferredInvoke:=True variant.

Bridge 2 — Messages

Messages are values that travel in either direction. Use them for notifications and ad-hoc payloads where you don’t want to define a method signature ahead of time.

BASIC → page

PostWebMessage sends a value to the page; the page receives it through a message event on window.chrome.webview:

WebView.PostWebMessage "Hello from twinBASIC!"
window.chrome.webview.addEventListener('message', (e) => {
    alert("Host sent: " + e.data);
});

Strings arrive as JavaScript strings; every other type is JSON-encoded before transit. If you already have serialised JSON, PostWebMessageJSON sends it through verbatim.

Page → BASIC

The page calls window.chrome.webview.postMessage(value); BASIC receives it as the JsMessage event:

function sendHostAMessage() {
    window.chrome.webview.postMessage("This is a message from JavaScript.");
}
Private Sub WebView_JsMessage(ByVal Message As Variant) _
        Handles WebView.JsMessage
    Debug.Print "Page sent: "; Message
End Sub

Both directions require IsWebMessageEnabled (default True).

Bridge 3 — Scripted calls

When the page exposes named JS functions, BASIC can call them directly. There are three variants:

Method Returns Use it when
JsRun Variant, synchronously You need the result inline and the JS is quick.
JsRunAsync LongLong token; result via JsAsyncResult The JS may take a while and you don’t want to block the UI.
ExecuteScript nothing (fire-and-forget) You just want to trigger something — no return value needed.

JsRun (synchronous)

Given a page-side function:

function multiplyTheseNumbers(a, b) {
    return a * b;
}

BASIC can call it and read the result on the same line:

Dim product As Long = WebView.JsRun("multiplyTheseNumbers", 5, 6)
Debug.Print product   ' 30

The call blocks for up to JsCallTimeOutSeconds (default 0 — wait forever).

JsRunAsync (asynchronous)

Private Sub btnRun_Click() Handles btnRun.Click
    WebView.JsRunAsync "multiplyTheseNumbers", 5, 6
End Sub

Private Sub WebView_JsAsyncResult( _
        ByVal Result As Variant, Token As LongLong, ErrString As String) _
        Handles WebView.JsAsyncResult
    If LenB(ErrString) = 0 Then
        Debug.Print "Async result: "; Result
    Else
        Debug.Print "Async error: "; ErrString
    End If
End Sub

The return value of JsRunAsync is a token; the JsAsyncResult event carries the same token so a single handler can demultiplex multiple in-flight calls.

ExecuteScript (fire-and-forget)

WebView.ExecuteScript "startTimer()"

No return value, no event. The simplest way to nudge the page into doing something.

Re-entrancy

The Edge runtime forbids host code from calling back into the WebView2 object model while a host-object method is still executing — re-entry deadlocks the browser process. The control protects most events by deferring them through the BASIC message loop (UseDeferredEvents), but host-object method calls are synchronous by default.

The full discussion lives in the Re-entrancy tutorial; the short summary is:

  • AddObject(name, obj) — synchronous calls; the page can read return values but the BASIC method must not call back into the WebView2 control.
  • AddObject(name, obj, UseDeferredInvoke:=True) — asynchronous calls; the BASIC method is free to call any WebView2 member but the page cannot read a return value.

Where next