Public/Step-HDTWizardPage.ps1
|
function Step-HDTWizardPage { <# .SYNOPSIS Decides which wizard page is current, and what the shell shows around it. .DESCRIPTION THE NAVIGATOR FOR HDTWizardShell.xaml, and every branch the shell has lives here. The shell window opens ONCE and the page inside it is swapped in place - MDT's LiteTouch behaviour, and the reason the rail is a list rather than a TabControl: the order is the deployment's order, and a page becomes reachable by being reached. SO SOMETHING HAS TO DECIDE, ON EVERY CLICK, which page is now current, what the rail shows, whether Back is available, what the Next button says, and whether Next has run off the end of the list. That is this command, and it is pure - no window, no file system, no WPF, no clock. WHY THE SPLIT IS NOT NEGOTIABLE. New-HDTWizardHost is exempt from TDD as a thin WPF adapter (CLAUDE.md rule 1), and the price of that exemption is that the adapter must have NOTHING IN IT WORTH TESTING. The moment page order lived in the host, the host would be worth testing and could not be exempt - which is the trap it fell into once already, when it read the network and then crashed the first time it was really run, in WinPE, on a bench. Done IS THE ONE THAT MATTERS. It is what closes the window and lets the deployment start, so every other answer leaves it false. An off-by-one at the end of this list is a disk partitioned one page before the technician confirmed it. THE RAIL CARRIES NO COLOUR. Each row states what it IS - Done, Current or Pending - and HDTWizardShell.xaml's DataTemplate decides what those look like. A brush computed here would be a second place the look is defined, and the first one to drift from HDTTheme.xaml. .PARAMETER Page The ordered pages this deployment will actually ask, already filtered - a page that is skipped is not in this list and does not appear in the rail either (DESIGN 11.2). Each entry carries Id, Title, Heading, Subheading and the page's own markup. .PARAMETER Index Which page is current now, zero-based. .PARAMETER Action 'Start' to render the current page unchanged, 'Next' or 'Back' to move. Anything else is refused rather than guessed at. .INPUTS None. This command does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject with Index, Page, Heading, Subheading, Rail, BackEnabled, NextCaption, Done and Action. .EXAMPLE Step-HDTWizardPage -Page $page -Index 0 -Action 'Start' The first page, as the shell opens. .EXAMPLE $state = Step-HDTWizardPage -Page $page -Index $state.Index -Action 'Next' if ($state.Done) { $window.Close() } What the host does on every click of Next. #> [CmdletBinding()] [OutputType([pscustomobject])] param( # EMPTY IS ALLOWED THROUGH THE BINDER SO IT CAN BE REFUSED BY NAME. The # binder's own message for an empty mandatory array names the parameter # and nothing else; the refusal below names the decision the caller got # wrong, which is showing a wizard that has nothing to ask. [Parameter(Mandatory = $true, Position = 0)] [AllowNull()] [AllowEmptyCollection()] [object[]] $Page, [Parameter(Mandatory = $true)] [int] $Index, [Parameter(Mandatory = $true)] [ValidateSet('Start', 'Next', 'Back')] [string] $Action ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $ordered = @($Page) $count = @($ordered).Count # AN EMPTY LIST IS NOT AN EMPTY WIZARD, IT IS NO WIZARD. Every page skipped # means the shell must not be SHOWN, and that is the caller's decision - the # same rule HDTSkipWelcome follows in Get-HDTWizardSkip. A shell opened on # nothing would return an Action for a window with no content in it, which # is indistinguishable from a technician answering one that had. if ($count -eq 0) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Category InvalidArgument ` -Message ('the wizard has no pages to show. Every page being skipped means NOT SHOWING THE WIZARD, which is the caller''s decision - see DESIGN 11.2 and HDTSkipWizard.'))) } if ($Index -lt 0 -or $Index -ge $count) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Category InvalidArgument ` -Message ('page index {0} is outside this wizard, which has {1} page(s). Valid indexes are 0 to {2}.' -f $Index, $count, ($count - 1)))) } # WHERE THE CLICK LANDS. Back stops at the front rather than wrapping: # the shell disables the button there, but a disabled button is a # presentation choice and this is the guarantee underneath it. $next = $Index if ($Action -eq 'Next') { $next = $Index + 1 } if ($Action -eq 'Back' -and $Index -gt 0) { $next = $Index - 1 } $done = $next -ge $count $current = $null $heading = '' $subheading = '' if (-not $done) { $current = $ordered[$next] $heading = [string] $current.Heading $subheading = [string] $current.Subheading } # THE RAIL STILL LISTS EVERY PAGE WHEN THE WIZARD IS DONE. It is the last # thing on screen before the window closes, and a rail that emptied itself # would flash blank on the way out. $rail = @() for ($position = 0; $position -lt $count; $position++) { $state = 'Pending' if ($position -lt $next) { $state = 'Done' } if ($position -eq $next) { $state = 'Current' } $rail += [pscustomobject] @{ Id = [string] $ordered[$position].Id Title = [string] $ordered[$position].Title State = $state } } # DEPLOY, NOT NEXT, ON THE LAST PAGE. MDT's Finish. The button that starts a # deployment must not read like the button that turns a page. $caption = 'Next' if (-not $done -and $next -eq ($count - 1)) { $caption = 'Deploy' } return [pscustomobject] @{ Index = $next Page = $current Heading = $heading Subheading = $subheading Rail = $rail BackEnabled = ($next -gt 0 -and -not $done) NextCaption = $caption Done = $done Action = $Action } } |