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

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?

link | flag

 
  

5 Answers

up vote 9 down vote accepted
+100

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); 
 
} 
} 
link | flag
 
 
Love it. Thanks so much! Worked perfectly. –  Kirk Jun 9 '10 at 17:33

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

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)
link | flag
 
 
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? –  Kirk Jun 1 '10 at 3:28
 
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? –  lc Jun 1 '10 at 3:51

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.

link | flag
 
  

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; 
   
} 
} 
link | flag
 
 
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. –  Ray Burns Jun 9 '10 at 0:23
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值