Driving Monaco from twinBASIC
A case study combining everything from the previous tutorials: a form with two CefBrowser controls — the Microsoft Monaco editor on the left, a live HTML preview on the right. As the user types, Monaco posts the edited source to twinBASIC, which mirrors it into the preview pane.
The complete project ships as Sample 1b — Chromium Embedded Framework Examples in the New-Project dialog (form Example 3).
Architecture
The editor runs as a local web app under a virtual hostname; the preview pane is fed raw HTML through NavigateToString.
Runtime version requirement
Monaco uses modern JavaScript features that don’t exist in older Chromium versions. The sample checks at startup and warns if the loaded runtime is too old:
If WebView.CefMajorVersion < 109 Then
MsgBox "Sorry, Monaco is not supported by this old version of CEF."
End If
In practice this means v109 or v145 for this tutorial — v49 lacks the JavaScript surface Monaco depends on. See Getting started for picking the right package reference.
Setting up the editor’s assets
The Monaco editor ships as a ~2 MB collection of JavaScript, CSS, and font files. Drop them into a Resources sub-folder of your project — call it MONACO_DEMO — alongside an index.html and a small bootstrap script.js. The Hosting local web assets tutorial describes the layout.
The page itself is a single <div id='container'> plus the bootstrap script that listens for an initial-content message from the host:
<!DOCTYPE html>
<html>
<head>
<script src="/vs/loader.js"></script>
<script src="/script.js"></script>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div id="container"></div>
</body>
</html>
window.chrome.webview.addEventListener('message', (event) => {
let initialHTML = event.data;
require.config({ paths: { 'vs': 'https://monaco.example/vs' } });
require(["vs/editor/editor.main"], () => {
let editor = monaco.editor.create(document.getElementById('container'), {
value: initialHTML,
language: 'html',
theme: 'vs-dark',
minimap: { enabled: false }
});
editor.onDidChangeModelContent(() => {
// Inform the host of every edit.
window.chrome.webview.postMessage(editor.getValue());
});
});
});
The BASIC side
Drop two CefBrowser controls on a form — WebView (the editor) and WebViewPreview (the renderer). The Ready handler deploys the assets, registers the virtual host, and navigates:
Private localPath As String
Private Sub WebView_Ready() Handles WebView.Ready
localPath = Environ$("USERPROFILE") & "\Documents\tbMonacoDemo"
CopyResourcesFolderContentsToLocalPath "MONACO_DEMO", localPath
WebView.SetVirtualHostNameToFolderMapping _
"monaco.example", localPath & "\"
WebView.Navigate "https://monaco.example/index.html"
End Sub
(CopyResourcesFolderContentsToLocalPath is the helper from Hosting local web assets.)
The two controls share a single helper browser process — the first CefBrowser to reach Ready launches it, the second one attaches to the existing process. That sharing is what makes the two-pane pattern cheap.
Pushing the initial content
Once Monaco has finished loading, the bootstrap script listens for a message event carrying the HTML to seed the editor with. Fire that message after the editor’s NavigationComplete:
Private Sub WebView_NavigationComplete( _
ByVal IsSuccess As Boolean, ByVal WebErrorStatus As Long) _
Handles WebView.NavigationComplete
If WebView.DocumentURL <> "https://monaco.example/index.html" Then Exit Sub
Dim initialHTML As String = _
StrConv(LoadResData("initial-editor-html.html", "MONACO_DEMO"), vbFromUTF8)
WebView.PostWebMessage(initialHTML)
WebViewPreview.NavigateToString(initialHTML)
End Sub
LoadResData returns the resource bytes; StrConv(..., vbFromUTF8) decodes them. PostWebMessage hands the string to Monaco’s message listener; NavigateToString seeds the preview pane with the same text rendered as HTML.
The If guard at the top is important — NavigationComplete fires for every navigation, including internal Monaco asset loads. Only seed the editor on the navigation to index.html.
Live preview
Every keystroke in Monaco fires its onDidChangeModelContent callback, which postMessages the new content back to BASIC. That arrives as the JsMessage event — feed it straight into the preview:
Private Sub WebView_JsMessage(ByVal Message As Variant) Handles WebView.JsMessage
WebViewPreview.NavigateToString(Message)
End Sub
That’s it — the preview pane re-renders on every edit.
Detecting a missing runtime
A reasonable fraction of users will run the application on a machine where the CEF runtime ZIP has not been installed. The Error event surfaces this case with the exact path the control searched:
Private Sub WebView_Error(ByVal code As Long, ByVal msg As String) _
Handles WebView.Error
MsgBox "Failed to initialize the CEF control." & vbCrLf & vbCrLf & _
"Code: " & Hex$(code) & vbCrLf & _
msg, vbExclamation, "CEF"
End Sub
The fix is to install the matching runtime ZIP from github.com/twinbasic/cef-runtimes, or to ship the runtime alongside the application and point EnvironmentOptions.BrowserExecutableFolder at it during the Create event. See Getting started for the install path and the ZIPs.
Where next
- Hosting local web assets — the
CopyResourcesFolderContentsToLocalPathhelper and virtual-host pattern this tutorial builds on. - JavaScript interop — the two bridges between BASIC and JavaScript.
- Re-entrancy — why the live-preview pattern is safe even though it’s mostly synchronous-looking.
- CefBrowser reference — every property, method, and event.
- Driving Monaco (WebView2) — the parallel implementation using the WebView2 control.