XUL-J bridge · .NET WinForms

net-bridge

Serve an existing, unmodified WinForms application to browsers. Each browser session gets its own instance of the app's main form, kept off-screen. The bridge streams the live control tree as XUL-J operations and turns clicks and edits in the browser into real events on the real controls.

Status: prototype. Tested end to end on Linux with Mono's WinForms under Xvfb (18 checks covering dialogs, menus, selection, context menus and theming, plus an agent driving the demo through MCP). It targets the .NET Framework 4.x API, but it has not yet been run on Windows. There is no authentication.

Quick start

Linux, with Docker (Mono + Xvfb)

The build embeds the browser client from the base repository, so clone both side by side:

git clone https://github.com/xul-j/xul-j
git clone https://github.com/xul-j/net-bridge && cd net-bridge
docker build -t xulj-mono docker
./build.sh        # → out/xulj-host.exe, out/0Harmony.dll, out/LegacyOrders.exe
docker run -d --init --name xulj-bridge -p 8092:8092 -v "$PWD/out":/app:ro \
  xulj-mono xvfb-run -a mono /app/xulj-host.exe /app/LegacyOrders.exe --port 8092

Open http://localhost:8092/. Every browser tab gets its own copy of the demo app.

Windows

Compile the sources with a C# 7.3 compiler: Roslyn's csc from Visual Studio or the Build Tools. The csc.exe under Windows\Microsoft.NET\Framework only understands C# 5 and will not work. Fetch 0Harmony.dll (Lib.Harmony 2.3.3, lib/net472) into lib\ first, or run build.sh once on any machine with Docker.

csc -langversion:7.3 -target:exe -out:xulj-host.exe ^
  -r:System.Windows.Forms.dll -r:System.Drawing.dll -r:System.Net.dll -r:lib\0Harmony.dll ^
  -resource:..\xul-j\public\index.html,public/index.html ^
  -resource:..\xul-j\public\xulj.js,public/xulj.js ^
  -resource:..\xul-j\public\xul.css,public/xul.css ^
  src\*.cs

xulj-host.exe C:\Apps\Orders\Orders.exe --host localhost --port 8092

On a desktop no virtual display is needed. Off-screen forms are moved to (−32000, −32000). HttpListener needs a URL reservation for any host other than localhost: netsh http add urlacl url=http://*:8092/ user=Everyone.

Command line

xulj-host.exe App.exe [--form Namespace.MainForm] [--port 8092] [--host *]
OptionMeaning
App.exeThe WinForms assembly to host. Its own DLLs are resolved from its folder, which also becomes the working directory.
--formThe form to instantiate per session (full or short type name). Without it: MainForm, then Form1, then the only form with a parameterless constructor. If none fits, the candidates are listed.
--portListening port (default 8092).
--hostListening host for HttpListener (default *). Use localhost unless the machine is behind your own access control.

The app's own Main() is not run, so setup done there (configuration, login, data connections) is skipped. If the app needs it, use embedding instead.

Embedding

If you can change the app, replace its Application.Run call and keep everything Main does:

[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Config.Load();                                    // your startup code still runs
    XulJBridge.Run(() => new MainForm(), new BridgeOptions
    {
        Port = 8092,
        Host = "localhost",
        TickMs = 50,                                  // how often the UI is diffed
        SessionTtl = TimeSpan.FromMinutes(10),        // idle sessions are closed after this
        Log = Console.Out,
    });
}

How it works

  1. Sessions. When a browser opens /stream?session=…, the bridge creates the form on the UI thread, moves it off-screen and shows it, so Load fires and the app's timers start. Forms the app opens later (dialogs, more windows) belong to the session that acted last.
  2. Rendering. Every TickMs the bridge walks the session's forms and builds a virtual XUL-J tree, inferring layout as described below.
  3. Diffing. The new tree is compared with the previous one, and only the differences are sent, in an order the client can apply: command changes, removals, attribute updates, new elements (parents first), then row data. Grid rows are appended when the old rows are a prefix of the new ones.
  4. Streaming. Operations are numbered and logged per session. A client that reconnects with Last-Event-ID receives only what it missed. Downloads and notifications are transient and never replayed.
  5. Intents. A browser click arrives as {"op":"do","command":"cmd_addButton"}. It is checked synchronously on the UI thread (unknown target 404, disabled 409), then run with BeginInvoke, so an app that opens a modal dialog never blocks the HTTP server. Clicks call the control's own OnClick (or ToolStripItem.PerformClick), so Click handlers and Button.DialogResult behave as usual. Edits set Text, Checked, SelectedIndex or Value, which raise the app's change events.

Layout inference

WinForms positions controls absolutely; XUL-J lays them out with flexible boxes. The bridge converts as follows:

WinFormsXUL-J
Dock Top / Bottom / Left / Right / FillNested vbox and hbox, processed in WinForms' order (the last control in Controls docks first). Fill gets flex: 1.
Absolutely placed controlsGrouped into rows by vertical overlap and ordered by Left. Each control's width runs up to the next control's Left, which keeps the designer's label column aligned.
Anchor Left | Rightflex: 1 (stretches)
Anchor Right onlyA spacer before it (right-aligned)
Anchor Top | BottomIts row grows (flex, align: stretch)
AutoSize controlsKeep their natural size, because their text can change
FlowLayoutPanel / TableLayoutPanelhbox or vbox / one hbox per table row
SplitContainerTwo panels, the first sized by SplitterDistance

Element ids come from the control's Name, then from a strip item's text (Export CSV… → exportCsv), then from its type. Commands are cmd_<id>. &Add becomes Alt+A, and CancelButton becomes Escape.

Control mapping

WinFormsXUL-J
Formwindow (modal when shown with ShowDialog)
Buttonbutton + command (AcceptButton is primary)
TextBox, MaskedTextBox, RichTextBox, NumericUpDowntextbox (password, multiline)
CheckBox, RadioButtoncheckbox (radio exclusivity stays WinForms' own)
ComboBox, ToolStripComboBoxmenulist
DataGridView, ListView, ListBoxtree with a row source (formatted cell values)
ProgressBar, TrackBar, ToolStripProgressBarprogressmeter
TabControl / TabPagetabbox / tabpanel
GroupBox, Panel, UserControl, SplitContainer, FlowLayoutPanel, TableLayoutPanelgroupbox / boxes
ToolStrip, StatusStriptoolbar, statusbar (Spring labels flex)
Label, DateTimePickerlabel, read-only textbox
anything elsea muted [TypeName] placeholder

MenuStrip becomes a menubar. Every ToolStripMenuItem with drop-down items becomes a menu (submenus included), separators become menuseparator, and leaf items become menuitem with a command. &File opens with Alt+F, ShortcutKeys are shown next to the item and bound, and Checked / CheckOnClick items show a check mark. ToolStripDropDownButton and ToolStripSplitButton become drop-down menus in the toolbar.

Menus open and close in the browser with no round trip. They support hovering across the bar, arrow keys, Home and End, and Escape. Only choosing an item reaches the app, as PerformClick.

Dialogs

On Windows, MessageBox and the common dialogs are native windows that the bridge could neither see nor drive. At startup it patches them with Harmony (MIT). On the bridge's UI thread they then show ordinary managed forms, rendered as modal XUL-J windows. Calls from other threads run the original implementation.

The app callsThe browser sees
MessageBox.Show(…), all overloadsA modal window with the message, icon and the same buttons. The default button responds to Enter and the cancel button to Escape. The app gets the matching DialogResult.
OpenFileDialogA file picker (accept from the first Filter, Multiselect honoured). The browser uploads, and FileName / FileNames point at the uploaded copies, which keep their original names.
SaveFileDialogA file-name prompt. The app writes to a temporary path; once the file has stopped changing, the browser downloads it.
Microsoft.VisualBasic.Interaction.InputBoxA prompt with a text box (an empty string on Cancel, as in VB)
Print, Color, Font, FolderBrowser dialogsCancelled, with a notification
The app's own Form.ShowDialog()A modal window; the owner is disabled meanwhile

An exception thrown by an app event handler appears as an error notification in the browser.

Theme

The form's BackColor, ForeColor and Font, and the system highlight colour, become XUL-J theme tokens; density follows the font size (8.25 pt is compact). A dark app stays dark in dark mode. Labels painted in a colour of their own get a role instead of a raw colour: a Firebrick error label becomes danger, an orange one warning, green success, grey muted.

The browser applies these as design tokens, never as CSS, and the viewer stays in charge: in high-contrast mode producer colours are ignored, in dark mode only a dark palette is used, and colour pairs that would fall below WCAG contrast are dropped.

Selection & context menus

DataGridView (full-row or cell selection), ListBox and ListView report their selection, and selections made in the browser are applied as a user's would be, so SelectionChanged fires. Double-click or Enter raises CellDoubleClick, ItemActivate or DoubleClick.

A control's ContextMenuStrip becomes a context menu. Right-click selects the row and opens it at the pointer; the bridge sets SourceControl and runs the app's Opening handler, so items enable and relabel as on the desktop ("Delete 2 orders…").

AI agents (MCP)

The XUL-J MCP server lets an AI agent operate the app through the same semantic tree the browser renders: labelled elements, explicit commands, enabled state, tables and dialogs. No screenshots, no pixel guessing. Each agent connection is its own session, with its own instance of the app.

claude mcp add legacy-orders -- node ../xul-j/mcp/server.js --url http://127.0.0.1:8092
ToolUse
get_ui, list_commandsRead the screen as an outline with ids, values, commands, tables and dialogs
set_value, do_commandFill fields, then press buttons or choose menu items
read_table, select_rows, activate_rowWork with grids and lists
open_context_menuOpen a right-click menu; the app enables its items
upload_file, download_fileAnswer open and save dialogs
wait_forWait for something slow to finish

Every action answers with what changed, so the agent rarely needs to re-read the screen. test/mcp-e2e.js has an agent fix a validation error, duplicate a row from the right-click menu, answer a confirmation, and export and re-import a CSV.

HTTP endpoints

RequestPurpose
GET /The browser client (embedded in xulj-host.exe)
GET /stream?session=<id>Server-sent events. Creates the session (and its form) on first use, and resumes from Last-Event-ID or ?from=.
POST /intent?session=<id>{"op":"do","command":…}, {"op":"input","id":…,"value":…}, {"op":"select","id":…,"rows":[…]}, {"op":"activate","id":…,"row":…} or {"op":"contextmenu","id":…,"target":…}. Returns 202, or 400 (malformed), 404 (unknown target) or 409 (disabled).
POST /upload?session=<id>&id=<picker>A file for a file picker, with the name in X-Filename. Returns 202, 404 (not a file picker) or 413 (over 100 MB).
GET /download/<token>A file the app saved, sent as an attachment. The token is 128 random bits.

Security

Limitations

Troubleshooting

The Docker container starts but serves nothing, and logs nothing.
Run it with --init. As PID 1, xvfb-run never sees Xvfb's ready signal and waits forever.
HttpListenerException: Access is denied on Windows.
Use --host localhost, or add a URL reservation with netsh http add urlacl.
“dialog patches unavailable” in the log.
0Harmony.dll is missing from next to xulj-host.exe. Without it, native dialogs stay invisible to the browser.
A control shows as [SomeControl].
It has no mapping yet. Please open an issue with the control type.

Development

src/Renderer.cs     control tree → virtual XUL-J tree (layout inference, mapping, menus)
src/Reconciler.cs   frame diffing → minimal ops
src/Bridge.cs       sessions, UI thread, HttpListener, SSE, intents, uploads, downloads
src/Dialogs.cs      Harmony patches and browser-friendly dialog forms
src/Launcher.cs     xulj-host.exe command line
src/Json.cs         dependency-free JSON
demo/LegacyOrders.cs  a designer-style demo app with menus, grid, timers and dialogs
test/bridge-e2e.js  end-to-end test against a running bridge (needs ../xul-j)
test/mcp-e2e.js     an agent operating the demo through the XUL-J MCP server
node test/bridge-e2e.js http://127.0.0.1:8092
node test/mcp-e2e.js http://127.0.0.1:8092

Source and issues: github.com/xul-j/net-bridge. MIT licensed.