Windows 10 ScrollIntoView() is not scrolling to the items in the middle of a listview
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.
Amar Zeno
Amar Zeno by name, Engineer by education, Mobile and Wearable Application Developer by passion. SOreadytohelp
Updated on June 11, 2022Comments
-
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 over 8 yearsEven 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 over 8 yearsWorked like a charm. Thanks Justin :)
-
Amar Zeno over 8 yearsHey 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 over 8 yearsConsider wrapping the logic inside an attached property. Have a look at stackoverflow.com/q/8370209/231837
-
Amar Zeno over 8 yearsThat 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 about 7 yearsYou are a genius. I love you!
-
Vijay Nirmal over 6 years@JustinXL Thanks a lot. With your help, I have successfully created SmoothScrollingHelper
-
Vijay Nirmal over 6 years@JustinXL If I use
ItemsStackPanel
asItemsPanel
then scrolling long distance is not working as expected. Do you know the reason for it? -
Justin XL over 6 yearsThat's the default item panel that enables virtualization. The updated answer was designed specifically for this.
-
Vijay Nirmal over 6 years@JustinXL I used the updated answer but I am expecting the problem
-
Vijay Nirmal over 6 years
-
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 over 6 years@JustinXL I have created the repo with your original code
-
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 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 checkif (newIndex < currentIndex)
, iftrue
, then first callawait ScrollToIndex(newIndex - 1)
and then callawait ScrollToIndex(newIndex)
again. Make sense? Also, you might want to use aListView
instead in this case. -
Vijay Nirmal over 6 years@JustinXL Can you report it to MSDN?
-
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 about 6 yearsScrolling large distance is not working for ItemsStackPanel. In this 'ScrollToItem' method 'selectedItem' is null
-
Justin XL about 6 years@SureshBalaraman that's exactly what the updated answer does.