PoshCode

Community resources for PowerShell coders

Advanced Show-Object 1.0

Download Advanced Show-Object 1.0.ps1

This is a jacked up version of Lee's Show-Object. Uses WMF and a customized tree view grid (Got the C# from MSDN some place, lost the link). Still pretty early on, will likely update it.


function Show-Object{
param(
    ## The object to examine
    [Parameter(ValueFromPipeline = $true)]
    $InputObject
)

#custom controls for treeview... found it on MSDN a while ago, lost link :-/
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
add-type @"
using System;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Data;
using System.Globalization;


namespace _treeListView
{
    public class TreeListView : TreeView
    {
        protected override DependencyObject GetContainerForItemOverride()
        {
            return new TreeListViewItem();
        }

        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is TreeListViewItem;
        }
    }

    public class TreeListViewItem : TreeViewItem
    {
        private int _level = -1;
        public int Level
        {
            get
            {
                if (_level != -1) return _level;
                var parent = ItemsControl.ItemsControlFromItemContainer(this) as TreeListViewItem;
                _level = (parent != null) ? parent.Level + 1 : 0;
                return _level;
            }
        }

        public TreeListViewItem(object header)
        {
            Header = header;
        }

        public TreeListViewItem(){}

        protected override DependencyObject GetContainerForItemOverride()
        {
            return new TreeListViewItem();
        }

        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is TreeListViewItem;
        }
    }

    public class LevelToIndentConverter : IValueConverter
    {
        private const double c_IndentSize = 19.0;
        public object Convert(object o, Type type, object parameter,CultureInfo culture)
        {
            return new Thickness((int)o * c_IndentSize, 0, 0, 0);
        }

        public object ConvertBack(object o, Type type, object parameter,CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }

}
"@ -ReferencedAssemblies presentationFramework,PresentationCore,WindowsBase,System.Xaml -ErrorAction SilentlyContinue
## form layout
[xml]$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	    xmlns:s='clr-namespace:System;assembly=mscorlib' 
	    xmlns:l='clr-namespace:_treeListView;assembly=$([_treeListView.LevelToIndentConverter].Assembly)' 
		Title="TreeListView" Width="640" Height="480">
    <Window.Resources>
    <Style x:Key="ExpandCollapseToggleStyle"
           TargetType="{x:Type ToggleButton}">
            <Setter Property="Focusable" Value="False"/>
            <Setter Property="Width" Value="19"/>
            <Setter Property="Height" Value="13"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ToggleButton}">
                        <Border Width="19" Height="13" Background="Transparent">
                            <Border Width="9" Height="9" BorderThickness="1" BorderBrush="#FF7898B5" CornerRadius="1" SnapsToDevicePixels="true">
                                <Border.Background>
                                    <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
                                        <LinearGradientBrush.GradientStops>
                                            <GradientStop Color="White" Offset=".2"/>
                                            <GradientStop Color="#FFC0B7A6" Offset="1"/>
                                        </LinearGradientBrush.GradientStops>
                                    </LinearGradientBrush>
                                </Border.Background>
                                <Path x:Name="ExpandPath" Margin="1,1,1,1" Fill="Black" 
                                      Data="M 0 2 L 0 3 L 2 3 L 2 5 L 3 5 L 3 3 L 5 3 L 5 2 L 3 2 L 3 0 L 2 0 L 2 2 Z"/>
                            </Border>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsChecked" Value="True">
                                <Setter Property="Data" TargetName="ExpandPath" Value="M 0 2 L 0 3 L 5 3 L 5 2 Z"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <l:LevelToIndentConverter x:Key="LevelToIndentConverter"/>

        <DataTemplate x:Key="CellTemplate_Name">
            <DockPanel>
                <ToggleButton x:Name="Expander" 
                      Style="{StaticResource ExpandCollapseToggleStyle}" 
                      Margin="{Binding Level, Converter={StaticResource LevelToIndentConverter},
                             RelativeSource={RelativeSource AncestorType={x:Type l:TreeListViewItem}}}"
                      IsChecked="{Binding Path=IsExpanded, RelativeSource=
                    {RelativeSource AncestorType={x:Type l:TreeListViewItem}}}"
                      ClickMode="Press"/>
                <TextBlock Text="{Binding Name}"/>
            </DockPanel>
            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding Path=HasItems,RelativeSource={RelativeSource AncestorType={x:Type l:TreeListViewItem}}}" Value="False">
                    <Setter TargetName="Expander" Property="Visibility" Value="Hidden"/>
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>

        <GridViewColumnCollection x:Key="gvcc">
            <GridViewColumn Header="Name" CellTemplate="{StaticResource CellTemplate_Name}" />
            <GridViewColumn Header="MemberType" DisplayMemberBinding="{Binding MemberType}" />
            <GridViewColumn Header="Definition" DisplayMemberBinding="{Binding Definition}" />
            <GridViewColumn Header="Value" DisplayMemberBinding="{Binding Value}" />
        </GridViewColumnCollection>

        <Style TargetType="{x:Type l:TreeListViewItem}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type l:TreeListViewItem}">
                        <StackPanel>
                            <Border Name="Bd"
                      Background="{TemplateBinding Background}"
                      BorderBrush="{TemplateBinding BorderBrush}"
                      BorderThickness="{TemplateBinding BorderThickness}"
                      Padding="{TemplateBinding Padding}">
                                <GridViewRowPresenter x:Name="PART_Header" 
                                      Content="{TemplateBinding Header}" 
                                      Columns="{StaticResource gvcc}" />
                            </Border>
                            <ItemsPresenter x:Name="ItemsHost" />
                        </StackPanel>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsExpanded" Value="false">
                                <Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
                            </Trigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="HasHeader" Value="false"/>
                                    <Condition Property="Width" Value="Auto"/>
                                </MultiTrigger.Conditions>
                                <Setter TargetName="PART_Header" Property="MinWidth" Value="75"/>
                            </MultiTrigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="HasHeader" Value="false"/>
                                    <Condition Property="Height" Value="Auto"/>
                                </MultiTrigger.Conditions>
                                <Setter TargetName="PART_Header" Property="MinHeight" Value="19"/>
                            </MultiTrigger>
                            <Trigger Property="IsSelected" Value="true">
                                <Setter TargetName="Bd" Property="Background" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
                            </Trigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsSelected" Value="true"/>
                                    <Condition Property="IsSelectionActive" Value="false"/>
                                </MultiTrigger.Conditions>
                                <Setter TargetName="Bd" Property="Background" Value="{DynamicResource  {x:Static SystemColors.ControlBrushKey}}"/>
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
                            </MultiTrigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="{x:Type l:TreeListView}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type l:TreeListView}">
                        <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
                            <DockPanel>
                                <GridViewHeaderRowPresenter Columns="{StaticResource gvcc}" DockPanel.Dock="Top"/>
                                <ScrollViewer CanContentScroll="True">
                                    <Grid>
                                    <ItemsPresenter/>
                                    </Grid>
                                </ScrollViewer>
                            </DockPanel>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <l:TreeListView x:Name="Tlv"/>
</Window>

"@

$rootVariableName = dir variable:\* -Exclude InputObject,Args |
    Where-Object {
        $_.Value -and
        ($_.Value.GetType() -eq $InputObject.GetType()) -and
        ($_.Value.GetHashCode() -eq $InputObject.GetHashCode())
}

## If we got multiple, pick the first
$rootVariableName = $rootVariableName| % Name | Select -First 1

## If we didn't find one, use a default name
if(-not $rootVariableName)
{
    $rootVariableName = "InputObject"
}

## A function to add an object to the display tree
function PopulateNode($node, $object)
{
    ## If we've been asked to add a NULL object, just return
    if(-not $object) { return }

    ## If the object is a collection, then we need to add multiple
    ## children to the node
    if([System.Management.Automation.LanguagePrimitives]::GetEnumerator($object))
    {
        ## Some very rare collections don't support indexing (i.e.: $foo[0]).
        ## In this situation, PowerShell returns the parent object back when you
        ## try to access the [0] property.
        $isOnlyEnumerable = $object.GetHashCode() -eq $object[0].GetHashCode()

        ## Go through all the items
        $count = 0
        foreach($childObjectValue in $object)
        {
            ## Create the new node to add, with the node text of the item and
            ## value, along with its type
            $newChildNode = New-Object _treeListView.TreeListViewItem
            #make sure the string version of it isnt more than a single line.
            $showValue = if(($arr="$childObjectValue" -split "\n" | ?{$_}).count -gt 1)
            {
                "$($arr[0].trim()) ..."
            }
            else
            {
                "$childObjectValue"
            }
            $newChildNode.ToolTip = "$childObjectValue"

            $newChildNode.Header = [pscustomobject] @{
                Name=$childObjectValue.GetType().name
                Value="$showValue"
                Definition=$childObjectValue.gettype()
                MemberType="Collection"
            }
            
            ## Use the node name to keep track of the actual property name
            ## and syntax to access that property.
            ## If we can't use the index operator to access children, add
            ## a special tag that we'll handle specially when displaying
            ## the node names.
            if($isOnlyEnumerable)
            {
                $newChildNode.Tag = "@"
            }

            $newChildNode.Tag += "[$count]"
            $null = $node.Items.Add($newChildNode)               

            ## If this node has children or properties, add a placeholder
            ## node underneath so that the node shows a '+' sign to be
            ## expanded.
            AddPlaceholderIfRequired $newChildNode $childObjectValue

            $count++
        }
    }
    else
    {
        ## If the item was not a collection, then go through its
        ## properties
        $members = Get-Member -InputObject $object
        foreach($member in $members)
        {
            $childNode = New-Object _treeListView.TreeListViewItem
            
            $memberValue = if($member.MemberType -like "*Propert*")
            {
                
                $prop = $object.($member.Name)                
                if($prop)
                {
                    $prop
                    $childnode.ToolTip = $prop
                    if($prop.gettype().fullname | ?{($_ -split '\.').count -gt 2})
                    {
                        AddPlaceholderIfRequired $childNode $prop
                    }
                }
                else { '$null' }
            }
            elseif($member.MemberType -eq "Method")
            {
               $childNode.ToolTip = ($object.($member.name) | Out-String).trim()
            }

            $showValue = if(($arr="$memberValue" -split "\n"|?{$_}).count -gt 1)
            {
                "$($arr[0].trim()) ..."
            }
            else
            {
                "$memberValue"
            }
            

            $childNode.Header = [pscustomobject] @{
                Name=$member.name
                Value=$showValue
                Definition=$member.Definition
                MemberType=$member.MemberType
            }

            $childNode.Tag = $member.Name
            $null = $node.Items.Add($childNode)
        }
    }
}

## A function to add a placeholder if required to a node.
## If there are any properties or children for this object, make a temporary
## node with the text "..." so that the node shows a '+' sign to be
## expanded.
function AddPlaceholderIfRequired($node, $object)
{
    if(-not $object) { return }

    if([System.Management.Automation.LanguagePrimitives]::GetEnumerator($object) -or
        @($object.PSObject.Properties))
    {
        $null = $node.Items.Add( (New-Object _treeListView.TreeListViewItem ([pscustomobject]@{Name="..."}) ) )
    }
}

## A function invoked when a node is selected.
function OnSelect
{
    param($Sender, $TreeViewEventArgs)

    ## Determine the selected node
    $nodeSelected = $TreeViewEventArgs.source

    ## Walk through its parents, creating the virtual
    ## PowerShell syntax to access this property.
    $nodePath = GetPathForNode $nodeSelected

    ## Now, invoke that PowerShell syntax to retrieve
    ## the value of the property.
    $resultObject = Invoke-Expression $nodePath
    #$outputPane.Text = $nodePath

    ## If we got some output, put the object's member
    ## information in the text box.
    
    if($resultObject)
    {
        $members = Get-Member -InputObject $resultObject | Out-String       
        #$outputPane.Text += "`n" + $members
    }
}

## A function invoked when the user is about to expand a node
function OnExpand
{
    param($Sender, $TreeViewCancelEventArgs)
    ## Determine the selected node
    $selectedNode = $TreeViewCancelEventArgs.Source
    ## If it has a child node that is the placeholder, clear
    ## the placeholder node.
    if($selectedNode.items.Count -eq 1 -and
        ($selectedNode.Items[0].Header.Name-eq "..."))
    {
        $selectedNode.items.Clear()
    }
    else
    {
        return
    }

    ## Walk through its parents, creating the virtual
    ## PowerShell syntax to access this property.
    $nodePath = GetPathForNode $selectedNode 
    $global:nodepath= $nodePath
    ## Now, invoke that PowerShell syntax to retrieve
    ## the value of the property.
    Invoke-Expression "`$resultObject = $nodePath"

    ## And populate the node with the result object.
    PopulateNode $selectedNode $resultObject
}

## A function to handle keypresses on the form.
## In this case, we capture ^C to copy the path of
## the object property that we're currently viewing.
function OnKeyPress
{
    param($Sender, $KeyPressEventArgs)

    ## [Char] 3 = Control-C
    if($KeyPressEventArgs.KeyChar -eq 3)
    {
        $KeyPressEventArgs.Handled = $true

        ## Get the object path, and set it on the clipboard
        $node = $Sender.SelectedNode
        $nodePath = GetPathForNode $node
        [System.Windows.Forms.Clipboard]::SetText($nodePath)

        $form.Close()
    }
}

## A function to walk through the parents of a node,
## creating virtual PowerShell syntax to access this property.
function GetPathForNode
{
    param($Node)

    $nodeElements = @()

    ## Go through all the parents, adding them so that
    ## $nodeElements is in order.
    while($Node.Tag)
    {
        $nodeElements = ,$Node + $nodeElements
        $Node = $Node.Parent
    }

    ## Now go through the node elements
    $nodePath = ""
    foreach($Node in $nodeElements)
    {
        $nodeName = $Node.Tag 

        ## If it was a node that PowerShell is able to enumerate
        ## (but not index), wrap it in the array cast operator.
        if($nodeName.StartsWith('@'))
        {
            $nodeName = $nodeName.Substring(1)
            $nodePath = "@(" + $nodePath + ")"
        }
        elseif($nodeName.StartsWith('['))
        {
            ## If it's a child index, we don't need to
            ## add the dot for property access
        }
        elseif($nodePath)
        {
            ## Otherwise, we're accessing a property. Add a dot.
            $nodePath += "."
        }

        ## Append the node name to the path
        $nodePath += $nodeName
    }

    ## And return the result
    $nodePath
}


## Create the TreeView, which will hold our object navigation
## area.

$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Form=[Windows.Markup.XamlReader]::Load( $reader )

$tlv = $Form.FindName('Tlv')


[System.Windows.RoutedEventHandler]$Script:Select = { OnSelect @args }
[System.Windows.RoutedEventHandler]$Script:Expand = { OnExpand @args }
[System.Windows.RoutedEventHandler]$Script:KeyPress = { OnKeyPress @args }
$tlv.AddHandler([_treeListView.TreeListViewItem]::SelectedEvent, $Script:Select)
$tlv.AddHandler([_treeListView.TreeListViewItem]::ExpandedEvent, $Script:Expand)
$tlv.AddHandler([_treeListView.TreeListViewItem]::KeyUpEvent, $Script:KeyPress)


## Create the root node, which represents the object
## we are trying to show.
$root = New-Object _treeListView.TreeListViewItem ([pscustomobject]@{
    Name=$InputObject.gettype().name
    Value="$InputObject"
    Definition=$InputObject.gettype().fullname})

#root.Header = "$InputObject : " + $InputObject.GetType()
$root.Tag = '$' + $rootVariableName
$root.ToolTip = "$InputObject"
$root.IsExpanded = $true

## And populate the initial information into the tree
## view.
PopulateNode $root $InputObject

$null = $tlv.Items.Add($root)
$null = $Form.ShowDialog()


}