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

10,578

Solution 1

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

Solution 2

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.

Share:
10,578
Amar Zeno
Author by

Amar Zeno

Amar Zeno by name, Engineer by education, Mobile and Wearable Application Developer by passion. SOreadytohelp

Updated on June 11, 2022

Comments

  • Amar Zeno
    Amar Zeno almost 2 years

    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?

  • Amar Zeno
    Amar Zeno over 8 years
    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.
  • Amar Zeno
    Amar Zeno over 8 years
    Worked like a charm. Thanks Justin :)
  • Amar Zeno
    Amar Zeno over 8 years
    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.
  • Justin XL
    Justin XL over 8 years
    Consider wrapping the logic inside an attached property. Have a look at stackoverflow.com/q/8370209/231837
  • Amar Zeno
    Amar Zeno over 8 years
    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/p37537v
  • Cole Peterson
    Cole Peterson about 7 years
    You are a genius. I love you!
  • Vijay Nirmal
    Vijay Nirmal over 6 years
    @JustinXL Thanks a lot. With your help, I have successfully created SmoothScrollingHelper
  • Vijay Nirmal
    Vijay Nirmal over 6 years
    @JustinXL If I use ItemsStackPanel as ItemsPanel then scrolling long distance is not working as expected. Do you know the reason for it?
  • Justin XL
    Justin XL over 6 years
    That's the default item panel that enables virtualization. The updated answer was designed specifically for this.
  • Vijay Nirmal
    Vijay Nirmal over 6 years
    @JustinXL I used the updated answer but I am expecting the problem
  • Vijay Nirmal
    Vijay Nirmal over 6 years
    @JustinXL For repo, you can use ItemsStackPanel for GridView in SmoothScrollingHelper
  • Vijay Nirmal
    Vijay Nirmal over 6 years
    @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.
  • Vijay Nirmal
    Vijay Nirmal over 6 years
    @JustinXL I have created the repo with your original code
  • Vijay Nirmal
    Vijay Nirmal over 6 years
    @JustinXL Scrolling large distance is not working. For example: Go to "500" and come back to "20". You will find the error
  • Justin XL
    Justin XL over 6 years
    @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.
  • Vijay Nirmal
    Vijay Nirmal over 6 years
    @JustinXL Can you report it to MSDN?
  • BCA
    BCA over 6 years
    @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.
  • Suresh Balaraman
    Suresh Balaraman about 6 years
    Scrolling large distance is not working for ItemsStackPanel. In this 'ScrollToItem' method 'selectedItem' is null
  • Justin XL
    Justin XL about 6 years
    @SureshBalaraman that's exactly what the updated answer does.