← All developer systems
Built and testedWindows (.NET)

Integrate ScreenSteer with Windows and .NET

Add the .NET 9 client to WPF, WinUI, or WinForms: window-only capture, named user approval, automatic reconnect, and a XAML-level choice of how much of your UI agents may operate.

The .NET 9 client and xUnit contract suite are build-verified.

Download and install

For WPF, WinUI, and WinForms apps on .NET 9. Install the NuGet package or vendor the source.

Requirements

  • Windows x64
  • .NET 9 SDK
  • LiveKit .NET RTC integration
dotnet add package ScreenSteer.Client --source ./downloads
QUICKSTART

Start with explicit consent

Replace pk_dev_… with the Development key shown once when you create it in the console. Use the one-time pk_live_… key from the Production environment after paid activation.

SupportSession.cs
C#
using ScreenSteer.Client;

var client = new ScreenSteerClient(new ScreenSteerConfig(
    new Uri("https://screensteer.com"),
    "pk_dev_…"));

// Start only after your user approves screen sharing.
var session = await client.CreateSessionAsync(user.Id);
// Connect LiveKit, publish the screen, then start DeviceAgent.
STEP 1

Install the packages

Reference three projects from the source archive (or the NuGet package for the core): ScreenSteer.Client (protocol, validator, registry, agent, reconnect policy), ScreenSteer.Client.LiveKit (room transport with token-refreshing reconnect, window capture, full-screen capture), and ScreenSteer.Client.Wpf (the XAML permission model).

The core client has no dependencies beyond the base class library and also targets netstandard2.1 for Unity and MAUI.

STEP 2

The consent flow: users approve a named person

StartAsync creates the session and the data channel only — no pixels are captured yet. When an agent joins, the transport raises AgentJoined with the agent's display name (agents must set one in the console before they can join).

Show one approval popup naming that person, for example "Allow Dana to securely connect and help with this app?". Keep it to three facts in plain words: who is asking, that the session is limited to this app, and that it can be stopped at any time. A wall of security detail at the moment of decision makes people hesitate rather than feel safe, so keep the mechanics for your help page. Only after approval do you call BeginSharingAsync, so the first frame to leave the machine is already consented and already masked. Say in that popup that the agent can press the controls you approved for remote assistance, because it is the only time you ask.

Then call SetControlPreauthorized(true). That tells the console the control question is already answered, so it completes the control request itself and control simply switches on: no agent-side button that would prompt nobody, and no second popup for your user. If the user later pauses control, the pre-authorization is withdrawn with it, so the next request raises your own "allow help again" popup instead of resuming behind their back.

STEP 3

Window-only capture

WindowCapturePublisher captures a single window's client area via PrintWindow: overlapping windows, other apps, and the rest of the desktop are never in the frame, even while your window is occluded. Minimized windows publish a solid dark paused frame, and resizes republish the track at the new aspect automatically.

All coordinates — action rectangles and redaction masks — are normalized against the window client area, so they survive moves, resizes, and DPI changes.

STEP 4

Choose the assist scope, straight from XAML

WpfAssistSurface keeps the interaction map and redaction masks in sync with the live window, so you write no per-control code and no coordinate math. Coverage defaults to AllControls — every visible control is operable except what you exclude; switch to MarkedOnly to invert the default so nothing is operable until you mark it.

Both exclusion attributes inherit down the visual tree, so one attribute protects an entire screen such as billing or admin.

MainWindow.xaml.cs
C#
_surface = new WpfAssistSurface(session.Registry, this, new WpfAssistOptions
{
    Coverage  = AssistCoverage.AllControls,        // or MarkedOnly
    Targeting = AssistTargeting.RegisteredControls, // or LiveHitTest for dense UIs
    DenyLabelFragments = WpfAssistOptions.CommonDestructiveLabels,
});
_surface.RedactionsChanged += rects => session.SetMasks(rects);
_surface.CoverageTruncated += r => Log($"{r.Dropped} controls over the cap");
_surface.Start();

// XAML — one attribute covers a whole subtree:
//   <TabItem Header="Billing" ss:ScreenSteerAssist.Policy="Deny" />
//   <StackPanel ss:ScreenSteerAssist.Redact="True" />
//   <GroupBox  ss:ScreenSteerAssist.Policy="Allow" />  (MarkedOnly opt-in)
STEP 5

Two targeting modes

RegisteredControls (default): every operable control is registered with its rectangle. Most auditable, but the interaction map is a single 15 KiB data packet, so at most about 70 controls fit at once — CoverageTruncated fires if a screen exceeds it.

LiveHitTest ("global touch"): clicks resolve against the live visual tree at execution time, so there is no per-button cap. Text inputs stay individually registered so the console knows to prompt for values, and every protection still applies at click time — Deny and Redact (checked on the exact pixels clicked, so a marked area can never leak through to a parent control), password boxes, and stale-click rejection: any geometry change bumps the layout revision so clicks minted against an outdated frame fail closed.

STEP 6

Keyboard shortcuts

An agent can fire the shortcuts you publish, and nothing else. List them in WpfAssistOptions.Shortcuts (empty by default, so a session has no keyboard until you add one) and the console shows a button per chord. There is no free-text field anywhere in the flow, which is what keeps a remote keyboard from reaching a password box.

Delivery tries your KeyBinding command first, through CanExecute, and falls back to real routed key events on the focused element, so Escape reaches a Cancel button and Ctrl+R reaches a KeyDown handler. Keys are refused while focus sits in a password box or a marked subtree, and every key needs active control consent.

If your app decides what a shortcut means in code, give the shortcut a Run delegate instead of relying on key delivery. Handlers that read Keyboard.Modifiers, and raw WM_KEYDOWN hooks, both read the physical keyboard, which no in-process key event can set, so a sent Ctrl+R would silently do nothing. A Run delegate calls your method on the UI thread; route it through the same guarded dispatch your key handler uses so suspension and sensitive-flow guards still apply.

MainWindow.xaml.cs
C#
new WpfAssistOptions
{
    Shortcuts =
    [
        // Key delivery: bound command, else a routed key event.
        new AssistShortcut("Escape", "Close the dialog"),
        // Handler-backed: your method, your guards, no key involved.
        new AssistShortcut("Ctrl+R", "Restart the workflow",
            () => _hotkeys.TryInvoke(Key.R, ctrlPressed: true)),
    ],
    // KeyDelivery = KeyDelivery.BoundCommandsOnly,  // never synthesize
}

// Alt+F4, Alt+Tab, Ctrl+Alt+Delete and friends are dropped at registration.
STEP 7

Custom controls that are not real buttons

A Border, Grid, or Image that acts as a button is operable when its click runs through a command: put a MouseBinding with MouseAction="LeftClick" and Command="{Binding ...}" in the element's InputBindings, and the SDK invokes that command (through CanExecute) exactly as a real click would. Its caption is read from the first TextBlock inside it, so the console shows the words the user sees and DenyLabelFragments matches them.

An element that is clickable only through a code-behind MouseLeftButtonDown handler stays out of reach: WPF exposes nothing to invoke, and the SDK never synthesizes input. Bind a command instead, or use a real Button with your own template.

STEP 8

Reconnects and long sessions

LiveKit join tokens expire after 15 minutes, so construct LiveKitRoomTransport with LiveKitReconnectOptions whose token provider calls RefreshDeviceTokenAsync. Terminal drops then rejoin automatically with backoff and republish the capture; surface Reconnecting/Reconnected in your banner.

The per-session bearer never expires and live sessions are never expired server-side for being long.

STEP 9

The user's control bar

Keep three controls visible for the whole session: Stop control (view only) via RevokeControlAsync — fails closed locally even if the network is down; Return control via ReturnControlAsync — re-grants the most recent controller without a new agent request; and Stop session via StopAsync, which always works.

The source archive's samples/ScreenSteer.Sample.Wpf implements the full flow: named approval, control bar, laser overlay, reconnect banner, and both Deny and Redact markings.

What the device enforces

  • Consent precedes pixels: capture starts only after the user approves the named agent, with masks applied from the first frame.
  • No OS-level click or keyboard injection; controls are driven through WPF automation peers, the same path assistive technology uses.
  • Deny, Redact, and password protections are enforced twice: at registration or hit-test time, and again at execution.
  • Controller, sequence, expiry, and layout-revision validation; geometry changes fail stale clicks closed in both targeting modes.
  • Private masks are applied before the frame is encoded; redacted pixels never leave the process and are never clickable.

Smoke test

  1. 1Run the bundled xUnit suites (core + WPF) and build the sample.
  2. 2Configure a Development publishable key outside source control.
  3. 3Request help, join from the console, and approve the named popup.
  4. 4Overlap another window and confirm the agent still sees only the app.
  5. 5Click across the UI with control granted; confirm Deny-marked and redacted areas refuse.
  6. 6Move or resize the window and verify stale commands fail before geometry refreshes.
  7. 7Cut the network briefly and confirm automatic reconnection with control intact.
  8. 8Use Stop control, Return control, and Stop session from the banner.

Troubleshooting

invalid_publishable_key

Create an active Development key for the same console app. Revoked and Production keys cannot start sandbox sessions.

Room connection fails on the application network

Confirm outbound HTTPS and WebRTC egress are permitted. Restrictive networks may force LiveKit to TCP/TLS with higher latency.

Video is black or shows a dark paused frame

A dark frame means the window is minimized (by design). Otherwise check screen-lock state and confirm the publisher is producing frames.

Action returns not_available

The reason code says why: element_denied, element_redacted, element_protected (password), element_not_allowed (MarkedOnly without Allow), element_disabled, element_hidden, or no_interactive_element_at_point.

Some controls do nothing in RegisteredControls mode

Check for a CoverageTruncated report — that screen exceeds the per-packet element cap. Switch Targeting to LiveHitTest or reduce visible controls.