From f44e06c57ea5e69ce4360c932e238e80263a308f Mon Sep 17 00:00:00 2001
From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com>
Date: Wed, 18 Jun 2025 16:10:15 -0700
Subject: [PATCH] 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.
---
FFUDevelopment/BuildFFUVM_UI.ps1 | 14 ++--
FFUDevelopment/BuildFFUVM_UI.xaml | 9 +--
.../FFUUI.Core/FFUUI.Core.Handlers.psm1 | 47 ++++++------
.../FFUUI.Core/FFUUI.Core.Initialize.psm1 | 71 +++++++++++++++++--
.../FFUUI.Core/FFUUI.Core.Shared.psm1 | 30 +++++---
5 files changed, 117 insertions(+), 54 deletions(-)
diff --git a/FFUDevelopment/BuildFFUVM_UI.ps1 b/FFUDevelopment/BuildFFUVM_UI.ps1
index 2a79b99..dbf1877 100644
--- a/FFUDevelopment/BuildFFUVM_UI.ps1
+++ b/FFUDevelopment/BuildFFUVM_UI.ps1
@@ -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 {
diff --git a/FFUDevelopment/BuildFFUVM_UI.xaml b/FFUDevelopment/BuildFFUVM_UI.xaml
index 335014d..626c140 100644
--- a/FFUDevelopment/BuildFFUVM_UI.xaml
+++ b/FFUDevelopment/BuildFFUVM_UI.xaml
@@ -198,19 +198,12 @@
-
-
-
-
-
-
-
-
+
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
index 7c0d4ef..9050699 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
@@ -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
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
index b52a905..098cbbb 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
@@ -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 {
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1
index e0673c8..6686e9e 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1
@@ -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,8 +356,8 @@ function Add-SelectableGridViewColumn {
# MODIFICATION: Store the actual ListView object in the item checkbox's Tag
$tagObject = [PSCustomObject]@{
- HeaderCheckboxName = $HeaderCheckBoxScriptVariableName
- ListViewControl = $ListView # Store the object itself
+ 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