Refactor USB drive list with dynamic selection and sorting

Updates the USB drive selection UI to align with other list views in the application. This change replaces the static "Select All" checkbox with a dynamic, selectable column that includes the checkbox in the header.

This refactoring provides a more consistent user experience and adds column sorting functionality to the USB drive list.

Additionally, the underlying shared function for creating selectable columns is improved to use the central UI state object for managing controls, removing the dependency on script-scoped variables for better encapsulation.
This commit is contained in:
rbalsleyMSFT
2025-06-18 16:10:15 -07:00
parent b46b904504
commit f44e06c57e
5 changed files with 117 additions and 54 deletions
+7 -7
View File
@@ -205,7 +205,6 @@ $window.Add_Loaded({
$script:uiState.Controls.chkSelectSpecificUSBDrives.IsEnabled = $false
$script:uiState.Controls.chkSelectSpecificUSBDrives.IsChecked = $false
$script:uiState.Controls.lstUSBDrives.Items.Clear()
$script:uiState.Controls.chkSelectAllUSBDrives.IsChecked = $false
})
$script:uiState.Controls.chkSelectSpecificUSBDrives.Add_Checked({
$script:uiState.Controls.usbSelectionPanel.Visibility = 'Visible'
@@ -213,7 +212,6 @@ $window.Add_Loaded({
$script:uiState.Controls.chkSelectSpecificUSBDrives.Add_Unchecked({
$script:uiState.Controls.usbSelectionPanel.Visibility = 'Collapsed'
$script:uiState.Controls.lstUSBDrives.Items.Clear()
$script:uiState.Controls.chkSelectAllUSBDrives.IsChecked = $false
})
$script:uiState.Controls.chkSelectSpecificUSBDrives.IsEnabled = $script:uiState.Controls.chkBuildUSBDriveEnable.IsChecked
$script:uiState.Controls.chkAllowExternalHardDiskMedia.Add_Checked({
@@ -961,8 +959,8 @@ $btnLoadConfig.Add_Click({
# Update the ListView's ItemsSource after populating the data list
$lstAppsScriptVars.ItemsSource = $script:uiState.Data.appsScriptVariablesDataList.ToArray()
# Update the header checkbox state
if ($null -ne (Get-Variable -Name 'chkSelectAllAppsScriptVariables' -Scope Script -ErrorAction SilentlyContinue)) {
Update-SelectAllHeaderCheckBoxState -ListView $lstAppsScriptVars -HeaderCheckBox $script:chkSelectAllAppsScriptVariables
if ($null -ne $script:uiState.Controls.chkSelectAllAppsScriptVariables) {
Update-SelectAllHeaderCheckBoxState -ListView $lstAppsScriptVars -HeaderCheckBox $script:uiState.Controls.chkSelectAllAppsScriptVariables
}
# Update USB Drive selection if present in config
@@ -1016,9 +1014,11 @@ $btnLoadConfig.Add_Click({
}
$script:uiState.Controls.lstUSBDrives.Items.Refresh()
# Update the Select All checkbox state
$allSelected = $script:uiState.Controls.lstUSBDrives.Items.Count -gt 0 -and -not ($script:uiState.Controls.lstUSBDrives.Items | Where-Object { -not $_.IsSelected })
$script:uiState.Controls.chkSelectAllUSBDrives.IsChecked = $allSelected
# Update the Select All header checkbox state
$headerChk = $script:uiState.Controls.chkSelectAllUSBDrivesHeader
if ($null -ne $headerChk) {
Update-SelectAllHeaderCheckBoxState -ListView $script:uiState.Controls.lstUSBDrives -HeaderCheckBox $headerChk
}
WriteLog "LoadConfig: USBDriveList processing complete."
}
else {
+1 -8
View File
@@ -198,19 +198,12 @@
<!-- Button and Select All row -->
<DockPanel Grid.Row="0" Margin="0,5" LastChildFill="False">
<Button x:Name="btnCheckUSBDrives" Content="Check USB drives" DockPanel.Dock="Left" Padding="10,5"/>
<CheckBox x:Name="chkSelectAllUSBDrives" Content="Select All" DockPanel.Dock="Left" Margin="15,0,0,0" VerticalAlignment="Center"/>
</DockPanel>
<!-- ListView row -->
<ListView x:Name="lstUSBDrives" Grid.Row="1" Margin="0,5" Height="150">
<ListView.View>
<GridView>
<GridViewColumn Header="Selected" Width="70">
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Model" DisplayMemberBinding="{Binding Model}" Width="200"/>
<GridViewColumn Header="Serial Number" DisplayMemberBinding="{Binding SerialNumber}" Width="150"/>
<GridViewColumn Header="Size (GB)" DisplayMemberBinding="{Binding Size}" Width="80"/>
@@ -12,30 +12,23 @@ function Register-EventHandlers {
$localState.Controls.lstUSBDrives.Items.Clear()
$usbDrives = Get-USBDrives
foreach ($drive in $usbDrives) {
$localState.Controls.lstUSBDrives.Items.Add([PSCustomObject]$drive)
$driveObject = [PSCustomObject]$drive
# Explicitly add and initialize the IsSelected property for each new item.
$driveObject | Add-Member -MemberType NoteProperty -Name 'IsSelected' -Value $false -Force
$localState.Controls.lstUSBDrives.Items.Add($driveObject)
}
if ($localState.Controls.lstUSBDrives.Items.Count -gt 0) {
$localState.Controls.lstUSBDrives.SelectedIndex = 0
}
})
$State.Controls.chkSelectAllUSBDrives.Add_Checked({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
foreach ($item in $localState.Controls.lstUSBDrives.Items) { $item.IsSelected = $true }
$localState.Controls.lstUSBDrives.Items.Refresh()
})
$State.Controls.chkSelectAllUSBDrives.Add_Unchecked({
param($eventSource, $routedEventArgs)
# This event also fires for indeterminate state, so only act if it's explicitly false.
if ($eventSource.IsChecked -eq $false) {
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
foreach ($item in $localState.Controls.lstUSBDrives.Items) { $item.IsSelected = $false }
$localState.Controls.lstUSBDrives.Items.Refresh()
WriteLog "Check USB Drives: Found $($localState.Controls.lstUSBDrives.Items.Count) USB drives."
# After clearing and repopulating, update the 'Select All' header checkbox state
$headerChk = $localState.Controls.chkSelectAllUSBDrivesHeader
if ($null -ne $headerChk) {
Update-SelectAllHeaderCheckBoxState -ListView $localState.Controls.lstUSBDrives -HeaderCheckBox $headerChk
}
})
$State.Controls.lstUSBDrives.Add_KeyDown({
param($eventSource, $keyEvent)
if ($keyEvent.Key -eq 'Space') {
@@ -45,10 +38,11 @@ function Register-EventHandlers {
if ($selectedItem) {
$selectedItem.IsSelected = -not $selectedItem.IsSelected
$localState.Controls.lstUSBDrives.Items.Refresh()
# After toggling, update the 'Select All' checkbox state
$items = $localState.Controls.lstUSBDrives.Items
$allSelected = $items.Count -gt 0 -and ($items | Where-Object { -not $_.IsSelected }).Count -eq 0
$localState.Controls.chkSelectAllUSBDrives.IsChecked = $allSelected
# After toggling, update the 'Select All' header checkbox state
$headerChk = $localState.Controls.chkSelectAllUSBDrivesHeader
if ($null -ne $headerChk) {
Update-SelectAllHeaderCheckBoxState -ListView $localState.Controls.lstUSBDrives -HeaderCheckBox $headerChk
}
}
}
})
@@ -56,10 +50,11 @@ function Register-EventHandlers {
param($eventSource, $selChangeEvent)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
$items = $localState.Controls.lstUSBDrives.Items
# Update the 'Select All' checkbox state based on current selections
$allSelected = $items.Count -gt 0 -and ($items | Where-Object { -not $_.IsSelected }).Count -eq 0
$localState.Controls.chkSelectAllUSBDrives.IsChecked = $allSelected
# Update the 'Select All' header checkbox state based on current selections
$headerChk = $localState.Controls.chkSelectAllUSBDrivesHeader
if ($null -ne $headerChk) {
Update-SelectAllHeaderCheckBoxState -ListView $localState.Controls.lstUSBDrives -HeaderCheckBox $headerChk
}
})
# Hyper-V tab event handlers
@@ -37,7 +37,6 @@ function Initialize-UIControls {
$State.Controls.chkPreviewCU = $window.FindName('chkUpdatePreviewCU')
$State.Controls.btnCheckUSBDrives = $window.FindName('btnCheckUSBDrives')
$State.Controls.lstUSBDrives = $window.FindName('lstUSBDrives')
$State.Controls.chkSelectAllUSBDrives = $window.FindName('chkSelectAllUSBDrives')
$State.Controls.chkBuildUSBDriveEnable = $window.FindName('chkBuildUSBDriveEnable')
$State.Controls.usbSection = $window.FindName('usbDriveSection')
$State.Controls.chkSelectSpecificUSBDrives = $window.FindName('chkSelectSpecificUSBDrives')
@@ -254,7 +253,7 @@ function Initialize-DynamicUIElements {
$State.Controls.lstDriverModels.View = $driverModelsGridView # Assign GridView to ListView first
# Add the selectable column using the new function
Add-SelectableGridViewColumn -ListView $State.Controls.lstDriverModels -HeaderCheckBoxScriptVariableName "chkSelectAllDriverModels" -ColumnWidth 70
Add-SelectableGridViewColumn -ListView $State.Controls.lstDriverModels -State $State -HeaderCheckBoxKeyName "chkSelectAllDriverModels" -ColumnWidth 70
# Add other sortable columns with left-aligned headers
Add-SortableColumn -gridView $driverModelsGridView -header "Make" -binding "Make" -width 100 -headerHorizontalAlignment Left
@@ -286,7 +285,7 @@ function Initialize-DynamicUIElements {
$State.Controls.lstWingetResults.ItemContainerStyle = $itemStyleWingetResults
# Add the selectable column using the new function
Add-SelectableGridViewColumn -ListView $State.Controls.lstWingetResults -HeaderCheckBoxScriptVariableName "chkSelectAllWingetResults" -ColumnWidth 60
Add-SelectableGridViewColumn -ListView $State.Controls.lstWingetResults -State $State -HeaderCheckBoxKeyName "chkSelectAllWingetResults" -ColumnWidth 60
# Add other sortable columns with left-aligned headers
Add-SortableColumn -gridView $wingetGridView -header "Name" -binding "Name" -width 200 -headerHorizontalAlignment Left
@@ -322,7 +321,7 @@ function Initialize-DynamicUIElements {
# The GridView for lstAppsScriptVariables is defined in XAML. We need to get it and add the column.
if ($State.Controls.lstAppsScriptVariables.View -is [System.Windows.Controls.GridView]) {
Add-SelectableGridViewColumn -ListView $State.Controls.lstAppsScriptVariables -HeaderCheckBoxScriptVariableName "chkSelectAllAppsScriptVariables" -ColumnWidth 60
Add-SelectableGridViewColumn -ListView $State.Controls.lstAppsScriptVariables -State $State -HeaderCheckBoxKeyName "chkSelectAllAppsScriptVariables" -ColumnWidth 60
# Make Key and Value columns sortable
$appsScriptVarsGridView = $State.Controls.lstAppsScriptVariables.View
@@ -375,6 +374,70 @@ function Initialize-DynamicUIElements {
else {
WriteLog "Initialize-DynamicUIElements: Could not build features grid. Panel or defaults missing."
}
# USB Drives ListView setup
# Set ListViewItem style to stretch content horizontally so cell templates fill the cell
$itemStyleUSBDrives = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
$itemStyleUSBDrives.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
$State.Controls.lstUSBDrives.ItemContainerStyle = $itemStyleUSBDrives
if ($State.Controls.lstUSBDrives.View -is [System.Windows.Controls.GridView]) {
# Add the selectable column using the shared function
Add-SelectableGridViewColumn -ListView $State.Controls.lstUSBDrives -State $State -HeaderCheckBoxKeyName "chkSelectAllUSBDrivesHeader" -ColumnWidth 70
# Make other columns sortable
$usbDrivesGridView = $State.Controls.lstUSBDrives.View
# Model Column (index 0 in XAML, now 1)
if ($usbDrivesGridView.Columns.Count -gt 1) {
$modelColumn = $usbDrivesGridView.Columns[1]
$modelHeader = New-Object System.Windows.Controls.GridViewColumnHeader
$modelHeader.Content = "Model"
$modelHeader.Tag = "Model" # Property to sort by
$modelHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
$modelColumn.Header = $modelHeader
}
# Serial Number Column (index 1 in XAML, now 2)
if ($usbDrivesGridView.Columns.Count -gt 2) {
$serialColumn = $usbDrivesGridView.Columns[2]
$serialHeader = New-Object System.Windows.Controls.GridViewColumnHeader
$serialHeader.Content = "Serial Number"
$serialHeader.Tag = "SerialNumber" # Property to sort by
$serialHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
$serialColumn.Header = $serialHeader
}
# Size Column (index 2 in XAML, now 3)
if ($usbDrivesGridView.Columns.Count -gt 3) {
$sizeColumn = $usbDrivesGridView.Columns[3]
$sizeHeader = New-Object System.Windows.Controls.GridViewColumnHeader
$sizeHeader.Content = "Size (GB)"
$sizeHeader.Tag = "Size" # Property to sort by
$sizeHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
$sizeColumn.Header = $sizeHeader
}
# Add Click event handler for sorting
$State.Controls.lstUSBDrives.AddHandler(
[System.Windows.Controls.GridViewColumnHeader]::ClickEvent,
[System.Windows.RoutedEventHandler] {
param($eventSource, $e) # $eventSource is the ListView control
$header = $e.OriginalSource
if ($header -is [System.Windows.Controls.GridViewColumnHeader] -and $header.Tag) {
# Retrieve the main UI state object from the window's Tag property
$listViewControl = $eventSource
$window = [System.Windows.Window]::GetWindow($listViewControl)
$uiStateFromWindowTag = $window.Tag
Invoke-ListViewSort -listView $eventSource -property $header.Tag -State $uiStateFromWindowTag
}
}
)
}
else {
WriteLog "Warning: lstUSBDrives.View is not a GridView. Selectable column not added, and sorting cannot be enabled."
}
}
function Initialize-VMSwitchData {
@@ -283,7 +283,9 @@ function Add-SelectableGridViewColumn {
[Parameter(Mandatory)]
[System.Windows.Controls.ListView]$ListView,
[Parameter(Mandatory)]
[string]$HeaderCheckBoxScriptVariableName,
[psobject]$State,
[Parameter(Mandatory)]
[string]$HeaderCheckBoxKeyName,
[Parameter(Mandatory)]
[double]$ColumnWidth,
[string]$IsSelectedPropertyName = "IsSelected"
@@ -335,8 +337,8 @@ function Add-SelectableGridViewColumn {
}
})
Set-Variable -Name $HeaderCheckBoxScriptVariableName -Value $headerCheckBox -Scope Script -Force
WriteLog "Add-SelectableGridViewColumn: Stored header checkbox in script variable '$HeaderCheckBoxScriptVariableName'."
$State.Controls[$HeaderCheckBoxKeyName] = $headerCheckBox
WriteLog "Add-SelectableGridViewColumn: Stored header checkbox in State.Controls with key '$HeaderCheckBoxKeyName'."
$selectableColumn = New-Object System.Windows.Controls.GridViewColumn
$selectableColumn.Header = $headerCheckBox
@@ -354,7 +356,7 @@ function Add-SelectableGridViewColumn {
# MODIFICATION: Store the actual ListView object in the item checkbox's Tag
$tagObject = [PSCustomObject]@{
HeaderCheckboxName = $HeaderCheckBoxScriptVariableName
HeaderCheckboxKeyName = $HeaderCheckBoxKeyName
ListViewControl = $ListView # Store the object itself
}
$checkBoxFactory.SetValue([System.Windows.FrameworkElement]::TagProperty, $tagObject)
@@ -364,17 +366,25 @@ function Add-SelectableGridViewColumn {
$itemCheckBox = $eventSourceLocal -as [System.Windows.Controls.CheckBox]
$tagData = $itemCheckBox.Tag
$headerCheckboxNameFromTag = $tagData.HeaderCheckboxName
$headerCheckboxKeyFromTag = $tagData.HeaderCheckboxKeyName
$targetListView = $tagData.ListViewControl # Get the control directly from the tag
WriteLog "Add-SelectableGridViewColumn: Item Click. ListView: '$($targetListView.Name)', HeaderChkName: '$headerCheckboxNameFromTag'"
# Get the state from the window tag
$window = [System.Windows.Window]::GetWindow($targetListView)
if ($null -eq $window -or $null -eq $window.Tag) {
WriteLog "Add-SelectableGridViewColumn: ERROR - Could not get window or state from window tag."
return
}
$localState = $window.Tag
$headerChk = Get-Variable -Name $headerCheckboxNameFromTag -Scope Script -ValueOnly -ErrorAction SilentlyContinue
WriteLog "Add-SelectableGridViewColumn: Item Click. ListView: '$($targetListView.Name)', HeaderChkKey: '$headerCheckboxKeyFromTag'"
$headerChk = $localState.Controls[$headerCheckboxKeyFromTag]
if ($null -ne $headerChk) {
Update-SelectAllHeaderCheckBoxState -ListView $targetListView -HeaderCheckBox $headerChk
}
else {
WriteLog "Add-SelectableGridViewColumn: Error - Could not retrieve script variable for header checkbox named '$headerCheckboxNameFromTag'."
WriteLog "Add-SelectableGridViewColumn: Error - Could not retrieve header checkbox from state with key '$headerCheckboxKeyFromTag'."
}
})
@@ -411,7 +421,9 @@ function Update-SelectAllHeaderCheckBoxState {
}
$selectedCount = ($collectionToInspect | Where-Object { $_.IsSelected }).Count
WriteLog "Update-SelectAllHeaderCheckBoxState: Selected count is $selectedCount for ListView '$($ListView.Name)'."
$totalItemCount = $collectionToInspect.Count # Get the total count from the collection being inspected
WriteLog "Update-SelectAllHeaderCheckBoxState: Total item count is $totalItemCount for ListView '$($ListView.Name)'."
if ($totalItemCount -eq 0) {
# Handle empty list case specifically