Doing AutoScroll with ScrollViewer.ScrollToEnd() o

2019-06-26 19:56发布

问题:

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.

回答1:

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;
    }


回答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.