Doing AutoScroll with ScrollViewer.ScrollToEnd() only worked while debugging, event handler too simple
Asked Answered
C

2

7

Looking at this solution for a better autoscroll I thought myself to be so clever to find an easier solution, but it works only in a debug session:

    private void scrollviewer_Messages_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        ScrollViewer sv = sender as ScrollViewer;
        if (sv.VerticalOffset == sv.ScrollableHeight)
        {
            sv.ScrollToEnd();//debug breakpoint
        }
        return;
    }

While adding content to the textblock in this ScrollViewer, autoscroll works, the bottom of the text stays in view. When the user scrolls upwards, and more content is added, the bottom goes out of view, autoscroll is off, which is fine. When the user scrolls back to the bottom, ScrollToEnd() should turn autoscroll back on, but when more content is added, the bottom still scrolls out of view.

When I set the breakpoint, I can verify that ScrollToEnd() is indeed called. Then, after removing the breakpoint, and adding more content, autoscroll works again.

I add content by pressing a button, with code in the ViewModel, and Binding. So I am sure there is no concurrency problem. There is much time between adding content and manual scrolling.

This really baffles me, while I was so happy with my simple autoscroll solution. How can this not work?


edit:

I found out that autoscroll works again after scrolling back to the bottom, but somehow it is not so easy to really hit the bottom. I need to move the slider way down, AND click the down arrow of the scrollbar. I will now experiment with replacing the == sign in my code to allow a few pixels difference.


edit:

Would this problem be caused by the fact that the content is a TextBlock with a multi-line text string and TextWrap?

    <ScrollViewer Name="scrollviewer_Messages" DockPanel.Dock="Top" 
                  Height="100" Width="200"           
                  ScrollChanged="scrollviewer_Messages_ScrollChanged">
        <TextBlock Name="tb_Message"
               Margin="10" TextWrapping="Wrap"
               Text="{Binding Path=Messages}">
        </TextBlock>
    </ScrollViewer>

edit:

The problem went away with changing the formula in the event handler to:

 sv.ScrollableHeight - sv.VerticalOffset < 20

I experimented already with < 10 but pushpraj (see answer below) made me try larger numbers. Still unclear why this works, as the problem is not that ScrollToEnd() was not called.


about the solution:

The <20 is not needed because it is about fractional numbers. In general, two real numbers are never equal, but here that is not true. The double numbers for offset and height are really equal when the slider is at the end.

The problem is that, apparently, ScrollToEnd/Bottom() does not work while scrolling with the slider. That's it. I would call it a bug, but it might as well be a 'feature': one should not change the behaviour of the slider while the user is sliding it and expecting to be in control.

The fix is that first we slide the slider to the end, making Offset == Height. Step two is that adding content will increase Height, due to above bug the slider will move up just a little, in my case about 15 points. This raises a ScrollChanged event and the threshold of <20 is ample enough to get a second call of ScrollToBottom. This step two happens each time content is added.

My earlier edit mentioning clicking the down button works similar. Apparently, ScrollToEnd works for the down button.

The catch of course is that a bug is a bug. When adding more content at once, the threshold may not work, and autoscroll might stop.

The ultimate solution, not as simple as I was hoping for, but still not too complex, should be the one in my answer below.

Crop answered 10/9, 2014 at 9:25 Comment(7)
all you need is to scroll to the last when the text is added?Christiansen
@Christiansen Indeed. But not while the user scrolls to inspect older content. But again when the user scrolls back to the bottom.Crop
is your scrollviewer inside an ItemsControl ?Stinson
@eranotzap No, it is in a DockPanel. And ScrollToBottom() makes no difference.Crop
I think the problem is not that it does not occur , I think it tries to scroll before the content is actually added .Stinson
write a Debug.WriteLine("Scrolled"); , and check it not in debugStinson
@eranotzap I followed your advice but with printing the values of Height and Offset at each event. Also, I ran in debug mode, otherwise Debug.Write would not give output, but of course without a debug breakpoint. But no surprising results :-(Crop
C
7

The cause of the problem is that ScrollToEnd() has nothing to do with autoscroll. This call just scrolls to the end and that is it. By putting the call in the event handler it will scroll pretty often to the end but for a true autoscroll it is necessary to determine who fired the event: the user by moving the slider, or the slider by moving because of change of content size. Instead of ignoring "useless" events by looking at ExtentHeight, this property is now used to determine the who or what fired the event.

This solution saves the state of the autoscroll bit in the tag of the control. It would even be nicer to subclass to a new usercontrol AutoScrollViewer.

After all, this solution is not much "simpler" than the previous solutions as mentioned above in the question, it is merely a variation, but it is (hopefully) more accurate.

    /// <summary>
    /// If the scrollviewer is at the bottom, keep the bottom in view.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void scrollviewer_Messages_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        ScrollViewer sv = sender as ScrollViewer;
        bool AutoScrollToEnd = true;
        if (sv.Tag != null)
        {
            AutoScrollToEnd = (bool)sv.Tag;
        }
        if (e.ExtentHeightChange == 0)// user scroll
        {                
            AutoScrollToEnd = sv.ScrollableHeight == sv.VerticalOffset;
        }
        else// content change
        {                
            if (AutoScrollToEnd)
            {
                sv.ScrollToEnd();
            }
        }
        sv.Tag = AutoScrollToEnd;
        return;
    }
Crop answered 10/9, 2014 at 12:19 Comment(1)
I like your solution :-)Faletti
C
2

here is an improved version of the same

    private void scrollviewer_Messages_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        ScrollViewer sv = sender as ScrollViewer;
        //if (e.ExtentHeightChange != 0 && Math.Abs(sv.VerticalOffset - sv.ScrollableHeight) < 20)
        if(sv.ScrollableHeight - sv.VerticalOffset < 20)
        {
            sv.ScrollToEnd();
        }
    }

I have added a condition to see if the content height has changed otherwise it will be invoked for every scroll event, secondly I have added some tolerance (20) in this case as it is not always possible to precisely satisfy the condition sv.VerticalOffset == sv.ScrollableHeight. 20 is just a figure which give nice results.

in your example, it stop working after dragging the scroll bar but if you press scroll down button to reach to last you may make it work as expected which you usually see after debug.

Christiansen answered 10/9, 2014 at 10:8 Comment(3)
Thanks! I added a semicolon, and tried it, it works. But as I explained in my edit to the question, it does not need ExtentHeightChange or Abs. Also, why the tolerance of almost zero is needed is unclear as == really caused ScrollToEnd to be called. I will wait a little if an answer comes in that explains that, before marking your answer as accepted.Crop
the ; was removed in a attempt to removed the comment from original code. math.abs came to mind to make it positive, but reversing the offset and height is nice. i added ExtentHeightChange in a hope to avoid few extra calls to scroll to end. last but not the least happy coding :)Christiansen
This solution is pretty simple and will perhaps work in most cases. The catch is in the hard coded number. When adding content that causes more than 20 points of height, the autoscroll stops working. A subtle bug that might not be too annoying, or be hard to find.Crop

© 2022 - 2024 — McMap. All rights reserved.