DumpAsSummaryDetailTest

Hi Joe,

I wanted to share this in case you want to make it a feature....
Sometimes you just want dump to show detail with elbow space...

Code is here: https://share.linqpad.net/2fxsjre2.linq

Comments

  • sample code

    void Main()
    {
    
        var temp = Enumerable.Range(0, 2).Select(x => new
        {
            ha = x,
            name = new { first = $"name {x}"} ,
            hobby =  $"my hobby is {x}",
            detail = new { test = $"{x} Detail spans multiple columns"}
        });
        temp.DumpAsSummaryDetailTest( "detail","check out this thing");
    }
    
    
  • Is the idea to minimize horizontal scrolling?

  • clem0338
    edited August 21

    I really like the idea,

    There are 2 things:

    • The column names are taken from the first item, if you provide 2 different object types, the properties are taken from the first only
    • Performance issue on big lists, this code relies heavily on Reflection

    Anyway, here is my updated version with

    • PropertyInfo cache
    • Single enumeration of items
    • Single CSS declaration
    • A little bit of refactoring to my liking

    Thanks @ehassen

    void Main() {
        var temp = Enumerable.Range(0, 2).Select(x => new {
            ha = x,
            name = new { first = $"name {x}" },
            hobby = $"my hobby is {x}",
            detail = new {
                test = $"{x} hi i am the detail. I should span multiple columns and be shown as the next row after this. Instead of showing me in the original row, user would click a detail column with ... as text"
            }
        });
    
        temp.DumpAsSummaryDetailTest(title: "check out this thing");
    }
    
    static class DumpHelperTest {
        private static bool StylesAdded = false;
        private static Dictionary<Type, IReadOnlyDictionary<string, PropertyInfo>> propertiesCache = new();
        private static IReadOnlyDictionary<string, PropertyInfo> GetCachedProperties(Type type) {
            if (!propertiesCache.TryGetValue(type, out var props))
                propertiesCache.Add(type, props = type.GetProperties().ToDictionary(p => p.Name));
            return props;
        }
        private static bool IsNumber(this object? obj) {
            if (obj == null)
                return false;
    
            var objType = obj.GetType();
            objType = Nullable.GetUnderlyingType(objType) ?? objType;
            return objType.IsPrimitive && objType.IsValueType;
        }
        public static T DumpAsSummaryDetailTest<T>(this T obj, string detailFieldName = "detail", string title = "") {
            if (!StylesAdded) {
                Util.HtmlHead.AddStyles("""
                .DSD_header {
                    background-color: #ffeb3b;
                    color: #222;
                    font-weight: bold;
                    letter-spacing: 1px;
                }
                .DSD_num {text-align: right;}
                """);
            }
    
            if (obj is IEnumerable<object> enumerable) {
                var enumerator = enumerable.GetEnumerator();
                if (enumerator.MoveNext()) {
                    var firstItem = enumerator.Current;
                    var firstItemProps = GetCachedProperties(firstItem.GetType());
                    if (!firstItemProps.ContainsKey(detailFieldName)) {
                        Console.Error.WriteLine($"Property '{detailFieldName}' does not exist on type. Valid field names are: {string.Join(", ", firstItemProps.Keys)}");
                        obj.Dump(title, 1);
                        return obj;
                    }
    
                    var headerRow = new TableRow { CssClass = "DSD_header" };
                    var fieldNames = firstItemProps.Keys.Where(k => k != detailFieldName).ToList();
    
                    foreach (var fieldName in fieldNames)
                        headerRow.Cells.Add(new TableCell(new Label(fieldName)));
                    headerRow.Cells.Add(new TableCell(new DumpContainer(Util.HorizontalRun(true, "detail", new Hyperlinq(() => obj.Dump(true), "...")))));
    
                    IEnumerable<TableRow> GetRows() {
                        for (object item; (item = enumerator.Current) != null; enumerator.MoveNext()) {
                            var itemProps = GetCachedProperties(item.GetType());
                            var detailRow = new TableRow(new TableCell(new Control(new DumpContainer(Util.VerticalRun($"{detailFieldName}:", itemProps.TryGetValue(detailFieldName, out var prop) ? prop.GetValue(item) : "N/A")))) { ColSpan = fieldNames.Count() + 1 }) { Visible = false };
                            var mainRow = new TableRow();
                            mainRow.Cells.AddRange([
                                ..fieldNames.Select(f => itemProps.TryGetValue(f, out var valueProp) ? valueProp.GetValue(item, null) : "N/A").Select(v => new TableCell(new Control(new DumpContainer(v)) { CssClass = IsNumber(v) ? "DSD_num" : "" })),
                                new TableCell(new Hyperlink("...", h => detailRow.Visible = !detailRow.Visible))
                            ]);
    
                            yield return mainRow;
                            yield return detailRow;
                        }
                    }
    
                    new Table([headerRow, .. GetRows()]).Dump(title, 1);
                }
            }
            return obj;
        }
    }
    
  • Just to add my two pennies' worth.

    I like this and can understand the need, but I think that currently it is better as a 'user-supplied' routine rather than added to LinqPad.

    The problem, as I see it, is that (apart from the easily changed yellow header), it looks very much like a specialised dump command. But we are spoilt by all the little extras that linqpad's dump provides and if I saw this option in Linqpad, I would assume all the other features would work with it.

    Cue lots of 'Why does it do this?' or even bug reports when things don't work : for example

    • It doesn't respect MaxQueryRows.
    • Why isn't it collapsible or it doesn't respond to Alt-1.
    • How do use repeatHeadersAt:5 with it.
    • And how you I get it to work on a nested class, etc
    • Why are the details evaluated even if they are never shown.

    Some of those can be fixed, but probably not all.

    At the moment, my preferred way of saving screen space is to use Util.OnDemand , i.e.

        var temp2 = Enumerable.Range(2, 2).Select(x => new
        {
            ha = x,
            name = new { first = $"name {x}"} ,
            hobby =  $"my hobby is {x}",
            detail = Util.OnDemand("...", () => new { test = $"{x} hi i am the detail. I should span multiple columns and be shown as the next row after this. Instead of showing me in the original row, user would click a detail column with ... as text" })
        }).Dump();
    
    

    Compressed, this looks similar to your version, although as you can see below it doesn't like quite as nice expanded or when shrunk back down.

    But sometimes I find this isn't quite enough, so I have several hacks that I use.
    I did have hack to add details as an extra row in a grid, but it was always shown. So inspired by your code, I tried modifying it to add the show/hide options (which actually makes it better now than it did before!).

    Not saying my way it is better than yours, just better is some ways and worse in others.

    It's better because I think it avoids the issues I've mentioned, and it works with lamdas which means that long running routines will only be called if they are needed (which is a big requirement for me)

    But it is worse in that it

    • only really works on the last column,
    • it does not display correctly in grids
    • Can't be called on an existing enumerable (as the detail object needs to be wrapped in a wrapper class)

    All I can really say is that it works better for me, but and again it is a hack and not really good enough to be included in LinqPad.

    It can also be used with another hack allows you to show or hide an object in the same cell.

    So you can get results that look like this :

    If anyone is interested, the code is available at https://share.linqpad.net/bqndrb4a.linq