ActiveX SDK
For ISVs with access to their application's source code, WebStream offers an ActiveX integration layer that enables richer, in-application capabilities beyond what publishing as-is provides.
When to use deep integration
Most ISVs start by publishing their application as-is. Consider the ActiveX layer when you want your application to participate directly in browser-side capabilities — for example, tighter control over file exchange, browser-native interactions, or edge features — and you are able to make targeted changes to your code.
What it enables
- Programmatic interaction with WebStream's file and browser bridge from within your application.
- Richer integration of edge features into your product's own workflows.
- A path to differentiate your browser-delivered product beyond the standard streaming experience.
Getting the SDK
The SDK is installed with WebStream — there is nothing to download and nothing to register
by hand. The installer places it in C:\WebStream\sdk and registers the control for all
users on the machine:
C:\WebStream\sdk\
webstreamAX.dll the control (x86, .NET Framework 4.7.2)
webstreamAX.tlb type library - reference this from VB6
Newtonsoft.Json.dll dependencies
websocket-sharp.dll
register.bat re-register after replacing the DLL by hand
unregister.bat
samples\
ActiveXSdkVB6Sample\ VB6 source, prebuilt EXE under bin\
ActiveXSdkSample\ C# WinForms source, solution, prebuilt bin\
The latest ActiveX control, open-source API samples, and issues are on the WebStream GitHub repository. See the API Reference for links and entry points.
Do not move or replace C:\WebStream\sdk\webstreamAX.dll. It is registered with
/codebase pointing at that folder, and it resolves Newtonsoft.Json and
websocket-sharp from alongside itself. The WebStream install root one level up ships
.NET 6 builds of those same two assemblies for the streamer, so a control registered from
there binds to the wrong ones and fails on the first call that serialises anything.
This is about the registered copy that COM hosts reach through CreateObject.
A .NET application is different: it should carry its own private copy beside its EXE, which is both
supported and recommended — see Deploying a .NET application.
Deep integration is optional. You can ship a fully browser-delivered, governed product with no code changes by publishing as-is — see Publishing Your App.
Consuming the SDK
There is one integration surface: the COM/ActiveX control webstreamAx. It exposes the complete method set to every host language, C# and VB.NET included — there is no separate or reduced .NET API to choose between. Every method is available in both a blocking form that returns the result and an asynchronous form that returns a request GUID and delivers the result as an event.
| Host | How to bind | Notes |
|---|---|---|
| VB6, VBA, Delphi, any COM-capable host | Nothing to do — the installer already registered it machine-wide. Reference C:\WebStream\sdk\webstreamAX.tlb from the host IDE; it then resolves by GUID. | Worked example: sdk\samples\ActiveXSdkVB6Sample, with a prebuilt EXE under its bin\. On a developer machine without WebStream installed, run register.bat elevated from a copy of the sdk folder. |
| .NET (C#, VB.NET) | Add an assembly reference to webstreamAX.dll and let it copy local. Registration is irrelevant on this path — see Deploying a .NET application. | DispId members surface as ordinary .NET methods and properties, and IWebStreamEvents members as ordinary .NET events. Worked example: sdk\samples\ActiveXSdkSample (C# WinForms), with a prebuilt EXE under its bin\. |
WebStreamClient is an internal class belonging to VirtualDesktop, not an ISV-facing library. It implements an older subset of the protocol and has no Identity, screen capture, on-screen keyboard, render throttling, or logging control. .NET applications should use the COM control described above.
Calling from C# / .NET
The recipes below are written in VB for brevity. Three things differ when calling from C#:
- Run blocking calls off the UI thread.
ExecuteJavaScript,BrowserInfo,FileDialogandIdentity.ValidateUserblock until the browser answers, so wrap them inTask.Runto keep your window repainting while it waits. Alternatively use the…Asyncvariant and handle the matching event. - Events are raised on background threads. Marshal back with
Invoke/BeginInvokebefore touching any control. - Optional arguments are honoured, so
ws.Identity.GetLocalUsername()andws.ShowKeyboard()compile without the trailing parameters.
Deploying a .NET application
Ship the control with your application rather than pointing at the installed copy. Reference
webstreamAX.dll as a plain assembly reference and leave Copy Local on; its two
dependencies follow automatically, and you get a self-contained output folder:
YourApp\
YourApp.exe
YourApp.exe.config supportedRuntime v4.0, .NETFramework 4.7.2
webstreamAX.dll the control
Newtonsoft.Json.dll its dependencies - the .NET Framework builds
websocket-sharp.dll
The CLR probes the application directory first, so your app always binds to the copies it shipped
with. Nothing is registered, nothing is elevated, and nothing depends on where WebStream happens to be
installed — the folder can be xcopy-deployed, and it builds and runs on a developer machine with no
WebStream on it at all (the control simply reports Enabled = False until a session hosts
it). The prebuilt sdk\samples\ActiveXSdkSample\bin is laid out exactly this way, so it works
as a template as well as a demo.
Two build settings are not optional:
| Setting | Value | Why |
|---|---|---|
| Platform target | x86 | The shipped control is a 32-bit image. An AnyCPU host starts 64-bit on a 64-bit OS and throws BadImageFormatException the moment it loads the control. AnyCPU + Prefer 32-bit also works for an EXE, but an explicit x86 says so plainly and carries over to any library that references the control. |
| Target framework | net472 or a later .NET Framework | The control and its dependencies are .NET Framework assemblies. From a .NET 5+ host, activate the registered control through COM instead of referencing the assembly. |
In an SDK-style project, that is:
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net472</TargetFramework>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<Reference Include="webstreamAX">
<HintPath>..\lib\webstreamAX.dll</HintPath>
</Reference>
</ItemGroup>
Keep a copy of the three DLLs somewhere in your own source tree — a lib\ folder
beside the project is the usual choice — so the build does not depend on a WebStream installation
being present on the build agent.
Adding the type library through Add Reference → COM fails with “Type library
'webstreamAx' was exported from a CLR assembly and cannot be re-imported as a CLR assembly”,
which then shows up as CS0103 on every webstreamAX identifier.
webstreamAX.tlb is there for VB6 and other native COM hosts. A .NET host references the
assembly directly, which already contains those types.
Two things to watch over the life of the integration. Your private copy does not change when WebStream
is upgraded on a server, so refresh it from C:\WebStream\sdk and rebuild when you adopt a new
release. And never deploy your application into the WebStream install root: that folder holds
.NET 6 builds of Newtonsoft.Json and websocket-sharp for the streamer, and the
control would bind to those and fail on the first call that serialises anything.
Walkthrough: publish and run the VB6 sample
The installer ships a prebuilt copy of the VB6 sample, so you can put a working SDK application in front of a browser before writing any code of your own. It is worth doing once: it proves the whole chain end to end — COM registration, the control channel back to the streamer, and policy enforcement — which makes it far easier later to tell an integration bug from an environment one. Allow about ten minutes.
A working WebStream server, and a user who belongs to a group you can grant. If you do not have those yet, follow the Guided Walkthrough first — it creates a user, a group and a test application in the same admin console you will use here.
1. Publish the application
Open Applications in the admin console and click Add Application. Ignore the Quick Setup Presets, which are for the built-in Windows apps, and fill the fields in directly:
| Field | Value |
|---|---|
| Name | WebStream VB6 Sample |
| Executable path | C:\WebStream\sdk\samples\ActiveXSdkVB6Sample\bin\WebStreamVB6Sample.exe |
| Arguments | Leave empty. |
| Icon path | Optional — the tile falls back to a default. |
| Status | Active |
| Published (visible to users) | On |
Click Create. Point the path at the installed bin folder rather than copying the EXE somewhere more convenient: WebStreamVB6Sample.exe.manifest sits beside it and has to travel with it, or Windows applies installer detection to a filename containing “Sample” and prompts for elevation. Publish an Application covers the full form.
2. Create the workspace with lowtrust
- Open Workspaces and click Add Workspace.
- Name it
VB6 SDK Sample. - Type: select App Collection, so the sample streams as a single window rather than a whole desktop.
- Policy Set: select
lowtrustrather than leaving the dropdown on None (use organization default). - Turn ON
Published (visible to users), then click Create. - On the Applications tab, click Add Application, select
WebStream VB6 Sample, and click Add. - On the Access Rights tab, click Grant Access, select your group, and click Grant Access.
Choose lowtrust deliberately, not for convenience. It confines the session to a constrained set of locations — Public and User Documents — and that restriction is exactly what makes the file and print buttons worth testing, because you see the SDK operating under the governance a real deployment would impose rather than an unrealistically permissive one. Trust Levels explains the four levels; Policy Sets covers the built-in policies.
Steps 6 and 7 are not optional. A workspace created without an application and without a granted group is invisible to every user, and nothing tells you why.
3. Launch it
Sign in at https://<your-server-address>/ as a member of the group you granted, open the VB6 SDK Sample workspace, and click the application tile. The form streams into the browser after a moment.
You do not need to click Start first. The sample calls StartWebStream from Form_Load, because a published application streams itself and there is no operator standing by to press a button. Your own application should do the same; the button exists only so you can re-attach after a deliberate stop.
Everything the sample does is timestamped into the log pane along the bottom of the form. That pane is the primary output for the rest of this walkthrough.
4. What the buttons should do
These are the ones worth clicking first — each exercises a different part of the bridge, and the rest are variations on them.
| Button | Expected outcome |
|---|---|
| Status (Active/Enabled) | The label reads Active: True Enabled: True. Active means the control channel is connected right now; Enabled means a WebStream environment was detected. Enabled true with Active false means the app launched but the control never connected — check registration and the diagnostic log. |
| Browser Info | Two log lines, not one: the request GUID, then OnBrowserInfo (async) carrying JSON with viewWidth/viewHeight, username and IP. This is the asynchronous pattern; the synchronous BrowserInfo returns inline and raises no event. |
| Execute JavaScript | A single line with the result inline — the page title and viewport size, for example WebStream @ 1920x937. Blocking call, no event. |
| Create HTML | A dark rounded banner appears at the top right of the browser, above the streamed window. Click its text and the app logs OnReceiveMessage: banner-clicked. Tap the tap to type field and the on-screen keyboard opens in its alpha view; leaving the field closes it again. |
| Clear HTML | The banner disappears. Injected DOM is purged automatically at logout anyway, so this is a convenience rather than an obligation. |
| File Dialog | A browser-native picker, not a Windows dialog. Choose a file and the result JSON is logged inline. Under lowtrust the reachable locations are constrained — that is the policy working, not a fault. |
| Download File | The app writes a small text file on the host and the browser downloads it; OnDownloadEnd then logs the delivered filename. |
| Slow Render 30s | Motion in the streamed window becomes visibly choppy for 30 seconds, around 4 frames per second, then recovers by itself. Resume ends it early and Force Render (keyframe) resyncs the image immediately. |
| Screen Capture | Writes a PNG to C:\Temp\webstream_capture.png on the session host, creating the folder if it is missing, and logs the path. If the session account cannot write there under the active policy, the log says ScreenCapture failed — again, the policy working. |
| Get Local Username | The IAM username you signed in with in the browser, read from the session environment with no network call. |
| Validate User (IAM) | Prompts for a username and password, then logs accepted or rejected. Expect rejected on a default server: the identity bridge is off unless it has been enabled in app.config.xml. See Identity. |
Stop calls StopWebStream, which ends the entire session and signs the user out; the streamed app closes with it. It is not a pause. To stop rendering temporarily use Pause 30s or Slow Render 30s followed by Resume. For the same reason the sample deliberately does not call StopWebStream from Form_Unload — closing a window should not log the user off.
The three print buttons each ask for a PDF path through an input box before doing anything, and that path is on the session host rather than the client, so point it at a PDF that exists on the server. Print PDF opens the browser print flow, Preview PDF opens the viewer only, and Print PDF (Direct) skips both — although it is genuinely silent only when the client browser runs with kiosk printing enabled, and otherwise still shows the browser's own print dialog.
Once the prebuilt sample behaves as described, open the same project in VB6 from C:\WebStream\sdk\samples\ActiveXSdkVB6Sample to see how each button is wired. The C# equivalent sits alongside it in ActiveXSdkSample, and it is prebuilt too — publish it exactly as above, with the executable path pointed at C:\WebStream\sdk\samples\ActiveXSdkSample\bin\ActiveXSdkSample.exe. The two exercise the same operations, so they can be read side by side.
API catalogue (COM)
Methods and properties on IWebStreamAx. DispIds are stable and additive — existing integrations never break.
| DispId | Member | Purpose |
|---|---|---|
| 1 | StartWebStream(hWnd) | Connect the control channel to the per-user streamer for the given window. |
| 2 | StopWebStream() | End the session gracefully: logs the user off (closing the streamed app and Windows session) and raises OnSessionEnd. For a temporary stop that keeps the session alive, freeze/resume rendering instead. |
| 3 | UploadFile(filePath) | Open the browser upload flow; returns the request GUID; completion via OnFileUpload. |
| 4 | DownloadFile(filePath) | Push a server-side file to the user's browser as a download; completion via OnDownloadEnd. |
| 5 | BrowserInfoAsync() | Request browser info; returns the request GUID; result via OnBrowserInfo. |
| 6 | GetURL(filePath) | Publish a server file and return the URL it is served from. |
| 7 | ExecuteJavaScript(js, timeoutSeconds) | Run JavaScript in the browser and block for the result string. |
| 8 | hwnd | Window handle to stream (property). |
| 9 | OpenURL(url) | Open a URL in a new browser tab. |
| 10 | CreateHTML(id, html) | Render custom HTML chrome in the browser session. Injected DOM is auto-purged on logout / new-session handoff. |
| 11 | FileDialogAsync(filePath, filterOptions) | Open the browser file dialog; returns the request GUID; result via OnFileDialogClose. |
| 12 | BrowserInfo() | Blocking browser/environment info as JSON (user agent, view size, username, IP…). |
| 13 | ExecuteJavaScriptAsync(js) | Fire-and-forget JavaScript; result via OnEvalJavaScript. |
| 14 | FileDialog(filePath, filterOptions) | Open the browser (headless) file dialog and block until the user picks or cancels; returns result JSON. |
| 15 | PrintPDF(filePath) | Deliver a PDF to the browser and invoke the print flow. |
| 16 | EnableDebug(flag) | Toggle SDK debug logging. |
| 17 | PauseRender(ms) | Freeze the streamed canvas (client-side paint hold) for ms (1000–30000), or until ResumeRender. Does not reduce bandwidth — the pipeline keeps flowing; input/messages continue. |
| 18 | ResumeRender() | End any active PauseRender and SlowRender immediately; unfreeze and request a keyframe. |
| 19 | UploadFileEx(uploadDir) | Upload with a server-side destination directory; result paths point at the final location. |
| 20 | SendMessage(data) | Send a string to browser JavaScript (received as the webstream:message DOM event). |
| 21 | PrintPDFEx(filePath, mode) | mode = print (default) or preview — open the PDF in a viewer tab without invoking print. |
| 22 | Active | Read-only: control channel currently connected. |
| 23 | Enabled | Read-only: WebStream environment detected (connected, or streamer port answers a quick probe). Use at startup to gate web-specific code. |
| 24 | PrintPDFDirect(filePath) | Immediate print with no preview or prompt. Silent to the client's default printer only when the client browser runs with kiosk printing (--kiosk-printing); otherwise it falls back to the browser's print dialog. |
| 25 | RemoveHTML(id) | Remove a single CreateHTML element by id. |
| 26 | ClearHTML() | Remove all HTML this session injected via CreateHTML. Optional convenience — injected DOM is purged automatically on logout / new-session handoff. |
| 27 | ScreenCapture(filePath) | Capture the streamed window on the host and save it to filePath. Format inferred from extension (.png/.jpg). Synchronous; returns True on success. |
| 28 | ScreenCaptureEx(filePath, jpegQuality) | As ScreenCapture with explicit JPG quality (1–100; ignored for PNG). |
| 29 | SlowRender(ms) | Throttle the server capture rate to ~4fps for ms (1000–30000) to cut bandwidth/CPU while an app is busy. Auto-restores; re-issue to extend or ResumeRender to end early. Input/messages keep flowing. |
| 30 | ShowKeyboard([view]) | Show the browser on-screen keyboard; its keystrokes are injected into the streamed app. Optional view: alpha (default), caps, numeric (aka symbols/special), or gamepad. Same as window.webstream.showKeyboard(view). |
| 31 | HideKeyboard() | Hide the on-screen keyboard (and gamepad overlay). Same as window.webstream.hideKeyboard(). |
| 32 | ForceRender() | Push a full keyframe on the next capture tick, immediately resyncing the browser to the current window content. Use after a pause/slow period or when the client image may be stale. Fire-and-forget. |
| 33 | LoggingEnabled (property) | Enable/disable the control's diagnostic file log at runtime. Off by default (opt-in; never hard-coded on). Set LogPath first. |
| 34 | LogPath (property) | Directory the diagnostic log is written to (caller-selectable). Changing it while logging is enabled rolls the file over to the new location. |
| 35 | Identity (property) | Bridge to WebStream IAM — see Identity below. Lets a legacy app reuse the signed-in identity instead of keeping a second set of credentials. |
Identity (IIdentity, via ws.Identity)
| DispId | Method | Purpose |
|---|---|---|
| 1 | GetLocalUsername([organisation]) | The WebStream IAM username the user signed in with in the browser. Read from the session environment — no network call, and usable before StartWebStream. Pass organisation to assert the session belongs to it (returns an empty string if not). Returns an empty string when no WebStream identity is published. |
| 2 | ValidateUser(username, password, [group], [organisation]) | Check credentials against WebStream IAM so the app can use it as its authentication backend. Synchronous; returns True/False. Credential-only — no WebStream session is created. group additionally requires membership of that IAM group; organisation selects the organisation to check against. Requires the identity bridge to be enabled on the server. |
| 3 | ValidateUserAsync(username, password, [group], [organisation]) | Non-blocking form; returns a request GUID and delivers the outcome on OnValidateUserResult. |
Events (IWebStreamEvents)
| DispId | Event | Raised when |
|---|---|---|
| 100 | OnBrowserInfo(guid, data) | Browser info result for BrowserInfoAsync. |
| 101 | OnEvalJavaScript(guid, data) | Result for ExecuteJavaScriptAsync. |
| 102 | OnFileDialogClose(guid, data) | Browser file dialog closed (result JSON or cancel). |
| 103 | OnFileUpload(guid, data) | An upload completed; data lists final server paths. |
| 104 | OnPrint(eventType, data) | Print flow events (PdfOpen, PrintDialogClose…). |
| 105 | OnReceiveMessage(data) | Browser JavaScript called window.webstream.sendMessage(data). |
| 106 | OnDownloadEnd(guid, fileName) | A download you initiated completed (or was blocked/errored) in the browser. |
| 107 | OnSessionEnd() | The streamer signalled session end (browser closed / logout) — clean up and save state. |
| 108 | OnValidateUserResult(guid, success, message) | Result for Identity.ValidateUserAsync. Not raised for the blocking ValidateUser. |
VirtualUI migration map
If you are coming from Thinfinity® VirtualUI, this table maps its SDK surface to the WebStream equivalent.
| VirtualUI | WebStream | Notes |
|---|---|---|
Start() / Stop() | StartWebStream() / StopWebStream() | StopWebStream() ends the session (logoff), unlike a mere pause — matches VirtualUI's session-ending Stop(). |
Active / Enabled | Active / Enabled | Same semantics: connected vs. environment detected. |
DownloadFile / UploadFile | DownloadFile + OnDownloadEnd; UploadFile / UploadFileEx(dir) + OnFileUpload | Completion events on both directions; uploads can target a directory. |
| StdDialogs (dialog virtualization) | Platform headless / overlay dialogs — zero code | Native Windows file dialogs are detected and rendered in the browser automatically; use FileDialog() only for explicit SDK-driven flows. |
PrintPdf / PreviewPdf | PrintPDF(path) / PrintPDFEx(path, "preview") | Preview opens the PDF viewer without invoking print. |
BrowserInfo | BrowserInfo() JSON, or BrowserInfoAsync() + OnBrowserInfo | Includes authenticated username and connection IP. |
SendMessage / OnReceiveMessage | SendMessage / OnReceiveMessage | Browser side: window.webstream.sendMessage() and the webstream:message event. |
| HTMLDoc | CreateHTML + ExecuteJavaScript | No separate document object model; inject and script directly. |
| jsRO (remote objects) | ExecuteJavaScript + SendMessage patterns | See recipes below — no remoting framework required. |
OnClose | OnSessionEnd | |
| Recorder, FS/registry filters | ACP platform policy & audit | Governance lives in the platform, not the SDK. |
Recipes
Call browser JavaScript and use the result
' VB / COM — blocking call, result returned directly
Dim title As String
title = ws.ExecuteJavaScript("document.title")
From C#, keep the blocking call off the UI thread so the streamed window keeps repainting:
// C# — same call, awaited rather than blocking the message pump
string title = await Task.Run(() => ws.ExecuteJavaScript("document.title"));
Two-way messaging with your web page
Application side:
ws.SendMessage("{""cmd"":""highlight"",""id"":42}") ' app → browser
Private Sub ws_OnReceiveMessage(ByVal data As String) ' browser → app
' parse and act
End Sub
The same from C#. IWebStreamEvents members are ordinary .NET events, but they are
raised on a background thread, so marshal before touching the UI:
// C#
ws.SendMessage("{\"cmd\":\"highlight\",\"id\":42}"); // app → browser
ws.OnReceiveMessage += data => // browser → app
BeginInvoke(new Action(() => HandleMessage(data)));
Browser side. The listener has to live in a script that actually runs — either a page
script you control, or one registered through ExecuteJavaScript:
window.addEventListener('webstream:message', (e) => {
const msg = JSON.parse(e.detail.data); // from the app
});
window.webstream.sendMessage('{"clicked":"ok"}'); // to the app
A <script> block inside CreateHTML markup will not
execute. Injected HTML is applied with innerHTML, and the HTML specification requires
browsers to ignore scripts added that way. Inline handlers such as onclick are preserved
and do work. Register listeners and helper functions with ExecuteJavaScript instead —
it is evaluated, so it genuinely runs. The worked example below shows the pattern.
Worked example: a browser-native app toolbar
This is the practical replacement for VirtualUI's HTMLDoc: a toolbar that lives in the browser rather than in your streamed window. Because it is real DOM, it stays crisp at any zoom, costs no streaming bandwidth, and behaves properly on touch devices — while your application remains the single source of truth for document state. It has a Save and a Print button, plus a status indicator the app keeps up to date.
1. Register the browser-side bridge. Do this once, after
StartWebStream, by passing the script to ExecuteJavaScript. This step is
what makes the whole pattern work: the toolbar markup injected in step 2 cannot carry its own
<script>, so the listener and any helpers must be registered here.
window.appBar = {
// Browser → app: build the command envelope in one place, so the
// inline handlers in the markup stay short and readable.
send: function (cmd) {
window.webstream.sendMessage(JSON.stringify({ cmd: cmd }));
},
// App → browser: reflect the document state the app pushes.
render: function (s) {
var status = document.getElementById('appbar-status');
var save = document.getElementById('appbar-save');
if (status) {
status.textContent = s.name + (s.dirty ? ' — unsaved' : ' — saved');
status.style.color = s.dirty ? '#f59e0b' : '#22c55e';
}
if (save) save.disabled = !s.dirty;
}
};
window.addEventListener('webstream:message', function (e) {
window.appBar.render(JSON.parse(e.detail.data));
});
2. Inject the toolbar. The markup must be a single top-level
<div>. Inline onclick attributes survive injection, so they can call
straight into the helper registered above.
ws.CreateHTML "appbar", _
"<div style='position:fixed;top:0;left:0;right:0;height:34px;z-index:9999;" & _
"display:flex;align-items:center;gap:12px;padding:0 12px;" & _
"background:#1e293b;color:#fff;font:13px sans-serif'>" & _
"<button id='appbar-save' onclick=""appBar.send('save')"">Save</button>" & _
"<button onclick=""appBar.send('print')"">Print</button>" & _
"<span id='appbar-status'>no document</span>" & _
"</div>"
3. Push state whenever the document changes. This is the app → browser direction, and it is the reason the toolbar never drifts out of step with the application.
Private Sub PushDocState()
ws.SendMessage "{""name"":""" & docName & """,""dirty"":" & LCase$(CStr(isDirty)) & "}"
End Sub
4. Handle the commands coming back. Act on the command, then re-push the state so the button enablement and status text follow automatically — that closes the loop.
Private Sub ws_OnReceiveMessage(ByVal data As String)
Select Case ParseCmd(data) ' data is {"cmd":"save"}
Case "save"
SaveDocument
PushDocState ' toolbar greys out Save, shows "saved"
Case "print"
ws.PrintPDF ExportToPdf()
End Select
End Sub
A few practical notes. Route every browser → app message through one envelope with a
cmd discriminator, as above; it keeps the handler a single readable
Select Case as the toolbar grows, and it is the pattern that replaces VirtualUI's jsRO
remote objects. If the browser tab reloads or rebinds to a new session, both the injected DOM and
the registered helpers are gone, so re-run steps 1 and 2. Teardown is
ws.RemoveHTML "appbar", though injected chrome is purged automatically at logout.
File handoff with completion
' Let the user pick a file in the browser dialog (blocks)
Dim resultJson As String = ws.FileDialog("document.docx")
' Upload into a specific server directory
ws.UploadFileEx("C:\AppData\Inbox")
' → OnFileUpload fires with the final path under C:\AppData\Inbox
' Send a report to the user and know when it arrived
ws.DownloadFile("C:\Reports\summary.pdf")
' → OnDownloadEnd(guid, fileName) fires when the browser accepted it
Custom HTML lifecycle
ws.CreateHTML "toolbar", "<div class='bar'>…</div>" ' inject chrome
ws.RemoveHTML "toolbar" ' remove one element
ws.ClearHTML ' remove all this app injected
The platform tags every CreateHTML element and prunes it automatically
when the session ends (logout) or the browser tab is rebound to a new session, so leftover
chrome never leaks into another session on a pooled tab. RemoveHTML/ClearHTML
are optional in-session teardown helpers — you do not need to supply your own cleanup.
Content must be wrapped in a single top-level <div>; the client rejects anything
else. Inline event attributes such as onclick are preserved, but
<script> blocks are not executed — register behaviour with
ExecuteJavaScript instead, as shown in the
app toolbar example.
Print or preview a PDF
ws.PrintPDF("C:\Reports\invoice.pdf") ' print flow (print dialog)
ws.PrintPDFEx("C:\Reports\invoice.pdf", "preview") ' viewer only
ws.PrintPDFDirect("C:\Reports\invoice.pdf") ' immediate print (silent only under --kiosk-printing)
Capture the screen to a file
If ws.ScreenCapture("C:\Temp\shot.png") Then ' PNG, host-side path
' saved
End If
ws.ScreenCaptureEx "C:\Temp\shot.jpg", 85 ' JPG at quality 85
Captures the streamed window on the host (the machine running your app and the streamer) and writes it to the given host path — it is not the client browser's view and does not download to the end-user's machine. The parent folder is created if needed; format is chosen from the file extension.
Reduce load or hide churn while busy
ws.SlowRender 30000 ' server drops to ~4fps for up to 30s (saves bandwidth/CPU)
ws.PauseRender 30000 ' client holds the last frame for up to 30s (hides visual churn)
ws.ResumeRender ' end either one early, back to full rate
SlowRender throttles the server capture rate, so it genuinely
reduces bandwidth and CPU; PauseRender is a client paint freeze that
hides visual updates but does not save bandwidth. Both are bounded (max 30s, auto-restore),
keep the connection alive, and never block mouse/keyboard or SendMessage traffic.
Re-issue before the period elapses to extend, or call ResumeRender to stop early.
On-screen keyboard
ws.ShowKeyboard ' alphabetic (default)
ws.ShowKeyboard "numeric" ' numbers + special characters
ws.ShowKeyboard "caps" ' alphabetic, shift on
ws.ShowKeyboard "gamepad" ' touch game-controller overlay
ws.HideKeyboard ' dismiss keyboard + gamepad
Shows the client's on-screen keyboard for touch/tablet users; the keys it emits are
injected into your streamed app. The same controls are available to browser-side scripts as
window.webstream.showKeyboard(view) / hideKeyboard(), so you can also
trigger them from ExecuteJavaScript or CreateHTML markup. (This is the
client overlay keyboard, not the Windows on-screen keyboard on the host.)
Auto-trigger on focus. Because CreateHTML preserves inline
event attributes, you can pop the right view when a field gains focus and dismiss it on blur.
Pick the view from the input type — e.g. numeric for numbers:
ws.CreateHTML "entry", _
"<input type='text' onfocus=""window.webstream.showKeyboard('alpha')"" onblur=""window.webstream.hideKeyboard()"" />" & _
"<input type='number' onfocus=""window.webstream.showKeyboard('numeric')"" onblur=""window.webstream.hideKeyboard()"" />"
Force a fresh keyframe
ws.ResumeRender ' end a pause/slow period
ws.ForceRender ' immediately resync the browser to current window content
ForceRender asks the streamer to encode and send a full keyframe on the next
capture tick (same path the browser uses to resync). Handy after a PauseRender/
SlowRender window, after large overlay changes, or any time you suspect the client
image is stale. It is fire-and-forget and safe to call repeatedly.
Bridge legacy user management to WebStream IAM
A legacy app usually has its own login. Bridging it to WebStream IAM avoids a second sign-in and a second set of credentials: read who is already signed in, and check passwords against IAM instead of a private credential store.
' 1. Who is already signed in? No network call - safe to use at startup.
Dim iamUser As String
iamUser = ws.Identity.GetLocalUsername()
If Len(iamUser) > 0 Then
SignInSilently iamUser ' skip your own login screen entirely
End If
' 2. Where you must prompt, validate against WebStream IAM rather than your own store
If ws.Identity.ValidateUser(user, pwd) Then ...
' Optional: also require membership of an IAM group, and pick the organisation
If ws.Identity.ValidateUser(user, pwd, "accounts_payable", "contoso") Then ...
In C#, the trailing arguments are optional and the validation call belongs off the UI thread:
// 1. Who is already signed in? No network call - safe to use at startup.
string iamUser = ws.Identity.GetLocalUsername();
if (!string.IsNullOrEmpty(iamUser))
SignInSilently(iamUser); // skip your own login screen entirely
// 2. Where you must prompt, validate against WebStream IAM rather than your own store
bool ok = await Task.Run(() => ws.Identity.ValidateUser(user, pwd));
// Optional: also require membership of an IAM group, and pick the organisation
bool approver = await Task.Run(() =>
ws.Identity.ValidateUser(user, pwd, "accounts_payable", "contoso"));
Prefer ValidateUserAsync if you would rather not block at all: it returns a request
GUID immediately and reports the outcome on OnValidateUserResult.
GetLocalUsername returns the IAM username of the session, taken from the
entitlement environment published for the session, so it costs nothing and works before
StartWebStream. ValidateUser is credential-only: it verifies the
password and leaves no WebStream session behind, so it is safe to call for your own
re-authentication prompts (supervisor overrides, transaction approvals).
Server policy. ValidateUser is off by default
because it exposes a credential-verification path to the streamed application. Enable it
per server in app.config.xml:
<trust>
<level>noTrust</level>
<identityBridge>true</identityBridge>
</trust>
While disabled, the call returns False. GetLocalUsername is
unaffected by this setting — it only reads the session's own identity. Repeated failures
are subject to the platform's normal account lockout and rate limiting.
Diagnostic logging (opt-in)
ws.LogPath = "C:\ProgramData\MyApp\wslogs" ' choose the directory first
ws.LoggingEnabled = True ' start writing a dated log file
' … reproduce the issue …
ws.LoggingEnabled = False ' stop and flush
The control's file log is off by default and is enabled entirely through the
interface — it is never hard-coded on, and the destination is caller-selectable via
LogPath. While disabled, logging calls are no-ops and no files are created, so it is
safe to leave off in production and switch on only when diagnosing a problem.
Adapt to the browser environment
If ws.Enabled Then ' running under WebStream?
Dim info As String = ws.BrowserInfo()
' JSON includes viewWidth/viewHeight, username, ipAddress, orientation…
End If
All SDK file and print actions ride the same delivery paths as the zero-code platform features, so ACP trust policy, data-transfer gating, and audit logging apply automatically — the SDK cannot bypass governance.