# Function to update status of a specific item in a ListView function Update-ListViewItemStatus { [CmdletBinding()] param( [Parameter(Mandatory)] [object]$WindowObject, # Changed type to [object] [Parameter(Mandatory)] [object]$ListView, # Changed type to [object] [Parameter(Mandatory)] [string]$IdentifierProperty, [Parameter(Mandatory)] [string]$IdentifierValue, [Parameter(Mandatory)] [string]$StatusProperty, [Parameter(Mandatory)] [string]$StatusValue ) # Ensure we are in UI mode and objects are of correct WPF types if ($WindowObject -is [System.Windows.Window] -and $ListView -is [System.Windows.Controls.ListView]) { # Directly update UI elements as this function is now called on the UI thread try { $itemToUpdate = $ListView.Items | Where-Object { $_.$IdentifierProperty -eq $IdentifierValue } | Select-Object -First 1 if ($null -ne $itemToUpdate) { $itemToUpdate.$StatusProperty = $StatusValue $ListView.Items.Refresh() # Refresh the view to show the change } else { # Log if item not found (for debugging) WriteLog "Update-ListViewItemStatus: Item with $IdentifierProperty '$IdentifierValue' not found in ListView." } } catch { WriteLog "Update-ListViewItemStatus: Error updating ListView: $($_.Exception.Message)" } } # End of if ($WindowObject -is [System.Windows.Window]...) else { # Log if called in non-UI mode or with incorrect types (should not happen if Invoke-ParallelProcessing $isUiMode is correct) WriteLog "Update-ListViewItemStatus: Skipped UI update for $IdentifierValue due to non-UI mode or incorrect object types." } } # Function to update overall progress bar and status text label function Update-OverallProgress { [CmdletBinding()] param( [Parameter(Mandatory)] [object]$WindowObject, # Changed type to [object] [Parameter(Mandatory)] [int]$CompletedCount, [Parameter(Mandatory)] [int]$TotalCount, [Parameter(Mandatory)] [string]$StatusText, [Parameter(Mandatory)] [string]$ProgressBarName, [Parameter(Mandatory)] [string]$StatusLabelName ) # Ensure we are in UI mode and WindowObject is of correct WPF type if ($WindowObject -is [System.Windows.Window]) { # Directly update UI elements as this function is now called on the UI thread try { # Find controls by name using the $WindowObject $pb = $WindowObject.FindName($ProgressBarName) $lbl = $WindowObject.FindName($StatusLabelName) if ($null -eq $pb) { WriteLog "Update-OverallProgress: ProgressBar '$ProgressBarName' not found." return } if ($null -eq $lbl) { WriteLog "Update-OverallProgress: StatusLabel '$StatusLabelName' not found." return } # Update the progress bar if ($TotalCount -gt 0) { $percentComplete = ($CompletedCount / $TotalCount) * 100 $pb.Value = $percentComplete } else { $pb.Value = 0 } # Update the status label $lbl.Text = $StatusText } catch { WriteLog "Update-OverallProgress: Error updating progress: $($_.Exception.Message)" } } # End of if ($WindowObject -is [System.Windows.Window]) else { # Log if called in non-UI mode or with incorrect types WriteLog "Update-OverallProgress: Skipped UI update ($StatusText) due to non-UI mode or incorrect WindowObject type." } } # Helper function to enqueue progress updates to the UI thread function Invoke-ProgressUpdate { param( [Parameter(Mandatory)] [System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue, [Parameter(Mandatory)] [string]$Identifier, [Parameter(Mandatory)] [string]$Status ) $ProgressQueue.Enqueue(@{ Identifier = $Identifier; Status = $Status }) } # Add a function to create a sortable list view function Add-SortableColumn { param( [System.Windows.Controls.GridView]$gridView, [string]$header, [string]$binding, [int]$width = 'Auto', [bool]$isCheckbox = $false, [System.Windows.HorizontalAlignment]$headerHorizontalAlignment = [System.Windows.HorizontalAlignment]::Stretch ) $column = New-Object System.Windows.Controls.GridViewColumn $commonPadding = New-Object System.Windows.Thickness(5, 2, 5, 2) $headerControl = New-Object System.Windows.Controls.GridViewColumnHeader $headerControl.Tag = $binding # Used for sorting if ($isCheckbox) { # Cell template for a column of checkboxes $cellTemplate = New-Object System.Windows.DataTemplate $gridFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Grid]) $checkBoxFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox]) $checkBoxFactory.SetBinding([System.Windows.Controls.CheckBox]::IsCheckedProperty, (New-Object System.Windows.Data.Binding("IsSelected"))) $checkBoxFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Center) $checkBoxFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center) $checkBoxFactory.AddHandler([System.Windows.Controls.CheckBox]::ClickEvent, [System.Windows.RoutedEventHandler] { param($eventSourceLocal, $eventArgsLocal) # Sync logic would be needed here if this column had a header checkbox }) $gridFactory.AppendChild($checkBoxFactory) $cellTemplate.VisualTree = $gridFactory $column.CellTemplate = $cellTemplate } else { # For regular text columns $headerControl.HorizontalContentAlignment = $headerHorizontalAlignment $headerControl.Content = $header $headerTextElementFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock]) $headerTextElementFactory.SetValue([System.Windows.Controls.TextBlock]::TextProperty, $header) $headerTextBlockPadding = New-Object System.Windows.Thickness($commonPadding.Left, $commonPadding.Top, $commonPadding.Right, $commonPadding.Bottom) $headerTextElementFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, $headerTextBlockPadding) $headerTextElementFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center) $headerDataTemplate = New-Object System.Windows.DataTemplate $headerDataTemplate.VisualTree = $headerTextElementFactory $headerControl.ContentTemplate = $headerDataTemplate $cellTemplate = New-Object System.Windows.DataTemplate $textBlockFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock]) $textBlockFactory.SetBinding([System.Windows.Controls.TextBlock]::TextProperty, (New-Object System.Windows.Data.Binding($binding))) # Adjust left padding to 0 for cell text to align with header text $cellTextBlockPadding = New-Object System.Windows.Thickness(0, $commonPadding.Top, $commonPadding.Right, $commonPadding.Bottom) $textBlockFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, $cellTextBlockPadding) $textBlockFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Left) $textBlockFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center) $cellTemplate.VisualTree = $textBlockFactory $column.CellTemplate = $cellTemplate } $column.Header = $headerControl if ($width -ne 'Auto') { $column.Width = $width } $gridView.Columns.Add($column) } # Function to add a selectable GridViewColumn with a "Select All" header CheckBox function Add-SelectableGridViewColumn { [CmdletBinding()] param( [Parameter(Mandatory)] [System.Windows.Controls.ListView]$ListView, [Parameter(Mandatory)] [string]$HeaderCheckBoxScriptVariableName, [Parameter(Mandatory)] [double]$ColumnWidth, [string]$IsSelectedPropertyName = "IsSelected" ) # Ensure the ListView has a GridView if ($null -eq $ListView.View -or -not ($ListView.View -is [System.Windows.Controls.GridView])) { WriteLog "Add-SelectableGridViewColumn: ListView '$($ListView.Name)' does not have a GridView or View is null. Cannot add column." return } $gridView = $ListView.View # Create the "Select All" CheckBox for the header $headerCheckBox = New-Object System.Windows.Controls.CheckBox $headerCheckBox.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Center # MODIFICATION: Store the actual ListView object in the header's Tag $headerTagObject = [PSCustomObject]@{ PropertyName = $IsSelectedPropertyName ListViewControl = $ListView # Store the object itself } $headerCheckBox.Tag = $headerTagObject $headerCheckBox.Add_Checked({ param($senderCheckBoxLocal, $eventArgsCheckedLocal) $tagData = $senderCheckBoxLocal.Tag $localPropertyName = $tagData.PropertyName $actualListView = $tagData.ListViewControl # Get the control directly from the tag $collectionToUpdate = if ($null -ne $actualListView.ItemsSource) { $actualListView.ItemsSource } else { $actualListView.Items } if ($null -ne $collectionToUpdate) { foreach ($item in $collectionToUpdate) { $item.$($localPropertyName) = $true } $actualListView.Items.Refresh() } }) $headerCheckBox.Add_Unchecked({ param($senderCheckBoxLocal, $eventArgsUncheckedLocal) if ($senderCheckBoxLocal.IsChecked -eq $false) { $tagData = $senderCheckBoxLocal.Tag $localPropertyName = $tagData.PropertyName $actualListView = $tagData.ListViewControl # Get the control directly from the tag $collectionToUpdate = if ($null -ne $actualListView.ItemsSource) { $actualListView.ItemsSource } else { $actualListView.Items } if ($null -ne $collectionToUpdate) { foreach ($item in $collectionToUpdate) { $item.$($localPropertyName) = $false } $actualListView.Items.Refresh() } } }) Set-Variable -Name $HeaderCheckBoxScriptVariableName -Value $headerCheckBox -Scope Script -Force WriteLog "Add-SelectableGridViewColumn: Stored header checkbox in script variable '$HeaderCheckBoxScriptVariableName'." $selectableColumn = New-Object System.Windows.Controls.GridViewColumn $selectableColumn.Header = $headerCheckBox $selectableColumn.Width = $ColumnWidth $cellTemplate = New-Object System.Windows.DataTemplate $borderFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Border]) $borderFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch) $borderFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Stretch) $checkBoxFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox]) $checkBoxFactory.SetBinding([System.Windows.Controls.CheckBox]::IsCheckedProperty, (New-Object System.Windows.Data.Binding($IsSelectedPropertyName))) $checkBoxFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Center) $checkBoxFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center) # MODIFICATION: Store the actual ListView object in the item checkbox's Tag $tagObject = [PSCustomObject]@{ HeaderCheckboxName = $HeaderCheckBoxScriptVariableName ListViewControl = $ListView # Store the object itself } $checkBoxFactory.SetValue([System.Windows.FrameworkElement]::TagProperty, $tagObject) $checkBoxFactory.AddHandler([System.Windows.Controls.CheckBox]::ClickEvent, [System.Windows.RoutedEventHandler] { param($eventSourceLocal, $eventArgsLocal) $itemCheckBox = $eventSourceLocal -as [System.Windows.Controls.CheckBox] $tagData = $itemCheckBox.Tag $headerCheckboxNameFromTag = $tagData.HeaderCheckboxName $targetListView = $tagData.ListViewControl # Get the control directly from the tag WriteLog "Add-SelectableGridViewColumn: Item Click. ListView: '$($targetListView.Name)', HeaderChkName: '$headerCheckboxNameFromTag'" $headerChk = Get-Variable -Name $headerCheckboxNameFromTag -Scope Script -ValueOnly -ErrorAction SilentlyContinue 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'." } }) $borderFactory.AppendChild($checkBoxFactory) $cellTemplate.VisualTree = $borderFactory $selectableColumn.CellTemplate = $cellTemplate $gridView.Columns.Insert(0, $selectableColumn) WriteLog "Add-SelectableGridViewColumn: Successfully added selectable column to '$($ListView.Name)'." } # Function to update the IsChecked state of a "Select All" header CheckBox function Update-SelectAllHeaderCheckBoxState { param( [Parameter(Mandatory)] [System.Windows.Controls.ListView]$ListView, [Parameter(Mandatory)] [System.Windows.Controls.CheckBox]$HeaderCheckBox ) $collectionToInspect = $null if ($null -ne $ListView.ItemsSource) { $collectionToInspect = @($ListView.ItemsSource) } elseif ($ListView.HasItems) { # Check if Items collection has items and ItemsSource is null $collectionToInspect = @($ListView.Items) } # If no items to inspect (either ItemsSource was null and Items was empty, or ItemsSource was empty) if ($null -eq $collectionToInspect -or $collectionToInspect.Count -eq 0) { $HeaderCheckBox.IsChecked = $false return } $selectedCount = ($collectionToInspect | Where-Object { $_.IsSelected }).Count $totalItemCount = $collectionToInspect.Count # Get the total count from the collection being inspected if ($totalItemCount -eq 0) { # Handle empty list case specifically $HeaderCheckBox.IsChecked = $false } elseif ($selectedCount -eq $totalItemCount) { $HeaderCheckBox.IsChecked = $true } elseif ($selectedCount -eq 0) { $HeaderCheckBox.IsChecked = $false } else { # Indeterminate state $HeaderCheckBox.IsChecked = $null } } # Function to sort ListView items function Invoke-ListViewSort { param( [System.Windows.Controls.ListView]$listView, [string]$property, [PSCustomObject]$State ) # Toggle sort direction if clicking the same column if ($State.Flags.lastSortProperty -eq $property) { $State.Flags.lastSortAscending = -not $State.Flags.lastSortAscending } else { $State.Flags.lastSortAscending = $true } $State.Flags.lastSortProperty = $property # Get items from ItemsSource or Items collection $currentItemsSource = $listView.ItemsSource $itemsToSort = @() if ($null -ne $currentItemsSource) { $itemsToSort = @($currentItemsSource) } else { $itemsToSort = @($listView.Items) } if ($itemsToSort.Count -eq 0) { return } $selectedItems = @($itemsToSort | Where-Object { $_.IsSelected }) $unselectedItems = @($itemsToSort | Where-Object { -not $_.IsSelected }) # Define the primary sort criterion $primarySortDefinition = @{ Expression = { $val = $_.$property if ($null -eq $val) { '' } else { $val } } Ascending = $State.Flags.lastSortAscending } $sortCriteria = [System.Collections.Generic.List[hashtable]]::new() $sortCriteria.Add($primarySortDefinition) # Determine secondary sort property based on the ListView $secondarySortPropertyName = $null if ($listView.Name -eq 'lstDriverModels') { $secondarySortPropertyName = "Model" } elseif ($listView.Name -eq 'lstWingetResults') { $secondarySortPropertyName = "Name" } elseif ($listView.Name -eq 'lstAppsScriptVariables') { if ($property -eq "Key") { $secondarySortPropertyName = "Value" } elseif ($property -eq "Value") { $secondarySortPropertyName = "Key" } else { # Default secondary sort for IsSelected or other properties $secondarySortPropertyName = "Key" } } if ($null -ne $secondarySortPropertyName -and $property -ne $secondarySortPropertyName) { $itemsHaveSecondaryProperty = $false if ($unselectedItems.Count -gt 0) { if ($null -ne $unselectedItems[0].PSObject.Properties[$secondarySortPropertyName]) { $itemsHaveSecondaryProperty = $true } } elseif ($selectedItems.Count -gt 0) { if ($null -ne $selectedItems[0].PSObject.Properties[$secondarySortPropertyName]) { $itemsHaveSecondaryProperty = $true } } if ($itemsHaveSecondaryProperty) { # Create a scriptblock for the secondary sort expression dynamically $expressionScriptBlock = [scriptblock]::Create("`$_.$secondarySortPropertyName") $secondarySortDefinition = @{ Expression = { $val = Invoke-Command -ScriptBlock $expressionScriptBlock -ArgumentList $_ if ($null -eq $val) { '' } else { $val } } Ascending = $true # Secondary sort always ascending } $sortCriteria.Add($secondarySortDefinition) } } $sortedUnselected = $unselectedItems | Sort-Object -Property $sortCriteria.ToArray() # Ensure $sortedUnselected is not null before attempting to add its range if ($null -eq $sortedUnselected) { $sortedUnselected = @() } # Combine sorted items: selected items first, then sorted unselected items $newSortedList = [System.Collections.Generic.List[object]]::new() $newSortedList.AddRange($selectedItems) $newSortedList.AddRange($sortedUnselected) # Set the new sorted list as the ItemsSource # Try nulling out ItemsSource first to force a more complete refresh $listView.ItemsSource = $null $listView.ItemsSource = $newSortedList.ToArray() } # -------------------------------------------------------------------------- # SECTION: Modern Folder Picker # -------------------------------------------------------------------------- # 1) Define a C# class that uses the correct GUIDs for IFileDialog, IFileOpenDialog, and FileOpenDialog, # while omitting conflicting "GetResults/GetSelectedItems" from IFileDialog. Add-Type -TypeDefinition @" using System; using System.Runtime.InteropServices; public static class ModernFolderBrowser { // Flags for IFileDialog [Flags] private enum FileDialogOptions : uint { OverwritePrompt = 0x00000002, StrictFileTypes = 0x00000004, NoChangeDir = 0x00000008, PickFolders = 0x00000020, ForceFileSystem = 0x00000040, AllNonStorageItems = 0x00000080, NoValidate = 0x00000100, AllowMultiSelect = 0x00000200, PathMustExist = 0x00000800, FileMustExist = 0x00001000, CreatePrompt = 0x00002000, ShareAware = 0x00004000, NoReadOnlyReturn = 0x00008000, NoTestFileCreate = 0x00010000, DontAddToRecent = 0x02000000, ForceShowHidden = 0x10000000 } // IFileDialog (GUID from Windows SDK) // - Omitting GetResults / GetSelectedItems to avoid overshadow. [ComImport] [Guid("42F85136-DB7E-439C-85F1-E4075D135FC8")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IFileDialog { [PreserveSig] int Show(IntPtr parent); void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec); void SetFileTypeIndex(uint iFileType); void GetFileTypeIndex(out uint piFileType); void Advise(IntPtr pfde, out uint pdwCookie); void Unadvise(uint dwCookie); void SetOptions(FileDialogOptions fos); void GetOptions(out FileDialogOptions pfos); void SetDefaultFolder(IShellItem psi); void SetFolder(IShellItem psi); void GetFolder(out IShellItem ppsi); void GetCurrentSelection(out IShellItem ppsi); void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName); void GetFileName(out IntPtr pszName); void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle); void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText); void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel); void GetResult(out IShellItem ppsi); void AddPlace(IShellItem psi, int fdap); void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); void Close(int hr); void SetClientGuid(ref Guid guid); void ClearClientData(); void SetFilter(IntPtr pFilter); // NOTE: We intentionally do NOT define GetResults and GetSelectedItems here, // because they cause overshadow warnings in IFileOpenDialog. } // IFileOpenDialog extends IFileDialog by adding 2 new methods with the same name, // which otherwise cause overshadow warnings. We'll define them only here. [ComImport] [Guid("D57C7288-D4AD-4768-BE02-9D969532D960")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IFileOpenDialog : IFileDialog { // These two come after the parent's vtable: void GetResults(out IntPtr ppenum); void GetSelectedItems(out IntPtr ppsai); } // The coclass for creating an IFileOpenDialog [ComImport] [Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")] private class FileOpenDialog { } // IShellItem [ComImport] [Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IShellItem { void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv); void GetParent(out IShellItem ppsi); void GetDisplayName(uint sigdnName, out IntPtr ppszName); void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs); void Compare(IShellItem psi, uint hint, out int piOrder); } private const uint SIGDN_FILESYSPATH = 0x80058000; public static string ShowDialog(string title, IntPtr parentHandle) { // Create COM dialog instance IFileOpenDialog dialog = (IFileOpenDialog)(new FileOpenDialog()); // Get current options FileDialogOptions opts; dialog.GetOptions(out opts); // Add flags for picking folders opts |= FileDialogOptions.PickFolders | FileDialogOptions.PathMustExist | FileDialogOptions.ForceFileSystem; dialog.SetOptions(opts); // Set title if (!string.IsNullOrEmpty(title)) { dialog.SetTitle(title); } // Show the dialog int hr = dialog.Show(parentHandle); // 0 = S_OK. 1 or 0x800704C7 often means user canceled. Return null if so. if (hr != 0) { if ((uint)hr == 0x800704C7 || hr == 1) { return null; // Canceled } else { Marshal.ThrowExceptionForHR(hr); } } // Retrieve the selection (IShellItem) IShellItem shellItem; dialog.GetResult(out shellItem); if (shellItem == null) return null; // Convert to file system path IntPtr pszPath = IntPtr.Zero; shellItem.GetDisplayName(SIGDN_FILESYSPATH, out pszPath); if (pszPath == IntPtr.Zero) return null; string folderPath = Marshal.PtrToStringAuto(pszPath); Marshal.FreeCoTaskMem(pszPath); return folderPath; } } "@ -Language CSharp # 2) Define a PowerShell function that invokes our C# wrapper function Show-ModernFolderPicker { param( [string]$Title = "Select a folder" ) # For a simple test, pass IntPtr.Zero as the parent window handle return [ModernFolderBrowser]::ShowDialog($Title, [IntPtr]::Zero) } Export-ModuleMember -Function *