MasterPromptPS.ps1

<#
.SYNOPSIS
    Tag Servers with Inventory data.
.DESCRIPTION
    Tag a local system or remote server or servers inventory data.
.PARAMETER SettingsFile
    Specifies the json file with the settings and menu definition.
.PARAMETER HideProgressBar
    Hide the SCCM task sequence progress bar.
.PARAMETER Test
    Test GUI generation only, do not attempt to create Task Sequence variables.
.LINK
    http://www.1st-technologies.com/library/masterpromptps
.INPUTS
    None. MasterPromptPS does not accept object inputs.
.OUTPUTS
    None, MasterPromptPS does not output any objects.
.EXAMPLE
    C:\ PS> .\MasterPromptPS
    Display a prompt as defiend by the MasterPromptPS.json file (default) located in the currect directory, store results in task sequence variables
.EXAMPLE
    C:\ PS> .\MasterPromptPS -HideProgressBar
    Hide the SCCM progress bar, then display a prompt as defiend by the MasterPromptPS.json file (default) located in the currect directory, store results in task sequence variables
.EXAMPLE
    C:\ PS> .\MasterPromptPS -SettingsFile .\server.json
    Display a prompt as defiend by .\server.json, store results in task sequence variables
.NOTES
    Name      : MasterPromptPS
    Version   : 0.1.3
    Author    : Alfredo Blanco
    Date      : November 5, 2021
    Copyright : (c) 2021 1st Technologies, Inc. All Rights Reserved
    Changes   : 0.1.0 - Initial Release
                0.1.1 - Add custom validation
                0.1.2 - Add cascade field and data file version validation
                        Add footer field
                        Remove Cancel button, bind the "Escape" only for -Test and remove control box "X" button
                0.1.3 - Add support for multiple radio button groups, misc. improvements
#>




[CmdletBinding()]
Param(
[Parameter(Mandatory = $false, HelpMessage="Specifies the settings file.")]
    [ValidateScript({
        If(Test-Path -Path $_ -PathType Leaf){
            $true
        }else{
            Throw "Settings file not found: $_"
        }
    })]
    [ValidateNotNullOrEmpty()]
    [string]$SettingsFile = "$PSScriptRoot\MasterPromptPS.json",

    [Parameter(Mandatory = $false, HelpMessage="Hide the SCCM progress bar.")]
    [switch]$HideProgressBar,

    [Parameter(Mandatory = $false, HelpMessage="Test GUI generation only, do not attempt to create Task Sequence variables.")]
    [switch]$Test
)



Begin{
    $Version = "0.1.3"

    Write-Verbose -Message "Defining Functions..."
    Function WMIQuery{
        Param(
            [Parameter(Mandatory = $true, HelpMessage="A valid WQL query.")]
            [ValidateNotNullOrEmpty()]
            [string]$query,

            [Parameter(Mandatory = $false, HelpMessage="Specifies the WMI namespace to query. Default [root\cimv2]")]
            [ValidateNotNullOrEmpty()]
            [string]$namespace="root\cimv2"
        )

        $class = Get-WmiObject -Query $query -Namespace $namespace
        If ($class.Properties.IsArray){
            return $class.$($class.properties.name)[0]
        }else{
            return $class.$($class.properties.name)
        }
    }

    Function SetTSVariable{
        Param(
            [Parameter(Mandatory = $True, HelpMessage = "Specify the variable name")]
            [ValidateNotNullOrEmpty()]
            [string]$Name,
            [Parameter(Mandatory = $True, HelpMessage = "Specify the variable value")]
            [ValidateNotNullOrEmpty()]
            [string]$Value
        )

        $smsts = New-Object -ComObject Microsoft.SMS.TSEnvironment
        $smsts[$Name] = $Value
    }

    Write-Verbose -Message "Reading Settings file..."
    $Settings = Get-Content -Path $SettingsFile | ConvertFrom-Json
    If($Settings.__version__ -ne $Version){
        Throw "Invalid Settings file version $($Settings.__version__)."
    }



    Write-Verbose -Message "Loading Assemblies..."
    Try{
        [void][System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
        [void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
    }Catch{
        Throw("Error loading assembly: {0}" -f $($_.exception.message)).ToString().Trim()
    }

    Function PopulateCascade($mCB, $mDCB, $mCascade){
        $mCB.items.Clear()
        $cascadeitems = $($($Settings.fields | Where-Object{$_.name -eq $mCascade}).options.$($mDCB.SelectedItem.Value)).psobject.properties

        $objects = @()
        Foreach ($selectoption in $cascadeitems){
            $obj = new-object System.Object
            $obj | add-member -type NoteProperty -name Label -value $selectoption.value
            $obj | add-member -type NoteProperty -name Value -value $selectoption.name
            $objects += $obj
        }
       
        $mCB.items.addrange($objects)
        $mCB.DisplayMember = "label"
        $mCB.ValueMember = "Value"

        $mCB.Items.Insert(0, "Select an option")
        $mCB.SelectedIndex = 0

        $mCB.Enabled = $True
    }
}

Process{
    If($HideProgressBar){
        $smsui = New-Object -ComObject "Microsoft.SMS.TSProgressUI"
        $smsui.CloseProgressDialog()
    }


    Write-Verbose -Message "Drawing form..."
    $objForm = New-Object System.Windows.Forms.Form
    $objForm.Text = $Settings.title
    $objForm.Size = [system.drawing.size]::new($Settings.width, $Settings.height)
    $objForm.StartPosition = "CenterScreen"
    $objIicon = [System.Drawing.Icon]::new("$PSScriptRoot\images\favicon.ico")
    $objForm.Icon = $objIicon
    $objForm.BackgroundImage = $objIimage
    $objForm.BackgroundImageLayout = "None"
    $objForm.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
    $objForm.MaximizeBox = $false
    $objForm.MinimizeBox = $false
    $objForm.ControlBox = $false


    Write-Verbose -Message "Defining fonts..."
    $LabelFont = [System.Drawing.Font]::new("Arial",10,[System.Drawing.FontStyle]::Bold)
    $FieldFont = [System.Drawing.Font]::new("Arial",10,[System.Drawing.FontStyle]::Regular)
    $HeaderFont = [System.Drawing.Font]::new("Arial",18,[System.Drawing.FontStyle]::Bold)

    Write-Verbose -Message "Wireing keyboard hooks..."
    $objForm.KeyPreview = $True
    $objForm.Add_KeyDown({
        if ($_.KeyCode -eq "Enter"){
            $OKButton.PerformClick()
        }
    })

    If($Test){
        $objForm.Add_KeyDown({
            if ($_.KeyCode -eq "Escape"){
                $objForm.Close()
            }
        })
    }


    $objIimage = [system.drawing.image]::FromFile("$PSScriptRoot\images\logo.png")
    $oPictureBox = New-Object Windows.Forms.PictureBox
    $oPictureBox.Image = $objIimage
    $oPictureBox.Width =
    $oPictureBox.AutoSize = $true
    $oPictureBox.Location = [system.drawing.size]::new(8,17)

    $objHeaderPanel = New-Object System.Windows.Forms.Panel
    $objHeaderPanel.BackColor = [System.Drawing.ColorTranslator]::FromHtml($Settings.headercolor)
    $objHeaderPanel.Dock = [System.Windows.Forms.DockStyle]::Top
    $objHeaderPanel.SendToBack()

    $objHeaderLabel = New-Object System.Windows.Forms.Label
    $objHeaderLabel.Location = [system.drawing.size]::new(10,5)
    $objHeaderLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
    $objHeaderLabel.Size = [System.Drawing.Size]::new($Settings.width, 20)
    $objHeaderLabel.Text = $Settings.header
    $objHeaderLabel.Font = $HeaderFont
    $objHeaderLabel.BackColor = "Transparent"
    $objHeaderLabel.ForeColor = "White"
    $objHeaderPanel.Controls.Add($oPictureBox)
    $objHeaderPanel.Controls.Add($objHeaderLabel)
    $objForm.Controls.Add($objHeaderPanel)


    ForEach ($Field in $Settings.fields){
        Write-Verbose -Message "Generating $($Field.type) field..."
        Switch ($Field.type){
            {($_ -eq "info") -or ($_ -eq "text") -or ($_ -eq "password") -or ($_ -eq "radio") -or ($_ -eq "select") -or ($_ -eq "check") -or ($_ -eq "cascade")} {
                $Settings.rowy += $Settings.rowheight
                Set-Variable -Name "l$($Field.name)" -Value $(New-Object -TypeName System.Windows.Forms.Label)
                Invoke-Expression('$l' + $Field.name + '.Text = "' + $Field.label + '"')
                Invoke-Expression('$l' + $Field.name + '.AutoSize = $true')
                Invoke-Expression('$l' + $Field.name + '.Location = [system.drawing.size]::new(' + $Settings.labelcolumnX + ', ' + $Settings.rowy + ')')
                Invoke-Expression('$l' + $Field.name + '.Font = $LabelFont')
                Invoke-Expression('$objForm.Controls.Add($l' + $Field.name + ')')
            }

            "info" {
                Set-Variable -Name "f$($Field.name)" -Value $(New-Object -TypeName System.Windows.Forms.Label)
                Invoke-Expression('$f' + $Field.name + '.Text = WMIQuery(' + [char]34 + $Field.query + [char]34 +')')
                Invoke-Expression('$f' + $Field.name + '.AutoSize = $true')
                Invoke-Expression('$objForm.Controls.Add($f' + $Field.name + ')')
            }

            "text" {
                Set-Variable -Name "f$($Field.name)" -Value $(New-Object -TypeName System.Windows.Forms.TextBox)
                Invoke-Expression('$f' + $Field.name + '.MaxLength = ' + $Field.maxLength)
                If($Field.default){    
                    Invoke-Expression('$f' + $Field.name + '.Text = WMIQuery(' + [char]34 + $Field.default + [char]34 +')')
                }
                Invoke-Expression('$objForm.Controls.Add($f' + $Field.name + ')')
            }

            "password" {
                Set-Variable -Name "f$($Field.name)" -Value $(New-Object -TypeName System.Windows.Forms.TextBox)
                Invoke-Expression('$f' + $Field.name + '.PasswordChar = "*"')
                Invoke-Expression('$objForm.Controls.Add($f' + $Field.name + ')')
            }

           
            "radio" {
                Set-Variable -Name "f$($Field.name)" -Value $(New-Object -TypeName System.Windows.Forms.GroupBox)
                Invoke-Expression('$f' + $Field.name + '.Size = [system.drawing.size]::new(' + $Settings.textboxsize + ', ' + ($Settings.rowheight * 2) + ')')
                Invoke-Expression('$f' + $Field.name + '.Location = [system.drawing.size]::new(' + $Settings.fieldcolumnX + ', ' + ($Settings.rowy - 2) + ')')

                Foreach ($radiooption in $Field.options.psobject.properties){
                    Write-Verbose -Message "Generating Radio Button Option: $($radiooption.name)"
                    Set-Variable -Name "f$($Field.name)_$($radiooption.name)" -Value $(New-Object -TypeName System.Windows.Forms.RadioButton)
                    Invoke-Expression('$f' + $Field.name + '_' + $radiooption.name + '.dock = [System.Windows.Forms.DockStyle]::Left')
                    Invoke-Expression('$f' + $Field.name + '_' + $radiooption.name + '.Text = "' + $radiooption.value + '"')
                    Invoke-Expression('$f' + $Field.name + '_' + $radiooption.name + '.Font = $FieldFont')
                    Invoke-Expression('$f' + $Field.name + '.Controls.Add($f' + $Field.name + "_" + $radiooption.name + ')')
                    If($Field.default -eq $radiooption.name){
                        Invoke-Expression('$f' + $Field.name + '_' + $radiooption.name + '.Checked = $True')
                    }
                }
                $Settings.rowy += ($Settings.rowheight * 1.3)
                Invoke-Expression('$objForm.Controls.Add($f' + $Field.name + ')')
            }

            "check" {
                Set-Variable -Name "f$($Field.name)" -Value $(New-Object -TypeName System.Windows.Forms.CheckBox)
                Invoke-Expression('$f' + $Field.name + '.Autosize = $true')
                Invoke-Expression('$f' + $Field.name + '.Location = [system.drawing.size]::new(' + $Settings.fieldcolumnX + ', ' + ($Settings.rowy + 4) + ')')
                Invoke-Expression('$objForm.Controls.Add($f' + $Field.name + ')')
                If($Field.checked){    
                    Invoke-Expression('$f' + $Field.name + '.Checked = $True')
                }
            }

            "select" {
                Set-Variable -Name "f$($Field.name)" -Value $(New-Object -TypeName System.Windows.Forms.Combobox)
                Invoke-Expression('$f' + $Field.name + '.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList')

                $objects = @()
                Foreach ($selectoption in $Field.options.psobject.properties){
                    $obj = new-object System.Object
                    $obj | add-member -type NoteProperty -name Label -value $selectoption.value
                    $obj | add-member -type NoteProperty -name Value -value $selectoption.name
                    $objects += $obj
                }

                Invoke-Expression('$f' + $Field.name + '.items.addrange($objects)')
                Invoke-Expression('$f' + $Field.name + '.DisplayMember = "Label"')
                Invoke-Expression('$f' + $Field.name + '.ValueMember = "Value"')

                Invoke-Expression('$f' + $Field.name + '.Items.Insert(0, "Select an option")')
                Invoke-Expression('$f' + $Field.name + '.SelectedIndex = 0')
                Invoke-Expression('$objForm.Controls.Add($f' + $Field.name + ')')
            }

            "cascade" {
                Invoke-Expression('$f' + $Field.name + ' = New-Object System.Windows.Forms.Combobox')
                Invoke-Expression('$f' + $Field.name + '.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList')
                Invoke-Expression('$f' + $Field.name + '.Enabled = $False')

                Invoke-Expression('$f' + $Field.dependsOn + '.add_SelectedIndexChanged{PopulateCascade $f' + $Field.name + ' $f' + $Field.dependsOn + ' ' + $Field.name + '}')

                Invoke-Expression('$f' + $Field.name + '.Items.Insert(0, "Select an option")')
                Invoke-Expression('$f' + $Field.name + '.SelectedIndex = 0')
                Invoke-Expression('$objForm.Controls.Add($f' + $Field.name + ')')
            }

            {($_ -eq "text") -or ($_ -eq "password") -or ($_ -eq "select") -or ($_ -eq "cascade")} {
                Invoke-Expression('$f' + $Field.name + '.Size = [system.drawing.size]::new(' + $Settings.textboxsize + ', 20)')
            }

            {($_ -eq "info") -or ($_ -eq "text") -or ($_ -eq "password") -or ($_ -eq "select") -or ($_ -eq "cascade")} {
                Invoke-Expression('$f' + $Field.name + '.Font = $FieldFont')
                Invoke-Expression('$f' + $Field.name + '.Location = [system.drawing.size]::new(' + $Settings.fieldcolumnX + ', ' + $Settings.rowy + ')')
            }

            Default {throw "Unknown prompt rule $($Field.type) in $SettingsFile"}
        }
    }

    $objForm.Controls | ForEach-Object{
        Write-Verbose -Message "=== $_ ==="
    }

    $Settings.rowy += $Settings.rowheight + 20


    $chkDebug = New-Object System.Windows.Forms.CheckBox
    $chkDebug.Autosize = $true
    $chkDebug.BringToFront()
    $chkDebug.Location = [System.Drawing.Size]::new($settings.width - 35, 105)
    $chkDebug.BringToFront()
    $objForm.Controls.Add($chkDebug)


    $okButtonX = ($objForm.Width/2)-55
    $OKButton = New-Object System.Windows.Forms.Button
    $OKButton.Location = [system.drawing.size]::new($okButtonX, $Settings.rowy)
    $OKButton.Size =  [system.drawing.size]::new(100, 25)
    $OKButton.Text = "OK"
    $OKButton.Add_Click{
        $valid = $true
        $cvalid = $true
        ForEach ($Field in $Settings.fields){
            Switch ($Field.type){
                {($_ -eq "text") -or ($_ -eq "password")}{
                    If($Field.required -eq $true){
                        Invoke-Expression('if ($f' + $Field.name + '.text.length -eq 0){$valid = $false; $f' + $Field.name + '.Backcolor = [System.Drawing.ColorTranslator]::FromHtml("' + $Settings.errorcolor + '")}else{$f' + $Field.name + '.ResetBackColor()}')
                        If(Test-Path -Path "$PSScriptroot\customvalidators\$($Field.name).ps1"){
                            $cvalid=(. .\customvalidators\$($Field.name).ps1 -Value (Invoke-Expression('$f' + $Field.name + '.text')))
                        }
                    }
                }
               
                {($_ -eq "select") -or ($_ -eq "cascade")}{
                    Invoke-Expression('if ($f' + $Field.name + '.SelectedIndex -eq 0){$valid = $false; $f' + $Field.name + '.Backcolor = [System.Drawing.ColorTranslator]::FromHtml("' + $Settings.errorcolor + '")}else{$f' + $Field.name + '.ResetBackColor()}')
                }
            }
        }
        if(($valid -eq $true) -and ($cvalid -eq $true)){
            Write-Verbose -Message "Get field values"
            ForEach ($Field in $Settings.fields){
                Switch ($Field.type){
                    "text"{
                        $fval = Invoke-Expression('$f' + $Field.name + '.Text')
                        If(!($Test)){SetTSVariable -Name $Field.name -Value $fval}
                    }

                    "password"{
                        $fval = Invoke-Expression('$f' + $Field.name + '.Text') | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString
                        If(!($Test)){SetTSVariable -Name $Field.name -Value $fval}
                    }

                    "radio" {
                        Foreach ($radiooption in $Field.options.psobject.properties){
                            Invoke-Expression('if($f' + $Field.name + '_' + $radiooption.name + '.checked -eq $true){$Fval = $f' + $Field.name + '_' + $radiooption.name + '}')
                        }
                        $flabel = $Field.options.psobject.Properties | Where-Object {$_.Value -eq $($fval.Text)} | Select-Object -ExpandProperty Name
                        If(!($Test)){SetTSVariable -Name $Field.name -Value $fval.Text}
                        If(!($Test)){SetTSVariable -Name "$($Field.Name)_label" -Value $flabel}
                    }

                    "check" {
                        $fval = Invoke-Expression('$f' + $Field.name + '.Checked')
                        If(!($Test)){SetTSVariable -Name $Field.name -Value $fval}
                    }

                    "select" {
                        $fval = Invoke-Expression('$f' + $Field.name + '.SelectedItem')
                        If(!($Test)){SetTSVariable -Name $Field.name -Value $fval.Value}
                        If(!($Test)){SetTSVariable -Name "$($Field.Name)_label" -Value $fval.Label}
                    }

                    "cascade" {
                        $fval = Invoke-Expression('$f' + $Field.name + '.SelectedItem')
                        If(!($Test)){SetTSVariable -Name $Field.name -Value $fval.Value}
                        If(!($Test)){SetTSVariable -Name "$($Field.Name)_label" -Value $fval.Label}
                    }
                }
            }
            $objForm.Close()
        }
    }

    $objForm.Controls.Add($OKButton)

    $footerfield = New-Object System.Windows.Forms.Label
    $footerfield.Dock = [System.Windows.Forms.DockStyle]::Bottom
    $footerfield.Text = $Settings.footer
    $footerfield.Font = $LabelFont
    $footerfield.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
    $footerfield.Height = 50
    $footerfield.BackColor = [System.Drawing.ColorTranslator]::FromHtml($Settings.errorcolor)
    $objForm.Controls.Add($footerfield)

    Write-Verbose -Message "Show dialog"
    $objForm.Topmost = $True
    $objForm.Add_Shown({$objForm.Activate()})
    [void]$objForm.ShowDialog()
}


End{
    Write-Verbose -Message "Done"
}

Document Actions