Amar Zeno Amar Zeno - 12 days ago 7
C# Question

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

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?

Answer

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