mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
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.
This commit is contained in:
@@ -169,19 +169,6 @@ $window.Add_Loaded({
|
|||||||
$script:uiState.Controls.spModelFilterSection.Visibility = 'Collapsed'
|
$script:uiState.Controls.spModelFilterSection.Visibility = 'Collapsed'
|
||||||
$script:uiState.Controls.lstDriverModels.Visibility = 'Collapsed'
|
$script:uiState.Controls.lstDriverModels.Visibility = 'Collapsed'
|
||||||
$script:uiState.Controls.spDriverActionButtons.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)
|
# Office interplay (Keep existing logic)
|
||||||
$script:uiState.Flags.installAppsCheckedByOffice = $false
|
$script:uiState.Flags.installAppsCheckedByOffice = $false
|
||||||
if ($script:uiState.Controls.chkInstallOffice.IsChecked) {
|
if ($script:uiState.Controls.chkInstallOffice.IsChecked) {
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ function Get-MicrosoftDriversModelList {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
WriteLog "Getting Surface driver information from $url"
|
WriteLog "Getting Surface driver information from $url"
|
||||||
WriteLog "Using UserAgent: $UserAgent"
|
|
||||||
WriteLog "Using Headers: $($Headers | Out-String)"
|
|
||||||
$OriginalVerbosePreference = $VerbosePreference
|
$OriginalVerbosePreference = $VerbosePreference
|
||||||
$VerbosePreference = 'SilentlyContinue'
|
$VerbosePreference = 'SilentlyContinue'
|
||||||
# Use passed-in UserAgent and Headers
|
# Use passed-in UserAgent and Headers
|
||||||
@@ -41,7 +39,7 @@ function Get-MicrosoftDriversModelList {
|
|||||||
$cellMatches = [regex]::Matches($rowContent, $cellPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
$cellMatches = [regex]::Matches($rowContent, $cellPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
||||||
|
|
||||||
if ($cellMatches.Count -ge 2) {
|
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()
|
$secondTdContent = $cellMatches[1].Groups[1].Value.Trim()
|
||||||
# $linkPattern = '<a[^>]+href="([^"]+)"[^>]*>'
|
# $linkPattern = '<a[^>]+href="([^"]+)"[^>]*>'
|
||||||
# Change linkPattern to match https://www.microsoft.com/download/details.aspx?id=
|
# Change linkPattern to match https://www.microsoft.com/download/details.aspx?id=
|
||||||
|
|||||||
@@ -124,27 +124,39 @@ function Search-DriverModels {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteLog "Filtering models with text: '$filterText'"
|
# Ensure the ItemsSource is always the master list. This prevents inconsistency.
|
||||||
|
if ($State.Controls.lstDriverModels.ItemsSource -ne $State.Data.allDriverModels) {
|
||||||
# Filter the full list based on the Model property (case-insensitive)
|
$State.Controls.lstDriverModels.ItemsSource = $State.Data.allDriverModels
|
||||||
# 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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 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
|
# 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 = New-Object System.Windows.Forms.OpenFileDialog
|
||||||
$ofd.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*"
|
$ofd.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*"
|
||||||
$ofd.Title = "Import Drivers"
|
$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) {
|
if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
||||||
try {
|
try {
|
||||||
@@ -362,16 +374,29 @@ function Import-DriversJson {
|
|||||||
Arch = ""
|
Arch = ""
|
||||||
DownloadStatus = "Imported"
|
DownloadStatus = "Imported"
|
||||||
}
|
}
|
||||||
$State.Data.allDriverModels += $newDriverModel
|
$State.Data.allDriverModels.Add($newDriverModel)
|
||||||
$newModelsAdded++
|
$newModelsAdded++
|
||||||
WriteLog "Import-DriversJson: Added new model '$($newDriverModel.Make) - $($newDriverModel.Model)' from import. ID: $($newDriverModel.Id), Link: $($newDriverModel.Link)"
|
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"
|
$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)
|
[System.Windows.MessageBox]::Show($message, "Import Successful", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
||||||
|
|||||||
@@ -501,8 +501,8 @@ function Register-EventHandlers {
|
|||||||
$window.Cursor = [System.Windows.Input.Cursors]::Wait
|
$window.Cursor = [System.Windows.Input.Cursors]::Wait
|
||||||
$eventSource.IsEnabled = $false
|
$eventSource.IsEnabled = $false
|
||||||
try {
|
try {
|
||||||
# Get previously selected models from the master list ($localState.Data.allDriverModels)
|
# Get ALL previously selected models to preserve them, regardless of make.
|
||||||
$previouslySelectedModels = @($localState.Data.allDriverModels | Where-Object { $_.IsSelected })
|
$allPreviouslySelectedModels = @($localState.Data.allDriverModels | Where-Object { $_.IsSelected })
|
||||||
|
|
||||||
# Get newly fetched models for the current make
|
# Get newly fetched models for the current make
|
||||||
$newlyFetchedStandardizedModels = Get-ModelsForMake -SelectedMake $selectedMake -State $localState
|
$newlyFetchedStandardizedModels = Get-ModelsForMake -SelectedMake $selectedMake -State $localState
|
||||||
@@ -510,28 +510,37 @@ function Register-EventHandlers {
|
|||||||
$combinedModelsList = [System.Collections.Generic.List[PSCustomObject]]::new()
|
$combinedModelsList = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||||
$modelIdentifiersInCombinedList = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
$modelIdentifiersInCombinedList = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
# Add previously selected models first
|
# Add all previously selected models first to preserve their 'IsSelected' state.
|
||||||
foreach ($item in $previouslySelectedModels) {
|
foreach ($item in $allPreviouslySelectedModels) {
|
||||||
$combinedModelsList.Add($item)
|
$combinedModelsList.Add($item)
|
||||||
$modelIdentifiersInCombinedList.Add("$($item.Make)::$($item.Model)") | Out-Null
|
$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
|
$addedNewCount = 0
|
||||||
foreach ($item in $newlyFetchedStandardizedModels) {
|
foreach ($item in $newlyFetchedStandardizedModels) {
|
||||||
if (-not $modelIdentifiersInCombinedList.Contains("$($item.Make)::$($item.Model)")) {
|
if ($modelIdentifiersInCombinedList.Add("$($item.Make)::$($item.Model)")) {
|
||||||
$combinedModelsList.Add($item)
|
$combinedModelsList.Add($item)
|
||||||
$modelIdentifiersInCombinedList.Add("$($item.Make)::$($item.Model)") | Out-Null
|
|
||||||
$addedNewCount++
|
$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
|
$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.lstDriverModels.ItemsSource = $localState.Data.allDriverModels
|
||||||
$localState.Controls.txtModelFilter.Text = ""
|
$localState.Controls.txtModelFilter.Text = ""
|
||||||
|
|
||||||
@@ -540,14 +549,14 @@ function Register-EventHandlers {
|
|||||||
$localState.Controls.lstDriverModels.Visibility = 'Visible'
|
$localState.Controls.lstDriverModels.Visibility = 'Visible'
|
||||||
$localState.Controls.spDriverActionButtons.Visibility = 'Visible'
|
$localState.Controls.spDriverActionButtons.Visibility = 'Visible'
|
||||||
$statusText = "Displaying $($localState.Data.allDriverModels.Count) models."
|
$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."
|
$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) {
|
elseif ($addedNewCount -gt 0) {
|
||||||
$statusText = "Added $addedNewCount new models for $selectedMake. Displaying $($localState.Data.allDriverModels.Count) total models."
|
$statusText = "Added $addedNewCount new models for $selectedMake. Displaying $($localState.Data.allDriverModels.Count) total models."
|
||||||
}
|
}
|
||||||
elseif ($newlyFetchedStandardizedModels.Count -eq 0 -and $selectedMake -eq 'Lenovo' ) {
|
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) {
|
elseif ($newlyFetchedStandardizedModels.Count -eq 0) {
|
||||||
$statusText = "No new models found for $selectedMake. Displaying $($localState.Data.allDriverModels.Count) previously selected models."
|
$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")
|
[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 *
|
Export-ModuleMember -Function *
|
||||||
|
|||||||
Reference in New Issue
Block a user