#requires -version 2.0 if([threading.thread]::currentthread.apartmentstate -ne "STA") { throw "This module requires Windows PowerShell to be run in Single Threaded Apartment mode. Please run: PowerShell.exe -STA" } ### Import the WPF assemblies Add-Type -AssemblyName PresentationFramework Add-Type -AssemblyName PresentationCore Set-StrictMode -Version Latest ## Oddly enough, this will get flipped, so the initial sort is actually "Descending" $SortOrder = "Ascending" ## Displays objects in a grid view and returns (only) the selected ones when closed. ######################################################################################################################## ## Usage: ## ls | Select-Grid Name, Length, Mode, LastWriteTime ## ps | Select-Grid ProcessName, Id, VM, WS, PM, Product, Path ## Select-Grid Name, Length, Mode, LastWriteTime -Input (ls) ## ## Take advantage of the graphing: ## ls | Select-Grid Name,Length,LastWriteTime,Mode -Title $pwd -Sort Length -Graph Length ## ps | Select-Grid ProcessName, Id, VM, WS, PM -Title Processes -Graph WS -Sort WS ## Kill the selected processes: ## ps | Select-Grid ProcessName, Id, VM, WS, PM, Product, Path -Title "Processes" -Sort WS -Graph VM,WS,PM | kill ######################################################################################################################## ## History: ## v3.4 - Refactored and cleaned up -- note the New-SortDescription function ## v3.3 - Fixed a bug with StrictMode when you don't specify columns ## v3.2 - Fixed a bug with duplicate columns ## -- (re)fixed the column order to preserve the command-line order (if specified) ## -- Changed default sort order to descending (when you click a new column, the big values are on top) ## v3.1 - Fixed a bug with not passing the graph parameter ## v3.0 - Added CellTemplate for graphing (first release to PowerShellCentral.com/scripts) ## v2.5 - Added Multi-select and made it output the selected items ## v2.3 - Added Title and made columns dragable ## v2.2 - Fixed pipeline problems ## v2.1 - Added "Get-Default" to populate blank rows ## v2.0 - Added clickable headers and sorting ## -- broken on columns with blanks? ## v1.0 - Basic grid with data-binding ## -- broken on pipeline ######################################################################################################################## Cmdlet Select-Grid -ConfirmImpact low -snapin Huddled.WPF { Param ( [Parameter(Position=0)][string[]]$Properties, [Parameter(Position=1)][string[]]$Title, [Parameter(Position=2)][string[]]$Sort, [Parameter(Position=3)][string[]]$Graph, [Parameter(Mandatory=$true, ValueFromPipeline=$true)] $InputObjects ) BEGIN { if ($CommandLineParameters.ContainsKey("InputObjects")) { $outputObjects = @(,$InputObjects) } else { $outputObjects = @() } } PROCESS { ### Collect together all input objects $outputObjects += $InputObjects } END { ### Create our window and listview $window = New-Object System.Windows.Window $window.SizeToContent = "WidthAndHeight" $window.SnapsToDevicePixels = $true $window.Content = New-Object System.Windows.Controls.ListView if($Title) { $window.Title = $Title } else { $window.Title = $outputObjects[-1].GetType().Name } ### The ListView takes ViewBase object which controls the layout and appearance ### We'll use a GridView $window.Content.View = New-Object System.Windows.Controls.GridView $window.Content.View.AllowsColumnReorder = $true $columns = Get-PropertyTypes $outputObjects ([ref]$Properties) ### Make columns (use Properties instead of Columns.Keys to preserve order) foreach($Name in $Properties) { ### Try to ensure that every object has _some_ value for each column (so sorting works) $outputObjects | add-member -Type NoteProperty -Name $Name -value (Get-DefaultValue($columns[$name])) -ea SilentlyContinue ## For each property, make a column $gvc = New-Object System.Windows.Controls.GridViewColumn ## And bind the data ... $gvc.DisplayMemberBinding = New-Object System.Windows.Data.Binding $Name ## In order to add sorting, we need to create the header ourselves $gvc.Header = New-Object System.Windows.Controls.GridViewColumnHeader $gvc.Header.Content = $Name ## Add a click handler to enable sorting ... $gvc.Header.add_click({ $view = [System.Windows.Data.CollectionViewSource]::GetDefaultView( $outputObjects ) $view.SortDescriptions.Clear() $view.SortDescriptions.Add( (New-SortDescription $this.Content) ) $view.Refresh() } ) ## Use that column in the view $window.Content.View.Columns.Add($gvc) } $Graph = @($Graph | Where-Object { $Properties -contains $_ }) if( $Graph.Count -gt 0 ) { FormatColumn-Percent $outputObjects $window.Content.View $Graph } ## Databind the argument $window.Content.ItemsSource = $outputObjects ## Add an initial sort ... [System.Windows.Data.CollectionViewSource]::GetDefaultView( $outputObjects ).SortDescriptions.Add( (New-SortDescription (&{ if($Sort) { $Sort } else { $Properties[0] } })) ) ## Show the window $Null = $window.ShowDialog() $window.Content.SelectedItems } } function New-SortDescription($column) { if($Sort -eq $column) { switch($SortOrder) { "Ascending" { $SortOrder = "Descending" } "Descending" { $SortOrder = "Ascending" } } } else { $SortOrder = "Descending" $Sort = $column } return new-object System.ComponentModel.SortDescription $column, $SortOrder } ## return a hash of property names and maximum values for each function Get-Max($collection,$properties) { $max = @{} $collection | Measure-Object $properties -Max | ForEach-Object { $max[$($_.Property)] = $($_.Maximum)} return $max } ## a quick and easy function to create default-value instances of any type function Get-DefaultValue([type]$type) { if($type -and $type.IsValueType) { [Activator]::CreateInstance($type) } else { $null } } ## Determine which properties are actually present in the objects and what type they are function Get-PropertyTypes($outputObjects, [ref]$Properties) { ### Collect the columns we're going to use $columns = @{} ### if we have a list, use all the items on the list that are defined ### but take great pains to preserve the OriginalOrder if($Properties.Value) { $Properties.Value = $outputObjects | get-member $Properties.Value | add-member ScriptProperty OriginalOrder {[array]::indexof($Properties.Value,$this.Name)} -passthru | Sort OriginalOrder -unique | ForEach-Object { $_.Name } } else { ### if we don't have a list, make one, from all the items... $Properties.Value = $outputObjects | Get-Member -type Properties | Sort Name -Unique | ForEach-Object { $_.Name } } ### Figure out the types Set-StrictMode -Version 1 ## I'm going to test for the existence of properties by accessing them, so ... ForEach($Name in $Properties.Value) { $columns[$name] = $Null ForEach($obj in $outputObjects) { if( $obj.($Name) ) { ## This line will ERROR with Set-StrictMode -Version Latest $columns[$name] = $obj.($Name).GetType() break; } } } Set-StrictMode -Version Latest return $columns } ############################################################# ## Conditionally format the columns for a GridView ... ## Currently only adds a Cell Template for the specified columns ## Note: the $properties should only contain the names of numerical properties! function FormatColumn-Percent( $outputObjects, $gridview, $properties) { # Calculate the max values $max = Get-Max $outputObjects $properties # And finally, set the CellTemplate on those columns... foreach($property in $properties) { # And then calculate the percentages, based on that... # $outputObjects.Value | Add-Member ScriptProperty "$($property)Percent" {(`$this.${property} -as [int]) / $($max.($property))} # $outputObjects | Add-Member ScriptProperty "$($property)Percent" $executioncontext.InvokeCommand.NewScriptBlock( # "(`$this.$($property) -as [double]) / $($max.($property))") foreach($obj in $outputObjects) { Add-Member NoteProperty "$($property)Percent" (($($obj.$($property)) -as [double]) / $($max.($property))) -input $obj } $column = @($gridview.Columns | ? { $_.Header.Content -eq $property })[0]; ## dump the binding and use a template instead... (this shouldn't be necessary)... $column.DisplayMemberBinding = $null $column.CellTemplate = ` [Windows.Markup.XamlReader]::Load( (New-Object System.Xml.XmlNodeReader ( [Xml]" "))) } } Export-ModuleMember Select-Grid