07 — Patterns, Gotchas & Recipes
Critical gotchas (these cause silent failures)
UTF-8 with BOM for any non-ASCII content (see README rule 2). No BOM → garbled/blank in Windows PowerShell 5.1.
$Name(and other param) shadowing inside-Children { }. Container cmdlets declare-Name,-Width,-Value, etc. A function-local$Nameis shadowed inside the block. Alias first:powershellfunction Build-Section { param([string]$Name) $sec = $Name # alias BEFORE the -Children block Add-UICanvasCard -Children { Add-UICanvasLabel "Section: $sec" } }No
-Paddingon a stack-layout page. It can break the layout root and blank the whole page. Use a childMarginfor top spacing:Add-UICanvasLabel 'Title' -Properties @{ Margin = '0,8,0,0' }.Runtime vs authoring cmdlets.
New-UICanvasState,Watch-UICanvasState,Add-UICanvasShortcut,Add-UICanvasWizardNav,Add-UICanvasWorkflowStepare authoring (build the page).Set-/Get-UICanvasState,Set-/Get-UICanvasValue,Set-UICanvasProperty,Show-UICanvasToast/Dialog/Flyout,Show-UICanvasPage,Submit-UICanvas,Start-UICanvasAsync,Set-UICanvasAnimateare runtime (call from actions).-Refreshis in seconds, and it only fires when-Label/-Valueis a scriptblock.Nested data goes in the dedicated param, not
-Properties: grid-Items, tree-Nodes, chart-Datasets. Putting nested objects in the-Propertieshashtable does not round-trip.Donut/center positioning, alignment in Canvas: under a
Canvaslayout root,VAlign/HAlignare ignored (use-X/-Y). Under stack/grid layouts, useVAlign/HAlign.Before rebuilding/iterating, kill a running instance of the engine if you launched one, or a locked DLL ships a stale build. When testing: stop
PoshUIprocess, then relaunch.A clickable row should be a
Panel, not aCard. Cards always draw a 1px border and a card background — that is unconditional, so-Background 'Transparent'on a Card will not remove the box.Add-UICanvasPanel -Action { }is just as clickable (hand cursor, transparent hit-test area) and draws no chrome;HoverScaleworks on both.-Refreshwith a-Labelscriptblock evaluates it ONCE IN THE HOST at build time, to seed the initial text — before the engine even launches. A "run once" guard based on a marker file therefore fires in the host first and the real in-app tick then short-circuits. Either clear the guard after building the canvas and beforeShow-PoshUICanvas, or arm on the first tick and act on the second.
Idioms
- Live label:
Add-UICanvasLabel -Refresh 1 -Label { Get-Date -Format HH:mm:ss }. - Disable a button while working:
Set-UICanvasProperty go Enabled $false……$true. - Busy spinner:
Set-UICanvasProperty icon Spin $true/$false, orAdd-UICanvasProgressRing. - Two-column form: a
-Layout Grid -Columns 2 -Spacing 12panel; each child takes a column. - App shell: a
-Layout Dockpage with a left navPanel(docked) and a contentPanelfilling the rest. - Cards from data:
Add-UICanvasRepeaterinside a grid panel (static) or aDataGrid -BindItems(live).
Recipe A — Form that returns values
Import-Module .\PoshUI\PoshUI.Canvas\PoshUI.Canvas.psd1 -Force
New-PoshUICanvas -Title 'New user' -Theme Dark -Width 520 -Height 420 | Out-Null
Set-UITheme -Preset Slate -Accent Indigo | Out-Null
Add-UICanvasPage -Title 'New user' -Layout VStack | Out-Null
Add-UICanvasLabel 'Create a user' -FontSize 20 -FontWeight Bold -Properties @{ Margin='0,8,0,4' }
Add-UICanvasCard -Layout VStack -Spacing 12 -Padding 18 -Children {
Add-UICanvasTextBox -Name userName -Placeholder 'User name'
Add-UICanvasDropdown -Name role -Choices @('Admin','Operator','Read-only') -Value 'Operator'
Add-UICanvasCheckbox 'Send welcome email' -Name welcome -Value $true
Add-UICanvasPanel -Layout HStack -Spacing 10 -Children {
Add-UICanvasButton 'Create' -Style Accent -Action {
if (-not (Get-UICanvasValue userName)) { Show-UICanvasToast -Message 'Name required' -Severity warning; return }
Submit-UICanvas
}
Add-UICanvasButton 'Cancel' -Style Secondary -Action { Submit-UICanvas }
}
}
$r = Show-PoshUICanvas
"User: $($r.userName), role $($r.role), welcome=$($r.welcome)"Recipe B — Reactive counter + computed label
New-PoshUICanvas -Title 'Reactive' -Theme Dark -Width 520 -Height 320 | Out-Null
Add-UICanvasPage -Title 'Reactive' -Layout VStack | Out-Null
New-UICanvasState @{ count = 0; name = 'World' }
Add-UICanvasLabel -Bind 'Hello {name}, count is {count}' -FontSize 17 -FontWeight SemiBold
Add-UICanvasTextBox -Bind 'name' -Width 280
Add-UICanvasPanel -Layout HStack -Spacing 10 -Children {
Add-UICanvasButton '+1' -Style Accent -Action { Set-UICanvasState count ((Get-UICanvasState count) + 1) }
Add-UICanvasButton 'Reset' -Style Subtle -Action { Set-UICanvasState count 0 }
}
Watch-UICanvasState -Key 'count' -Action {
if ((Get-UICanvasState count) -ge 5) { Show-UICanvasToast -Message 'Reached 5!' -Severity success }
}
Show-PoshUICanvas | Out-NullRecipe C — Live dashboard (charts + grid streaming from state)
New-PoshUICanvas -Title 'Dashboard' -Theme Dark -Width 1000 -Height 640 | Out-Null
Set-UITheme -Preset Slate -Accent Sky | Out-Null
Add-UICanvasPage -Title 'Dash' -Layout VStack | Out-Null
New-UICanvasState @{
cpu = @(20,35,28,50,42,66)
rows = @( @{ Host='web-01'; Status='Online' } )
}
Add-UICanvasLabel -Name ticker -Refresh 2 -Foreground '#34D399' -Properties @{ Margin='0,6,0,0' } -Label {
$c = @(Get-UICanvasState cpu) + (Get-Random -Min 20 -Max 90); if ($c.Count -gt 12) { $c = $c[-12..-1] }
Set-UICanvasState cpu $c
"● live $(Get-Date -Format HH:mm:ss)"
}
Add-UICanvasPanel -Layout Grid -Columns 2 -Spacing 12 -Children {
Add-UICanvasChartCard 'CPU' -Name cpuChart -Type Area -Spline -Bind 'cpu' -Foreground '#38BDF8' -ChartHeight 130
Add-UICanvasCard -Layout VStack -Spacing 6 -Padding 14 -Children {
Add-UICanvasLabel 'Servers' -FontWeight SemiBold
Add-UICanvasDataGrid -Name grid -BindItems 'rows' -Height 130
Add-UICanvasButton 'Add server' -Style Accent -Action {
$r = @(Get-UICanvasState rows); $r += @{ Host=('node-{0:D2}' -f ($r.Count+1)); Status='Provisioning' }
Set-UICanvasState rows $r
}
}
}
Show-PoshUICanvas | Out-NullRecipe D — Action flyout menu
New-PoshUICanvas -Title 'Flyout' -Theme Dark -Width 480 -Height 240 | Out-Null
Add-UICanvasPage -Title 'Flyout' -Layout VStack | Out-Null
Add-UICanvasButton 'Server actions ▾' -Name actionsBtn -Style Accent -Action {
$choice = Show-UICanvasFlyout -Target actionsBtn -Title 'Server' -Items @('Restart','Shut down','View logs') -Placement Bottom
if ($choice) { Show-UICanvasToast -Message "Chose: $choice" -Severity success }
}
Show-PoshUICanvas | Out-NullRunning & verifying
- Run:
powershell.exe -NoProfile -File MyApp.ps1(Windows PowerShell 5.1, not pwsh 7). - The engine logs to
PoshUI\bin\logs\PoshUI.log. Lines with[diag]flag would-be-silent failures (e.g.Set-UICanvasValue: no control named 'x') — useful when a control name is wrong. - Set env
POSHUI_CANVAS_DIAG=1to also surface those as toasts (strict mode).
Diagnosing "it built fine and looks wrong"
The engine accepts a -Properties bag and reads the keys it understands. Anything else used to be dropped in silence, which turned author mistakes into wrong-looking screens with a clean log and exit code 0. The factory now reports them.
After each page is built it logs, at warning level, every -Properties key nothing in the build path ever read:
[unused property] Panel > Image: 'ImageWidth' was supplied but nothing read it -
it is only read for a Banner hero image; size a plain Image with -Width/-Height
[unused property] Panel > Label: 'Margins' was supplied but nothing read it.Check PoshUI\bin\logs\PoshUI.log first whenever a screen renders wrong. It catches typos and — more usefully — keys that are real for a different control type, which is why they look correct in review.
The module also warns at author time when a -Properties value is a nested object, since nested values do not round-trip into the definition; use the dedicated parameter (-Nodes, -Datasets, -Items, -Choices) instead.
This does not catch everything. A key that IS read but has no effect in that position still passes silently. Rules for those were prototyped and withdrawn because they fired on known-good code — for example -Padding on a VStack card is correct and common, so warning about "Padding on a stack" produced seven false positives in one small app. A diagnostic that cries wolf is worse than none.
When to reach for a XAML island
Add-UICanvasXaml is not a last resort, but it is not free either: inside an island you leave the engine's layout, theming helpers and control set behind, and take on raw WPF.
Use an island when the screen needs something the properties bag genuinely does not model:
- motion driven by hover (there is no author-facing hover event;
EventTrigger+Storyboardis) - clipping, opacity masks, transforms
- custom-drawn instruments (gauges, arcs, dials)
- an exact reproduction of a supplied design
Keep on engine controls: chrome, navigation, forms, data widgets, and anything that has to run PowerShell — though -Actions now binds scriptblocks to named controls inside an island too, so a tile strip can slide on hover and stay clickable.
Decide the boundary before building the screen. Retrofitting an island around a finished layout costs more than planning one, and a page that has quietly become 90% island is a signal that the screen wanted to be in-process WPF all along.
Once the boundary is decided, 16-animation-and-motion.md covers what to do inside one: storyboard mechanics (they must self-start — PowerShell cannot call .Begin() from an action), the Storyboard.TargetProperty paths that actually work, a catalogue of techniques, and how to verify the result headlessly.
