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?

Amar Zeno
  • 412
  • 1
  • 6
  • 23

4 Answers4

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);
Community
  • 1
  • 1
Justin XL
  • 38,763
  • 7
  • 88
  • 133
  • 1
    Worked like a charm. Thanks Justin :) – Amar Zeno Sep 14 '15 at 10:01
  • 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. – Amar Zeno Sep 17 '15 at 03:54
  • 2
    Consider wrapping the logic inside an attached property. Have a look at http://stackoverflow.com/q/8370209/231837 – Justin XL Sep 17 '15 at 04:36
  • 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 http://tinyurl.com/p37537v – Amar Zeno Sep 17 '15 at 08:01
  • 1
    You are a genius. I love you! – Cole Peterson Mar 02 '17 at 05:03
  • 1
    @JustinXL Thanks a lot. With your help, I have successfully created [SmoothScrollingHelper](https://github.com/Vijay-Nirmal/SmoothScrollingHelper) – Vijay Nirmal Sep 05 '17 at 19:17
  • @JustinXL If I use `ItemsStackPanel` as `ItemsPanel` then scrolling long distance is not working as expected. Do you know the reason for it? – Vijay Nirmal Sep 06 '17 at 12:30
  • That's the default item panel that enables virtualization. The updated answer was designed specifically for this. – Justin XL Sep 06 '17 at 12:49
  • @JustinXL I used the updated answer but I am expecting the problem – Vijay Nirmal Sep 06 '17 at 13:27
  • @JustinXL For repo, you can use `ItemsStackPanel` for `GridView` in [SmoothScrollingHelper](https://github.com/Vijay-Nirmal/SmoothScrollingHelper) – Vijay Nirmal Sep 06 '17 at 19:10
  • @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 Sep 07 '17 at 08:22
  • @JustinXL I have created [the repo](https://github.com/Vijay-Nirmal/Repo/tree/master) with your original code – Vijay Nirmal Sep 07 '17 at 15:08
  • @JustinXL Scrolling large distance is not working. For example: Go to "500" and come back to "20". You will find the error – Vijay Nirmal Sep 08 '17 at 10:34
  • @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. – Justin XL Sep 08 '17 at 11:20
  • @JustinXL Can you report it to MSDN? – Vijay Nirmal Sep 08 '17 at 11:38
  • @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. – BCA Dec 01 '17 at 19:59
  • Scrolling large distance is not working for ItemsStackPanel. In this 'ScrollToItem' method 'selectedItem' is null – Suresh Balaraman Feb 07 '18 at 13:04
  • @SureshBalaraman that's exactly what the updated answer does. – Justin XL Feb 07 '18 at 17:56
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.

Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • 2
    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 Sep 14 '15 at 06:03
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

EJL
  • 205
  • 2
  • 10
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);
IQ.feature
  • 600
  • 6
  • 16