private/Controls/New-UiDataGridToolbar.ps1
|
function New-UiDataGridToolbar { <# .SYNOPSIS Builds the filter/copy/export/column picker toolbar that sits above an embedded DataGrid. #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Windows.Controls.DataGrid]$DataGrid, [Parameter(Mandatory)] [hashtable]$Colors, [switch]$NoFilter, [switch]$NoExport, [switch]$NoCopy, [switch]$NoColumnPicker, [string[]]$AllProperties, [string[]]$DefaultProperties, [string[]]$PopulatedProperties, [scriptblock]$ItemsProvider ) $toolbar = [System.Windows.Controls.Grid]@{ Margin = [System.Windows.Thickness]::new(0, 0, 0, 6) } $colIdx = 0 $iconColIdx = -1 $filterColIdx = -1 $countColIdx = -1 $colsBtnColIdx = -1 $copyBtnColIdx = -1 $exportBtnColIdx = -1 if (!$NoFilter) { $iconCol = [System.Windows.Controls.ColumnDefinition]::new() $iconCol.Width = [System.Windows.GridLength]::Auto [void]$toolbar.ColumnDefinitions.Add($iconCol) $iconColIdx = $colIdx; $colIdx++ $filterCol = [System.Windows.Controls.ColumnDefinition]::new() $filterCol.Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) [void]$toolbar.ColumnDefinitions.Add($filterCol) $filterColIdx = $colIdx; $colIdx++ } else { $spacerCol = [System.Windows.Controls.ColumnDefinition]::new() $spacerCol.Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) [void]$toolbar.ColumnDefinitions.Add($spacerCol) $colIdx++ } # Row count sits between the filter and the icon buttons. Autowidth so the filter keeps the star slot and the count never encroaches the buttons. $countCol = [System.Windows.Controls.ColumnDefinition]::new() $countCol.Width = [System.Windows.GridLength]::Auto [void]$toolbar.ColumnDefinitions.Add($countCol) $countColIdx = $colIdx; $colIdx++ if (!$NoColumnPicker) { $cbCol = [System.Windows.Controls.ColumnDefinition]::new() $cbCol.Width = [System.Windows.GridLength]::Auto [void]$toolbar.ColumnDefinitions.Add($cbCol) $colsBtnColIdx = $colIdx; $colIdx++ } if (!$NoCopy) { $cpCol = [System.Windows.Controls.ColumnDefinition]::new() $cpCol.Width = [System.Windows.GridLength]::Auto [void]$toolbar.ColumnDefinitions.Add($cpCol) $copyBtnColIdx = $colIdx; $colIdx++ } if (!$NoExport) { $exCol = [System.Windows.Controls.ColumnDefinition]::new() $exCol.Width = [System.Windows.GridLength]::Auto [void]$toolbar.ColumnDefinitions.Add($exCol) $exportBtnColIdx = $colIdx; $colIdx++ } $filterBox = $null if (!$NoFilter) { $searchIcon = [System.Windows.Controls.TextBlock]@{ Text = [PsUi.ModuleContext]::GetIcon('Search') FontFamily = [PsUi.ModuleContext]::ActiveIconFontFamily FontSize = 14 VerticalAlignment = 'Center' Foreground = ConvertTo-UiBrush $Colors.SecondaryText Margin = [System.Windows.Thickness]::new(0, 0, 6, 0) Tag = 'SecondaryTextBrush' } [PsUi.ThemeEngine]::RegisterElement($searchIcon) [System.Windows.Controls.Grid]::SetColumn($searchIcon, $iconColIdx) [void]$toolbar.Children.Add($searchIcon) $filterResult = New-FilterBoxWithClear -Width 0 -Height 26 -AdditionalTagData @{ Grid = $DataGrid Timer = $null } # New-FilterBoxWithClear sets a fixed width - undo it for the Grid column stretch. $filterResult.Container.Width = [double]::NaN $filterResult.Container.HorizontalAlignment = 'Stretch' # Push the trailing icon buttons off the filter's clear icon. $filterResult.Container.Margin = [System.Windows.Thickness]::new(0, 0, 6, 0) [System.Windows.Controls.Grid]::SetColumn($filterResult.Container, $filterColIdx) [void]$toolbar.Children.Add($filterResult.Container) $filterBox = $filterResult.TextBox } # "12 of 142" when filtered, "142" otherwise. Not all views expose Count. $countTextBlock = [System.Windows.Controls.TextBlock]@{ VerticalAlignment = 'Center' Margin = [System.Windows.Thickness]::new(8, 0, 8, 0) FontSize = 12 Tag = 'SecondaryTextBrush' } $countTextBlock.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'SecondaryTextBrush') [PsUi.ThemeEngine]::RegisterElement($countTextBlock) [System.Windows.Controls.Grid]::SetColumn($countTextBlock, $countColIdx) [void]$toolbar.Children.Add($countTextBlock) $countGridRef = $DataGrid $countTbRef = $countTextBlock $countProviderRef = $ItemsProvider $updateCount = { try { if ($null -eq $countTbRef) { return } $coll = if ($countProviderRef) { & $countProviderRef } else { $null } $total = if ($coll -is [System.Collections.ICollection]) { $coll.Count } elseif ($coll) { @($coll).Count } else { 0 } $view = $countGridRef.ItemsSource -as [System.ComponentModel.ICollectionView] $shown = $total if ($view) { $listView = $view -as [System.Windows.Data.ListCollectionView] if ($listView) { $shown = $listView.Count } else { $shown = 0 foreach ($entry in $view) { $shown++ } } } $countTbRef.Text = if ($shown -eq $total) { [string]$total } else { "$shown of $total" } } catch { Write-Debug "Row count update failed: $_" } }.GetNewClosure() $countTextBlock.Add_Loaded({ & $updateCount }.GetNewClosure()) # CollectionChanged needs to follow ItemsSource swaps. Old view stays subscribed and the new view's mutations get ignored otherwise. Mirrors the pattern in Add-UiDataGridEmptyOverlay. $countSubState = @{ View = $null } $countHandler = { & $updateCount }.GetNewClosure() $countAttach = { if ($countSubState.View) { try { $countSubState.View.remove_CollectionChanged($countHandler) } catch { } $countSubState.View = $null } $view = $DataGrid.ItemsSource -as [System.ComponentModel.ICollectionView] if (!$view) { return } try { $view.add_CollectionChanged($countHandler) $countSubState.View = $view } catch { Write-Debug "Row count subscribe failed: $_" } & $updateCount }.GetNewClosure() # DependencyPropertyDescriptor.AddValueChanged fires on ItemsSource swap. Leak prone (static dictionary, holds a strong reference to the DataGrid) so hook subscribe + Closed cleanup together inside Loaded. $itemsSourcePropDesc = [System.ComponentModel.DependencyPropertyDescriptor]::FromProperty( [System.Windows.Controls.ItemsControl]::ItemsSourceProperty, [System.Windows.Controls.DataGrid]) $itemsSourceChangedHandler = [System.EventHandler]{ param($sender, $eventArgs) & $countAttach }.GetNewClosure() # Window.Closed beats Unloaded - tab virtualization unloads on tab switch and would freeze the count. $onWindowClosed = { try { $itemsSourcePropDesc.RemoveValueChanged($DataGrid, $itemsSourceChangedHandler) } catch { Write-Debug "Row count DPD detach failed: $_" } if ($countSubState.View) { try { $countSubState.View.remove_CollectionChanged($countHandler) } catch { } $countSubState.View = $null } }.GetNewClosure() $countHooked = @{ Value = $false } $DataGrid.Add_Loaded({ & $countAttach if ($countHooked.Value) { return } $window = [System.Windows.Window]::GetWindow($this) if (!$window) { return } $countHooked.Value = $true $itemsSourcePropDesc.AddValueChanged($DataGrid, $itemsSourceChangedHandler) $window.Add_Closed($onWindowClosed) }.GetNewClosure()) $colButton = $null if (!$NoColumnPicker) { # Pull model provider so the picker reflects whatever columns the grid currently has, including columns that arrive later via the seed handler on initially empty -ItemsSource grids. Falls back to grid.Columns headers when Tag isn't a hashtable yet. $gridForPicker = $DataGrid $allFallback = $AllProperties $defFallback = $DefaultProperties $popFallback = $PopulatedProperties $propertiesProvider = { $tag = $gridForPicker.Tag $all = if ($tag -is [hashtable] -and $tag.AllProperties) { @($tag.AllProperties) } elseif ($allFallback) { @($allFallback) } else { @($gridForPicker.Columns | ForEach-Object { [string]$_.Header } | Where-Object { $_ }) } $def = if ($tag -is [hashtable] -and $tag.DefaultProperties) { @($tag.DefaultProperties) } elseif ($defFallback) { @($defFallback) } else { @() } $pop = if ($tag -is [hashtable] -and $tag.PopulatedProperties) { @($tag.PopulatedProperties) } elseif ($popFallback) { @($popFallback) } else { @() } @{ All = $all; Default = $def; Populated = $pop } }.GetNewClosure() $popupParams = @{ DataGrid = $DataGrid PropertiesProvider = $propertiesProvider } if ($ItemsProvider) { $popupParams.ItemsProvider = $ItemsProvider } $popupResult = New-ColumnVisibilityPopup @popupParams $colButton = $popupResult.Button [System.Windows.Controls.Grid]::SetColumn($colButton, $colsBtnColIdx) [void]$toolbar.Children.Add($colButton) } # Same copy/export helpers the context menu uses - one place strips the _* columns. $copyButton = $null if (!$NoCopy) { $copyButton = New-UiDataGridToolbarIconButton -Icon 'Copy' -ToolTip 'Copy selected rows to clipboard' [System.Windows.Controls.Grid]::SetColumn($copyButton, $copyBtnColIdx) [void]$toolbar.Children.Add($copyButton) $gridRef = $DataGrid $copyButton.Add_Click({ Invoke-UiDataGridCopyToClipboard -DataGrid $gridRef }.GetNewClosure()) # Copy with zero selection does nothing, silently. Disable until the user picks a row. $copyButton.IsEnabled = ($DataGrid.SelectedItems.Count -gt 0) $copyBtnRef = $copyButton $DataGrid.add_SelectionChanged({ $copyBtnRef.IsEnabled = ($gridRef.SelectedItems.Count -gt 0) }.GetNewClosure()) } # Export stays enabled with nothing selected. $exportButton = $null if (!$NoExport) { $exportButton = New-UiDataGridToolbarIconButton -Icon 'SaveLocal' -ToolTip 'Export to CSV' [System.Windows.Controls.Grid]::SetColumn($exportButton, $exportBtnColIdx) [void]$toolbar.Children.Add($exportButton) $gridRef = $DataGrid $exportButton.Add_Click({ Invoke-UiDataGridExportToCsv -DataGrid $gridRef }.GetNewClosure()) } return @{ Container = $toolbar FilterBox = $filterBox ColumnButton = $colButtona CopyButton = $copyButton ExportButton = $exportButton } } |