Modernizing PowerBuilder with Web Technologies (Appeon Spain 2026, Barcelona)
This is the demo of my session at the Appeon PowerBuilder Regional Conference Spain 2026 (Barcelona, October 27, 2026): a PowerBuilder 2025 application, a .NET 10 API and a React 19 web app. The full source code is attached.
The problem
The ERP I maintain every day has 1,010 windows, 1,777 DataWindows, 114 libraries and 17 years of business logic. Rewriting it is not an option. It never was.
But the customer wants to clock in from the phone, see a decent dashboard and have an attractive grid that filters and exports. And on the web, all of that is already solved. So the question is not "do I migrate it?". It is "how do I give it web without migrating it?".
The idea
You don't rewrite the application. You open a window to the web.
The same web app, one single build, three behaviors depending on who opens it.
The door is already in the box: the PowerBuilder WebBrowser control, built on WebView2. With it PowerBuilder renders HTML, talks to JavaScript and lets JavaScript talk back to it.
And the continuity with my 2024 talk: "Two years ago I showed you a mobile clock-in app next to PowerBuilder. Today I show it to you inside, and outside at the same time - and it is the same code."
The staircase
Each step is more tightly coupled to PowerBuilder than the previous one. And you can stop at any of them: each one is useful on its own.
|
# |
Step |
What it shows |
|
0 |
Decorative web: the MDI background |
HTML as paint. Zero risk |
|
1 |
The web that displays: a Chart.js dashboard |
PowerBuilder passes data to the web |
|
2 |
The web that replaces a control: AG Grid |
A modern grid without touching the data source |
|
3 |
The web that answers: from JavaScript to PowerBuilder |
Double-click and a native window opens |
|
4 |
The web that is the application: clocking in inside the ERP |
A whole React app, without logging in again |
|
5 |
The same app outside: browser and phone |
Clock in outside, it shows up inside |
Step 3 is the one that surprises most: the web does not replace PowerBuilder, it hands it the work.
Step 0 - Decorative web: HTML as paint
The background of the MDI window is a WebBrowser (u_web_background) that shows an HTML page with the logo as a watermark. Color and opacity change with the active theme: GetTheme().
Zero risk: if anything fails, the background is just grey. And the logo travels with the application. At first it was downloaded from my website, and the day I had no Wi-Fi the background came out empty. That is why everything travels with the demo - the logo, Chart.js, the web and the API - and the talk was given with Wi-Fi switched off.
Step 1 - The web that displays: PowerBuilder passes the data
A sales dashboard with Chart.js, which travels with the application. PowerBuilder loads the invoices of the year from a JSON file, totals them by month and generates the page with the data already inside:
wb_1.NavigateToString(ls_html)
Even the chart colors come from the PowerBuilder theme. And the "Facturas Emitidas" (issued invoices) card already opens the invoice list: the way back is starting to show. More on that in step 3.
Step 2 - The web that replaces a control: a modern grid, the same DataWindow
Behind the AG Grid there is an invisible DataWindow, which is the one holding the data. PowerBuilder describes its columns and exports the rows to JSON:
window.setColumns(ls_cols)
window.loadData(dw_1.ExportJson(False))
The user can sort, filter, move, resize and hide columns, and export to Excel.
The web paints, but the desktop application remembers. When the window closes, PowerBuilder asks the grid for its layout (column order, widths, hidden columns) and saves it to a file; when it opens again, it hands it back. No database, no cookies, no server.
Step 3 - The web that answers: double-click in the grid, native window
Until now PowerBuilder gave the orders. Now it is the web that asks for something: double-click an invoice in the web grid and the NATIVE PowerBuilder maintenance window opens, with that invoice loaded.
wb_1.RegisterEvent("ue_dblclick")
The grid sends the DataWindow row number, not the key: the one who knows which invoice it is is still PowerBuilder. It reads the invoice from that row of its DataWindow and opens the window with OpenSheetWithParm.
The two paths
There - PowerBuilder gives the orders
// 1. Sync: waits and gives you the result
wb_1.EvaluateJavascriptSync( &
"window.setColumns(" + ls_cols + ")")
// 2. Async: fires it and carries on
wb_1.EvaluateJavascriptAsync( &
"window.setTheme('dark')")
Back - the web asks
// 3. Opens the door: once, when loading
wb_1.RegisterEvent("ue_dblclick")
// 4. The web calls PB without blocking
window.webBrowser.asyncfun.prototype
.ue_dblclick(String(fila))
· Without RegisterEvent, JavaScript cannot call PowerBuilder. Once, with the page loaded.
· One string parameter, or none. Several values? A JSON, and in PowerBuilder, JsonParser.
· The signature rules, and it fails silently: if it does not match, the event does not run and the web receives "".
A lesson learned in production: you don't control the web by simulating clicks. In one of my applications the window was closed by simulating a click on a web button; the day that button changed its class, the click failed silently and the window stayed open forever. With a registered event, that does not happen.
Step 4 - The web that is the application: a whole React app, no login
The Fichar (clock in) button on the ribbon opens a PowerBuilder window that, inside, is the clock-in web app: calendar, today's records and the clock-in and clock-out buttons.
It gets in without asking for a user: the ERP opens the session with the app user assigned to it (RASANFE becomes RAMON). The ERP and the app do not share users, as in any real customer, and both passwords are stored encrypted with AES in the file.
Inside the ERP the badge says DENTRO DEL ERP (inside the ERP), there is a Close button, and the message on clock-in is painted by PowerBuilder. The real leap: the web is no longer a control, it is the application.
The session bridge
Getting into the web without typing the password again. And doing it right.
The easy way: PowerBuilder logs in, gets its JWT and injects it into the web with EvaluateJavascript. It works, and it is thirty seconds of work. The right way, which is what the demo does, is a one-time ticket:
|
# |
What happens |
Endpoint |
|
1 |
The ERP logs in, as its app user |
POST /api/Auth/Login |
|
2 |
It asks for a ticket: one use, 30 seconds |
POST /api/Auth/Ticket |
|
3 |
It navigates the WebBrowser |
/sso?t=<ticket>&embedded=true |
|
4 |
The web redeems it: its own JWT |
POST /api/Auth/Redeem |
The token never travels in the URL and never ends up in the history. In the API, making the ticket valid only once is just taking it out of the dictionary when it is redeemed (TryRemove): a second redeem returns 401.
The same login endpoint is used by all three: PowerBuilder, the browser and the phone. One [Authorize], three clients.
And the API does not trust the client: the employee comes from the token (403 if you ask for someone else's records).
&embedded=true - the same web, three behaviors
One build. One parameter in the URL. Three behaviors: browser, phone and inside the ERP.
|
|
Outside (browser / phone) |
Inside the ERP |
|
Right-hand button |
History |
Close: closes the PB window |
|
History |
That button |
Moves to an icon in the footer |
|
Message on clock-in |
None: it shows in the list |
Painted by PowerBuilder, native |
|
Badge |
WEB / MOVIL |
DENTRO DEL ERP |
|
Log out |
Icon in the header |
Not shown: you close the window |
On the web side, if the bridge exists it is used; if not, the web manages on its own.
And the theme: PowerBuilder tells the web which theme it is painting with (&tema=Flat Design Dark) and, if you change it in the ERP with the window open, the web repaints live. Desktop and embedded web stop looking like two applications.
Step 5 - The same app, outside: clock in outside, it shows up inside
The same web, without recompiling: browser (badge WEB) and phone (badge MOVIL), installed as a PWA, with no store and no APK.
The moment of the demo: the 07:14 clock-in was already there; I clocked OUT on the phone and, a few seconds later, it appeared in the ERP window I had open. A record just clocked carries a small clock: it can be deleted for 5 minutes, and that is enforced by the API, not only by the web.
Everything is served by one single process: the .NET 10 API also serves the web.
Tips and extra ideas
None of them comes from a manual: they came up building this demo, and one of them in production.
1. The MDI web background covers the sheets
The background is a WebBrowser placed on top of the MDI client area, and the sheets live inside it: when one was opened, it was hidden and only the logo could be seen. You have to hide it when a sheet opens and show it again when the last one closes.
If IsValid(w_frame) Then w_frame.iuo_web.Post of_set_visible(False)
And a floating window inside an MDI: popup!, never child!. The child! one stays under the background.
2. Closing the window INSIDE the bridge event does not work
JavaScript calls PowerBuilder synchronously and waits for the answer. If the window is destroyed right then, the answer never arrives and the web receives "No parameters in result". It is not intermittent: it happens every time. I found it in production, about a hundred errors in ten days in one window of the ERP.
Post Close(Parent) // the event answers and the window closes a moment later
The fix is one word, and it is the one the help itself gives: take the work out of the event with Post. And on the web, that call goes inside a try/catch.
3. EvaluateJavascriptSync returns the result WRAPPED
It does not give you the bare value but {"type":"string","value":"..."}, and it returns it by reference in the second argument: the function itself returns a code.
li_rc = wb_1.EvaluateJavascriptSync("window.getConfig()", ls_bruto)
If you store it as it comes, nothing fails... but nothing works either: the grid layout file was written and the grid did not change. It is in the help: unwrap it with JsonParser.
4. ImportJson matches columns by POSITION, not by name
The JSON had a field the DataWindow did not have, and from there on everything was shifted: the subtotal came out empty (it was receiving text) and the description showed the job site code. One extra field in the JSON and everything shifts. The fix: ImportJsonByKey, which matches by name.
These two fail SILENTLY: not an error, not an exception.
5. Compiling proves nothing
The p-code compiling (exit code 0) only says the syntax is correct. Writing files, navigating, loading images or painting a window is only seen by running it. Several of these issues compiled at the first try. What has not been run has not been tested.
6. Bonus: check your PowerBuilder editor font
In my case it was EditorFontName=Tahoma, I don't know since when, and it is not monospaced: anything aligned in columns in a script looks misaligned. Now I use Consolas: with a script editor open, Design > Options > Editor Font.
7. ApplyTheme crashes the application with a maximized MDI sheet
Changing the theme live is one of the moments of the demo: the ERP changes theme and the web pages dress the same way. With runtime 6464 (and also 6430) the application vanished without a single error: it dies inside pbtheme.dll (0xC0000005).
With MaximizeAllTabbedSheets all sheets are maximized, so it crashed with any window open. And it was not the WebBrowser: an empty sheet brings it down too. I isolated it in a minimal application and sent it to Appeon, with the workaround. The workaround, while the fix arrives:
// Maximized sheets, to Normal!
lw_sheet = w_frame.GetFirstSheet()
Do While IsValid(lw_sheet)
If lw_sheet.WindowState = Maximized! Then
li_n++
lw_max[li_n] = lw_sheet
lw_sheet.WindowState = Normal!
End If
lw_sheet = w_frame.GetNextSheet(lw_sheet)
Loop
ApplyTheme(ls_tema)
// ...and maximize them again
For li_i = 1 To li_n
lw_max[li_i].WindowState = Maximized!
Next
The moral: when something blows up, a minimal example is worth more than a thousand suspicions.
What is in the ZIP
|
Item |
What it is |
|
PersonDemo/ |
The PowerBuilder 2025 application (persondemo.pbsln): steps 0 to 4. Its data, in PersonDemo/Database. |
|
FicharDemoApi/ This message has an attachment file. Please Log in or Create an account to join the conversation. |