Add flexible device naming options to Unattend delivery

Introduces new parameters and UI controls to give users more choice over device naming when applying an Unattend.xml file.
Users can now specify a device name, use a static or template-based name with the `%serial%` variable, or continue using a list of prefixes.
The UI is updated with a new Device Naming expander to guide the user through the options and clearly indicate the requirements for each mode, ensuring that mutually exclusive options like Copy Unattend and Inject Unattend are not selected together.
Documentation is updated to reflect the new functionality.
This commit is contained in:
rbalsleyMSFT
2026-04-07 10:48:34 -07:00
parent 78212f06d7
commit 4a2d8e63ea
13 changed files with 764 additions and 61 deletions
@@ -36,6 +36,10 @@ function Get-UIConfig {
UseDriversAsPEDrivers = $State.Controls.chkUseDriversAsPEDrivers.IsChecked
CopyPPKG = $State.Controls.chkCopyPPKG.IsChecked
CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked
DeviceNamingMode = Get-SelectedDeviceNamingMode -State $State
DeviceNameTemplate = $State.Controls.txtDeviceNameTemplate.Text
DeviceNamePrefixesPath = $State.Controls.txtDeviceNamePrefixesPath.Text
DeviceNamePrefixes = @(Get-DeviceNamePrefixes -State $State)
CopyAdditionalFFUFiles = $State.Controls.chkCopyAdditionalFFUFiles.IsChecked
CreateDeploymentMedia = $State.Controls.chkCreateDeploymentMedia.IsChecked
InjectUnattend = $State.Controls.chkInjectUnattend.IsChecked
@@ -456,6 +460,24 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'chkCopyAutopilot' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyAutopilot' -State $State
Set-UIValue -ControlName 'chkCopyUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyUnattend' -State $State
Set-UIValue -ControlName 'chkCopyPPKG' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyPPKG' -State $State
Set-UIValue -ControlName 'txtDeviceNamePrefixesPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNamePrefixesPath' -State $State
Set-UIValue -ControlName 'txtDeviceNameTemplate' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNameTemplate' -State $State
Set-UIValue -ControlName 'txtDeviceNamePrefixes' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNamePrefixes' -TransformValue { param($val) if ($val -is [System.Array]) { $val -join [System.Environment]::NewLine } else { [string]$val } } -State $State
if ([string]::IsNullOrWhiteSpace($State.Controls.txtDeviceNamePrefixesPath.Text)) {
$State.Controls.txtDeviceNamePrefixesPath.Text = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text
}
$deviceNamingMode = 'None'
if ($ConfigContent.PSObject.Properties.Name -contains 'DeviceNamingMode') {
$deviceNamingMode = [string]$ConfigContent.DeviceNamingMode
}
if ($deviceNamingMode -notin @('None', 'Template', 'Prefixes')) {
$deviceNamingMode = 'None'
}
Set-DeviceNamingMode -State $State -Mode $deviceNamingMode
Import-DeviceNamePrefixesFromConfiguredPath -State $State
Update-DeviceNamingControls -State $State
# Post Build Cleanup group (Build Tab)
Set-UIValue -ControlName 'chkCleanupAppsISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupAppsISO' -State $State
@@ -27,6 +27,172 @@ function Update-VMNetworkingControls {
}
}
function Get-SelectedDeviceNamingMode {
param([PSCustomObject]$State)
if ($true -eq $State.Controls.rbDeviceNamingTemplate.IsChecked) {
return 'Template'
}
if ($true -eq $State.Controls.rbDeviceNamingPrefixes.IsChecked) {
return 'Prefixes'
}
return 'None'
}
function Set-DeviceNamingMode {
param(
[PSCustomObject]$State,
[ValidateSet('None', 'Template', 'Prefixes')]
[string]$Mode
)
$State.Controls.rbDeviceNamingNone.IsChecked = $Mode -eq 'None'
$State.Controls.rbDeviceNamingTemplate.IsChecked = $Mode -eq 'Template'
$State.Controls.rbDeviceNamingPrefixes.IsChecked = $Mode -eq 'Prefixes'
}
function Get-DeviceNamePrefixes {
param([PSCustomObject]$State)
if ($null -eq $State.Controls.txtDeviceNamePrefixes) {
return @()
}
return @(
$State.Controls.txtDeviceNamePrefixes.Text -split "\r?\n" |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
ForEach-Object { $_.Trim() }
)
}
function Import-DeviceNamePrefixesFile {
param(
[PSCustomObject]$State,
[string]$FilePath
)
if ([string]::IsNullOrWhiteSpace($FilePath) -or -not (Test-Path -Path $FilePath -PathType Leaf)) {
return $false
}
$prefixLines = @(Get-Content -Path $FilePath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
if ($null -ne $State.Controls.txtDeviceNamePrefixesPath) {
$State.Controls.txtDeviceNamePrefixesPath.Text = $FilePath
}
$State.Controls.txtDeviceNamePrefixes.Text = $prefixLines -join [System.Environment]::NewLine
WriteLog "Imported device name prefixes from $FilePath"
return $true
}
function Get-DefaultDeviceNamePrefixesPath {
param([string]$FFUDevelopmentPath)
if ([string]::IsNullOrWhiteSpace($FFUDevelopmentPath)) {
return $null
}
return Join-Path (Join-Path $FFUDevelopmentPath 'unattend') 'prefixes.txt'
}
function Import-DeviceNamePrefixesFromConfiguredPath {
param(
[PSCustomObject]$State,
[switch]$SkipIfTextPresent
)
if ($SkipIfTextPresent -and -not [string]::IsNullOrWhiteSpace($State.Controls.txtDeviceNamePrefixes.Text)) {
return
}
$prefixFilePath = $State.Controls.txtDeviceNamePrefixesPath.Text
if ([string]::IsNullOrWhiteSpace($prefixFilePath)) {
$prefixFilePath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text
if (-not [string]::IsNullOrWhiteSpace($prefixFilePath) -and $null -ne $State.Controls.txtDeviceNamePrefixesPath) {
$State.Controls.txtDeviceNamePrefixesPath.Text = $prefixFilePath
}
}
if (Test-Path -Path $prefixFilePath -PathType Leaf) {
Import-DeviceNamePrefixesFile -State $State -FilePath $prefixFilePath | Out-Null
}
}
function Test-DeviceNameTemplateUsesSerialToken {
param([PSCustomObject]$State)
return ((Get-SelectedDeviceNamingMode -State $State) -eq 'Template') -and ($State.Controls.txtDeviceNameTemplate.Text -match '(?i)%serial%')
}
function Update-UnattendSelectionControls {
param([PSCustomObject]$State)
$selectedDeviceNamingMode = Get-SelectedDeviceNamingMode -State $State
$isCopyUnattendSelected = $true -eq $State.Controls.chkCopyUnattend.IsChecked
$isInjectUnattendSelected = $true -eq $State.Controls.chkInjectUnattend.IsChecked
$deviceNameTemplateUsesSerialToken = Test-DeviceNameTemplateUsesSerialToken -State $State
if ($isCopyUnattendSelected -and $isInjectUnattendSelected) {
if (($selectedDeviceNamingMode -eq 'Prefixes') -or $deviceNameTemplateUsesSerialToken) {
$State.Controls.chkInjectUnattend.IsChecked = $false
$isInjectUnattendSelected = $false
}
else {
$State.Controls.chkCopyUnattend.IsChecked = $false
$isCopyUnattendSelected = $false
}
}
if (($selectedDeviceNamingMode -eq 'Prefixes') -or $deviceNameTemplateUsesSerialToken) {
if (-not $isCopyUnattendSelected) {
$State.Controls.chkCopyUnattend.IsChecked = $true
$isCopyUnattendSelected = $true
}
if ($isInjectUnattendSelected) {
$State.Controls.chkInjectUnattend.IsChecked = $false
$isInjectUnattendSelected = $false
}
$State.Controls.chkCopyUnattend.IsEnabled = $false
$State.Controls.chkInjectUnattend.IsEnabled = $false
return
}
if ($isCopyUnattendSelected) {
$State.Controls.chkCopyUnattend.IsEnabled = $true
$State.Controls.chkInjectUnattend.IsEnabled = $false
}
elseif ($isInjectUnattendSelected) {
$State.Controls.chkCopyUnattend.IsEnabled = $false
$State.Controls.chkInjectUnattend.IsEnabled = $true
}
else {
$State.Controls.chkCopyUnattend.IsEnabled = $true
$State.Controls.chkInjectUnattend.IsEnabled = $true
}
}
function Update-DeviceNamingControls {
param([PSCustomObject]$State)
if (($true -eq $State.Controls.chkInjectUnattend.IsChecked) -and ($true -eq $State.Controls.rbDeviceNamingPrefixes.IsChecked)) {
$State.Controls.rbDeviceNamingNone.IsChecked = $true
}
$selectedDeviceNamingMode = Get-SelectedDeviceNamingMode -State $State
$State.Controls.deviceNameTemplatePanel.Visibility = if ($selectedDeviceNamingMode -eq 'Template') { 'Visible' } else { 'Collapsed' }
$State.Controls.deviceNamePrefixesPanel.Visibility = if ($selectedDeviceNamingMode -eq 'Prefixes') { 'Visible' } else { 'Collapsed' }
$State.Controls.rbDeviceNamingPrefixes.IsEnabled = -not ($true -eq $State.Controls.chkInjectUnattend.IsChecked)
if ($selectedDeviceNamingMode -eq 'Prefixes') {
Import-DeviceNamePrefixesFromConfiguredPath -State $State -SkipIfTextPresent
}
Update-UnattendSelectionControls -State $State
}
function Register-EventHandlers {
param([PSCustomObject]$State)
WriteLog "Registering UI event handlers..."
@@ -242,7 +408,15 @@ function Register-EventHandlers {
$localState = $window.Tag
$selectedPath = Invoke-BrowseAction -Type 'Folder' -Title "Select FFU Development Path"
if ($selectedPath) {
$currentPrefixesPath = $localState.Controls.txtDeviceNamePrefixesPath.Text
$previousDefaultPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
$localState.Controls.txtFFUDevPath.Text = $selectedPath
$newDefaultPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $selectedPath
if ([string]::IsNullOrWhiteSpace($currentPrefixesPath) -or $currentPrefixesPath -ieq $previousDefaultPrefixesPath) {
$localState.Controls.txtDeviceNamePrefixesPath.Text = $newDefaultPrefixesPath
}
Import-DeviceNamePrefixesFromConfiguredPath -State $localState
Update-DeviceNamingControls -State $localState
}
})
@@ -256,6 +430,106 @@ function Register-EventHandlers {
}
})
$State.Controls.rbDeviceNamingNone.Add_Checked({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
Update-DeviceNamingControls -State $window.Tag
})
$State.Controls.rbDeviceNamingTemplate.Add_Checked({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
Update-DeviceNamingControls -State $window.Tag
})
$State.Controls.txtDeviceNameTemplate.Add_TextChanged({
param($eventSource, $textChangedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
if ($null -ne $window -and $null -ne $window.Tag) {
Update-DeviceNamingControls -State $window.Tag
}
})
$State.Controls.rbDeviceNamingPrefixes.Add_Checked({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
Update-DeviceNamingControls -State $window.Tag
})
$State.Controls.btnBrowseDeviceNamePrefixesPath.Add_Click({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
$currentPrefixesPath = $localState.Controls.txtDeviceNamePrefixesPath.Text
if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
$currentPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
}
$initialDirectory = if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
$null
}
else {
Split-Path $currentPrefixesPath -Parent
}
$fileName = if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) { 'prefixes.txt' } else { Split-Path $currentPrefixesPath -Leaf }
$selectedPath = Invoke-BrowseAction -Type 'OpenFile' -Title 'Select prefixes file path' -Filter 'Text files (*.txt)|*.txt|All files (*.*)|*.*' -InitialDirectory $initialDirectory -FileName $fileName
if (Import-DeviceNamePrefixesFile -State $localState -FilePath $selectedPath) {
Update-DeviceNamingControls -State $localState
}
})
$State.Controls.btnSaveDeviceNamePrefixes.Add_Click({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
$prefixLines = @(Get-DeviceNamePrefixes -State $localState)
if ($prefixLines.Count -eq 0) {
[System.Windows.MessageBox]::Show("Enter at least one prefix before saving the prefixes file.", "Prefixes Required", "OK", "Warning") | Out-Null
return
}
$currentPrefixesPath = $localState.Controls.txtDeviceNamePrefixesPath.Text
if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
$currentPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
if (-not [string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
$localState.Controls.txtDeviceNamePrefixesPath.Text = $currentPrefixesPath
}
}
if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
[System.Windows.MessageBox]::Show("Select a valid Prefixes File Path before saving prefixes.", "Prefixes File Path Required", "OK", "Warning") | Out-Null
return
}
try {
$prefixLines | Set-Content -Path $currentPrefixesPath -Encoding UTF8
$localState.Controls.txtDeviceNamePrefixesPath.Text = $currentPrefixesPath
WriteLog "Saved device name prefixes to $currentPrefixesPath"
}
catch {
[System.Windows.MessageBox]::Show("Saving prefixes failed for '$currentPrefixesPath'. $($_.Exception.Message)", "Save Prefixes Failed", "OK", "Error") | Out-Null
}
})
$State.Controls.chkCopyUnattend.Add_Checked({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
$localState.Controls.chkInjectUnattend.IsChecked = $false
Update-DeviceNamingControls -State $localState
})
$State.Controls.chkCopyUnattend.Add_Unchecked({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
Update-DeviceNamingControls -State $window.Tag
})
$State.Controls.chkInjectUnattend.Add_Checked({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
$localState.Controls.chkCopyUnattend.IsChecked = $false
Update-DeviceNamingControls -State $localState
})
$State.Controls.chkInjectUnattend.Add_Unchecked({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
Update-DeviceNamingControls -State $window.Tag
})
# Build USB Drive Settings Event Handlers
# The USB Expander is always visible; the checkbox controls child settings only
$State.Controls.chkBuildUSBDriveEnable.Add_Checked({
@@ -220,6 +220,16 @@ function Initialize-UIControls {
$State.Controls.chkAllowVHDXCaching = $window.FindName('chkAllowVHDXCaching')
$State.Controls.chkCreateDeploymentMedia = $window.FindName('chkCreateDeploymentMedia')
$State.Controls.chkInjectUnattend = $window.FindName('chkInjectUnattend')
$State.Controls.rbDeviceNamingNone = $window.FindName('rbDeviceNamingNone')
$State.Controls.rbDeviceNamingTemplate = $window.FindName('rbDeviceNamingTemplate')
$State.Controls.rbDeviceNamingPrefixes = $window.FindName('rbDeviceNamingPrefixes')
$State.Controls.deviceNameTemplatePanel = $window.FindName('deviceNameTemplatePanel')
$State.Controls.deviceNamePrefixesPanel = $window.FindName('deviceNamePrefixesPanel')
$State.Controls.txtDeviceNameTemplate = $window.FindName('txtDeviceNameTemplate')
$State.Controls.txtDeviceNamePrefixesPath = $window.FindName('txtDeviceNamePrefixesPath')
$State.Controls.btnBrowseDeviceNamePrefixesPath = $window.FindName('btnBrowseDeviceNamePrefixesPath')
$State.Controls.txtDeviceNamePrefixes = $window.FindName('txtDeviceNamePrefixes')
$State.Controls.btnSaveDeviceNamePrefixes = $window.FindName('btnSaveDeviceNamePrefixes')
$State.Controls.chkVerbose = $window.FindName('chkVerbose')
$State.Controls.chkCopyAutopilot = $window.FindName('chkCopyAutopilot')
$State.Controls.chkCopyUnattend = $window.FindName('chkCopyUnattend')
@@ -383,6 +393,12 @@ function Initialize-UIDefaults {
$State.Controls.chkCopyAutopilot.IsChecked = $State.Defaults.generalDefaults.CopyAutopilot
$State.Controls.chkCopyUnattend.IsChecked = $State.Defaults.generalDefaults.CopyUnattend
$State.Controls.chkCopyPPKG.IsChecked = $State.Defaults.generalDefaults.CopyPPKG
Set-DeviceNamingMode -State $State -Mode $State.Defaults.generalDefaults.DeviceNamingMode
$State.Controls.txtDeviceNameTemplate.Text = $State.Defaults.generalDefaults.DeviceNameTemplate
$State.Controls.txtDeviceNamePrefixesPath.Text = $State.Defaults.generalDefaults.DeviceNamePrefixesPath
$State.Controls.txtDeviceNamePrefixes.Text = ($State.Defaults.generalDefaults.DeviceNamePrefixes -join [System.Environment]::NewLine)
Import-DeviceNamePrefixesFromConfiguredPath -State $State
Update-DeviceNamingControls -State $State
$State.Controls.chkCleanupAppsISO.IsChecked = $State.Defaults.generalDefaults.CleanupAppsISO
$State.Controls.chkCleanupDeployISO.IsChecked = $State.Defaults.generalDefaults.CleanupDeployISO
$State.Controls.chkCleanupDrivers.IsChecked = $State.Defaults.generalDefaults.CleanupDrivers
@@ -1173,6 +1173,9 @@ function Invoke-BrowseAction {
if (-not [string]::IsNullOrWhiteSpace($InitialDirectory)) {
$dialog.InitialDirectory = $InitialDirectory
}
if (-not [string]::IsNullOrWhiteSpace($FileName)) {
$dialog.FileName = $FileName
}
if ($dialog.ShowDialog()) {
return $dialog.FileName
}
@@ -106,10 +106,12 @@ function Get-GeneralDefaults {
$peDriversPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "PEDrivers"
$vmLocationPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "VM"
$ffuCapturePath = Join-Path -Path $FFUDevelopmentPath -ChildPath "FFU"
$unattendPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "unattend"
$officePath = Join-Path -Path $appsPath -ChildPath "Office"
$appListJsonPath = Join-Path -Path $appsPath -ChildPath "AppList.json"
$userAppListPath = Join-Path -Path $appsPath -ChildPath "UserAppList.json"
$driversJsonPath = Join-Path -Path $driversPath -ChildPath "Drivers.json"
$deviceNamePrefixesPath = Join-Path -Path $unattendPath -ChildPath "prefixes.txt"
return [PSCustomObject]@{
# Build Tab Defaults
@@ -132,6 +134,10 @@ function Get-GeneralDefaults {
CopyUnattend = $false
CopyPPKG = $false
InjectUnattend = $false
DeviceNamingMode = 'None'
DeviceNameTemplate = ''
DeviceNamePrefixesPath = $deviceNamePrefixesPath
DeviceNamePrefixes = @()
CleanupAppsISO = $true
CleanupDeployISO = $true
CleanupDrivers = $false