The blog of dlaa.me

Don't let the gotchas getcha [Adding the ScrollIntoViewCentered method to WPF's ListBox]

I recently found myself wanting a method I could call to bring one of the items in a WPF ListBox into view. In my scenario, the user would recognize the item when he/she saw it, so my problem wasn't about setting focus or rendering a highlight; it was just about bringing the item into view so it could be seen and manipulated. I started in the obvious place: the ListBox.ScrollIntoView method. Sure enough, a call to this method scrolled the item into view just like it was supposed to - and yet I wasn't completely satisfied... :)

You see, I didn't just want the item to be visible, I wanted it to be centered as well. I can't imagine I'm the first person to want to do this, but a quick web search didn't turn up anything relevant. I figured it'd be easy enough to write some code to implement the behavior myself, so I turned the problem over a bit in my head and was all giddy when I caught a gotcha prior to coding. :) Then, having decided on the implementation, I went ahead and typed up my ScrollIntoViewCentered extension method and gave it a try...

Darn... While my code worked as well as ScrollIntoView, it also wasn't any better. Specifically, the item wasn't centered like I wanted... :( A little poking around and some targeted experimentation turned up a second gotcha I'd missed. Once I'd addressed the second issue, the code worked fine and I continued with what I was originally doing.

ListBoxExtensionsDemo sample

[Click here to download the code for the ListBoxExtensionsDemo sample application.]

 

You can find the complete code for ScrollIntoViewCentered below; here's a quick discussion of the two gotchas:

  • VirtualizingStackPanel.IsVirtualizing must be False. This is the gotcha I caught before coding. Basically, in order for the call to ItemContainerGenerator.ContainerFromItem to succeed, the container must have already been generated - but there may not be a container if virtualization is enabled! As you might expect, ScrollIntoView has this same problem - which it solves by calling the internal VirtualizingPanel.BringIndexIntoView method... :( Losing virtualization doesn't matter for my scenario, so ScrollIntoViewCentered simply requires that it be set. (FYI: I can think of a couple of ways to remove this restriction, but none of them is particularly elegant...)
  • ScrollViewer.CanContentScroll must be False. This is the gotcha I missed. It turns out that the same exact code I have that works fine when ScrollViewer.CanContentScroll is disabled fails when it's enabled. This highlights one of the differences between logical scrolling (item-based) and physical scrolling (pixel-based). I haven't thought about it enough to decide if I think it's a bug that the technique I use doesn't work properly with logical scrolling, but (like above) the difference is not relevant to my scenario. (FYI: Again, I can think of some ways to remove this restriction, but again they're kind of ugly.)

The ScrollIntoViewCentered method is hardly rocket science - but it is pretty handy. :) Thanks to the magic of extension methods, it looks just like the ListBox API it was inspired by and ends up being both easy to discover and easy to use.

I hope you like it!

 

/// <summary>
/// Class implementing helpful extensions to ListBox.
/// </summary>
public static class ListBoxExtensions
{
    /// <summary>
    /// Causes the object to scroll into view centered.
    /// </summary>
    /// <param name="listBox">ListBox instance.</param>
    /// <param name="item">Object to scroll.</param>
    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
        Justification = "Deliberately targeting ListBox.")]
    public static void ScrollIntoViewCentered(this ListBox listBox, object item)
    {
        Debug.Assert(!VirtualizingStackPanel.GetIsVirtualizing(listBox),
            "VirtualizingStackPanel.IsVirtualizing must be disabled for ScrollIntoViewCentered to work.");
        Debug.Assert(!ScrollViewer.GetCanContentScroll(listBox),
            "ScrollViewer.GetCanContentScroll must be disabled for ScrollIntoViewCentered to work.");

        // Get the container for the specified item
        var container = listBox.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
        if (null != container)
        {
            // Get the bounds of the item container
            var rect = new Rect(new Point(), container.RenderSize);

            // Find constraining parent (either the nearest ScrollContentPresenter or the ListBox itself)
            FrameworkElement constrainingParent = container;
            do
            {
                constrainingParent = VisualTreeHelper.GetParent(constrainingParent) as FrameworkElement;
            } while ((null != constrainingParent) &&
                     (listBox != constrainingParent) &&
                     !(constrainingParent is ScrollContentPresenter));

            if (null != constrainingParent)
            {
                // Inflate rect to fill the constraining parent
                rect.Inflate(
                    Math.Max((constrainingParent.ActualWidth - rect.Width) / 2, 0),
                    Math.Max((constrainingParent.ActualHeight - rect.Height) / 2, 0));
            }

            // Bring the (inflated) bounds into view
            container.BringIntoView(rect);
        }
    }
}
Tags: WPF