Collapsable panel control

Hi,

is there any collapsable panel (accordion ?) ready to use in LINQPad controls?

Or, can I dump an object with the root level collapsed (collapseTo: 0 collapses all levels, not just the root one, which makes sense).

Right now, I'm using Util.OnDemand method, I just prefer the default header of a collapsed pre-rendered Dump instead of just a clickable string

Regards

Best Answer

  • JeffMercado
    edited August 4 Answer ✓

    I've implemented one that has the appearance of Util.OnDemand, but can be collapsed back to the header. Nothing builtin yet. You're welcome to try this.

        public static class Dump;
    
        public static class DumpExtensions
        {
            extension(Dump)
            {
                public static ICollapsible<T> Collapsible<T>(T item, object label, bool collapsed = false) => Util.Collapsible(item, label, collapsed).Dump();
                public static ICollapsible<T> Collapsible<T>(T item, object label, string description, bool collapsed = false) => Util.Collapsible(item, label, collapsed).Dump(description);
                public static ICollapsible<T> Collapsible<T>(T item, object label, DumpOptions options, bool collapsed = false) => Util.Collapsible(item, label, collapsed).Dump(options);
            }
        }
    
        public static class UtilExtensions
        {
            extension(Util)
            {
                public static ICollapsible<T> Collapsible<T>(T item, object label, bool collapsed = false) => new CollapsibleImpl<T>(item, label, collapsed);
            }
    
            private class HeaderedImpl<T>(T item, object label) : IHeadered<T>
            {
                public object Label => label;
                public T Item => item;
    
                private object ToDump()
                {
                    return Renderer.FlexColumn([HeaderedLabel(), new DumpContainer(item).ToControl()], "headered headingpresenter");
    
                    object HeaderedLabel() => Renderer.ControlWithContent("h1", label, c => c.CssClass = "headingpresenter");
                }
            }
    
            private class CollapsibleImpl<T>(T item, object label, bool collapsed) : HeaderedImpl<T>(item, label), ICollapsible<T>
            {
                private bool collapsed = collapsed;
    
                public bool IsCollapsed
                {
                    get => collapsed;
                    set => SetCollapsed(value);
                }
    
                public void Toggle() => SetCollapsed(!collapsed);
                public void SetCollapsed(bool value)
                {
                    collapsed = value;
                    if (container is not null)
                    {
                        RefreshContainer(container, Item, value);
                    }
                }
    
                private DumpContainer? container;
                private object ToDump()
                {
                    if (container is null)
                        RefreshContainer(container = new(), Item, collapsed);
                    return Renderer.FlexColumn([TogglingLabel(), Renderer.ControlWithDumpContainer("div", container)], "collapsible");
    
                    object TogglingLabel()
                    {
                        var link = Renderer.ControlWithContent("a", Label, c => { c.CssClass = ""; c.IsMultithreaded = true; });
                        link.Click += (_, _) => Toggle();
                        return link;
                    }
                }
                private static void RefreshContainer(DumpContainer container, T item, bool collapsed)
                {
                    container.Style = collapsed ? "display: none;" : "";
                    if (!collapsed && container.Content != (object?)item)
                        container.UpdateContent(item);
                }
            }
        }
    
        public interface IHeadered<T>
        {
            object Label { get; }
            T Item { get; }
        }
    
        public interface ICollapsible<T> : IHeadered<T>
        {
            bool IsCollapsed { get; set; }
            void Toggle() => SetCollapsed(!IsCollapsed);
            void Expand() => SetCollapsed(false);
            void Collapse() => SetCollapsed(true);
            void SetCollapsed(bool value);
        }
    

    Oof, just realized the Renderer classes. Those are just convenience methods to create elements with bootstrap styling. should be able to replace with plain LINQPad.Controls.Control objects.

Answers

  • clem0338
    edited August 5

    @JeffMercado, based on your reply, here is my version.
    Thanks

    internal static class UtilEx {
        extension(Util) {
            public static ICollapsible<T> Collapsible<T>(T item, bool collapsed = true, DumpOptions? options = null) => new CollapsibleImpl<T>(item, collapsed, options);
        }
        private class CollapsibleImpl<T>(T item, bool collapsed, DumpOptions? options) : ICollapsible<T> {
            private readonly Label _arrow = new Label("");
            private readonly Label _label = new Label("Expand");
            private DumpContainer _dc = null!;
            private string? _description;
            public object? Content { get; set { if (object.Equals(value, field)) return; field = value; _dc.UpdateContent(value); RefreshContainer(true); } } = item;
            public bool IsCollapsed { get; set { if (value == field) return; Toggle(field = value); RefreshContainer(); } } = collapsed;
            public void Toggle(bool? collapsed = null) => IsCollapsed = collapsed ?? !IsCollapsed;
            private object ToDump() {
                if (_dc == null) {
                    _dc = new DumpContainer(item);
    
                    if (options != null) {
                        var dest = (IDictionary<string, object?>)Util.ToExpando(_dc.DumpOptions);
                        foreach (var (key, value) in (IDictionary<string, object?>)Util.ToExpando(options))
                            dest[key] = value;
                    }
                }
    
                RefreshContainer();
    
                var header = new StackPanel(true, _arrow, _label);
                header.Styles["font-size"] = ".95em";
                header.Styles["background-color"] = "#3887B5";
                header.Styles["color"] = "#eee";
                header.Styles["border"] = "1px solid #3887B5";
                header.Styles["padding"] = "0 0.3em 0.25em 0.2em";
                header.Click += (s, a) => Toggle();
    
                var dc = _dc.ToControl();
                dc.CssChildRules[">div>table", "margin"] = "0";
                dc.CssChildRules[">div>table", "border"] = "none";
                dc.CssChildRules[">div>table>thead", "display"] = "none";
    
                var result = new StackPanel(false, "0", header, dc);
                result.Styles["font-family"] = "Segoe UI, ui-sans-serif, system-ui, Inter, Verdana, sans-serif";
                result.Styles["font-size"] = "10pt";
                result.Styles["border-collapse"] = "collapse";
                result.Styles["border-spacing"] = "0";
                result.Styles["border"] = "2px solid #3887B5";
                result.Styles["margin"] = "0.3em 0.1em 0.2em 0.1em";
                return result;
            }
            private void RefreshContainer(bool force = false) {
                if (_description == null || force)
                    _description = Content?.ToString() ?? "NULL";
    
                if (IsCollapsed) {
                    _label.Text = $"{_description} (Collapsed)";
                    _dc.Style = "display: none;";
                    _arrow.CssClass = "arrow-down";
                } else {
                    _label.Text = $"{_description} (Expanded)";
                    _dc.Style = "";
                    _arrow.CssClass = "arrow-up";
                }
            }
        }
    }
    
    public interface ICollapsible<T> {
        bool IsCollapsed { get; set; }
        void Toggle(bool? collapsed = null);
        void Expand() => Toggle(false);
        void Collapse() => Toggle(true);
    }
    
  • @clem0338 I think your version ignores any DumpOptions that you pass to it.

    My attempt at this was very basic. No arrows, no support for DumpOptions and visually relies on me knowing that blue lines are links

    void Main()
    {
        var list = Enumerable.Range(1,100).ToList();
    
        (new { c1 = list.Collapsible() , c2 = list.Collapsible("List of Integers")}).Dump("Example");       
    }
    
    public static class Extensions
    {
        public static DumpContainer Collapsible<T>(this T obj, 
                [System.Runtime.CompilerServices.CallerArgumentExpression("obj")] string description = null!)
        {
            DumpContainer body = new DumpContainer(obj);
            ToggleVisibility(); // Switch from visible to hidden.   
    
            return new DumpContainer(Util.VerticalRun(new Hyperlinq(ToggleVisibility, description), body));
    
            void ToggleVisibility() => body.Style = String.IsNullOrEmpty(body.Style) ? "display: none;" : "";
        }
    }