From 87c9bc769e7f77451d546a60875b37c8d956d70a Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Tue, 17 Jun 2025 18:25:06 -0700 Subject: [PATCH] Improve driver list data handling and filtering Refactors the driver selection UI to enhance stability and performance by changing how the underlying data source is managed. Creating and re-assigning a new list when data changes, rather than modifying the bound collection in-place, prevents UI inconsistency errors. - Updates the model search to use the native WPF `CollectionView.Filter` for more efficient and reliable filtering. - Fixes an issue where HTML entities were not decoded in Microsoft driver model names. - Ensures selected drivers from one manufacturer are preserved when fetching models for another. - Centralizes driver-related button event handlers into the core initialization module. --- FFUDevelopment/BuildFFUVM_UI.ps1 | 13 ---- .../FFUUI.Core.Drivers.Microsoft.psm1 | 4 +- .../FFUUI.Core/FFUUI.Core.Drivers.psm1 | 69 +++++++++++++------ .../FFUUI.Core/FFUUI.Core.Initialize.psm1 | 59 ++++++++++++---- 4 files changed, 94 insertions(+), 51 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM_UI.ps1 b/FFUDevelopment/BuildFFUVM_UI.ps1 index 17c4faf..9eeac16 100644 --- a/FFUDevelopment/BuildFFUVM_UI.ps1 +++ b/FFUDevelopment/BuildFFUVM_UI.ps1 @@ -169,19 +169,6 @@ $window.Add_Loaded({ $script:uiState.Controls.spModelFilterSection.Visibility = 'Collapsed' $script:uiState.Controls.lstDriverModels.Visibility = 'Collapsed' $script:uiState.Controls.spDriverActionButtons.Visibility = 'Collapsed' - $script:uiState.Controls.btnClearDriverList.Add_Click({ - $script:uiState.Controls.lstDriverModels.ItemsSource = $null - $script:uiState.Data.allDriverModels = @() - $script:uiState.Controls.txtModelFilter.Text = "" - $script:uiState.Controls.txtStatus.Text = "Driver list cleared." - }) - $script:uiState.Controls.btnSaveDriversJson.Add_Click({ - Save-DriversJson -State $script:uiState - }) - $script:uiState.Controls.btnImportDriversJson.Add_Click({ - Import-DriversJson -State $script:uiState - }) - # Office interplay (Keep existing logic) $script:uiState.Flags.installAppsCheckedByOffice = $false if ($script:uiState.Controls.chkInstallOffice.IsChecked) { diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Microsoft.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Microsoft.psm1 index aec591a..3890cfb 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Microsoft.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Microsoft.psm1 @@ -11,8 +11,6 @@ function Get-MicrosoftDriversModelList { try { WriteLog "Getting Surface driver information from $url" - WriteLog "Using UserAgent: $UserAgent" - WriteLog "Using Headers: $($Headers | Out-String)" $OriginalVerbosePreference = $VerbosePreference $VerbosePreference = 'SilentlyContinue' # Use passed-in UserAgent and Headers @@ -41,7 +39,7 @@ function Get-MicrosoftDriversModelList { $cellMatches = [regex]::Matches($rowContent, $cellPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) if ($cellMatches.Count -ge 2) { - $modelName = ($cellMatches[0].Groups[1].Value).Trim() + $modelName = ([System.Net.WebUtility]::HtmlDecode(($cellMatches[0].Groups[1].Value).Trim())) $secondTdContent = $cellMatches[1].Groups[1].Value.Trim() # $linkPattern = ']+href="([^"]+)"[^>]*>' # Change linkPattern to match https://www.microsoft.com/download/details.aspx?id= diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.psm1 index a1102f2..fe721f7 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.psm1 @@ -124,27 +124,39 @@ function Search-DriverModels { return } - WriteLog "Filtering models with text: '$filterText'" - - # Filter the full list based on the Model property (case-insensitive) - # Ensure the result is always an array, even if only one item matches - $filteredModels = @($State.Data.allDriverModels | Where-Object { $_.Model -like "*$filterText*" }) - - # Update the ListView's ItemsSource with the filtered list - # Setting ItemsSource directly should work for simple scenarios - $State.Controls.lstDriverModels.ItemsSource = $filteredModels - - # Explicitly refresh the ListView's view to reflect the changes in the bound source - if ($null -ne $State.Controls.lstDriverModels.ItemsSource -and $State.Controls.lstDriverModels.Items -is [System.ComponentModel.ICollectionView]) { - $State.Controls.lstDriverModels.Items.Refresh() - } - elseif ($null -ne $State.Controls.lstDriverModels.ItemsSource) { - # Fallback refresh if not using ICollectionView (less common for direct ItemsSource binding) - $State.Controls.lstDriverModels.Items.Refresh() + # Ensure the ItemsSource is always the master list. This prevents inconsistency. + if ($State.Controls.lstDriverModels.ItemsSource -ne $State.Data.allDriverModels) { + $State.Controls.lstDriverModels.ItemsSource = $State.Data.allDriverModels } + # Get the default view of the items source, which supports filtering. + $collectionView = [System.Windows.Data.CollectionViewSource]::GetDefaultView($State.Controls.lstDriverModels.ItemsSource) + if ($null -eq $collectionView) { + WriteLog "Search-DriverModels: Could not get CollectionView. Filtering may not work." + return + } - WriteLog "Filtered list contains $($filteredModels.Count) models." + WriteLog "Applying filter with text: '$filterText'" + + if ([string]::IsNullOrWhiteSpace($filterText)) { + # If filter is empty, remove any existing filter + $collectionView.Filter = $null + } + else { + # Apply a filter predicate. This is the correct WPF way to filter. + $collectionView.Filter = { + param($item) + # $item is the PSCustomObject from the list + return $item.Model -like "*$filterText*" + } + } + + # The view will automatically refresh. No need to call .Refresh() explicitly for filtering. + $filteredCount = 0 + if ($null -ne $collectionView) { + foreach ($item in $collectionView) { $filteredCount++ } + } + WriteLog "Filter applied. View now contains $filteredCount models." } # Function to save selected driver models to a JSON file @@ -243,7 +255,7 @@ function Import-DriversJson { $ofd = New-Object System.Windows.Forms.OpenFileDialog $ofd.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*" $ofd.Title = "Import Drivers" - $ofd.InitialDirectory = $FFUDevelopmentPath + $ofd.InitialDirectory = Join-Path -Path $State.FFUDevelopmentPath -ChildPath "Drivers" if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { try { @@ -362,16 +374,29 @@ function Import-DriversJson { Arch = "" DownloadStatus = "Imported" } - $State.Data.allDriverModels += $newDriverModel + $State.Data.allDriverModels.Add($newDriverModel) $newModelsAdded++ WriteLog "Import-DriversJson: Added new model '$($newDriverModel.Make) - $($newDriverModel.Model)' from import. ID: $($newDriverModel.Id), Link: $($newDriverModel.Link)" } } } - $State.Data.allDriverModels = $State.Data.allDriverModels | Sort-Object @{Expression = { $_.IsSelected }; Descending = $true }, Make, Model + # Sort the full list of models + $sortedModels = $State.Data.allDriverModels | Sort-Object @{Expression = { $_.IsSelected }; Descending = $true }, Make, Model - Search-DriverModels -filterText $State.Controls.txtModelFilter.Text -State $script:uiState + # Create a new list from the sorted results and assign it to the state. + # This prevents the "ItemsControl inconsistent" error by replacing the source instead of modifying it. + $newList = [System.Collections.Generic.List[PSCustomObject]]::new() + if ($null -ne $sortedModels) { + foreach ($model in @($sortedModels)) { + $newList.Add($model) + } + } + $State.Data.allDriverModels = $newList + + # Update the UI and apply any existing filter + $State.Controls.lstDriverModels.ItemsSource = $State.Data.allDriverModels + Search-DriverModels -filterText $State.Controls.txtModelFilter.Text -State $State $message = "Driver import complete.`nNew models added: $newModelsAdded`nExisting models updated: $existingModelsUpdated" [System.Windows.MessageBox]::Show($message, "Import Successful", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information) diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 index 49c417b..740ab0b 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 @@ -501,8 +501,8 @@ function Register-EventHandlers { $window.Cursor = [System.Windows.Input.Cursors]::Wait $eventSource.IsEnabled = $false try { - # Get previously selected models from the master list ($localState.Data.allDriverModels) - $previouslySelectedModels = @($localState.Data.allDriverModels | Where-Object { $_.IsSelected }) + # Get ALL previously selected models to preserve them, regardless of make. + $allPreviouslySelectedModels = @($localState.Data.allDriverModels | Where-Object { $_.IsSelected }) # Get newly fetched models for the current make $newlyFetchedStandardizedModels = Get-ModelsForMake -SelectedMake $selectedMake -State $localState @@ -510,28 +510,37 @@ function Register-EventHandlers { $combinedModelsList = [System.Collections.Generic.List[PSCustomObject]]::new() $modelIdentifiersInCombinedList = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) - # Add previously selected models first - foreach ($item in $previouslySelectedModels) { + # Add all previously selected models first to preserve their 'IsSelected' state. + foreach ($item in $allPreviouslySelectedModels) { $combinedModelsList.Add($item) $modelIdentifiersInCombinedList.Add("$($item.Make)::$($item.Model)") | Out-Null } - # Add newly fetched models if they are not already in the list + # Add newly fetched models, but only if they are not already in the list. + # This prevents overwriting a selected model with an unselected one. $addedNewCount = 0 foreach ($item in $newlyFetchedStandardizedModels) { - if (-not $modelIdentifiersInCombinedList.Contains("$($item.Make)::$($item.Model)")) { + if ($modelIdentifiersInCombinedList.Add("$($item.Make)::$($item.Model)")) { $combinedModelsList.Add($item) - $modelIdentifiersInCombinedList.Add("$($item.Make)::$($item.Model)") | Out-Null $addedNewCount++ } } - # Sort the combined list and update the master list while preserving its List<> type + # Sort the combined list $sortedModels = $combinedModelsList | Sort-Object @{Expression = { $_.IsSelected }; Descending = $true }, Make, Model - $localState.Data.allDriverModels.Clear() - $sortedModels.ForEach({ $localState.Data.allDriverModels.Add($_) }) - # Update the UI + # Create a new list object from the sorted results. This is safer than modifying the existing list + # that the UI is bound to, which can cause inconsistency errors. + $newList = [System.Collections.Generic.List[PSCustomObject]]::new() + if ($null -ne $sortedModels) { + # Sort-Object can return a single object or an array. Ensure it's always treated as a collection. + foreach ($model in @($sortedModels)) { + $newList.Add($model) + } + } + $localState.Data.allDriverModels = $newList + + # Update the UI ItemsSource to point to the new list and clear the filter $localState.Controls.lstDriverModels.ItemsSource = $localState.Data.allDriverModels $localState.Controls.txtModelFilter.Text = "" @@ -540,14 +549,14 @@ function Register-EventHandlers { $localState.Controls.lstDriverModels.Visibility = 'Visible' $localState.Controls.spDriverActionButtons.Visibility = 'Visible' $statusText = "Displaying $($localState.Data.allDriverModels.Count) models." - if ($newlyFetchedStandardizedModels.Count -gt 0 -and $addedNewCount -eq 0 -and $previouslySelectedModels.Count -gt 0) { + if ($newlyFetchedStandardizedModels.Count -gt 0 -and $addedNewCount -eq 0 -and $allPreviouslySelectedModels.Count -gt 0) { $statusText = "Fetched $($newlyFetchedStandardizedModels.Count) models for $selectedMake; all were already in the selected list. Displaying $($localState.Data.allDriverModels.Count) total selected models." } elseif ($addedNewCount -gt 0) { $statusText = "Added $addedNewCount new models for $selectedMake. Displaying $($localState.Data.allDriverModels.Count) total models." } elseif ($newlyFetchedStandardizedModels.Count -eq 0 -and $selectedMake -eq 'Lenovo' ) { - $statusText = if ($previouslySelectedModels.Count -gt 0) { "No new models found for $selectedMake. Displaying $($previouslySelectedModels.Count) previously selected models." } else { "No models found for $selectedMake." } + $statusText = if ($allPreviouslySelectedModels.Count -gt 0) { "No new models found for $selectedMake. Displaying $($allPreviouslySelectedModels.Count) previously selected models." } else { "No models found for $selectedMake." } } elseif ($newlyFetchedStandardizedModels.Count -eq 0) { $statusText = "No new models found for $selectedMake. Displaying $($localState.Data.allDriverModels.Count) previously selected models." @@ -681,6 +690,30 @@ function Register-EventHandlers { [System.Windows.MessageBox]::Show("Driver downloads processed, but some errors occurred. Please check the status column for each driver and the log file for details.", "Download Process Finished with Errors", "OK", "Warning") } }) + + $State.Controls.btnClearDriverList.Add_Click({ + param($eventSource, $routedEventArgs) + $window = [System.Windows.Window]::GetWindow($eventSource) + $localState = $window.Tag + $localState.Controls.lstDriverModels.ItemsSource = $null + $localState.Data.allDriverModels.Clear() + $localState.Controls.txtModelFilter.Text = "" + $localState.Controls.txtStatus.Text = "Driver list cleared." + }) + + $State.Controls.btnSaveDriversJson.Add_Click({ + param($eventSource, $routedEventArgs) + $window = [System.Windows.Window]::GetWindow($eventSource) + $localState = $window.Tag + Save-DriversJson -State $localState + }) + + $State.Controls.btnImportDriversJson.Add_Click({ + param($eventSource, $routedEventArgs) + $window = [System.Windows.Window]::GetWindow($eventSource) + $localState = $window.Tag + Import-DriversJson -State $localState + }) } Export-ModuleMember -Function *