From d7d0cb3a063ff5361da643063e839ed13b71ea26 Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Thu, 19 Jun 2025 12:11:57 -0700 Subject: [PATCH] Refactor: Centralize UI event handlers and abstract list clearing Moves UI event handling logic from the main script into a dedicated core module to improve separation of concerns. Introduces a new shared function, `Clear-ListViewContent`, to consolidate duplicated logic for clearing list views. This generic function handles user confirmation, data source clearing, and UI updates for multiple tabs, significantly reducing code redundancy. --- FFUDevelopment/BuildFFUVM_UI.ps1 | 97 +---------- .../FFUUI.Core/FFUUI.Core.Handlers.psm1 | 152 +++++++++++++++++- .../FFUUI.Core/FFUUI.Core.Shared.psm1 | 76 +++++++++ .../FFUUI.Core/FFUUI.Core.Winget.psm1 | 13 ++ 4 files changed, 238 insertions(+), 100 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM_UI.ps1 b/FFUDevelopment/BuildFFUVM_UI.ps1 index 018821c..546a66f 100644 --- a/FFUDevelopment/BuildFFUVM_UI.ps1 +++ b/FFUDevelopment/BuildFFUVM_UI.ps1 @@ -119,23 +119,7 @@ $script:uiState.Data.versionData | Add-Member -MemberType ScriptMethod -Name Not $script:uiState.Data.versionData | Add-Member -MemberType NoteProperty -Name PropertyChanged -Value $null $script:uiState.Data.versionData | Add-Member -TypeName "System.ComponentModel.INotifyPropertyChanged" -function Update-WingetVersionFields { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [string]$wingetText, - [Parameter(Mandatory)] - [string]$moduleText - ) - # Force UI update on the UI thread - $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Normal, [Action] { - $script:uiState.Controls.txtWingetVersion.Text = $wingetText - $script:uiState.Controls.txtWingetModuleVersion.Text = $moduleText - # Force immediate UI refresh - [System.Windows.Forms.Application]::DoEvents() - }) -} $window.Add_Loaded({ # Pass the state object to all initialization functions @@ -204,75 +188,8 @@ $window.Add_Loaded({ $script:uiState.Controls.wingetSearchPanel.Visibility = 'Collapsed' # Keep search hidden initially - $script:uiState.Controls.btnCheckWingetModule.Add_Click({ - param($buttonSender, $clickEventArgs) - $buttonSender.IsEnabled = $false - $window.Cursor = [System.Windows.Input.Cursors]::Wait - # Initial UI update before calling the core function - Update-WingetVersionFields -wingetText "Checking..." -moduleText "Checking..." - $statusResult = $null - try { - # Call the Core function to perform checks and potential install/update - # Pass the UI update function as a callback - $statusResult = Confirm-WingetInstallationUI -UiUpdateCallback { - param($wingetText, $moduleText) - Update-WingetVersionFields -wingetText $wingetText -moduleText $moduleText - } - # Display appropriate message based on the result - if ($statusResult.Success -and $statusResult.UpdateAttempted) { - # Update attempted and successful - [System.Windows.MessageBox]::Show("Winget components installed/updated successfully.", "Winget Installation Complete", "OK", "Information") - } - elseif (-not $statusResult.Success) { - # Error occurred - $errorMessage = if (-not [string]::IsNullOrWhiteSpace($statusResult.Message)) { $statusResult.Message } else { "An unknown error occurred during Winget check/install." } - [System.Windows.MessageBox]::Show($errorMessage, "Winget Error", "OK", "Error") - } - # If Winget components were already up-to-date ($statusResult.Success -eq $true -and $statusResult.UpdateAttempted -eq $false), no message box is shown. - - # Show search panel only if the final status is successful and checkbox is still checked - if ($statusResult.Success -and $script:uiState.Controls.chkInstallWingetApps.IsChecked) { - $script:uiState.Controls.wingetSearchPanel.Visibility = 'Visible' - } - else { - $script:uiState.Controls.wingetSearchPanel.Visibility = 'Collapsed' # Hide if not successful or unchecked - } - } - catch { - # Catch errors from the Confirm-WingetInstallationUI call itself (less likely now) - Update-WingetVersionFields -wingetText "Error" -moduleText "Error" - [System.Windows.MessageBox]::Show("Unexpected error checking/installing Winget components: $($_.Exception.Message)", "Error", "OK", "Error") - $script:uiState.Controls.wingetSearchPanel.Visibility = 'Collapsed' # Ensure search is hidden on error - } - finally { - $buttonSender.IsEnabled = $true - $window.Cursor = $null - } - }) - $script:uiState.Controls.btnWingetSearch.Add_Click({ - Search-WingetApps -State $script:uiState - }) - $script:uiState.Controls.txtWingetSearch.Add_KeyDown({ - param($eventSrc, $keyEvent) - if ($keyEvent.Key -eq 'Return') { - Search-WingetApps -State $script:uiState; $keyEvent.Handled = $true - } - }) - $script:uiState.Controls.btnSaveWingetList.Add_Click({ - Save-WingetList -State $script:uiState - }) - $script:uiState.Controls.btnImportWingetList.Add_Click({ - Import-WingetList -State $script:uiState - }) - $script:uiState.Controls.btnClearWingetList.Add_Click({ - $script:uiState.Controls.lstWingetResults.ItemsSource = @() # Set ItemsSource to an empty array - $script:uiState.Controls.txtWingetSearch.Text = "" - if ($script:uiState.Controls.txtStatus) { - $script:uiState.Controls.txtStatus.Text = "Cleared all applications from the list" - } - }) $script:uiState.Controls.btnDownloadSelected.Add_Click({ param($buttonSender, $clickEventArgs) @@ -371,10 +288,7 @@ $window.Add_Loaded({ $openDialog.InitialDirectory = $initialDir if ($openDialog.ShowDialog()) { Import-BYOApplicationList -Path $openDialog.FileName -State $script:uiState; Update-CopyButtonState -State $script:uiState } }) - $script:uiState.Controls.btnClearBYOApplications.Add_Click({ - $result = [System.Windows.MessageBox]::Show("Are you sure you want to clear all applications?", "Clear Applications", [System.Windows.MessageBoxButton]::YesNo, [System.Windows.MessageBoxImage]::Question) - if ($result -eq [System.Windows.MessageBoxResult]::Yes) { $script:uiState.Controls.lstApplications.Items.Clear(); Update-CopyButtonState -State $script:uiState } - }) + $script:uiState.Controls.btnCopyBYOApps.Add_Click({ param($buttonSender, $clickEventArgs) @@ -606,14 +520,7 @@ $window.Add_Loaded({ } }) - $script:uiState.Controls.btnClearAppsScriptVariables.Add_Click({ - $script:uiState.Data.appsScriptVariablesDataList.Clear() - $script:uiState.Controls.lstAppsScriptVariables.ItemsSource = $script:uiState.Data.appsScriptVariablesDataList.ToArray() - # Update the header checkbox state - if ($null -ne $script:uiState.Controls.chkSelectAllAppsScriptVariables) { - Update-SelectAllHeaderCheckBoxState -ListView $script:uiState.Controls.lstAppsScriptVariables -HeaderCheckBox $script:uiState.Controls.chkSelectAllAppsScriptVariables - } - }) + # Initial state for chkDefineAppsScriptVariables based on chkInstallApps if ($script:uiState.Controls.chkInstallApps.IsChecked) { diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 index 55fd5c2..d125ca9 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 @@ -285,7 +285,145 @@ function Register-EventHandlers { $localState.Controls.wingetSearchPanel.Visibility = 'Collapsed' }) - # M365 Apps/Office tab Event Handlers + $State.Controls.btnClearBYOApplications.Add_Click({ + param($eventSource, $routedEventArgs) + $window = [System.Windows.Window]::GetWindow($eventSource) + $localState = $window.Tag + + Clear-ListViewContent -State $localState ` + -ListViewControl $localState.Controls.lstApplications ` + -ConfirmationTitle "Clear BYO Applications" ` + -ConfirmationMessage "Are you sure you want to clear all 'Bring Your Own' applications?" ` + -StatusMessage "BYO application list cleared." ` + -PostClearAction { Update-CopyButtonState -State $State } + }) + + $State.Controls.btnClearAppsScriptVariables.Add_Click({ + param($eventSource, $routedEventArgs) + $window = [System.Windows.Window]::GetWindow($eventSource) + $localState = $window.Tag + + $postClearScriptBlock = { + $headerChk = $State.Controls.chkSelectAllAppsScriptVariables + if ($null -ne $headerChk) { + Update-SelectAllHeaderCheckBoxState -ListView $ListViewControl -HeaderCheckBox $headerChk + } + } + + Clear-ListViewContent -State $localState ` + -ListViewControl $localState.Controls.lstAppsScriptVariables ` + -BackingDataList $localState.Data.appsScriptVariablesDataList ` + -ConfirmationTitle "Clear Apps Script Variables" ` + -ConfirmationMessage "Are you sure you want to clear all Apps Script Variables?" ` + -StatusMessage "Apps Script Variables list cleared." ` + -PostClearAction $postClearScriptBlock + }) + + $State.Controls.btnCheckWingetModule.Add_Click({ + param($eventSource, $routedEventArgs) + $window = [System.Windows.Window]::GetWindow($eventSource) + $localState = $window.Tag + $buttonSender = $eventSource + + $buttonSender.IsEnabled = $false + $window.Cursor = [System.Windows.Input.Cursors]::Wait + # Initial UI update before calling the core function + Update-WingetVersionFields -State $localState -wingetText "Checking..." -moduleText "Checking..." + + $statusResult = $null + try { + # Call the Core function to perform checks and potential install/update + # Pass the UI update function as a callback + $statusResult = Confirm-WingetInstallationUI -UiUpdateCallback { + param($wingetText, $moduleText) + Update-WingetVersionFields -State $localState -wingetText $wingetText -moduleText $moduleText + } + + # Display appropriate message based on the result + if ($statusResult.Success -and $statusResult.UpdateAttempted) { + # Update attempted and successful + [System.Windows.MessageBox]::Show("Winget components installed/updated successfully.", "Winget Installation Complete", "OK", "Information") + } + elseif (-not $statusResult.Success) { + # Error occurred + $errorMessage = if (-not [string]::IsNullOrWhiteSpace($statusResult.Message)) { $statusResult.Message } else { "An unknown error occurred during Winget check/install." } + [System.Windows.MessageBox]::Show($errorMessage, "Winget Error", "OK", "Error") + } + # If Winget components were already up-to-date ($statusResult.Success -eq $true -and $statusResult.UpdateAttempted -eq $false), no message box is shown. + + # Show search panel only if the final status is successful and checkbox is still checked + if ($statusResult.Success -and $localState.Controls.chkInstallWingetApps.IsChecked) { + $localState.Controls.wingetSearchPanel.Visibility = 'Visible' + } + else { + $localState.Controls.wingetSearchPanel.Visibility = 'Collapsed' # Hide if not successful or unchecked + } + } + catch { + # Catch errors from the Confirm-WingetInstallationUI call itself (less likely now) + Update-WingetVersionFields -State $localState -wingetText "Error" -moduleText "Error" + [System.Windows.MessageBox]::Show("Unexpected error checking/installing Winget components: $($_.Exception.Message)", "Error", "OK", "Error") + $localState.Controls.wingetSearchPanel.Visibility = 'Collapsed' # Ensure search is hidden on error + } + finally { + $buttonSender.IsEnabled = $true + $window.Cursor = $null + } + }) + + $State.Controls.btnWingetSearch.Add_Click({ + param($eventSource, $routedEventArgs) + $window = [System.Windows.Window]::GetWindow($eventSource) + $localState = $window.Tag + Search-WingetApps -State $localState + }) + + $State.Controls.txtWingetSearch.Add_KeyDown({ + param($eventSource, $keyEvent) + if ($keyEvent.Key -eq 'Return') { + $window = [System.Windows.Window]::GetWindow($eventSource) + $localState = $window.Tag + Search-WingetApps -State $localState + $keyEvent.Handled = $true + } + }) + + $State.Controls.btnSaveWingetList.Add_Click({ + param($eventSource, $routedEventArgs) + $window = [System.Windows.Window]::GetWindow($eventSource) + $localState = $window.Tag + Save-WingetList -State $localState + }) + + $State.Controls.btnImportWingetList.Add_Click({ + param($eventSource, $routedEventArgs) + $window = [System.Windows.Window]::GetWindow($eventSource) + $localState = $window.Tag + Import-WingetList -State $localState + }) + + $State.Controls.btnClearWingetList.Add_Click({ + param($eventSource, $routedEventArgs) + $window = [System.Windows.Window]::GetWindow($eventSource) + $localState = $window.Tag + + $postClearScriptBlock = { + $headerChk = $State.Controls.chkSelectAllWingetResults + if ($null -ne $headerChk) { + Update-SelectAllHeaderCheckBoxState -ListView $ListViewControl -HeaderCheckBox $headerChk + } + } + + Clear-ListViewContent -State $localState ` + -ListViewControl $localState.Controls.lstWingetResults ` + -ConfirmationTitle "Clear Winget List" ` + -ConfirmationMessage "Are you sure you want to clear the Winget application list and search results?" ` + -StatusMessage "Winget application list cleared." ` + -TextBoxesToClear @($localState.Controls.txtWingetSearch) ` + -PostClearAction $postClearScriptBlock + }) + + # M365 Apps/Office tab Event $State.Controls.chkInstallOffice.Add_Checked({ param($eventSource, $routedEventArgs) $window = [System.Windows.Window]::GetWindow($eventSource) @@ -555,10 +693,14 @@ function Register-EventHandlers { 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." + + Clear-ListViewContent -State $localState ` + -ListViewControl $localState.Controls.lstDriverModels ` + -BackingDataList $localState.Data.allDriverModels ` + -ConfirmationTitle "Clear Driver List" ` + -ConfirmationMessage "Are you sure you want to clear the driver list?" ` + -StatusMessage "Driver list cleared." ` + -TextBoxesToClear @($localState.Controls.txtModelFilter) }) $State.Controls.btnSaveDriversJson.Add_Click({ diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1 index 6686e9e..8131748 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1 @@ -740,4 +740,80 @@ function Show-ModernFolderPicker { return [ModernFolderBrowser]::ShowDialog($Title, [IntPtr]::Zero) } +function Clear-ListViewContent { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [psobject]$State, + + [Parameter(Mandatory = $true)] + [System.Windows.Controls.ListView]$ListViewControl, + + [Parameter(Mandatory = $true)] + [string]$ConfirmationTitle, + + [Parameter(Mandatory = $true)] + [string]$ConfirmationMessage, + + [Parameter(Mandatory = $false)] + [System.Collections.IList]$BackingDataList, + + [Parameter(Mandatory = $false)] + [string]$StatusMessage, + + [Parameter(Mandatory = $false)] + [System.Windows.Controls.TextBox[]]$TextBoxesToClear, + + [Parameter(Mandatory = $false)] + [scriptblock]$PostClearAction + ) + + $result = [System.Windows.MessageBox]::Show($ConfirmationMessage, $ConfirmationTitle, [System.Windows.MessageBoxButton]::YesNo, [System.Windows.MessageBoxImage]::Question) + if ($result -ne [System.Windows.MessageBoxResult]::Yes) { + return + } + + try { + # If a backing data list is provided, clear it and rebind. This is the preferred method. + if ($null -ne $BackingDataList) { + $BackingDataList.Clear() + $ListViewControl.ItemsSource = $BackingDataList.ToArray() + } + # If no backing list, determine how to clear the control. + else { + # If ItemsSource is in use, the only valid way to clear is to set it to null or an empty collection. + if ($null -ne $ListViewControl.ItemsSource) { + $ListViewControl.ItemsSource = $null + } + # If ItemsSource is NOT in use, we can safely clear the Items collection directly (for BYO Apps). + elseif ($null -ne $ListViewControl.Items) { + $ListViewControl.Items.Clear() + } + } + + $ListViewControl.Items.Refresh() + + # Clear any specified textboxes + if ($null -ne $TextBoxesToClear) { + foreach ($textBox in $TextBoxesToClear) { + $textBox.Clear() + } + } + + # Update the status message if provided + if (-not [string]::IsNullOrWhiteSpace($StatusMessage) -and $null -ne $State.Controls.txtStatus) { + $State.Controls.txtStatus.Text = $StatusMessage + } + + # Execute any post-clear custom actions. The scriptblock will have access to the $State and $ListViewControl variables from this function's scope. + if ($null -ne $PostClearAction) { + & $PostClearAction + } + } + catch { + WriteLog "Error in Clear-ListViewContent for $($ListViewControl.Name): $($_.Exception.Message)" + [System.Windows.MessageBox]::Show("An error occurred while clearing the list: $($_.Exception.Message)", "Error", "OK", "Error") + } +} + Export-ModuleMember -Function * \ No newline at end of file diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 index 3f82dd6..e447ade 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 @@ -686,6 +686,19 @@ function Start-WingetAppDownloadTask { return $returnObject } +function Update-WingetVersionFields { + param( + [psobject]$State, + [string]$wingetText, + [string]$moduleText + ) + $State.Window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Normal, [Action] { + $State.Controls.txtWingetVersion.Text = $wingetText + $State.Controls.txtWingetModuleVersion.Text = $moduleText + [System.Windows.Forms.Application]::DoEvents() + }) +} + # -------------------------------------------------------------------------- # SECTION: Module Export # --------------------------------------------------------------------------