Home
Options

Automatically scrolling when updating a scrollable StackPanel

I have a stack panel which I don't want to continue grow so I have fixed the height and set it to scroll.
Is there way I can get automatically scroll to display the last contents.
eg

Util.AutoScrollResults = true; // (Doesn't work inside StackPanel)
new StackPanel(true, new Button("Button1"), new Button("Button2")).Dump();

List<int> list = new List<int>();
DumpContainer d = new DumpContainer(list);

StackPanel sp1 = new StackPanel(false, d);
sp1.Styles["height"] ="250px";
sp1.Styles["border"] = "black 1px solid";
sp1.Styles["overflow-y"] = "scroll";

sp1.Dump();

new StackPanel(true, new Button("Button3"),  new Button("Button4")).Dump();

foreach(var i in Enumerable.Range(1, 15))
{
    list.Add(i);
    d.Refresh();
    System.Threading.Thread.Sleep(500);
}

which looks like

It would be nice if I could see the newest items automatically.

I know I can use

d.Content = showAll? list : list.Skip(list.Count - 9);

and have a button to toggle the showAll variable, but this is messy if the contents of the list are not of the same size, so I was wondering if there is a better way.

Comments

  • Options

    You could inject a script that could scroll for you. Though I'm not sure if there's a reliable way to identify elements that have been dumped. This seems to work.

        Util.HtmlHead.AddScript("""
        function AlwaysScrollToEnd(id) {
            new MutationObserver(mutations => {
                ScrollToEnd(mutations[0].addedNodes[0].id);
            }).observe(document.getElementById(id), {subtree: true, childList: true});
        }
        function ScrollToEnd(id) {
            var element = document.getElementById(id);
            var latest = element.querySelector('table > tbody > tr:last-child');
            latest?.scrollIntoViewIfNeeded();
        }
        """);
    
        Util.AutoScrollResults = true; // (Doesn't work inside StackPanel)
        new StackPanel(true, new Button("Button1"), new Button("Button2")).Dump();
    
        List<int> list = new List<int>();
        DumpContainer d = new DumpContainer(list);
    
        StackPanel sp1 = new StackPanel(false, d);
        sp1.Styles["height"] ="250px";
        sp1.Styles["border"] = "black 1px solid";
        sp1.Styles["overflow-y"] = "scroll";
    
        sp1.Dump();
    
        new StackPanel(true, new Button("Button3"),  new Button("Button4")).Dump();
    
        // need to check for mutations
        sp1.HtmlElement.InvokeScript(false, "AlwaysScrollToEnd", sp1.HtmlElement.ID);
    
        foreach(var i in Enumerable.Range(1, 15))
        {
            list.Add(i);
            d.Refresh();
            //sp1.HtmlElement.InvokeScript(false, "ScrollToEnd", sp1.HtmlElement.ID); // doesn't see the refreshed list unfortunately so will always be behind
            System.Threading.Thread.Sleep(500);
        }
    

    Side note, rather than refreshing the dump container to redraw the updated list, could using an observable work for your case?

  • Options

    That's great. Thank you very much.

    As for using an observable, I must admit I haven't used them for real.
    In my real example, I had tried using IAsyncEnumerable but that didn't work as I wanted to use Util.Highlight and it doesn't display that correctly.
    Also your AlwaysScrollToEnd script doesn't work as you can see if you run this script

    bool useIAsyncEnumerable = Util.ReadLine<Boolean>("Use useIAsyncEnumerable", true);
    
    Util.HtmlHead.AddScript("""
        function AlwaysScrollToEnd(id) {
            new MutationObserver(mutations => {
                ScrollToEnd(mutations[0].addedNodes[0].id);
            }).observe(document.getElementById(id), {subtree: true, childList: true});
        }
        function ScrollToEnd(id) {
            var element = document.getElementById(id);
            var latest = element.querySelector('table > tbody > tr:last-child');
            latest?.scrollIntoViewIfNeeded();
        }
        """);
    
    Util.AutoScrollResults = true; // (Doesn't work inside StackPanel)
    new StackPanel(true, new Button("Button1"), new Button("Button2")).Dump();
    
    List<object> list = new List<object>();
    DumpContainer d = new DumpContainer(useIAsyncEnumerable ? GetInfo() : list) ;  
    
    StackPanel sp1 = new StackPanel(false, d);
    sp1.Styles["height"] = "250px";
    sp1.Styles["border"] = "black 1px solid";
    sp1.Styles["overflow-y"] = "scroll";
    
    sp1.Dump();
    
    new StackPanel(true, new Button("Button3"), new Button("Button4")).Dump();
    
    // need to check for mutations
    sp1.HtmlElement.InvokeScript(false, "AlwaysScrollToEnd", sp1.HtmlElement.ID);
    
    if (!useIAsyncEnumerable)
    {
        foreach (var i in Enumerable.Range(1, 15))
        {
            var o = Util.HighlightIf(i % 2 == 0, new { i = 1, sq = i * i} );
            await System.Threading.Tasks.Task.Delay(100);
            list.Add(o);
            d.Refresh();
        }
    }
    
    async IAsyncEnumerable<object> GetInfo()
    {
        foreach (var i in Enumerable.Range(1, 15))
        {
            var o = Util.HighlightIf(i % 2 == 0, new { i = 1, sq = i * i });
            await System.Threading.Tasks.Task.Delay(100);       
            yield return o;
        }
    }
    
  • Options
    edited April 5

    The issue with the highlighting is an issue with how observables are dumped and how highlighting is handled. I don't think the highlighter was designed to be used with observable sequences. If you remove the highlighting, it will be dumped as a table like you'd expect.

    You can play around with observables with System.Reactive (check the System.Reactive.Linq namespace). An equivalent GetInfo() method could look like:

        IObservable<object> GetInfo2()
        {
            return Observable.Create(async (IObserver<object> obs) =>
            {
                foreach (var i in Enumerable.Range(1, 15))
                {
                    await Task.Delay(100);
                    var o = Util.HighlightIf(i % 2 == 0, new { i = 1, sq = i * i });
                    obs.OnNext(o);
                }
                obs.OnCompleted();
            });
        }
    
  • Options

    And to fix the scrolling with the observable version, the kinds of mutations changed so you'll need a new observer. Rather than re-rendering the whole table, the row is added directly to the existing table. So the added node could just be scrolled to.

        Util.HtmlHead.AddScript("""
            function AlwaysScrollToEnd(id) {
                new MutationObserver(mutations => {
                    ScrollToEnd(mutations[0].addedNodes[0].id);
                }).observe(document.getElementById(id), {subtree: true, childList: true});
            }
            function ScrollToEnd(id) {
                var element = document.getElementById(id);
                var latest = element.querySelector('table > tbody > tr:last-child');
                latest?.scrollIntoViewIfNeeded();
            }
    
            function AlwaysScrollToEndObservable(id) {
                new MutationObserver(mutations => {
                    mutations[0].addedNodes[0]?.scrollIntoViewIfNeeded();
                }).observe(document.getElementById(id), {subtree: true, childList: true});
            }
            """);
    
        // need to check for mutations
        sp1.HtmlElement.InvokeScript(false, useIAsyncEnumerable ? "AlwaysScrollToEndObservable" : "AlwaysScrollToEnd", sp1.HtmlElement.ID);
    
    
  • Options

    Thanks for you help.

    I've changed my real project to use observables and it is early days, but it appears to work OK.

    Off to read up more on Observable and MutationObserver.

Sign In or Register to comment.