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

10

54

ListView.ScrollIntoView(object) currently finds an object in the ListView and scrolls to it. If you are positioned beneath the object you are scrolling to, it scrolls the object to the top row. If you are positioned above, it scrolls it into view at the bottom row.

I'd like to have the item be scrolled right into the center of my list view if it is currently not visible. Is there an easy way to accomplish this?

Gregoor answered 1/6, 2010 at 2:18 Comment(0)
K
91

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);
  }
}
Krys answered 8/6, 2010 at 23:10 Comment(7)
Love it. Thanks so much! Worked perfectly.Gregoor
Actually, it doesn't work with any other ItemsControl. I didn't test all possibilities, but at the very least, it doesn't work with DataGrid with virtualization turned on. You see, in case the target item is too far away from the viewport, ContainerForItem returns null, and your method gives up at that point and returns false. And scheduling it until "after everything loads" doesn't help much either, because nothing is going to load until the scroll position changes. (see next comment)Granvillegranvillebarker
One can add a special case for this, just like you did for ListBox, but I'm pretty sure any other virtualization situation will yield same result. Any other ideas that are mighty "encapsulated" and "cover all possibilities cleanly"?Granvillegranvillebarker
Can you please post a edited version of this for scroll into top row?Pentstemon
@RayBurns This code does not work with a DataGrid with virtualization turned off. It doesn't even scroll..Theall
@RayBurns - How do you actually call this extension? Thanks.Ossified
Got this code working with slight modifications in minutes for a DataGrid. Great solution, thank you! Sweating over these little details is really what separates good UI from great UI!Retrogressive
G
9

Ray Burns' excellent answer above is WPF specific.

Here is a modified version that works in Silverlight:

 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( 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 (UIElement vis = container; vis != null ; vis = VisualTreeHelper.GetParent(vis) as UIElement)
                if ((presenter = vis as ScrollContentPresenter) != null)
                    break;
            if (presenter == null) return false;

            // Find the IScrollInfo 
            var scrollInfo =
                !presenter.CanVerticallyScroll ? 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.TransformToVisual((UIElement)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(UIElement visual)
        {
            if (visual == null) return null;
            if (VisualTreeHelper.GetChildrenCount(visual) == 0) return null;
            return VisualTreeHelper.GetChild(visual, 0);
        }
    } 
Gusset answered 11/8, 2010 at 17:2 Comment(0)
H
4

Ray Burns' excellent answer above and the comment from Fyodor Soikin:

"Actually, it doesn't work with any other ItemsControl... doesn't work with DataGrid with virtualization turned on..."

Use:

if (listBox.SelectedItem != null)
{
   listBox.ScrollIntoView(listBox.SelectedItem);
   listBox.ScrollToCenterOfView(listBox.SelectedItem);
}

@all: cant comment at the moment, need 50 reputation

Heed answered 28/11, 2016 at 11:5 Comment(0)
B
1

I seem to recall doing something like this myself at some point. As far as my memory goes, what I did was:

  1. Determine if the object is already visible or not.
  2. If it's not visible, get the index of the object you want, and the number of objects currently displayed.
  3. (index you want) - (number of objects displayed / 2) should be the top row, so scroll to that (making sure you don't go negative, of course)
Backlash answered 1/6, 2010 at 2:27 Comment(2)
Get stuck at steps 1 and 2. Know the syntax to check all of the objects that are visible in a ListView in C#/WPF?Gregoor
Actually that's a really good question. I was doing this in WinForms, and I think it was just a normal old ListBox...I can't seem to find a way to do this. Maybe digging into Reflector will uncover something or someone else knows?Backlash
R
1

If you look at the template of a Listbox it is simply a scrollviewer with an itemspresenter inside. You'll need to calculate the size of your items and use scroll horizontally or vertically to position the items in your scrollviewer. The april silverlight toolkit has an extension method GetScrollHost that you can call on a listbox to get your underlying scrollviewer.

Once you have that you can use the current Horizontal or Vertical Offset as a frame of reference and move your list accordinly.

Routinize answered 8/6, 2010 at 23:25 Comment(0)
H
1

The below sample will find the scrollviewer of the listview and use it to scroll the item to me middle of the listview.

XAML:

<Window x:Class="ScrollIntoViewTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="300" Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ListView Grid.Row="0" ItemsSource="{Binding Path=Data}" Loaded="OnListViewLoaded"/>
        <ComboBox Grid.Row="1" ItemsSource="{Binding Path=Data}" SelectionChanged="OnScrollIntoView" />
    </Grid>
</Window>

Code behind:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace ScrollIntoViewTest
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();

            Data = new List<string>();
            for (int i = 0; i < 100; i++)
            {
                Data.Add(i.ToString());    
            }

            DataContext = this;
        }

        public List<string> Data { get; set; }

        private void OnListViewLoaded(object sender, RoutedEventArgs e)
        {
            // Assumes that the listview consists of a scrollviewer with a border around it
            // which is the default.
            Border border = VisualTreeHelper.GetChild(sender as DependencyObject, 0) as Border;
            _scrollViewer = VisualTreeHelper.GetChild(border, 0) as ScrollViewer;
        }

        private void OnScrollIntoView(object sender, SelectionChangedEventArgs e)
        {
            string item = (sender as ComboBox).SelectedItem as string;
            double index = Data.IndexOf(item) - Math.Truncate(_scrollViewer.ViewportHeight / 2);
            _scrollViewer.ScrollToVerticalOffset(index);
        }

        private ScrollViewer _scrollViewer;
    }
}
Halogenate answered 8/6, 2010 at 23:34 Comment(1)
This works in the very restricted case where you have a default ListView with no custom template and a default panel, your data is available in the same class and is trivially bound (no filtering, grouping, sorting, etc), and you don't mind hard-coding everything together. I also don't like it because it isn't clean or WPF-ish and won't work well with a ViewModel. I prefer to encapsulate everything in a single extension method that handles all possible scenarios cleanly. See my answer for more details.Krys
E
1

I found an additional approach to solve this problem, assuming some of us just need a way to find out the Height of the visual item according to the item template this would greatly save you time.

Ok, I assume your XAML is structured somehow similiar to this:

:
<Window.Resources>
   <DataTemplate x:Key="myTemplate">
      <UserControls1:myControl DataContext="{Binding}" />
   </DataTemplate>
</Window.Resources>
:
<ListBox Name="myListBox" ItemTemplate="{StaticResource ResourceKey=myTemplate}" />

And you want to calculate in order to scroll to the center but you have no idea what is the current height of each item in your listbox.. this is how you can find out:

listBoxItemHeight = (double)((DataTemplate)FindResource("myTemplate")).LoadContent().GetValue(HeightProperty);
Expensive answered 28/5, 2012 at 4:54 Comment(0)
P
1

I used Ray Burns's excellent answer. However, it will not work when the VirtualizingStackPanel.ScrollUnit is set to "Pixel", only when the scroll unit is set to "Item". There is no need for adjustment to logical scrolling when the unit is the pixel. One quick fix will do the trick and the code will work for both cases:

Change

// Adjust for logical scrolling
if (scrollInfo is StackPanel || scrollInfo is VirtualizingStackPanel)

To

// Adjust for logical scrolling
if (scrollInfo is StackPanel || (scrollInfo is VirtualizingStackPanel && VirtualizingPanel.GetScrollUnit(itemsControl) == ScrollUnit.Item))

It will bypass the adjustment for logical scrolling when scrolling by pixel.

Pyrite answered 20/9, 2022 at 5:36 Comment(1)
This does not provide an answer to the question. Once you have sufficient reputation you will be able to comment on any post; instead, provide answers that don't require clarification from the asker. - From ReviewGavel
R
0

If the issue is the scroll inconsistency (the difference between scrolling from above / below), this can be solved by first scrolling to the top of the list, and then scrolling to the desired row + half the visible row count. An additional range check will be required to avoid an IndexOutOfRange.

// we add +1 to row height for grid width
var offset = (int)(mDataGrid.RenderSize.Height / (mDataGrid.MinRowHeight + 1) / 2);
// index is the item's index in the list
if (index + offset >= mDataGrid.Items.Count) offset = 0;

mDataGrid.ScrollIntoView(mDataGrid.Items[0]);
mDataGrid.ScrollIntoView(mDataGrid.Items[index + offsest]);
Referential answered 14/1, 2021 at 16:7 Comment(0)
E
0

I know this post is old but I wanted to provide the UWP version of Ray Burns' excellent answer above

        public static async 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);

                await itemsControl.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
                {
                    itemsControl.TryScrollToCenterOfView(item);
                });
            }
        }
         


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

            var scrollPresenter = container.FindParent(typeof(ScrollContentPresenter)) as ScrollContentPresenter;

            if (scrollPresenter == null) return false;                      
         
            Size size = container.RenderSize;

            var center = container.TransformToVisual(scrollPresenter).TransformPoint(new Point(size.Width / 2, size.Height / 2));

            center.Y += scrollPresenter.VerticalOffset;
            center.X += scrollPresenter.HorizontalOffset;
           

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



        public static FrameworkElement FindParent(this FrameworkElement o, Type type)
        {

            for (var element = VisualTreeHelper.GetParent(o) as FrameworkElement;
                    element != null;
                    element = VisualTreeHelper.GetParent(element) as FrameworkElement)
            {

                if (element?.GetType() == type) return element;

            }

            return null;

        }

        private static double CenteringOffset(double center, double viewport, double extent)
        {
            return Math.Min(extent - viewport, Math.Max(0, center - viewport / 2));
        }
Emery answered 21/1, 2022 at 17:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.