Out-SexyGridview.psm1
function Out-SexyGridView { <# .SYNOPSIS Displays objects in a modern, interactive grid view with advanced navigation and search capabilities. .DESCRIPTION Out-SexyGridView is an enhanced replacement for PowerShell's built-in Out-GridView cmdlet. It provides a modern WPF-based interface with advanced features including: - Real-time search functionality across all object properties - Recursive navigation through complex nested objects via clickable buttons - Smart detection and handling of simple vs complex properties - Support for arrays with automatic Index/Value column creation - Dark and Light theme support - Data summary panel with object type statistics - Two view modes: Default (optimized columns) and Full (all properties) The cmdlet automatically detects complex properties (objects, hashtables, arrays) and displays them as "View Object" buttons that open new grid windows for drilling down into nested data structures. .PARAMETER InputObject Specifies the objects to display in the grid view. This parameter accepts input from the pipeline. Supports any .NET object, PowerShell custom objects, hashtables, and arrays. .PARAMETER Title Specifies the title for the grid view window. By default, includes the command invocation unless RemoveTitleSuffix is used. Default value: "Sexy Grid View" .PARAMETER RemoveTitleSuffix When specified, removes the command invocation from the window title, showing only the base title. .PARAMETER ViewMode Specifies how properties are displayed in the grid: - Default: Shows optimized columns based on PowerShell's default property selection - Full: Shows all available properties as columns Default value: "Default" .PARAMETER Theme Specifies the visual theme for the grid view: - Dark: Dark background with light text (optimized for low-light environments) - Light: Light background with dark text (traditional Windows appearance) Default value: "Dark" .INPUTS System.Object You can pipe any objects to Out-SexyGridView. .OUTPUTS None Out-SexyGridView does not generate any output. It displays objects in an interactive window. .EXAMPLE Get-Process | Out-SexyGridView Displays all running processes in the sexy grid view with default settings. .EXAMPLE Get-Service | Out-SexyGridView -Title "System Services" -Theme Light Displays all services with a custom title using the light theme. .EXAMPLE Get-ChildItem C:\Windows\System32\*.exe | Out-SexyGridView -ViewMode Full Shows all executable files in System32 with all available properties displayed as columns. .EXAMPLE $servers = @( [PSCustomObject]@{ Name = "Server01" Config = [PSCustomObject]@{ CPU = "Intel Xeon" Network = @{IP = "192.168.1.10"; Ports = @(80, 443)} } } ) $servers | Out-SexyGridView -Title "Server Infrastructure" Demonstrates complex object navigation. The Config property will show as a "View Object" button that opens a new window with the nested configuration details. .EXAMPLE @("Production", "Staging", "Development") | Out-SexyGridView -Title "Environments" Shows how arrays of simple types are handled with automatic Index/Value columns. .EXAMPLE Get-WmiObject Win32_ComputerSystem | Out-SexyGridView -RemoveTitleSuffix -Theme Light Displays computer system information with a clean title (no command suffix) in light theme. .NOTES Requirements: - PowerShell 5.1 or later - .NET Framework 4.7.2 or later - Windows with WPF support The cmdlet uses WPF (Windows Presentation Foundation) for the user interface and requires the PresentationFramework, PresentationCore, and WindowsBase assemblies. For optimal performance with large datasets, consider filtering data before piping to Out-SexyGridView. .LINK Out-GridView .LINK https://github.com/barto90/sexygridview #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object]$InputObject, [Parameter(Mandatory = $false)] [string]$Title = 'Sexy Grid View', [Parameter(Mandatory = $false)] [switch]$RemoveTitleSuffix, [Parameter(Mandatory = $false)] [ValidateSet('Default', 'Full')] [string]$ViewMode = 'Default', [Parameter(Mandatory = $false)] [string]$Theme = 'Dark' ) begin { $allObjects = @() if(-not $RemoveTitleSuffix) { $commandInvocation = $MyInvocation.Line $Title = "$($Title) - $($commandInvocation)" } } process { if ($null -ne $InputObject) { $allObjects += $InputObject } } end { if ($allObjects.Count -gt 0) { Out-SexyGridViewForm -Title $Title -AllObjects $allObjects -ViewMode $ViewMode -Theme $Theme } else { Write-Warning "No objects to display" return; } } } function Get-Theme { [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [ValidateSet('Dark', 'Light')] [string]$Theme = 'Dark' ) $themes = @{ Dark = @{ Window = @{ Width = 1000; Height = 600 } DataGrid = @{ Background = 'White'; Foreground = 'Black'; BorderBrush = '#464647' } Background = '#2D2D30' Foreground = '#F1F1F1' HeaderBackground = '#3F3F46' SelectionBackground = '#007ACC' BorderBrush = '#464647' GridLinesBrush = '#404040' TextForeground = '#F1F1F1' } Light = @{ Window = @{ Width = 1000; Height = 600 } DataGrid = @{ Background = 'White'; Foreground = 'Black'; BorderBrush = '#464647' } Background = '#FFFFFF' Foreground = '#1E1E1E' HeaderBackground = '#F3F3F3' SelectionBackground = '#0078D4' BorderBrush = '#D1D1D1' GridLinesBrush = '#E0E0E0' TextForeground = '#1E1E1E' } } return $themes[$Theme] } function Out-SexyGridViewForm { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object]$AllObjects, [Parameter(Mandatory = $false)] [string]$Title, [Parameter(Mandatory = $false)] [string]$ViewMode, [Parameter(Mandatory = $false)] [string]$Theme ) $requiredAssemblies = @('PresentationFramework', 'PresentationCore', 'WindowsBase') foreach ($assembly in $requiredAssemblies) { Add-Type -AssemblyName $assembly -ErrorAction Stop } $themeConfig = Get-Theme -Theme $Theme if($null -eq $themeConfig) { Write-Error "Failed to get theme" return } $window = New-MainWindow -Title $Title -Theme $themeConfig $mainGrid = New-MainGridLayout $dataGrid = New-DataGrid -Theme $themeConfig -AllObjects $AllObjects -ViewMode $ViewMode $searchBar = New-SearchBar -Theme $themeConfig -DataGrid $dataGrid $dataSummary = New-DataSummaryPanel -Theme $themeConfig -DataGrid $dataGrid [System.Windows.Controls.Grid]::SetRow($searchBar, 0) [System.Windows.Controls.Grid]::SetRow($dataSummary, 1) [System.Windows.Controls.Grid]::SetRow($dataGrid, 2) $mainGrid.Children.Add($searchBar) | Out-Null $mainGrid.Children.Add($dataSummary) | Out-Null $mainGrid.Children.Add($dataGrid) | Out-Null $window.Content = $mainGrid $window.ShowDialog() | Out-Null $window.Activate() | Out-Null } function New-MainWindow { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$Title, [Parameter(Mandatory = $true)] [hashtable]$Theme ) $window = New-Object System.Windows.Window $window.Title = $Title $window.Width = $Theme.Window.Width $window.Height = $Theme.Window.Height $window.WindowStartupLocation = 'CenterScreen' $window.Background = $Theme.Background return $window; } function New-MainGridLayout { $grid = New-Object System.Windows.Controls.Grid $searchRow = New-Object System.Windows.Controls.RowDefinition $searchRow.Height = 'Auto' $grid.RowDefinitions.Add($searchRow) | Out-Null $summaryRow = New-Object System.Windows.Controls.RowDefinition $summaryRow.Height = 'Auto' $grid.RowDefinitions.Add($summaryRow) | Out-Null $dataGridRow = New-Object System.Windows.Controls.RowDefinition $dataGridRow.Height = '*' $grid.RowDefinitions.Add($dataGridRow) | Out-Null return $grid } function New-SearchBar { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [hashtable]$Theme, [Parameter(Mandatory = $true)] [System.Windows.Controls.DataGrid]$DataGrid ) $mainStackPanel = New-Object System.Windows.Controls.StackPanel $mainStackPanel.Margin = '5' $searchBox = New-Object System.Windows.Controls.TextBox $searchBox.HorizontalAlignment = 'Stretch' $searchBox.Margin = '0,0,0,5' $searchBox.Background = $Theme.Background $searchBox.Foreground = $Theme.TextForeground $searchBox.BorderBrush = $Theme.BorderBrush $searchBox.Padding = '5,2' $searchBox.FontSize = 12 $searchBox.Text = 'Search...' $searchBox.Foreground = [System.Windows.Media.Brushes]::LightGray $resultsText = New-Object System.Windows.Controls.TextBlock $resultsText.Margin = '0,5,0,0' $resultsText.Foreground = $Theme.Foreground $resultsText.FontSize = 11 $resultsText.Text = "Total Items: $($DataGrid.Items.Count)" $script:resultsTextBlock = $resultsText $searchBox.Add_GotFocus({ if ($this.Text -eq 'Search...') { $this.Text = '' $this.Foreground = [System.Windows.Media.Brushes]::LightGray } }) $searchBox.Add_LostFocus({ if ($this.Text -eq '') { $this.Text = 'Search...' $this.Foreground = [System.Windows.Media.Brushes]::LightGray } }) $searchBox.Add_TextChanged({ param($sender, $e) $searchText = $sender.Text.ToLower() if ($searchText -eq "search...") { $searchText = '' } $view = [System.Windows.Data.CollectionViewSource]::GetDefaultView($DataGrid.ItemsSource) if ($view) { $view.Filter = { param($item) if ([string]::IsNullOrEmpty($searchText)) { return $true } if ($item -is [hashtable]) { foreach ($value in $item.Values) { if ($value -ne $null -and $value.ToString().ToLower().Contains($searchText)) { return $true } } return $false } else { $properties = $item.PSObject.Properties foreach ($prop in $properties) { if ($prop.Value -ne $null -and $prop.Value.ToString().ToLower().Contains($searchText)) { return $true } } return $false } } $totalItems = $DataGrid.Items.Count $filteredItems = ($view | Where-Object { $_ -ne $null }).Count if ([string]::IsNullOrEmpty($searchText)) { $script:resultsTextBlock.Text = "Total Items: $totalItems" } else { $script:resultsTextBlock.Text = "Showing $filteredItems of $totalItems items" } if ($script:summaryTextBlock) { $typeCounts = @{} $totalCount = 0 foreach ($item in $DataGrid.ItemsSource) { $typeName = $item.GetType().Name $totalCount++ if ($typeCounts.ContainsKey($typeName)) { $typeCounts[$typeName]++ } else { $typeCounts[$typeName] = 1 } } $summary = "Total Objects: $totalCount`n`n" $sortedTypes = $typeCounts.GetEnumerator() | Sort-Object Value -Descending foreach ($type in $sortedTypes) { $percentage = [math]::Round(($type.Value / $totalCount) * 100, 1) $summary += "$($type.Key.PadRight(20)) : $($type.Value.ToString().PadLeft(6)) ($percentage%)`n" } $script:summaryTextBlock.Text = $summary } } }) $mainStackPanel.Children.Add($searchBox) | Out-Null $mainStackPanel.Children.Add($resultsText) | Out-Null return $mainStackPanel } function Get-CollectionProperties { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object]$AllObjects ) $allProperties = @() $firstObject = $AllObjects[0] if ($firstObject -is [hashtable]) { $allProperties = $firstObject.Keys } else { foreach ($object in $AllObjects) { $tableOutput = $object | Format-Table | Out-String $lines = $tableOutput -split "`n" $headerLine = $null for ($i = 0; $i -lt ($lines.Length - 1); $i++) { $currentLine = $lines[$i].Trim() $nextLine = $lines[$i + 1].Trim() if ($currentLine -ne '' -and $nextLine -match '^[-\s]+$') { $headerLine = $currentLine break } } if ($headerLine) { $defaultProperties = $headerLine -split '\s+' | Where-Object { $_ -ne '' } foreach ($property in $defaultProperties) { if(-not $allProperties.Contains($property)) { $allProperties += $property } } } } } return $allProperties } function New-DataGrid { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [hashtable]$Theme, [Parameter(Mandatory = $true)] [object]$AllObjects, [Parameter(Mandatory = $false)] [string]$ViewMode ) $dataGrid = New-Object System.Windows.Controls.DataGrid $dataGrid.AutoGenerateColumns = $false if($ViewMode -eq 'Full') { $dataGrid.AutoGenerateColumns = $true $dataGrid.ItemsSource = $AllObjects } else { $firstItem = $AllObjects[0] $isSimpleArray = $false if ($AllObjects.GetType().IsArray -and $firstItem -ne $null) { $simpleTypes = @('String', 'Int32', 'Int64', 'Double', 'Boolean', 'DateTime', 'Decimal', 'Single', 'Byte') if ($firstItem.GetType().Name -in $simpleTypes) { $isSimpleArray = $true } } if ($isSimpleArray) { $convertedObjects = @() for ($i = 0; $i -lt $AllObjects.Count; $i++) { $convertedObjects += [PSCustomObject]@{ Index = $i Value = $AllObjects[$i] } } $dataGrid.ItemsSource = $convertedObjects $indexColumn = New-Object System.Windows.Controls.DataGridTextColumn $indexColumn.Header = "Index" $indexColumn.Binding = New-Object System.Windows.Data.Binding "Index" $dataGrid.Columns.Add($indexColumn) | Out-Null $valueColumn = New-Object System.Windows.Controls.DataGridTextColumn $valueColumn.Header = "Value" $valueColumn.Binding = New-Object System.Windows.Data.Binding "Value" $dataGrid.Columns.Add($valueColumn) | Out-Null } else { $collectionProperties = Get-CollectionProperties -AllObjects $AllObjects foreach ($propertyName in $collectionProperties) { $sampleCount = 10 $isComplexProperty = $false for ($i = 0; $i -lt $sampleCount; $i++) { $sampleValue = $AllObjects[$i].$propertyName if ($null -ne $sampleValue) { $valueType = $sampleValue.GetType() $simpleTypes = @('String', 'Int32', 'Int64', 'Double', 'Boolean', 'DateTime', 'Decimal', 'Single', 'Byte') if ($valueType.Name -notin $simpleTypes -and ($sampleValue -is [PSObject] -or $sampleValue -is [Hashtable] -or $valueType.IsArray -or $valueType.IsClass -and $valueType.Name -ne 'String')) { $isComplexProperty = $true break } } } if($isComplexProperty) { $templateColumn = New-Object System.Windows.Controls.DataGridTemplateColumn $templateColumn.Header = $propertyName $templateColumn.Width = 120 $dataTemplate = New-Object System.Windows.DataTemplate $buttonFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Button]) $buttonFactory.SetValue([System.Windows.Controls.Button]::ContentProperty, "View Object") $buttonFactory.SetValue([System.Windows.Controls.Button]::BackgroundProperty, [System.Windows.Media.Brushes]::Transparent) $buttonFactory.SetValue([System.Windows.Controls.Button]::ForegroundProperty, [System.Windows.Media.Brushes]::Blue) $buttonFactory.SetValue([System.Windows.Controls.Button]::CursorProperty, [System.Windows.Input.Cursors]::Hand) $binding = New-Object System.Windows.Data.Binding $propertyName $buttonFactory.SetBinding([System.Windows.Controls.Button]::TagProperty, $binding) $clickHandler = [System.Windows.RoutedEventHandler] { param($sender, $e) $propertyValue = $sender.Tag if ($null -ne $propertyValue) { $propertyValue | Out-SexyGridView -Title "Object Details: $propertyName" -Theme $Theme } } $buttonFactory.AddHandler([System.Windows.Controls.Button]::ClickEvent, $clickHandler) $dataTemplate.VisualTree = $buttonFactory $templateColumn.CellTemplate = $dataTemplate $dataGrid.Columns.Add($templateColumn) | Out-Null } else { $column = New-Object System.Windows.Controls.DataGridTextColumn $column.Header = $propertyName $column.Binding = New-Object System.Windows.Data.Binding $propertyName $dataGrid.Columns.Add($column) | Out-Null } } $dataGrid.ItemsSource = $AllObjects } } $dataGrid.Background = $Theme.DataGrid.Background $dataGrid.Foreground = $Theme.DataGrid.Foreground $dataGrid.BorderBrush = $Theme.BorderBrush $dataGrid.BorderThickness = '1' $dataGrid.GridLinesVisibility = 'All' $dataGrid.HorizontalGridLinesBrush = '#E0E0E0' $dataGrid.VerticalGridLinesBrush = '#E0E0E0' $dataGrid.ColumnHeaderHeight = 25 $dataGrid.HeadersVisibility = 'Column' $dataGrid.RowHeight = 22 $dataGrid.AlternatingRowBackground = '#F8F8F8' $dataGrid.RowBackground = 'White' $dataGrid.SelectionMode = 'Extended' $dataGrid.SelectionUnit = 'FullRow' $dataGrid.IsReadOnly = $true $dataGrid.CanUserAddRows = $false $dataGrid.CanUserDeleteRows = $false $dataGrid.CanUserReorderColumns = $true $dataGrid.CanUserResizeColumns = $true $dataGrid.CanUserResizeRows = $false $dataGrid.CanUserSortColumns = $true $dataGrid.HorizontalAlignment = 'Stretch' $dataGrid.VerticalAlignment = 'Stretch' $dataGrid.Margin = '0' $dataGrid.HorizontalScrollBarVisibility = 'Auto' $dataGrid.VerticalScrollBarVisibility = 'Auto' return $dataGrid } function New-DataSummaryPanel { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [hashtable]$Theme, [Parameter(Mandatory = $true)] [System.Windows.Controls.DataGrid]$DataGrid ) $expander = New-Object System.Windows.Controls.Expander $expander.Header = "Data Summary" $expander.Foreground = $Theme.Foreground $expander.BorderBrush = $Theme.BorderBrush $expander.Margin = '0,5,0,0' $expander.IsExpanded = $false $summaryText = New-Object System.Windows.Controls.TextBlock $summaryText.Margin = '10,5,5,5' $summaryText.Foreground = $Theme.Foreground $summaryText.TextWrapping = 'Wrap' $summaryText.FontFamily = 'Consolas, Courier New, monospace' $summaryText.FontSize = 11 $typeCounts = @{} $totalCount = 0 foreach ($item in $DataGrid.ItemsSource) { $typeName = $item.GetType().Name $totalCount++ if ($typeCounts.ContainsKey($typeName)) { $typeCounts[$typeName]++ } else { $typeCounts[$typeName] = 1 } } $summary = "Total Objects: $totalCount`n`n" $sortedTypes = $typeCounts.GetEnumerator() | Sort-Object Value -Descending foreach ($type in $sortedTypes) { $percentage = [math]::Round(($type.Value / $totalCount) * 100, 1) $summary += "$($type.Key.PadRight(20)) : $($type.Value.ToString().PadLeft(6)) ($percentage%)`n" } $summaryText.Text = $summary $expander.Content = $summaryText $script:summaryTextBlock = $summaryText return $expander } |