Skip to content

Instantly share code, notes, and snippets.

@sean-m
Last active January 3, 2021 07:04
Show Gist options
  • Save sean-m/d03ec212468d9e461e3a0c3750110a10 to your computer and use it in GitHub Desktop.
Save sean-m/d03ec212468d9e461e3a0c3750110a10 to your computer and use it in GitHub Desktop.
Displays structure of an object/collection in a WPF TreeView.
function Show-ObjectGraph {
[Alias('sog')]
<#
.Synopsis
Displays structure of an object/collection in a WPF TreeView.
.DESCRIPTION
Recursively builds out an object graph which is then set to
the DataContext of a WPF TreeView. This will display all type and
member info for the object or collection elements. This is a blocking
operation.
.EXAMPLE
Get-ChildItem | Show-ObjectGraph
.EXAMPLE
Show-ObjectGraph $MyListOfStuff
#>
param (
[Parameter(Mandatory=$true,
ValueFromPipeline=$true,
Position=0)]
$InputObj
)
begin {
$Inputs = @()
try { Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase }
catch { throw "Failed to load Windows Presentation Framework assemblies." }
Add-Type -Language CSharp -ReferencedAssemblies PresentationCore,PresentationFramework,WindowsBase `
-TypeDefinition @"
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows.Media;
namespace ObjTree {
public static class GraphHelper {
public static ObjectViewModelHierarchy DisplayObjectGraph(object graph) {
var hierarchy = new ObjectViewModelHierarchy(graph);
if (hierarchy.FirstGeneration.Count > 0)
hierarchy.FirstGeneration[0].IsExpanded = true;
return hierarchy;
}
}
public class ObjectViewModel : INotifyPropertyChanged
{
ReadOnlyCollection<ObjectViewModel> _children;
readonly ObjectViewModel _parent;
readonly object _object;
readonly PropertyInfo _info;
readonly Type _type;
readonly int _depth = 0;
readonly Color _background = Color.FromRgb(222, 233, 244);
bool _isExpanded;
bool _isSelected;
public ObjectViewModel(object obj)
: this(obj, null, null) {
}
ObjectViewModel(object obj, PropertyInfo info, ObjectViewModel parent) {
_depth = (parent == null) ? _depth : parent._depth + 1;
_background = (parent == null) ? _background : ColorHelp.Lerp(parent._background, ColorHelp.Black, 0.1f);
_object = obj;
_info = info;
if (_object != null) {
_type = obj.GetType();
if (!IsPrintableType(_type)) {
// load the _children object with an empty collection to allow the + expander to be shown
_children = new ReadOnlyCollection<ObjectViewModel>(new ObjectViewModel[] { new ObjectViewModel(null) });
}
}
_parent = parent;
}
public void LoadChildren() {
if (_object != null) {
// exclude value types and strings from listing child members
if (!IsPrintableType(_type)) {
// the public properties of this object are its children
var children = _type.GetProperties()
.Where(p => !p.GetIndexParameters().Any()) // exclude indexed parameters for now
.Select(p => new ObjectViewModel(p.GetValue(_object, null), p, this))
.ToList();
// if this is a collection type, add the contained items to the children
var collection = _object as IEnumerable;
if (collection != null) {
foreach (var item in collection) {
children.Add(new ObjectViewModel(item, null, this)); // todo: add something to view the index value
}
}
_children = new ReadOnlyCollection<ObjectViewModel>(children);
this.OnPropertyChanged("Children");
}
}
}
/// <summary>
/// Gets a value indicating if the object graph can display this type without enumerating its children
/// </summary>
static bool IsPrintableType(Type type) {
return type != null && (
type.IsPrimitive ||
type.IsAssignableFrom(typeof(string)) ||
type.IsEnum);
}
public ObjectViewModel Parent {
get { return _parent; }
}
public PropertyInfo Info {
get { return _info; }
}
public ReadOnlyCollection<ObjectViewModel> Children {
get { return _children; }
}
public int Depth {
get { return _depth; }
}
public string Type {
get {
var type = string.Empty;
if (_object != null) {
type = string.Format("[{0}]", _type.Name);
}
else {
if (_info != null) {
type = string.Format("[{0}]", _info.PropertyType.Name);
}
}
return type;
}
}
public string Name {
get {
var name = "Anonymous";
if (_info != null) {
name = _info.Name;
}
return name;
}
}
public string Value {
get {
var value = string.Empty;
if (_object != null) {
if (IsPrintableType(_type)) {
value = _object.ToString();
}
}
else {
value = "<null>";
}
return value;
}
}
public Brush Background {
get { return new SolidColorBrush(_background); }
}
public Brush Foreground {
get { return (_depth < 5) ? new SolidColorBrush(ColorHelp.Black) : new SolidColorBrush(ColorHelp.White); }
}
#region Presentation Members
public bool IsExpanded {
get { return _isExpanded; }
set {
if (_isExpanded != value) {
_isExpanded = value;
if (_isExpanded) {
LoadChildren();
}
this.OnPropertyChanged("IsExpanded");
}
// Expand all the way up to the root.
if (_isExpanded && _parent != null) {
_parent.IsExpanded = true;
}
}
}
public bool IsSelected {
get { return _isSelected; }
set {
if (_isSelected != value) {
_isSelected = value;
this.OnPropertyChanged("IsSelected");
}
}
}
public bool NameContains(string text) {
if (String.IsNullOrEmpty(text) || String.IsNullOrEmpty(Name)) {
return false;
}
return Name.IndexOf(text, StringComparison.InvariantCultureIgnoreCase) > -1;
}
public bool ValueContains(string text) {
if (String.IsNullOrEmpty(text) || String.IsNullOrEmpty(Value)) {
return false;
}
return Value.IndexOf(text, StringComparison.InvariantCultureIgnoreCase) > -1;
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName) {
if (this.PropertyChanged != null) {
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
public static class ColorHelp
{
public static Color White = Color.FromRgb(0xFF, 0xFF, 0xFF);
public static Color Black = Color.FromRgb(0x00, 0x00, 0x00);
public static Color Grey = Color.FromRgb(0xB7, 0xB7, 0xB7);
public static Color LightGrey = Color.FromRgb(219, 219, 219);
public static float Lerp(this float start, float end, float amount) {
float difference = end - start;
float adjusted = difference * amount;
return start + adjusted;
}
public static Color Lerp(Color colour, Color to, float amount) {
// start colours as lerp-able floats
float sr = colour.R, sg = colour.G, sb = colour.B;
// end colours as lerp-able floats
float er = to.R, eg = to.G, eb = to.B;
// lerp the colours to get the difference
byte r = (byte)sr.Lerp(er, amount),
g = (byte)sg.Lerp(eg, amount),
b = (byte)sb.Lerp(eb, amount);
// return the new colour
return Color.FromRgb(r, g, b);
}
}
public class ObjectViewModelHierarchy
{
readonly ReadOnlyCollection<ObjectViewModel> _firstGeneration;
readonly ObjectViewModel _rootObject;
public ObjectViewModelHierarchy() { }
public ObjectViewModelHierarchy(object rootObject) {
_rootObject = new ObjectViewModel(rootObject);
_firstGeneration = new ReadOnlyCollection<ObjectViewModel>(new ObjectViewModel[] { _rootObject });
}
public ReadOnlyCollection<ObjectViewModel> FirstGeneration {
get { return _firstGeneration; }
}
}
}
"@
#==================================#
# ~~ UI Markup #
#==================================#
$inputXML = @"
<Window x:Class="ObjGraph.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:JbossDashboard"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="Object Tree"
Width="930"
Height="500"
mc:Ignorable="d">
<Grid>
<TreeView Name="logObjectTree" Grid.Row="1" ItemsSource="{Binding FirstGeneration}" Margin="8" FontSize="14" FontFamily="Consolas" Background="LightGray">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="Background" Value="{Binding Background}" />
<Setter Property="Foreground" Value="{Binding Foreground}" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid Margin="1" Grid.IsSharedSizeScope="True">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="A"/>
<ColumnDefinition SharedSizeGroup="B"/>
<ColumnDefinition SharedSizeGroup="C"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="{Binding Type}" Grid.Column="0" Grid.Row="0" Padding="2,0" />
<TextBlock Text="{Binding Name}" FontFamily="Lucida Console" Grid.Column="1" Grid.Row="0" Padding="2,0" Margin="2,0" FontWeight="SemiBold"/>
<TextBlock Text="{Binding Value}" Grid.Column="2" Grid.Row="0" Padding="2,0" />
</Grid>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Window>
"@
$inputXML = $inputXML -replace 'mc:Ignorable="d"','' -replace "x:N",'N' -replace '^<Win.*', '<Window'
[xml]$XAML = $inputXML
#Read XAML
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
try{$Form=[Windows.Markup.XamlReader]::Load( $reader )}
catch{Write-Host "Unable to load Windows.Markup.XamlReader. Double-check syntax and ensure .net is installed."}
#==================================================#
# ~~ Store Form Objects In PowerShell #
#==================================================#
$xaml.SelectNodes("//*[@Name]") | %{Set-Variable -Name "WPF$($_.Name)" -Value $Form.FindName($_.Name)}
function Get-FormVariables{
if ($global:ReadmeDisplay -ne $true){Write-host "If you need to reference this display again, run Get-FormVariables" -ForegroundColor Yellow;$global:ReadmeDisplay=$true}
write-host "Found the following interactable elements from our form" -ForegroundColor Cyan
get-variable WPF*
}
function ShowGraphForm {
try {
$hierarchy = [ObjTree.GraphHelper]::DisplayObjectGraph($Inputs)
if ($hierarchy.FirstGeneration.Count -gt 0) { $hierarchy.FirstGeneration[0].IsExpanded = $true }
$WPFlogObjectTree.DataContext = $hierarchy
# Show form
$Form.ShowDialog() | out-null
}
finally {
$Form = $null
}
}
}
process {
$Inputs += $InputObj
}
end {
ShowGraphForm
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment