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.
[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 beFalse
. This is the gotcha I caught before coding. Basically, in order for the call toItemContainerGenerator.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 internalVirtualizingPanel.BringIndexIntoView
method... :( Losing virtualization doesn't matter for my scenario, soScrollIntoViewCentered
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 beFalse
. This is the gotcha I missed. It turns out that the same exact code I have that works fine whenScrollViewer.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); } } }