Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to scroll to the next logical page in a ListBox with physical scrolling

I do have a listbox which must have CanContentScroll==false because i need to be able to scroll it smoothly. This enables physical scrolling.

I also want to scroll the listbox by page, but if i call the PageDown method on the listbox internal ScrollViewer the first row gets cutted because the listbox height is not a multiple of the row height.

I want the first row to be always completely visible, like when using logical scrolling.

Can someone give me a hint on how to do that?

like image 341
Zmaster Avatar asked Nov 05 '22 15:11

Zmaster


1 Answers

You'll get the same effect for a non virtualizing ItemsControl (ScrollViewer.CanContentScroll="False") as for a virtualizing one if you scroll down and then select the upper visible container with the mouse. This can also be done in code.

When CanContentScroll is set to false, virtualizing is turned off so all containers will be generated at all times. To get the top visible container we can iterate the containers from the top until we reach the VerticalOffset of the ScrollViewer. Once we got it we can simply call BringIntoView on it and it will align nicely at the top just like it would if virtualization was being used.

Example

<ListBox ItemsSource="{Binding MyCollection}"
         ScrollViewer.CanContentScroll="False"
         ScrollViewer.ScrollChanged="listBox_ScrollChanged" >

Call BringIntoView on the top visible container in the event handler

private void listBox_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
    ItemsControl itemsControl = sender as ItemsControl;
    ScrollViewer scrollViewer = e.OriginalSource as ScrollViewer;
    FrameworkElement lastElement = null;
    foreach (object obj in itemsControl.Items)
    {
        FrameworkElement element = itemsControl.ItemContainerGenerator.ContainerFromItem(obj) as FrameworkElement;
        double offset = element.TransformToAncestor(scrollViewer).Transform(new Point(0, 0)).Y + scrollViewer.VerticalOffset;
        if (offset > e.VerticalOffset)
        {
            if (lastElement != null)
                lastElement.BringIntoView();
            break;
        }
        lastElement = element;
    }
}

To only achieve this effect when you want to call PageDown, in a Button click for example, you can create an extension method for ListBox called LogicalPageDown.

listBox.LogicalPageDown();

ListBoxExtensions

public static class ListBoxExtensions
{
    public static void LogicalPageDown(this ListBox listBox)
    {
        ScrollViewer scrollViewer = VisualTreeHelpers.GetVisualChild<ScrollViewer>(listBox);
        ScrollChangedEventHandler scrollChangedHandler = null;
        scrollChangedHandler = (object sender2, ScrollChangedEventArgs e2) =>
        {
            scrollViewer.ScrollChanged -= scrollChangedHandler;
            FrameworkElement lastElement = null;
            foreach (object obj in listBox.Items)
            {
                FrameworkElement element = listBox.ItemContainerGenerator.ContainerFromItem(obj) as FrameworkElement;
                double offset = element.TransformToAncestor(scrollViewer).Transform(new Point(0, 0)).Y + scrollViewer.VerticalOffset;
                if (offset > scrollViewer.VerticalOffset)
                {
                    if (lastElement != null)
                        lastElement.BringIntoView();
                    break;
                }
                lastElement = element;
            }
        };
        scrollViewer.ScrollChanged += scrollChangedHandler;
        scrollViewer.PageDown();
    }
}

I noticed in your question that you already got the ScrollViewer but I'm adding an implementation to GetVisualChild if anyone else comes across this question

public static T GetVisualChild<T>(DependencyObject parent) where T : Visual
{
    T child = default(T);

    int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
    for (int i = 0; i < numVisuals; i++)
    {
        Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
        child = v as T;
        if (child == null)
        {
            child = GetVisualChild<T>(v);
        }
        if (child != null)
        {
            break;
        }
    }
    return child;
}
like image 155
Fredrik Hedblad Avatar answered Nov 14 '22 22:11

Fredrik Hedblad