diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectView.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectView.cs index 989cab1..fcf1be5 100644 --- a/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectView.cs +++ b/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectView.cs @@ -2,19 +2,18 @@ // Licensed under the MIT License. using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Management.Automation; using System.Reflection; +using System.Text; using System.Text.RegularExpressions; - using OutGridView.Models; - using Terminal.Gui; using Terminal.Gui.Trees; +using OutGridView.Cmdlet.TreeNodeCaching; namespace OutGridView.Cmdlet { @@ -147,7 +146,7 @@ private void SelectionChanged(object sender, SelectionChangedEventArgs<object> e { var selectedValue = e.NewValue; - if (selectedValue is CachedMemberResult cmr) + if (selectedValue is ICachedMemberResult cmr) { selectedValue = cmr.Value; } @@ -189,7 +188,7 @@ private bool IsRootObject(object o) public bool CanExpand(object toExpand) { - if (toExpand is CachedMemberResult p) + if (toExpand is ICachedMemberResult p) { return IsBasicType(p?.Value); } @@ -210,7 +209,7 @@ public IEnumerable<object> GetChildren(object forObject) return Enumerable.Empty<object>(); } - if (forObject is CachedMemberResult p) + if (forObject is ICachedMemberResult p) { if (p.IsCollection) { @@ -225,6 +224,11 @@ public IEnumerable<object> GetChildren(object forObject) return GetChildren(e.Value); } + if(forObject is PSObject pso) + { + return GetPSObjectChildren(pso); + } + List<object> children = new List<object>(); foreach (var member in forObject.GetType().GetMembers(BindingFlags.Instance | BindingFlags.Public).OrderBy(m => m.Name)) @@ -251,6 +255,20 @@ public IEnumerable<object> GetChildren(object forObject) return children; } + /// <summary> + /// We only deal with PSObject when there is no native type (e.g. Process). + /// For example when the PSObject.BaseObject is a PSCustomObject. + /// </summary> + /// <param name="pso"></param> + /// <returns></returns> + public IEnumerable<object> GetPSObjectChildren(PSObject pso) + { + foreach(var m in pso.Members.Where(PsoHelper.IsDisplayableMember)) + { + yield return new CachedPSObjectMemberResult(pso, m); + } + } + private static IEnumerable<object> GetExtraChildren(object forObject) { if (forObject is DirectoryInfo dir) @@ -272,7 +290,7 @@ internal static void Run(List<PSObject> objects, ApplicationData applicationData try { - window = new ShowObjectView(objects.Select(p => p.BaseObject).ToList(), applicationData); + window = new ShowObjectView(objects.Select(PsoHelper.MaybeUnwrap).ToList(), applicationData); Application.Top.Add(window); Application.Run(); } @@ -283,131 +301,7 @@ internal static void Run(List<PSObject> objects, ApplicationData applicationData } } - sealed class CachedMemberResultElement - { - public int Index; - public object Value; - - private string representation; - - public CachedMemberResultElement(object value, int index) - { - Index = index; - Value = value; - - try - { - representation = Value?.ToString() ?? "Null"; - } - catch (Exception) - { - Value = representation = "Unavailable"; - } - } - public override string ToString() - { - return $"[{Index}]: {representation}]"; - } - } - - sealed class CachedMemberResult - { - public MemberInfo Member; - public object Value; - public object Parent; - private string representation; - private List<CachedMemberResultElement> valueAsList; - - public bool IsCollection => valueAsList != null; - public IReadOnlyCollection<CachedMemberResultElement> Elements => valueAsList?.AsReadOnly(); - - public CachedMemberResult(object parent, MemberInfo mem) - { - Parent = parent; - Member = mem; - - try - { - if (mem is PropertyInfo p) - { - Value = p.GetValue(parent); - } - else if (mem is FieldInfo f) - { - Value = f.GetValue(parent); - } - else - { - throw new NotSupportedException($"Unknown {nameof(MemberInfo)} Type"); - } - - representation = ValueToString(); - - } - catch (Exception) - { - Value = representation = "Unavailable"; - } - } - - private string ValueToString() - { - if (Value == null) - { - return "Null"; - } - try - { - if (IsCollectionOfKnownTypeAndSize(out Type elementType, out int size)) - { - return $"{elementType.Name}[{size}]"; - } - } - catch (Exception) - { - return Value?.ToString(); - } - - - return Value?.ToString(); - } - - private bool IsCollectionOfKnownTypeAndSize(out Type elementType, out int size) - { - elementType = null; - size = 0; - - if (Value == null || Value is string) - { - - return false; - } - - if (Value is IEnumerable ienumerable) - { - var list = ienumerable.Cast<object>().ToList(); - - var types = list.Where(v => v != null).Select(v => v.GetType()).Distinct().ToArray(); - - if (types.Length == 1) - { - elementType = types[0]; - size = list.Count; - - valueAsList = list.Select((e, i) => new CachedMemberResultElement(e, i)).ToList(); - return true; - } - } - - return false; - } - - public override string ToString() - { - return Member.Name + ": " + representation; - } - } private sealed class RegexTreeViewTextFilter : ITreeViewFilter<object> { private readonly ShowObjectView parent; diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/CachedMemberResult.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/CachedMemberResult.cs new file mode 100644 index 0000000..d55ab1d --- /dev/null +++ b/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/CachedMemberResult.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Reflection; + +namespace OutGridView.Cmdlet.TreeNodeCaching +{ + + sealed class CachedMemberResult : CachedMemberResultBase + { + MemberInfo Member {get;} + + protected override string GetMemberName() + { + return Member.Name; + } + + public CachedMemberResult(object parent, MemberInfo mem) + { + Parent = parent; + Member = mem; + + try + { + if (mem is PropertyInfo p) + { + Value = p.GetValue(parent); + } + else if (mem is FieldInfo f) + { + Value = f.GetValue(parent); + } + else + { + throw new NotSupportedException($"Unknown {nameof(MemberInfo)} Type"); + } + + Representation = ValueToString(); + + } + catch (Exception) + { + Value = Representation = "Unavailable"; + } + } + } +} diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/CachedMemberResultBase.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/CachedMemberResultBase.cs new file mode 100644 index 0000000..8c06e08 --- /dev/null +++ b/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/CachedMemberResultBase.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace OutGridView.Cmdlet.TreeNodeCaching +{ + abstract class CachedMemberResultBase : ICachedMemberResult + { + public object Value {get; protected set;} + public object Parent; + protected string Representation; + private List<CachedMemberResultElement> valueAsList; + + + public bool IsCollection => valueAsList != null; + public IReadOnlyCollection<CachedMemberResultElement> Elements => valueAsList?.AsReadOnly(); + + + protected string ValueToString() + { + if (Value == null) + { + return "Null"; + } + try + { + if (IsCollectionOfKnownTypeAndSize(out Type elementType, out int size)) + { + return $"{elementType.Name}[{size}]"; + } + } + catch (Exception) + { + return Value?.ToString(); + } + + + return Value?.ToString(); + } + + private bool IsCollectionOfKnownTypeAndSize(out Type elementType, out int size) + { + elementType = null; + size = 0; + + if (Value == null || Value is string) + { + + return false; + } + + if (Value is IEnumerable ienumerable) + { + var list = ienumerable.Cast<object>().ToList(); + + var types = list.Where(v => v != null).Select(v => v.GetType()).Distinct().ToArray(); + + if (types.Length == 1) + { + elementType = types[0]; + size = list.Count; + + valueAsList = list.Select((e, i) => new CachedMemberResultElement(e, i)).ToList(); + return true; + } + } + + return false; + } + + public override string ToString() + { + return GetMemberName() + ": " + Representation; + } + + protected abstract string GetMemberName(); + } +} diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/CachedMemberResultElement.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/CachedMemberResultElement.cs new file mode 100644 index 0000000..05036ed --- /dev/null +++ b/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/CachedMemberResultElement.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace OutGridView.Cmdlet.TreeNodeCaching +{ + sealed class CachedMemberResultElement + { + public int Index; + public object Value; + + private string representation; + + public CachedMemberResultElement(object value, int index) + { + Index = index; + Value = value; + + try + { + representation = Value?.ToString() ?? "Null"; + } + catch (Exception) + { + Value = representation = "Unavailable"; + } + } + public override string ToString() + { + return $"[{Index}]: {representation}"; + } + } +} diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/CachedPSObjectMemberResult.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/CachedPSObjectMemberResult.cs new file mode 100644 index 0000000..db95c12 --- /dev/null +++ b/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/CachedPSObjectMemberResult.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Reflection; + +namespace OutGridView.Cmdlet.TreeNodeCaching +{ + sealed class CachedPSObjectMemberResult : CachedMemberResultBase + { + PSMemberInfo Member {get;} + public CachedPSObjectMemberResult(object parent, PSMemberInfo mem) + { + Parent = parent; + Member = mem; + + if(mem.Value is PSObject psoVal) + { + Value = PsoHelper.MaybeUnwrap(psoVal); + } + else + { + Value = mem.Value; + } + + + Representation = ValueToString(); + } + + protected override string GetMemberName() + { + return Member.Name; + } + } +} diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/ICachedMemberResult.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/ICachedMemberResult.cs new file mode 100644 index 0000000..e338d92 --- /dev/null +++ b/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/ICachedMemberResult.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace OutGridView.Cmdlet.TreeNodeCaching +{ + internal interface ICachedMemberResult + { + bool IsCollection { get; } + public object Value {get;} + + public IReadOnlyCollection<CachedMemberResultElement> Elements {get;} + } +} diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/PsoHelper.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/PsoHelper.cs new file mode 100644 index 0000000..883122d --- /dev/null +++ b/src/Microsoft.PowerShell.ConsoleGuiTools/TreeNodeCaching/PsoHelper.cs @@ -0,0 +1,61 @@ + +using System; +using System.Management.Automation; + +namespace OutGridView.Cmdlet.TreeNodeCaching +{ + class PsoHelper + { + internal static bool IsDisplayableMember(PSMemberInfo m) + { + if(!m.IsInstance) + { + return false; + } + + + if( + m.MemberType == PSMemberTypes.Method || + m.MemberType == PSMemberTypes.CodeMethod || + m.MemberType == PSMemberTypes.Event || + m.MemberType == PSMemberTypes.Methods + ){ + + return false; + } + + return true; + } + + /// <summary> + /// Unwraps the <see cref="PSObject.BaseObject"/> if it is likely to + /// be a better (e.g. native) representation of the users input. + /// </summary> + /// <param name="obj"></param> + /// <returns>The native object or the original reference if unwrapping is counter productive.</returns> + internal static object MaybeUnwrap(PSObject obj) + { + if(ShouldUnwrap(obj)) + { + return Unwrap(obj); + } + + return obj; + } + + private static bool ShouldUnwrap(PSObject obj) + { + if(obj.BaseObject is PSCustomObject) + { + return false; + } + + return true; + } + + private static object Unwrap(PSObject psoVal) + { + return psoVal.BaseObject; + } + } +} \ No newline at end of file