# Winforms To Psu

> Helps agents migrate PowerShell Windows Forms applications to PowerShell Universal App Framework apps by mapping WinForms controls, event handlers, state, desktop assumptions, and long-running workflows to PSU pages, components, endpoints, scopes, APIs, and automation jobs.

- Skill: `devolutions/winforms-to-psu` (Agent Skill)
- Install (CLI): `npx skillmds@latest add devolutions/winforms-to-psu`
- Raw SKILL.md: https://api.skillmd.com/api/skills/devolutions/winforms-to-psu/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: devolutions (https://skillmd.com/u/devolutions)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/devolutions/winforms-to-psu

---


# winforms-to-psu

Use this skill when converting an existing PowerShell Windows Forms script or application into a PowerShell Universal (PSU) App Framework app.

The goal is not a one-to-one visual rewrite. Convert a single-user, stateful desktop script into a web/reactive PSU application where the browser renders components, PowerShell runs on the PSU server, and state is scoped deliberately.

## When to use

- The user has a PowerShell WinForms script using `System.Windows.Forms`.
- The user wants a PSU app, dashboard, portal page, or App Framework implementation.
- The task involves translating controls such as `Form`, `Button`, `TextBox`, `DataGridView`, `TreeView`, dialogs, menus, or event handlers.
- The WinForms script uses control properties, global variables, synchronous click handlers, file dialogs, or desktop-only assumptions that need web equivalents.

## Avoid this skill when

- The user wants a custom JavaScript, React, Vue, Angular, Svelte, Vite, Tailwind, or static frontend hosted by PSU. Use `custom-frontend-psu` instead.
- The task is only to install or run PSU. Use `install-sandbox-psu` when available.
- The request is about a non-PSU web framework.

## Migration workflow

1. Inventory the WinForms app:
   - List each form/window, control, control ID/name, event handler, data source, background task, file interaction, and global/module variable.
   - Identify desktop-only dependencies such as local files, registry access, services, printers, COM automation, UI automation, or machine-specific paths.
   - Separate UI code from business logic before rewriting. Preserve reusable PowerShell functions where possible.
2. Choose the PSU app shape:
   - Convert the main window to `New-UDApp` with one or more `New-UDPage` instances.
   - Convert secondary windows to routes/pages, `Show-UDModal`, drawers, expansion panels, or wizard-like dynamic regions.
   - Use responsive `New-UDGrid`, `New-UDStack`, `New-UDContainer`, `New-UDCard`, and `New-UDPaper` layouts instead of fixed desktop coordinates.
3. Map controls to PSU components using the table below.
4. Convert event handlers to PSU component endpoint scriptblocks, API endpoints, or Automation jobs.
5. Decide state scope explicitly for every value that was stored in a control, local variable, module variable, or global variable.
6. Rebuild flows as reactive web interactions:
   - Initial render creates components.
   - Event endpoints read values with `Get-UDElement`, update components with `Set-UDElement`, or rerender dynamic regions with `Sync-UDElement`.
   - Long-running operations move to scripts/jobs with progress polling or loading components.
7. Verify multi-user behavior, refresh/navigation behavior, authorization, server-side file paths, large-data performance, and long-running operation UX.

## Control mapping reference

| WinForms control or pattern | PSU App Framework equivalent | Migration notes |
|---|---|---|
| `Form`, main window | `New-UDApp`, `New-UDPage` | Multi-window apps usually become multi-page apps, modal flows, drawers, or wizard pages. |
| `Button` | `New-UDButton`, `New-UDIconButton`, `New-UDFloatingActionButton`, `New-UDButtonGroup` | Use `-OnClick {}`. Use `-ShowLoading` for short server work. |
| `Label`, static text | `New-UDTypography`, `New-UDHeading`, `New-UDParagraph`, `New-UDSpan`, `New-UDMarkdown`, `New-UDSyntaxHighlighter` | Use typography/heading for labels; Markdown or syntax highlighter for rich/code output. |
| `TextBox` | `New-UDTextbox`, `New-UDCodeEditor`, `New-UDEditor` | `New-UDTextbox` supports text, password, email, number, time, date, color, multiline, masks, validation, `-OnChange`, `-OnBlur`, and `-OnEnter`. |
| `ComboBox`, dropdown | `New-UDSelect`, `New-UDSelectOption`, `New-UDSelectGroup`, `New-UDAutocomplete` | Use `New-UDSelect -Multiple -Checkbox` for checked multi-select. `-OnChange` receives selected value or values. |
| `CheckBox`, toggle | `New-UDCheckBox`, `New-UDCheckbox`, `New-UDSwitch`, `New-UDToggleButtonGroup`, `New-UDToggleButton` | `-OnChange` receives the current boolean/value. |
| `RadioButton`, radio group | `New-UDRadioGroup`, `New-UDRadio` | Put selection handling on the group. `-OnChange` receives the selected value. |
| `ListBox`, `ListView` | `New-UDList`, `New-UDListItem`, `New-UDTransferList`, `New-UDTable` | Use a table when rows need sorting, paging, search, columns, or actions. |
| `DataGridView` | `New-UDTable`, `New-UDTableColumn`, `New-UDDataGrid`, `New-UDDataGridColumn` | Prefer server-side `-LoadData`/`Out-UDTableData` or `-LoadRows` for large data, sorting, filtering, paging, and search. |
| `Panel`, `GroupBox`, layout containers | `New-UDCard`, `New-UDPaper`, `New-UDContainer`, `New-UDGrid`, `New-UDStack`, `New-UDExpansionPanel`, `New-UDExpansionPanelGroup`, `New-UDDrawer`, `New-UDDivider` | Replace fixed coordinates with responsive layout. Use cards/paper for grouped content. |
| `TabControl` | `New-UDTabs`, `New-UDTab`, or separate `New-UDPage` routes | Use pages for top-level navigation and tabs for related same-page content. |
| `MenuStrip`, `ToolStrip`, navigation | `New-UDAppBar`, `New-UDMenu`, `New-UDMenuItem`, `New-UDDrawer`, `New-UDListItem`, `New-UDSpeedDial`, `New-UDBreadcrumbs` | Use `New-UDApp -Navigation` with navigation lists and `Invoke-UDRedirect` for route changes. |
| `MessageBox`, dialog | `Show-UDModal`, `Hide-UDModal`, `Show-UDConfirm`, `Show-UDToast`, `Show-UDSnackbar`, `New-UDAlert`, `New-UDBackdrop` | Use toast/snackbar for feedback, confirm for yes/no, modal for forms or details. |
| `PictureBox`, `ImageList` | `New-UDImage`, `New-UDAvatar`, `New-UDIcon`, `New-UDCardMedia` | Images must be available by web URL, published path, or server-accessible path. Do not rely on desktop handles. |
| `ProgressBar`, spinner | `New-UDProgress`, `New-UDSkeleton`, `New-UDBackdrop`, `New-UDDynamic -LoadingComponent`, `New-UDButton -ShowLoading` | Prefer loading states and polling over blocking the UI. |
| `DateTimePicker`, `MonthCalendar` | `New-UDDatePicker`, `New-UDTimePicker`, `New-UDDateTime`, `New-UDDateRangePicker`, `New-UDTextbox -Type date`, `New-UDTextbox -Type time`, `New-UDTextbox -Type datetime-local` | Normalize date/time values server-side. |
| `OpenFileDialog` | `New-UDUpload` | Uploads go to the PSU server. The handler receives upload metadata such as name, file name, content type, and temp file. |
| `SaveFileDialog` | `Start-UDDownload` | Send string data, a local server file path, or a URL to the browser as a download. |
| `TreeView` | `New-UDTreeView`, `New-UDTreeNode` | Supports click handlers, expanded/prevent-collapse behavior, and lazy child generation with `-OnNodeClicked`. |
| Charts | `New-UDChartJS`, `New-UDNivoChart`, `New-UDSparkline`, `New-UDGauge` | Convert desktop chart binding to web chart datasets and refresh dynamic regions as data changes. |

## Event handler conversion

WinForms event delegates become PSU `[Endpoint]` scriptblock parameters on components. These endpoint scriptblocks run server-side in PSU runspaces and emit changes to browser-rendered React components.

Common conversions:

| WinForms pattern | PSU pattern | Notes |
|---|---|---|
| `$button.Add_Click({ ... })` | `New-UDButton -OnClick { ... }` | Use `Get-UDElement` for current values, `Set-UDElement` or `Sync-UDElement` for updates, and toast/modal components for feedback. |
| `$textBox.Add_TextChanged({ ... })` | `New-UDTextbox -OnChange { ... }` | `$EventData` is the current text/value. Avoid heavy work on every keystroke. Prefer `-OnBlur`, `-OnEnter`, or form submit for expensive logic. |
| Textbox validation | `New-UDTextbox -OnValidate { ... }` or `New-UDForm -OnValidate { ... }` | Return `New-UDValidationResult` according to the target PSU version's pattern. |
| `$comboBox.Add_SelectedIndexChanged({ ... })` | `New-UDSelect -OnChange { ... }` | `$EventData` is the selected value or array for multi-select. |
| `$checkBox.Add_CheckedChanged({ ... })` | `New-UDCheckBox -OnChange { ... }` or `New-UDSwitch -OnChange { ... }` | `$EventData` is the current boolean/value. |
| Radio button checked events | `New-UDRadioGroup -OnChange { ... }` | Handle selection at the group level. |
| OK/Apply button reads many controls | `New-UDForm -Content { ... } -OnSubmit { ... }` | `$EventData` contains input IDs and values. This is usually cleaner than reading each component manually. |
| Bound `DataGridView` refresh | `New-UDTable -LoadData { ... }` or `New-UDDataGrid -LoadRows { ... }` | `$EventData` contains paging, sort, filter, and search context for server-side data loading. |
| Reusable UI-independent operation | `New-PSUEndpoint -Url ... -Method ... -Endpoint { ... }` | Use for API-like operations called outside the app, custom frontends, integrations, or reusable server contracts. Add `-Authentication` and `-Role` as needed. |
| Long-running click handler | `Invoke-PSUScript` Automation job plus progress polling | Do not occupy app endpoint runspaces for long operations. Use loading components and `Sync-UDElement` to refresh status. |

Useful endpoint variables can include `$DashboardHub`, `$ConnectionId`, `$SessionId`, `$Session`, `$Endpoint`, `$Headers`, `$Cookies`, `$Query`, `$ClaimsPrincipal`, `$Roles`, `$PSUAppToken`, and `$PSUURL`. Prefer user/session variables and PSU authorization checks over hidden controls or client-only logic.

### Button conversion example

WinForms:

```powershell
$button.Add_Click({
    $label.Text = "Hello $($textBox.Text)"
})
```

PSU:

```powershell
New-UDTextbox -Id 'name' -Label 'Name'
New-UDTypography -Id 'result' -Text ''
New-UDButton -Text 'Say hello' -OnClick {
    $Name = (Get-UDElement -Id 'name').Value
    Set-UDElement -Id 'result' -Properties @{
        Text = "Hello $Name"
    }
}
```

### Form conversion example

Prefer forms when a WinForms handler reads several controls.

```powershell
New-UDForm -Content {
    New-UDTextbox -Id 'server' -Label 'Server'
    New-UDSelect -Id 'environment' -Label 'Environment' -Option {
        New-UDSelectOption -Name 'Development' -Value 'dev'
        New-UDSelectOption -Name 'Production' -Value 'prod'
    }
} -OnSubmit {
    $Server = $EventData.server
    $Environment = $EventData.environment

    Show-UDToast -Message "Starting work for $Server in $Environment"
}
```

## State and scope mapping

WinForms scripts often store state in control properties, local variables captured by handlers, module variables, or global variables. In PSU, choose state based on who should see the value and how long it should live.

| WinForms state source | PSU state target | Use when |
|---|---|---|
| Current control value such as `$TextBox.Text` or `$CheckBox.Checked` | Component/client state read by `Get-UDElement -Id ...` | The browser owns the current input value and the endpoint only needs it during an event. |
| Temporary per-tab flow value | `$Page:` scope | State is specific to one browser tab/page instance, such as a wizard step, selected row, search criteria, or transient form state. |
| Per-user workflow or preference | `$Session:` scope | State should survive page navigation/reload and be shared across tabs for the same user session. |
| Shared lookup data or expensive read-mostly data | `$Cache:` scope | Data is safe for all users and sessions. Never put per-user data, secrets, or mutable workflow state here. It does not expire automatically. |
| Local variables inside event handlers | Endpoint/local scope | Values are for one endpoint invocation. Do not treat them as durable UI state. |
| WinForms globals/module variables | Usually `$Page:`, `$Session:`, persistent storage, or a protected API/job boundary | Avoid process/global mutable state because PSU serves many users. |

Rule of thumb: every WinForms variable must answer two questions before migration: "Who can see this?" and "How long should it live?" Then choose component value, `$Page:`, `$Session:`, `$Cache:`, persistent storage, or job output.

## PSU web/reactive gotchas

- PowerShell runs on the PSU server or agent, not on the user's desktop. Local paths, registry, services, printers, mapped drives, UI automation, and installed applications refer to the server unless you explicitly connect to a remote target.
- Browser UI values are not live PowerShell object properties. Read them during an endpoint with `Get-UDElement` or receive them through `$EventData`.
- Assign stable `-Id` values to interactive components that need to be read, updated, validated, or refreshed.
- Replace direct control mutation such as `$TextBox.Text = 'Done'` with `Set-UDElement`, `Add-UDElement`, `Clear-UDElement`, or `New-UDDynamic` plus `Sync-UDElement`.
- Use `New-UDDynamic -LoadingComponent`, `New-UDButton -ShowLoading`, progress indicators, jobs, and polling for long operations. Do not block click handlers for work that should run asynchronously.
- Do not move single-user WinForms globals into `$Cache:` or process-global variables unless the data is intentionally shared across every connected user.
- Use `New-UDTable -LoadData` or `New-UDDataGrid -LoadRows` for large datasets. Do not load huge data into the page like a desktop-bound `DataGridView`.
- In table column renderers or nested components, capture row data into a separate variable before defining an inner `-OnClick`; nested event `$EventData` can overwrite the outer row context.
- `Set-UDElement`, `Add-UDElement`, `Clear-UDElement`, and `Sync-UDElement` can use `-Broadcast`. Default to updating the current connection unless cross-client updates are intentional.
- Component endpoints inherit app user context. Custom APIs need explicit `New-PSUEndpoint -Authentication`, `-Role`, timeout, and environment settings when appropriate.
- Browser file exchange uses uploads, downloads, published folders, or API endpoints. `OpenFileDialog` and `SaveFileDialog` do not translate to client filesystem access.

## Practical conversion checklist

- [ ] Create an inventory of controls, events, state variables, data sources, file interactions, and desktop-only dependencies.
- [ ] Decide app/page/modal/navigation structure before writing component code.
- [ ] Map every WinForms control to a PSU component and assign IDs to interactive components.
- [ ] Convert multi-input OK/Apply flows to `New-UDForm -OnSubmit` when practical.
- [ ] Convert event handlers to `-OnClick`, `-OnChange`, `-OnBlur`, `-OnEnter`, `-OnSubmit`, `-OnValidate`, table `-LoadData`, grid `-LoadRows`, API endpoints, or Automation jobs.
- [ ] Replace direct control property mutation with component updates or dynamic region refreshes.
- [ ] Re-scope every global/module/local variable to component state, `$Page:`, `$Session:`, `$Cache:`, persistent storage, endpoint-local variables, or job output.
- [ ] Move long-running work out of app event handlers into scripts/jobs and expose progress/status in the app.
- [ ] Replace desktop file dialogs with `New-UDUpload`, `Start-UDDownload`, published folders, or API endpoints.
- [ ] Add authentication and roles to pages, APIs, published folders, and server operations as needed.
- [ ] Verify refresh, multiple tabs, multiple users, authorization, server file paths, large data, and failure/error states.

## Useful prompt pattern

When delegating a migration to another agent, include PSU-specific constraints:

```text
Convert this PowerShell WinForms script to a PowerShell Universal App Framework app. Inventory controls, event handlers, state variables, file interactions, long-running operations, and desktop-only dependencies first. Map controls to PSU components, use stable component IDs, convert grouped input to New-UDForm where practical, and move long-running work to Invoke-PSUScript jobs with progress polling. Do not use global mutable state for per-user data; choose component values, $Page:, $Session:, or $Cache: deliberately. Remember that PowerShell runs on the PSU server, not the browser client.
```

## Final response guidance

When reporting a completed migration, include:

- The WinForms controls and major flows converted.
- The PSU pages, components, API endpoints, and Automation jobs created or changed.
- The state scopes chosen for important values.
- Any desktop assumptions that changed, especially file paths, client/server execution, authentication, and long-running operations.
- The validation performed, such as PSU sandbox run, page render check, event handler test, API test, or script/job execution.

