Windows 10 Scrollintoview() Is Not Scrolling to the Items in the Middle of a Listview

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

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

Make ListView.ScrollIntoView Scroll the Item into the Center of the ListView (C#)

It is very easy to do this in WPF with an extension method I wrote. All you have to do to scroll an item to the center of the view is to call a single method.

Suppose you have this XAML:

<ListView x:Name="view" ItemsSource="{Binding Data}" /> 
<ComboBox x:Name="box" ItemsSource="{Binding Data}"
SelectionChanged="ScrollIntoView" />

Your ScrollIntoView method will be simply:

private void ScrollIntoView(object sender, SelectionChangedEventArgs e)
{
view.ScrollToCenterOfView(box.SelectedItem);
}

Obviously this could be done using a ViewModel as well rather than referencing the controls explicitly.

Following is the implementation. It is very general, handling all the IScrollInfo possibilities. It works with ListBox or any other ItemsControl, and works with any panel including StackPanel, VirtualizingStackPanel, WrapPanel, DockPanel, Canvas, Grid, etc.

Just put this in a .cs file somewhere in your project:

public static class ItemsControlExtensions
{
public static void ScrollToCenterOfView(this ItemsControl itemsControl, object item)
{
// Scroll immediately if possible
if(!itemsControl.TryScrollToCenterOfView(item))
{
// Otherwise wait until everything is loaded, then scroll
if(itemsControl is ListBox) ((ListBox)itemsControl).ScrollIntoView(item);
itemsControl.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
itemsControl.TryScrollToCenterOfView(item);
}));
}
}

private static bool TryScrollToCenterOfView(this ItemsControl itemsControl, object item)
{
// Find the container
var container = itemsControl.ItemContainerGenerator.ContainerFromItem(item) as UIElement;
if(container==null) return false;

// Find the ScrollContentPresenter
ScrollContentPresenter presenter = null;
for(Visual vis = container; vis!=null && vis!=itemsControl; vis = VisualTreeHelper.GetParent(vis) as Visual)
if((presenter = vis as ScrollContentPresenter)!=null)
break;
if(presenter==null) return false;

// Find the IScrollInfo
var scrollInfo =
!presenter.CanContentScroll ? presenter :
presenter.Content as IScrollInfo ??
FirstVisualChild(presenter.Content as ItemsPresenter) as IScrollInfo ??
presenter;

// Compute the center point of the container relative to the scrollInfo
Size size = container.RenderSize;
Point center = container.TransformToAncestor((Visual)scrollInfo).Transform(new Point(size.Width/2, size.Height/2));
center.Y += scrollInfo.VerticalOffset;
center.X += scrollInfo.HorizontalOffset;

// Adjust for logical scrolling
if(scrollInfo is StackPanel || scrollInfo is VirtualizingStackPanel)
{
double logicalCenter = itemsControl.ItemContainerGenerator.IndexFromContainer(container) + 0.5;
Orientation orientation = scrollInfo is StackPanel ? ((StackPanel)scrollInfo).Orientation : ((VirtualizingStackPanel)scrollInfo).Orientation;
if(orientation==Orientation.Horizontal)
center.X = logicalCenter;
else
center.Y = logicalCenter;
}

// Scroll the center of the container to the center of the viewport
if(scrollInfo.CanVerticallyScroll) scrollInfo.SetVerticalOffset(CenteringOffset(center.Y, scrollInfo.ViewportHeight, scrollInfo.ExtentHeight));
if(scrollInfo.CanHorizontallyScroll) scrollInfo.SetHorizontalOffset(CenteringOffset(center.X, scrollInfo.ViewportWidth, scrollInfo.ExtentWidth));
return true;
}

private static double CenteringOffset(double center, double viewport, double extent)
{
return Math.Min(extent - viewport, Math.Max(0, center - viewport/2));
}
private static DependencyObject FirstVisualChild(Visual visual)
{
if(visual==null) return null;
if(VisualTreeHelper.GetChildrenCount(visual)==0) return null;
return VisualTreeHelper.GetChild(visual, 0);
}
}

ScrollIntoView property not working for gridview in windows 10 universal app

I have replied your same question in MSDN: https://social.msdn.microsoft.com/Forums/windowsapps/en-US/d0a772b3-80b9-4a11-92a9-89963c29a52f/scrollintoview-property-not-working-for-gridview-in-windows-10-universal-app?forum=wpdevelop

You need to have something more to distinguish items, for example, give every image a name since items you bind to GridView are same, ScrollIntoView default find the first one.

And commonly you need to set a height property for the GridView.

For more complex requirements, there is a good thread you can reference:
Windows 10 ScrollIntoView() is not scrolling to the items in the middle of a listview

Can I sync scrolling between multiple Listview items in UWP?

What you have described in your question can be done by the following steps:

  1. Find the ScrollViewer inside the ListView (check out GetScrollViewer from this answer).
  2. Subscribe to the ViewChanged event.
  3. Inside the event handler, call ChangeView on the other ScrollViewer.

Assume you are scrolling vertically -

private void SyncScrollViewers()
{
var scrollViewer1 = MyListView1.GetScrollViewer();
var scrollViewer2 = MyListView2.GetScrollViewer();

scrollViewer1.ViewChanged += (s, e) =>
{
scrollViewer2.ChangeView(null, scrollViewer1.VerticalOffset, null, false);
};
}

How do I ScrollIntoView after changing the filter on a ListView in a MVVM (WPF) App?

Keep the SelectedItem of your ListView in a property:

public MyTypeOfObject SelectedItem { get; set; }

Assign the binding to XAML:

<ListView Name="MyListView" SelectedItem="{Binding SelectedItem}"...></ListView>

Now whenever you change filter do:

if (SelectedItem != null)
MyListView.ScrollIntoView(SelectedItem);

EDIT:

To do it in your user control, in order to leave you view model clean from control references (ListView), catch a standard CollectionView event there or define your own that will be fired after filter or other job happened.

Synced 2 ListView ScrollViewer together. How to stop firing events multiple time?

In your this way to register both ViewChanged event of scrollViewer1 and scrollViewer2, the ViewChanged event of each ScrollViewer will trigger when manipulations such as scrolling and zooming have caused the view to change.

But you can operate the ViewChanged event handler to decide whether or when the ScrollViewer should scroll again as the following code.

private bool ScrollViewer1Scrolled, ScrollViewer2Scrolled;
private void SyncScrollViewers()
{
ScrollViewer scrollViewer1 = ListViewOne.GetScrollViewer();
ScrollViewer scrollViewer2 = ListViewTwo.GetScrollViewer();

if (scrollViewer1 == null || scrollViewer2 == null) return;
scrollViewer1.ViewChanged += (s, e) =>
{
ScrollViewer1Scrolled = true;
if (!ScrollViewer2Scrolled)
{
scrollViewer2.ChangeView(null, scrollViewer1.VerticalOffset, null, false);
Debug.WriteLine("scrollViewer2 scrolled");
}
ScrollViewer2Scrolled = false;
};

scrollViewer2.ViewChanged += (s, e) =>
{
ScrollViewer2Scrolled = true;
if (!ScrollViewer1Scrolled)
{
scrollViewer1.ChangeView(null, scrollViewer2.VerticalOffset, null, false);
Debug.WriteLine("scrollViewer1 scrolled");
}
ScrollViewer1Scrolled = false;
};
}


Related Topics



Leave a reply



Submit