I have a ItemsControl
which displays its items in a ScrollViewer
, and does virtualisation. I am trying to scroll that ScrollViewer
to an (offscreen, hence virtualised) item it contains. However, since the item is virtualised, it doesn't really exist on the screen and has no position (IIUC).
I have tried BringIntoView
on the child element, but it doesn't scroll into view. I have also tried manually doing it with TransformToAncestor
, TransformBounds
and ScrollToVerticalOffset
, but TransformToAncestor
never returns (I guess also because of the virtualisation, because it has no position, but I have no proof of that) and code after it never executes.
Is it possible to scroll to an item with a virtualising ItemsControl
? If so, how?
Poking around in the .NET source code leads me to recommend you the use of a ListBox
and its ScrollIntoView
method. The implementation of this method relies on a few internal
methods like VirtualizingPanel.BringIndexIntoView
which forces the creation of the item at that index and scrolls to it. The fact that many of those mechanism are internal means that if you try to do this on your own you're gonna have a bad time.
(To make the selection this brings with it invisible you can retemplate the ListBoxItems
)
I've been looking at getting a ItemsControl with a VirtualizingStackPanel to scroll to an item for a while now, and kept finding the "use a ListBox" answer. I didn't want to, so I found a way to do it. First you need to setup a control template for your ItemsControl that has a ScrollViewer in it (which you probably already have if you're using an items control). My basic template looks like the following (contained in a handy style for the ItemsControl)
<Style x:Key="TheItemsControlStyle" TargetType="{x:Type ItemsControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<Border BorderThickness="{TemplateBinding Border.BorderThickness}" Padding="{TemplateBinding Control.Padding}" BorderBrush="{TemplateBinding Border.BorderBrush}" Background="{TemplateBinding Panel.Background}" SnapsToDevicePixels="True">
<ScrollViewer Padding="{TemplateBinding Control.Padding}" Focusable="False" HorizontalScrollBarVisibility="Auto">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
So I've basically got a border with a scroll viewer thats going to contain my content.
My ItemsControl is defined with:
<ItemsControl x:Name="myItemsControl" [..snip..] Style="{DynamicResource TheItemsControlStyle}" ScrollViewer.CanContentScroll="True" VirtualizingStackPanel.IsVirtualizing="True">
Ok now for the fun part. I've created a extension method to attach to any ItemsControl to get it to scroll to the given item:
public static void VirtualizedScrollIntoView(this ItemsControl control, object item) {
try {
// this is basically getting a reference to the ScrollViewer defined in the ItemsControl's style (identified above).
// you *could* enumerate over the ItemsControl's children until you hit a scroll viewer, but this is quick and
// dirty!
// First 0 in the GetChild returns the Border from the ControlTemplate, and the second 0 gets the ScrollViewer from
// the Border.
ScrollViewer sv = VisualTreeHelper.GetChild(VisualTreeHelper.GetChild((DependencyObject)control, 0), 0) as ScrollViewer;
// now get the index of the item your passing in
int index = control.Items.IndexOf(item);
if(index != -1) {
// since the scroll viewer is using content scrolling not pixel based scrolling we just tell it to scroll to the index of the item
// and viola! we scroll there!
sv.ScrollToVerticalOffset(index);
}
} catch(Exception ex) {
Debug.WriteLine("What the..." + ex.Message);
}
}
So with the extension method in place you would use it just like ListBox's companion method:
myItemsControl.VirtualizedScrollIntoView(someItemInTheList);
Works great!
Note that you can also call sv.ScrollToEnd() and the other usual scrolling methods to get around your items.
I know I'm pretty late to the party but hopefully this may help someone else coming along looking for the solution...
int index = myItemsControl.Items.IndexOf(*your item*).FirstOrDefault();
int rowHeight = *height of your rows*;
myScrollView.ScrollToVerticalOffset(index*rowHeight);
//this will bring the given item to the top of the scrollViewer window
... and my XAML is setup like this...
<ScrollViewer x:Name="myScrollView">
<ItemsControl x:Name="myItemsControl">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<!-- data here -->
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
Using @AaronCook example, Created a behavior that works for my VirtualizingItemsControl. Here is the code for that:
public class ItemsControlScrollToSelectedBehavior : Behavior<ItemsControl>
{
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(object), typeof(ItemsControlScrollToSelectedBehavior),
new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(OnSelectedItemsChanged)));
public object SelectedItem
{
get => GetValue(SelectedItemProperty);
set => SetValue(SelectedItemProperty, value);
}
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ItemsControlScrollToSelectedBehavior target = (ItemsControlScrollToSelectedBehavior)d;
object oldSelectedItems = e.OldValue;
object newSelectedItems = target.SelectedItem;
target.OnSelectedItemsChanged(oldSelectedItems, newSelectedItems);
}
protected virtual void OnSelectedItemsChanged(object oldSelectedItems, object newSelectedItems)
{
try
{
var sv = VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(AssociatedObject, 0), 0) as ScrollViewer;
// now get the index of the item your passing in
int index = AssociatedObject.Items.IndexOf(newSelectedItems);
if (index != -1)
{
sv?.ScrollToVerticalOffset(index);
}
}
catch
{
// Ignore
}
}
}
and usage is:
<ItemsControl Style="{StaticResource VirtualizingItemsControl}"
ItemsSource="{Binding BoundItems}">
<i:Interaction.Behaviors>
<behaviors:ItemsControlScrollToSelectedBehavior SelectedItem="{Binding SelectedItem}" />
</i:Interaction.Behaviors>
</ItemsControl>
Helpful for those who like Behaviors and clean XAML, no code-behind.
I know this is an old thread, but in case someone else (like me) comes across it, I figured it would be worth an updated answer that I just discovered.
As of .NET Framework 4.5, VirtualizingPanel
has a public BringIndexIntoViewPublic
method which works like a charm, including with pixel based scrolling. You'll have to either sub-class your ItemsControl
, or use the VisualTreeHelper
to find its child VirtualizingPanel
, but either way it's now very easy to force your ItemsControl
to scroll precisely to a particular item/index.