Last active
January 3, 2021 07:04
-
-
Save sean-m/d03ec212468d9e461e3a0c3750110a10 to your computer and use it in GitHub Desktop.
Displays structure of an object/collection in a WPF TreeView.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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