Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scrolling to an element of a virtualising ItemsControl

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?

like image 967
Seth Carnegie Avatar asked Aug 25 '12 19:08

Seth Carnegie


2 Answers

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.

like image 179
Aaron Cook Avatar answered Oct 27 '22 14:10

Aaron Cook


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.

like image 33
TravisWhidden Avatar answered Oct 27 '22 16:10

TravisWhidden