Windows 10 ScrollIntoView() is not scrolling to the items in the middle of a listview
Asked Answered
T

4

19

I have a Listview with 20 items in it. I want to scroll the Listview programmatically.

ListView?.ScrollIntoView(ListView.Items[0])

will scroll the listview to the first item.

ListView?.ScrollIntoView(ListView.Items.Count - 1)

will scroll the listview to the bottom of the page.

However, I am unable to use the same function to scroll the listview to an item in middle.

Eg: ListView?.ScrollIntoView(ListView.Items[5])

should scroll and take me to the 5th item of the list. But instead its taking me to the first item of the list.

Would be great if this behaviour can be achieved with some workaround?

Trailer answered 14/9, 2015 at 4:15 Comment(0)
L
29

I think what you are looking for is a method to actually scroll an element to the top of the ListView.

In this post, I created an extension method that scrolls to a particular element within a ScrollViewer.

The idea is the same in your case.

You need to first find the ScrollViewer instance within your ListView, then the actual item to scroll to, that is, a ListViewItem.

Here is an extension method to get the ScrollViewer.

public static ScrollViewer GetScrollViewer(this DependencyObject element)
{
    if (element is ScrollViewer)
    {
        return (ScrollViewer)element;
    }

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
    {
        var child = VisualTreeHelper.GetChild(element, i);

        var result = GetScrollViewer(child);
        if (result == null)
        {
            continue;
        }
        else
        {
            return result;
        }
    }

    return null;
}

Once I get the ScrollViewer instance, I have created two more extension methods to scroll to an item based on its index or attached object respectively. Since ListView and GridView are sharing the same base class ListViewBase. These two extension methods should also work for GridView.

Update

Basically, the methods will first find the item, if it's already rendered, then scroll to it right away. If the item is null, it means the virtualization is on and the item has yet to be realized. So to realize the item first, call ScrollIntoViewAsync (task-based method to wrap the built-in ScrollIntoView, same as ChangeViewAsync, which offers much cleaner code), calculate the position and save it. Since now I know the position to scroll to, I need to first scroll the item all the way back to its previous position instantly (i.e. without animation), and then finally scroll to the desired position with animation.

public async static Task ScrollToIndex(this ListViewBase listViewBase, int index)
{
    bool isVirtualizing = default(bool);
    double previousHorizontalOffset = default(double), previousVerticalOffset = default(double);

    // get the ScrollViewer withtin the ListView/GridView
    var scrollViewer = listViewBase.GetScrollViewer();
    // get the SelectorItem to scroll to
    var selectorItem = listViewBase.ContainerFromIndex(index) as SelectorItem;

    // when it's null, means virtualization is on and the item hasn't been realized yet
    if (selectorItem == null)
    {
        isVirtualizing = true;

        previousHorizontalOffset = scrollViewer.HorizontalOffset;
        previousVerticalOffset = scrollViewer.VerticalOffset;

        // call task-based ScrollIntoViewAsync to realize the item
        await listViewBase.ScrollIntoViewAsync(listViewBase.Items[index]);

        // this time the item shouldn't be null again
        selectorItem = (SelectorItem)listViewBase.ContainerFromIndex(index);
    }

    // calculate the position object in order to know how much to scroll to
    var transform = selectorItem.TransformToVisual((UIElement)scrollViewer.Content);
    var position = transform.TransformPoint(new Point(0, 0));

    // when virtualized, scroll back to previous position without animation
    if (isVirtualizing)
    {
        await scrollViewer.ChangeViewAsync(previousHorizontalOffset, previousVerticalOffset, true);
    }

    // scroll to desired position with animation!
    scrollViewer.ChangeView(position.X, position.Y, null);
}

public async static Task ScrollToItem(this ListViewBase listViewBase, object item)
{
    bool isVirtualizing = default(bool);
    double previousHorizontalOffset = default(double), previousVerticalOffset = default(double);

    // get the ScrollViewer withtin the ListView/GridView
    var scrollViewer = listViewBase.GetScrollViewer();
    // get the SelectorItem to scroll to
    var selectorItem = listViewBase.ContainerFromItem(item) as SelectorItem;

    // when it's null, means virtualization is on and the item hasn't been realized yet
    if (selectorItem == null)
    {
        isVirtualizing = true;

        previousHorizontalOffset = scrollViewer.HorizontalOffset;
        previousVerticalOffset = scrollViewer.VerticalOffset;

        // call task-based ScrollIntoViewAsync to realize the item
        await listViewBase.ScrollIntoViewAsync(item);

        // this time the item shouldn't be null again
        selectorItem = (SelectorItem)listViewBase.ContainerFromItem(item);
    }

    // calculate the position object in order to know how much to scroll to
    var transform = selectorItem.TransformToVisual((UIElement)scrollViewer.Content);
    var position = transform.TransformPoint(new Point(0, 0));

    // when virtualized, scroll back to previous position without animation
    if (isVirtualizing)
    {
        await scrollViewer.ChangeViewAsync(previousHorizontalOffset, previousVerticalOffset, true);
    }

    // scroll to desired position with animation!
    scrollViewer.ChangeView(position.X, position.Y, null);
}

public static async Task ScrollIntoViewAsync(this ListViewBase listViewBase, object item)
{
    var tcs = new TaskCompletionSource<object>();
    var scrollViewer = listViewBase.GetScrollViewer();

    EventHandler<ScrollViewerViewChangedEventArgs> viewChanged = (s, e) => tcs.TrySetResult(null);
    try
    {
        scrollViewer.ViewChanged += viewChanged;
        listViewBase.ScrollIntoView(item, ScrollIntoViewAlignment.Leading);
        await tcs.Task;
    }
    finally
    {
        scrollViewer.ViewChanged -= viewChanged;
    }
}

public static async Task ChangeViewAsync(this ScrollViewer scrollViewer, double? horizontalOffset, double? verticalOffset, bool disableAnimation)
{
    var tcs = new TaskCompletionSource<object>();

    EventHandler<ScrollViewerViewChangedEventArgs> viewChanged = (s, e) => tcs.TrySetResult(null);
    try
    {
        scrollViewer.ViewChanged += viewChanged;
        scrollViewer.ChangeView(horizontalOffset, verticalOffset, null, disableAnimation);
        await tcs.Task;
    }
    finally
    {
        scrollViewer.ViewChanged -= viewChanged;
    }
}


A simpler approach, but without animation

You can also use the new overload of ScrollIntoView by specifying the second parameter to make sure the item is aligned on the top edge; however, doing so doesn't have the smooth scrolling transition in my previous extension methods.

MyListView?.ScrollIntoView(MyListView.Items[5], ScrollIntoViewAlignment.Leading);
Limann answered 14/9, 2015 at 7:46 Comment(18)
Worked like a charm. Thanks Justin :)Trailer
Hey Justin! I would like to know how I could append the same logic in a MVVM approach. Like using the changeview logic inside converter and binding it directly to a ListView scrollviewer XAML.Trailer
Consider wrapping the logic inside an attached property. Have a look at https://mcmap.net/q/366861/-how-to-scroll-to-the-bottom-of-a-scrollviewer-automatically-with-xaml-and-binding/231837Limann
That helped. However, I am attaching the arrow keys up and down to the scrolling behaviour. The scrolling behaviour works when the Listview doesn't have focus. When the listview has focus and when I use the arrow keys, the default scrolling behaviour of ListView is triggered and not my attached behaviour. Check tinyurl.com/p37537vTrailer
You are a genius. I love you!Ostrander
@JustinXL Thanks a lot. With your help, I have successfully created SmoothScrollingHelperAlaric
@JustinXL If I use ItemsStackPanel as ItemsPanel then scrolling long distance is not working as expected. Do you know the reason for it?Alaric
That's the default item panel that enables virtualization. The updated answer was designed specifically for this.Limann
@JustinXL I used the updated answer but I am expecting the problemAlaric
@JustinXL For repo, you can use ItemsStackPanel for GridView in SmoothScrollingHelperAlaric
@JustinXL I want to check it but that helper code is same as your original code. Only thing I added is an Enum to position the item after scrolling.Alaric
@JustinXL I have created the repo with your original codeAlaric
@JustinXL Scrolling large distance is not working. For example: Go to "500" and come back to "20". You will find the errorAlaric
@VijayNirmal interesting find! Looks like the built-in ChangeView animation doesn't work well when the new index is less than the current one, with virtualization on. What you can do is to check if (newIndex < currentIndex), if true, then first call await ScrollToIndex(newIndex - 1) and then call await ScrollToIndex(newIndex) again. Make sense? Also, you might want to use a ListView instead in this case.Limann
@JustinXL Can you report it to MSDN?Alaric
@JustinXL How would I modify your extension method(s) if I don't want any scrolling if the desired item is already fully in view? E.g. if items 5 to 10 are visible, and I do a ScrollToIndex(6), I don't want any scrolling to happen.Henka
Scrolling large distance is not working for ItemsStackPanel. In this 'ScrollToItem' method 'selectedItem' is nullChiasmus
@SureshBalaraman that's exactly what the updated answer does.Limann
A
1

ScrollIntoView just brings the item into the view, period, it does not scroll to a row.

If you call it on a member and it is below the bottom of the visible list it scrolls down until the the item is the last member in the visible list.

If you call it on a member and it is above the top of the list it scrolls up until the the item is the first member in the list.

If you call it on a member and it is currently visible it does no operation at all.

Aldershot answered 14/9, 2015 at 4:25 Comment(1)
Even when I call a member which is below the bottom of the visible list, the function doesn't work. Assume, I have 3 items in the visible listview. ListView.ScrollIntoView(abc.ItemsCollection[6].item) should ideally bring the 6th item as the last item of the visible list view. But nothing is happening in my case.Trailer
D
0

I solve this like:

 var sv = new ScrollViewerHelper().GetScrollViewer(listView);
        sv.UpdateLayout();
        sv.ChangeView(0, sv.ExtentHeight, null);

And the GetScrollViewer method:

public ScrollViewer GetScrollViewer(DependencyObject element)
    {
        if (element is ScrollViewer)
        {
            return (ScrollViewer)element;
        }

        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
        {
            var child = VisualTreeHelper.GetChild(element, i);

            var result = GetScrollViewer(child);
            if (result == null)
            {
                continue;
            }
            else
            {
                return result;
            }
        }

        return null;
    }

credits to the owner of the code

Deformity answered 22/11, 2016 at 3:4 Comment(0)
H
0

Try this:

listView.SelectedIndex = i;
SemanticZoomLocation location = new SemanticZoomLocation {
    Item = listView.SelectedItem
};
listView.MakeVisible(location);

Or scroll to middle, without selection item:

SemanticZoomLocation location = new SemanticZoomLocation {
    Item = listView.Items[listView.Items.Count / 2]
};
listView.MakeVisible(location);
Hyetal answered 6/9, 2021 at 2:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.