Make Listview.Scrollintoview Scroll the Item into the Center of the Listview (C#)

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

Centering selected item in a scroll viewer

Try ListView.ScrollIntoView() or ListView.MakeVisible first to scroll the container of the item into view and work around it being possibly virtualized out of the UI. Then use
ListView.ItemContainerGenerator.ContainerFromIndex() to get the container of the item and then the VisualTreeHelper to get its position relative to the ScrollViewer. Then scroll the scrollviewer by the calculated offset.

*EDIT - Example positioning logic:

Get the VisualTreeHelperExtensions from WinRT XAML Toolkit to get access to the ScrollViewer easily with GetFirstDescendantOfType() extension method that wraps some calls to the VisualTreeHelper.

XAML

<Page
x:Class="ListViewItemCentering.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ListViewItemCentering"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<Grid
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ListView
x:Name="listView">
<ListView.ItemTemplate>
<DataTemplate>
<Border
Width="400"
Height="100">
<ContentControl
Content="{Binding}"
FontSize="48"
Padding="20,10"/>
</Border>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>

<Button
Content="Skip"
Width="200"
Height="100"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Click="ButtonBase_OnClick"/>
</Grid>
</Page>

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using WinRTXamlToolkit.Controls.Extensions;

namespace ListViewItemCentering
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
private Random random = new Random();
public MainPage()
{
this.InitializeComponent();
this.listView.ItemsSource = Enumerable.Range(1, 1000);
this.listView.SelectionChanged += OnListViewSelectionChanged;
}

private async void OnListViewSelectionChanged(object sender, SelectionChangedEventArgs selectionChangedEventArgs)
{
if (listView.SelectedItem == null)
{
return;
}

var item = listView.SelectedItem;

// Calculations relative to screen or ListView
var listViewItem = (FrameworkElement)listView.ContainerFromItem(item);

if (listViewItem == null)
{
listView.ScrollIntoView(item);
}

while (listViewItem == null)
{
await Task.Delay(1); // wait for scrolling to complete - it takes a moment
listViewItem = (FrameworkElement)listView.ContainerFromItem(item);
}

var topLeft =
listViewItem
.TransformToVisual(listView)
.TransformPoint(new Point()).Y;
var lvih = listViewItem.ActualHeight;
var lvh = listView.ActualHeight;
var desiredTopLeft = (lvh - lvih) / 2.0;
var desiredDelta = topLeft - desiredTopLeft;

// Calculations relative to the ScrollViewer within the ListView
var scrollViewer = listView.GetFirstDescendantOfType<ScrollViewer>();
var currentOffset = scrollViewer.VerticalOffset;
var desiredOffset = currentOffset + desiredDelta;
scrollViewer.ScrollToVerticalOffset(desiredOffset);

// better yet if building for Windows 8.1 to make the scrolling smoother use:
// scrollViewer.ChangeView(null, desiredOffset, null);
}

private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
this.listView.SelectedIndex = random.Next(0, ((IEnumerable<int>)this.listView.ItemsSource).Count());
}
}
}

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

Scroll XAML ListView line-wise instead of item-wise

There is no easy way, to scroll line by line. However you can try this:

By default a ListBox/ListView scrolls "intelligently" one item at a time. This behaviour is provided by a Scrollviewer, by default the Property CanContentScroll is set to true which indicates that the items panel, f.e. a StackPanel is responsible for the scrolling and tries to scroll one item at a time.

To get close to your behaviour you simply set the Attached Property CanContentScroll to false, which enables to scroll pixel per pixel.

<ListView ScrollViewer.CanContentScroll="False" ... />

Also have a look at the link mentioned by Default which explains the interface IScrollInfo. As he mentioned you can find tutorials here. However in my opinion this is way too much effort, for some simple scrolling. I normally use the easy workaround.

mvvm how to make a list view auto scroll to a new Item in a list view

This solution is for a ListBox, but it could be modified for a ListView... This will scroll the selected item into view when you change the selected item from the ViewModel.

Class:

/// <summary>
/// ListBoxItem Behavior class
/// </summary>
public static class ListBoxItemBehavior
{
#region IsBroughtIntoViewWhenSelected

/// <summary>
/// Gets the IsBroughtIntoViewWhenSelected value
/// </summary>
/// <param name="listBoxItem"></param>
/// <returns></returns>
public static bool GetIsBroughtIntoViewWhenSelected(ListBoxItem listBoxItem)
{
return (bool)listBoxItem.GetValue(IsBroughtIntoViewWhenSelectedProperty);
}

/// <summary>
/// Sets the IsBroughtIntoViewWhenSelected value
/// </summary>
/// <param name="listBoxItem"></param>
/// <param name="value"></param>
public static void SetIsBroughtIntoViewWhenSelected(
ListBoxItem listBoxItem, bool value)
{
listBoxItem.SetValue(IsBroughtIntoViewWhenSelectedProperty, value);
}

/// <summary>
/// Determins if the ListBoxItem is bought into view when enabled
/// </summary>
public static readonly DependencyProperty IsBroughtIntoViewWhenSelectedProperty =
DependencyProperty.RegisterAttached(
"IsBroughtIntoViewWhenSelected",
typeof(bool),
typeof(ListBoxItemBehavior),
new UIPropertyMetadata(false, OnIsBroughtIntoViewWhenSelectedChanged));

/// <summary>
/// Action to take when item is brought into view
/// </summary>
/// <param name="depObj"></param>
/// <param name="e"></param>
static void OnIsBroughtIntoViewWhenSelectedChanged(
DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
ListBoxItem item = depObj as ListBoxItem;
if (item == null)
return;

if (e.NewValue is bool == false)
return;

if ((bool)e.NewValue)
item.Selected += OnListBoxItemSelected;
else
item.Selected -= OnListBoxItemSelected;
}

static void OnListBoxItemSelected(object sender, RoutedEventArgs e)
{
// Only react to the Selected event raised by the ListBoxItem
// whose IsSelected property was modified. Ignore all ancestors
// who are merely reporting that a descendant's Selected fired.
if (!Object.ReferenceEquals(sender, e.OriginalSource))
return;

ListBoxItem item = e.OriginalSource as ListBoxItem;
if (item != null)
item.BringIntoView();
}

#endregion // IsBroughtIntoViewWhenSelected
}

Add the xmlns to your view:

xmlns:util="clr-namespace:YourNamespaceHere.Classes"

Add the style to the resources of the Window/UserControl:

<Window.Resources>
<Style x:Key="ListBoxItemContainerStyle" TargetType="{x:Type ListBoxItem}"
BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="util:ListBoxItemBehavior.IsBroughtIntoViewWhenSelected" Value="true"/>
</Style>
</Window.Resources>

Implement the listbox:

<ListBox ItemsSource="{Binding MyView}"
DisplayMemberPath="Title"
SelectedItem="{Binding SelectedItem}"
ItemContainerStyle="{StaticResource ListBoxItemContainerStyle}"/>


Related Topics



Leave a reply



Submit