diff --git a/Shared/Util/Extensions/Type.cs b/Shared/Util/Extensions/Type.cs index 1a1f644..2242b99 100644 --- a/Shared/Util/Extensions/Type.cs +++ b/Shared/Util/Extensions/Type.cs @@ -204,5 +204,28 @@ public static Type ItemType(this Type type) { bool IsIEnum(Type t) => t == typeof(System.Collections.IEnumerable); bool ImplIEnumT(Type t) => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>); } + + public static IEnumerable BaseTypes(this Type t, bool genericDefinitions = false, bool andSelf = false) { + if (andSelf) { + yield return t; + } + if (t.IsGenericType && genericDefinitions) { + yield return t.GetGenericTypeDefinition(); + } + + foreach (var i in t.GetInterfaces()) { + yield return reduceToGeneric(i); + } + if (t.BaseType != null) { + foreach (var baseType in t.BaseType.BaseTypes(genericDefinitions, true)) { + yield return reduceToGeneric(baseType); + } + } + + Type reduceToGeneric(Type sourceType) { + if (sourceType.IsGenericType && genericDefinitions) { return sourceType.GetGenericTypeDefinition(); } + return sourceType; + } + } } } diff --git a/Visualizer.Shared/Converters.cs b/Visualizer.Shared/Converters.cs index 31c1423..08a620a 100644 --- a/Visualizer.Shared/Converters.cs +++ b/Visualizer.Shared/Converters.cs @@ -17,7 +17,7 @@ public abstract class ReadOnlyConverterBase : IValueConverter { } public class RootConverter : ReadOnlyConverterBase { - public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) => new[] { new KeyValuePair("", (ExpressionNodeData)value) }; + public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) => new[] { value }; } public class ConditionalFormatConverter : ReadOnlyConverterBase { diff --git a/Visualizer.Shared/VisualizerData.cs b/Visualizer.Shared/VisualizerData.cs index 2a3499d..db6f0f1 100644 --- a/Visualizer.Shared/VisualizerData.cs +++ b/Visualizer.Shared/VisualizerData.cs @@ -13,6 +13,7 @@ using ExpressionTreeVisualizer.Util; using static ExpressionToString.Globals; using System.Reflection; +using System.Diagnostics; namespace ExpressionTreeVisualizer { [Serializable] @@ -32,7 +33,7 @@ public string Language { set => this.NotifyChanged(ref _language, value, args => PropertyChanged?.Invoke(this, args)); } - [field:NonSerialized] + [field: NonSerialized] public event PropertyChangedEventHandler PropertyChanged; } @@ -57,8 +58,7 @@ public ExpressionNodeData FindNodeBySpan(int start, int length) { //if (start < NodeData.Span.start || end > NodeData.SpanEnd) { throw new ArgumentOutOfRangeException(); } var current = NodeData; while (true) { - // we should really use SingleOrDefault, except that multiple instances of the same ParameterExpression might be returned, because we can't figure out the right start and end for multiple ParameterExpression - var child = current.Children.Values().FirstOrDefault(x => x.Span.start <= start && x.SpanEnd >= end); + var child = current.Children.SingleOrDefault(x => x.Span.start <= start && x.SpanEnd >= end); if (child == null) { break; } current = child; } @@ -73,7 +73,7 @@ public VisualizerData(object o, VisualizerDataOptions options = null) { Source = WriterBase.Create(o, Options.Formatter, Options.Language, out var pathSpans).ToString(); PathSpans = pathSpans; CollectedEndNodes = new List(); - NodeData = new ExpressionNodeData(o, "", this, false); + NodeData = new ExpressionNodeData(o, ("", ""), this, false); // TODO it should be possible to write the following using LINQ Constants = new Dictionary>(); @@ -110,8 +110,9 @@ public VisualizerData(object o, VisualizerDataOptions options = null) { } [Serializable] + [DebuggerDisplay("{FullPath}")] public class ExpressionNodeData : INotifyPropertyChanged { - public List> Children { get; set; } + public List Children { get; set; } public string NodeType { get; set; } // ideally this should be an intersection type of multiple enums public string ReflectionTypeName { get; set; } public (int start, int length) Span { get; set; } @@ -121,7 +122,13 @@ public class ExpressionNodeData : INotifyPropertyChanged { public string Closure { get; set; } public EndNodeTypes? EndNodeType { get; set; } public bool IsDeclaration { get; set; } - public string Path { get; set; } = ""; + public string PathFromParent { get; set; } = ""; + public string FullPath { get; set; } = ""; + public (string @namespace, string typename, string propertyname)? ParentProperty { get; set; } + public (string @namespace, string enumTypename, string membername)? NodeTypeParts { get; set; } + + private List<(string @namespace, string typename)> _baseTypes; + public List<(string @namespace, string typename)> BaseTypes => _baseTypes; public EndNodeData EndNodeData => new EndNodeData { Closure = Closure, @@ -135,12 +142,20 @@ public ExpressionNodeData() { } private static HashSet propertyTypes = NodeTypes.SelectMany(x => new[] { x, typeof(IEnumerable<>).MakeGenericType(x) }).ToHashSet(); - internal ExpressionNodeData(object o, string path, VisualizerData visualizerData, bool isParameterDeclaration = false) { - Path = path; + internal ExpressionNodeData(object o, (string aggregatePath, string pathFromParent) path, VisualizerData visualizerData, bool isParameterDeclaration = false, PropertyInfo pi = null) { + var (aggregatePath, pathFromParent) = path; + PathFromParent = pathFromParent; + if (aggregatePath.IsNullOrWhitespace() || pathFromParent.IsNullOrWhitespace()) { + FullPath = aggregatePath + pathFromParent; + } else { + FullPath = $"{aggregatePath}.{pathFromParent}"; + } + var language = visualizerData.Options.Language; switch (o) { case Expression expr: NodeType = expr.NodeType.ToString(); + NodeTypeParts = (typeof(ExpressionType).Namespace, nameof(ExpressionType), NodeType); ReflectionTypeName = expr.Type.FriendlyName(language); IsDeclaration = isParameterDeclaration; @@ -186,6 +201,7 @@ internal ExpressionNodeData(object o, string path, VisualizerData visualizerData break; case MemberBinding mbind: NodeType = mbind.BindingType.ToString(); + NodeTypeParts = (typeof(MemberBindingType).Namespace, nameof(MemberBindingType), NodeType); Name = mbind.Member.Name; break; case CallSiteBinder callSiteBinder: @@ -197,17 +213,15 @@ internal ExpressionNodeData(object o, string path, VisualizerData visualizerData break; } - if (visualizerData.PathSpans.TryGetValue(Path, out var span)) { + if (visualizerData.PathSpans.TryGetValue(FullPath, out var span)) { Span = span; } - // TODO specify order for properties; sometimes alphabetical order is not preferred; e.g. Parameters then Body for LambdaExpression - // populate Children var type = o.GetType(); var preferredOrder = preferredPropertyOrders.FirstOrDefault(x => x.Item1.IsAssignableFrom(type)).Item2; Children = type.GetProperties() - .Where(prp => + .Where(prp => !(prp.DeclaringType.Name == "BlockExpression" && prp.Name == "Result") && propertyTypes.Any(x => x.IsAssignableFrom(prp.PropertyType)) ) @@ -218,14 +232,24 @@ internal ExpressionNodeData(object o, string path, VisualizerData visualizerData .ThenBy(prp => prp.Name) .SelectMany(prp => { if (prp.PropertyType.InheritsFromOrImplements()) { - return (prp.GetValue(o) as IEnumerable).Cast().Select((x, index) => ($"{prp.Name}[{index}]", x)); + return (prp.GetValue(o) as IEnumerable).Cast().Select((x, index) => ($"{prp.Name}[{index}]", x, prp)); } else { - return new[] { (prp.Name, prp.GetValue(o)) }; + return new[] { (prp.Name, prp.GetValue(o), prp) }; } }) .Where(x => x.Item2 != null) - .Select(x => KVP(x.Item1, new ExpressionNodeData(x.Item2, (Path.IsNullOrWhitespace() ? "" : $"{Path}.") + x.Item1, visualizerData))) + .Select(x => new ExpressionNodeData(x.Item2, (FullPath ?? "", x.Item1), visualizerData, false, x.Item3)) .ToList(); + + // populate URLs + if (pi != null) { + ParentProperty = (pi.DeclaringType.Namespace, pi.DeclaringType.Name, pi.Name); + } + + if (!baseTypes.TryGetValue(o.GetType(), out _baseTypes)) { + _baseTypes = o.GetType().BaseTypes(true, true).Where(x => x != typeof(object) && x.IsPublic).Select(x => (x.Namespace, x.Name)).Distinct().ToList(); + baseTypes[o.GetType()] = _baseTypes; + } } private static List<(Type, string[])> preferredPropertyOrders = new List<(Type, string[])> { @@ -245,6 +269,8 @@ internal ExpressionNodeData(object o, string path, VisualizerData visualizerData (typeof(DynamicExpression), new [] {"Binder", "Arguments"}) }; + private static Dictionary> baseTypes = new Dictionary>(); + public event PropertyChangedEventHandler PropertyChanged; private bool _isSelected; @@ -255,7 +281,7 @@ public bool IsSelected { public void ClearSelection() { IsSelected = false; - Children.ForEach(x => x.Value.ClearSelection()); + Children.ForEach(x => x.ClearSelection()); } } diff --git a/Visualizer.Shared/VisualizerDataControl.xaml b/Visualizer.Shared/VisualizerDataControl.xaml index 8dbda9f..7d082f2 100644 --- a/Visualizer.Shared/VisualizerDataControl.xaml +++ b/Visualizer.Shared/VisualizerDataControl.xaml @@ -4,32 +4,32 @@ - + - + - + - + - + - + - - - + + + - - - - - + + + + + @@ -129,7 +129,7 @@ diff --git a/Visualizer.Shared/VisualizerDataControl.xaml.cs b/Visualizer.Shared/VisualizerDataControl.xaml.cs index c1527f5..9df3350 100644 --- a/Visualizer.Shared/VisualizerDataControl.xaml.cs +++ b/Visualizer.Shared/VisualizerDataControl.xaml.cs @@ -70,7 +70,7 @@ private void changeSelection(object sender) { List selected = new List(); if (sender == tree) { - tree.SelectedItems>().Values().AddRangeTo(selected); + tree.SelectedItems().AddRangeTo(selected); } else if (sender == source) { var singleNode = visualizerData.FindNodeBySpan(source.SelectionStart, source.SelectionLength); if (singleNode != null) { selected.Add(singleNode); } @@ -88,7 +88,7 @@ private void changeSelection(object sender) { ExpressionNodeData toHighlight; if (sender == tree) { // use the last selected from the tree - toHighlight = tree.LastSelectedItem?>()?.Value; + toHighlight = tree.LastSelectedItem(); } else { toHighlight = selected.FirstOrDefault(); } @@ -129,12 +129,59 @@ public void LoadDataContext() { DataContext = ObjectProvider.TransferObject(Options); } - private void ContextMenu_Loaded(object sender, RoutedEventArgs e) { - var menu = sender as ContextMenu; - - } - private void Test_Click(object sender, RoutedEventArgs e) { - new Window().Show(); + private void HelpContextMenu_Loaded(object sender, RoutedEventArgs e) { + var menu = (MenuItem)sender; + var node = (ExpressionNodeData)menu.DataContext; + MenuItem mi; + + if (menu.Items.Any()) { return; } + + if (node.ParentProperty.HasValue) { + var (@namespace, typename, propertyname) = node.ParentProperty.Value; + mi = new MenuItem() { + Header = $"Property: {typename}.{propertyname}" + }; + mi.Click += (s1, e1) => { + var url = $"{BaseUrl}{new[] { @namespace, typename, propertyname }.Joined(".")}"; + Process.Start(url); + }; + menu.Items.Add(mi); + } + + if (node.ParentProperty.HasValue && node.NodeTypeParts.HasValue) { + menu.Items.Add(new Separator()); + } + + if (node.NodeTypeParts.HasValue) { + var (@namespace, typename, membername) = node.NodeTypeParts.Value; + mi = new MenuItem() { + Header = $"Node type: {typename}.{membername}" + }; + mi.Click += (s1, e1) => { + var url = $"{BaseUrl}{new[] { @namespace, typename }.Joined(".")}#{new[] { @namespace.Replace(".","_"), typename, membername }.Joined("_")}"; + Process.Start(url); + }; + menu.Items.Add(mi); + } + + if ((node.ParentProperty.HasValue || node.NodeTypeParts.HasValue) && node.BaseTypes.Any()) { + menu.Items.Add(new Separator()); + } + + if (node.BaseTypes != null) { + node.BaseTypes.ForEachT((@namespace, typename) => { + mi = new MenuItem() { + Header = $"Base type: {typename}" + }; + mi.Click += (s1, e1) => { + var url = $"{BaseUrl}{@namespace}.{typename.Replace("~", "-")}"; + Process.Start(url); + }; + menu.Items.Add(mi); + }); + } } + + private const string BaseUrl = "https://docs.microsoft.com/dotnet/api/"; } } diff --git a/_visualizerTests/Program.cs b/_visualizerTests/Program.cs index 8e38e15..6dc815c 100644 --- a/_visualizerTests/Program.cs +++ b/_visualizerTests/Program.cs @@ -85,50 +85,50 @@ static void Main(string[] args) { //IQueryable personSource = null; //Expression> expr = person => person.LastName.StartsWith("A"); - //var hour = Variable(typeof(int), "hour"); - //var msg = Variable(typeof(string), "msg"); - //var block = Block( - // // specify the variables available within the block - // new[] { hour, msg }, - // // hour = - // Assign(hour, - // // DateTime.Now.Hour - // MakeMemberAccess( - // MakeMemberAccess( - // null, - // typeof(DateTime).GetMember("Now").Single() - // ), - // typeof(DateTime).GetMember("Hour").Single() - // ) - // ), - // // if ( ... ) { ... } else { ... } - // IfThenElse( - // // ... && ... - // AndAlso( - // // hour >= 6 - // GreaterThanOrEqual( - // hour, - // Constant(6) - // ), - // // hour <= 18 - // LessThanOrEqual( - // hour, - // Constant(18) - // ) - // ), - // // msg = "Good day" - // Assign(msg, Constant("Good day")), - // // msg = Good night" - // Assign(msg, Constant("Good night")) - // ), - // // Console.WriteLine(msg); - // Call( - // typeof(Console).GetMethod("WriteLine", new[] { typeof(object) }), - // msg - // ), - // hour - //); - //Expression expr = Lambda(block); + var hour = Variable(typeof(int), "hour"); + var msg = Variable(typeof(string), "msg"); + var block = Block( + // specify the variables available within the block + new[] { hour, msg }, + // hour = + Assign(hour, + // DateTime.Now.Hour + MakeMemberAccess( + MakeMemberAccess( + null, + typeof(DateTime).GetMember("Now").Single() + ), + typeof(DateTime).GetMember("Hour").Single() + ) + ), + // if ( ... ) { ... } else { ... } + IfThenElse( + // ... && ... + AndAlso( + // hour >= 6 + GreaterThanOrEqual( + hour, + Constant(6) + ), + // hour <= 18 + LessThanOrEqual( + hour, + Constant(18) + ) + ), + // msg = "Good day" + Assign(msg, Constant("Good day")), + // msg = Good night" + Assign(msg, Constant("Good night")) + ), + // Console.WriteLine(msg); + Call( + typeof(Console).GetMethod("WriteLine", new[] { typeof(object) }), + msg + ), + hour + ); + Expression expr = Lambda(block); //var constant = Constant(new List()); //Expression expr = Or( @@ -186,28 +186,28 @@ static void Main(string[] args) { //); //Console.WriteLine(expr.ToString("C#")); - var writeline = typeof(Console).GetMethods().Single(x => { - if (x.Name != "WriteLine") { return false; } - var parameters = x.GetParameters(); - return parameters.Length == 1 && parameters[0].ParameterType == typeof(string); - }); - Expression expr = IfThenElse( - LessThanOrEqual( - Property( - Property(null, typeof(DateTime).GetProperty("Now")), - "Hour" - ), - Constant(18) - ), - Block( - Call(writeline, Constant("Good day!")), - Call(writeline, Constant("Have a nice day!")) - ), - Block( - Call(writeline, Constant("Good night!")), - Call(writeline, Constant("Have a nice night!")) - ) - ); + //var writeline = typeof(Console).GetMethods().Single(x => { + // if (x.Name != "WriteLine") { return false; } + // var parameters = x.GetParameters(); + // return parameters.Length == 1 && parameters[0].ParameterType == typeof(string); + //}); + //Expression expr = IfThenElse( + // LessThanOrEqual( + // Property( + // Property(null, typeof(DateTime).GetProperty("Now")), + // "Hour" + // ), + // Constant(18) + // ), + // Block( + // Call(writeline, Constant("Good day!")), + // Call(writeline, Constant("Have a nice day!")) + // ), + // Block( + // Call(writeline, Constant("Good night!")), + // Call(writeline, Constant("Have a nice night!")) + // ) + //); var visualizerHost = new VisualizerDevelopmentHost(expr, typeof(Visualizer), typeof(VisualizerDataObjectSource)); visualizerHost.ShowVisualizer();