Public/Show-HDTWizard.ps1

function Show-HDTWizard {
    <#
        .SYNOPSIS
            Shows the technician wizard and returns what the technician chose.
 
        .DESCRIPTION
            W1 of the WPF-first direction, and MDT's
            LiteTouch wizard is what it grows into: the window a technician sees
            when a machine boots the deployment image and nothing has been
            decided for it in advance.
 
            THE WINDOW IS NOT IN THIS FUNCTION. An injected IWizardHost owns
            everything WPF - Add-Type, XamlReader, ShowDialog - and this function
            owns the decisions. That is the same split every other service in
            this engine uses, and it is what lets the wizard be
            asserted on a developer machine with no display and no WinPE.
            New-HDTWizardHost is the real one, and it is branch-free BECAUSE it
            is not unit tested.
 
            A DISMISSED WINDOW IS A CANCEL, and that is the one piece of logic
            here that is not plumbing. Next leads to a task sequence that
            partitions a disk. A window closed with the X, a dialog killed by
            the shell, a host that returns nothing at all - none of those are a
            technician saying yes, so anything that is not on the allow-list
            comes back as 'Cancel'. The refusal is deliberate rather than
            defensive: reading an empty answer as approval is how an unattended
            machine wipes a disk nobody meant to wipe.
 
            THREE ANSWERS, AND ONLY THESE THREE:
 
              Next the technician approved. The only one that deploys.
              Cancel everything else, including silence.
              CommandPrompt MDT's "Exit to Command Prompt" - the escape hatch
                             for a wrong network, a missing driver, or diskpart.
 
            OPENING THE PROMPT IS THE CALLER'S JOB, NOT THIS COMMAND'S. This
            reports what the technician asked for; the payload decides what a
            prompt means on that machine - which shell, whether the wizard comes
            back afterwards, whether the console has to be un-hidden first. The
            same split that keeps the window out of this function keeps
            cmd.exe out of it.
 
            THE XAML IS CHECKED BEFORE THE WINDOW IS SHOWN. A file that is not
            there, or that is not well-formed, is refused by name - on the build
            host if the image is being tested there, and with a readable
            sentence in WinPE if not. What is checked is XML well-formedness,
            not XAML semantics: a tag that WPF dislikes still fails at Show, but
            a truncated or half-written file - the shape a bad copy into a boot
            image produces - fails here, before a technician is staring at it.
 
        .PARAMETER XamlPath
            The window to show. X:\HDT\UI\HDTWizard.xaml inside a boot image.
 
        .PARAMETER Title
            The window title.
 
        .PARAMETER Field
            What every box should say, from Get-HDTWizardField. Each entry is a
            control Name and its Text; the host applies them by name and this
            command interprets none of them. Omitted, the window opens with
            whatever the markup declares.
 
        .PARAMETER Pane
            Which panes are visible, from Get-HDTWizardSkip. Each entry is a
            control Name and a Visible flag; the host collapses the ones that
            are not. Omitted, every pane the markup declares is shown.
 
            NOTE THAT HDTSkipWelcome IS NOT HANDLED HERE. Skipping the whole
            window means NOT CALLING THIS COMMAND, which is the caller's
            decision - a Show-HDTWizard that sometimes showed nothing would
            return an Action for a window nobody saw.
 
        .PARAMETER WizardHost
            An IWizardHost. Defaults to the real adapter.
 
        .PARAMETER FileSystem
            An IFileSystem. Defaults to the real adapter.
 
        .INPUTS
            None. This command does not accept pipeline input.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject with Action ('Next',
            'Cancel' or 'CommandPrompt'), Title and XamlPath.
 
        .EXAMPLE
            Show-HDTWizard -XamlPath 'X:\HDT\UI\HDTWizard.xaml'
 
            What the payload calls in WinPE.
 
        .EXAMPLE
            $answer = Show-HDTWizard -XamlPath $p -WizardHost (New-HDTFakeWizardHost -Action 'Next')
            if ($answer.Action -ne 'Next') { return }
 
            How every caller must read it: proceed only on an explicit Next.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $XamlPath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $Title = 'Hephaestus Deployment Toolkit',

        [Parameter()]
        [AllowNull()]
        [object[]] $Field,

        [Parameter()]
        [AllowNull()]
        [object[]] $Pane,

        [Parameter()]
        [AllowNull()]
        [object] $WizardHost,

        [Parameter()]
        [AllowNull()]
        [object] $FileSystem,

        # F8, THE SAME AS EVERY OTHER WINDOW IN THIS IMAGE. The Welcome screen
        # is the first thing on the machine and the one shown when the share
        # cannot be reached - which is exactly when a technician wants a prompt.
        # A key that works on two of the three windows is a key nobody trusts.
        [Parameter()]
        [AllowNull()]
        [scriptblock] $CommandPrompt,

        # WHICH BOXES TO READ BACK, from Get-HDTWizardHarvest. Omitted, the
        # screen answers with an Action alone - which is what it did before this
        # existed, and why a corrected share was thrown away.
        [Parameter()]
        [AllowNull()]
        [AllowEmptyCollection()]
        [object[]] $Collect = @()
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    if ($null -eq $FileSystem) { $FileSystem = New-HDTFileSystem }
    if ($null -eq $WizardHost) { $WizardHost = New-HDTWizardHost }
    if ($null -eq $CommandPrompt) { $CommandPrompt = { [void] (Start-HDTCommandPrompt) } }

    # -- the window file, before anything is shown -------------------------

    if (-not $FileSystem.TestPath($XamlPath)) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $XamlPath -Category ObjectNotFound `
                    -Message ("the wizard window '{0}' is not there, so there is nothing to show. In a boot image it is staged to X:\HDT\UI\ by Update-HDTBootImage." -f $XamlPath)))
    }

    $xaml = [string] $FileSystem.ReadAllText($XamlPath)

    try {
        [void] ([xml] $xaml)
    } catch {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $XamlPath -Category InvalidData `
                    -Message ("the wizard window '{0}' is not well-formed XML, so it could not be shown: {1}" -f
                        $XamlPath, [string] $_.Exception.Message)))
    }

    # -- the text --------------------------------------------------------------
    #
    # THE MARKUP CARRIES NO PROSE, so the block that fills it is chosen here and
    # handed to the host, which applies it and decides nothing.
    #
    # THE FILE NAME IS THE BLOCK NAME, without the HDT and without the
    # extension: HDTWelcome.xaml is filled by Welcome, HDTFailure.xaml by
    # Failure. Every window in this module already follows it, and a convention
    # is one fewer table to keep in step with the markup.
    #
    # A WINDOW WITH NO BLOCK IS SHOWN ANYWAY. Tools and tests load scratch
    # markup that nobody has written strings for, and a wizard that refused one
    # would make the table a thing to be fed before anything can be drawn.
    $string = @{}

    try {
        $string = Get-HDTStringTable -Page (
            [System.IO.Path]::GetFileNameWithoutExtension($XamlPath) -replace '^HDT', '')
    } catch {
        Write-Verbose ("no string table block for '{0}': {1}" -f $XamlPath, [string] $_.Exception.Message)
    }

    # -- show it -----------------------------------------------------------

    $answer = [string] $WizardHost.Show($xaml, $Title, @($Field), @($Pane), $CommandPrompt, @($Collect), $string)

    # THE ALLOW-LIST, AND IT IS THE WHOLE SAFETY PROPERTY. See the header:
    # anything that is not one of these three exactly is a Cancel.
    #
    # MATCHED CASE-SENSITIVELY, and the ALLOW-LIST's spelling is what is
    # returned - never the host's string. Both halves matter. A host answering
    # 'next' is not the host this command was written against, and a widened
    # list that also normalises case is one step from "recognise anything that
    # looks close enough" - which, on the other side of Next, partitions a disk.
    $action = 'Cancel'
    foreach ($allowed in @('Next', 'Cancel', 'CommandPrompt')) {
        if ($answer -ceq $allowed) {
            $action = $allowed
            break
        }
    }

    return [pscustomobject] @{
        Action   = $action
        Title    = $Title
        XamlPath = $XamlPath

        # WHAT THE TECHNICIAN TYPED, keyed by control name. Empty when nothing
        # was asked for. The Welcome screen used to answer with an Action alone,
        # so a corrected deploy root went nowhere and the machine died on the
        # address that was already wrong - see Get-HDTWizardHarvest.
        Value    = $WizardHost.Value
    }
}