Home > WPF > More Fun with WPF ScrollViewer

More Fun with WPF ScrollViewer

February 17, 2009

In yesterday’s post, Fun with WPF ScrollViewer, I discussed how to detect when just the scroll bar was clicked on a draggable ListBox.  Today’s episode addresses something that happened to me on the other end: Dropping into the ListBox.

It probably makes sense that when a new item is dropped onto my ListBox, and therefore added to the list, I am setting the new item as the ListBox’s SelectedItem.  In theory, the ScrollViewer should automatically scroll the list so that the SelectedItem is in view.  In reality, this does not work (and I have it on good authority that it should.)  So in my scenario I am programmatically updating the SelectedItem, which now means I also need to programmatically instruct the ScrollViewer to scroll appropriately.

BringIntoView: The method that didn’t do anything

I want this code to run whenever the SelectionChanged event fires, so that is the method we will be using.  The first thing I tried was the BringIntoView method.  I tried it first on the ListBox itself, but realized that’s just silly since the ListBox is already in View.  What I really need is to tell the new SelectedItem to come into view.  If you have never done this, you may be surprised to find that you cannot cast the ListBox.SelectedItem object to a ListBoxItem: it is whatever type is DataBound to the List.  Instead, what I needed was a reference to the ListBoxItem that contains the SelectedItem.

In order to get the reference we need, we have to use the ListBox’s ItemContainerGenerator property.  This property exposes an ItemContainerGenerator object whose documentation is notably sparse.  But pull the object up in Intellisense and you’ll find a couple of useful methods, specifically ContainerFromItem and ContainerFromIndex.  We can use either one of these to retrieve a reference to the ListBoxItem container we need.  The code looks like this:

ListBoxItem item = (ListBoxItem)(ImageListBox.ItemContainerGenerator.ContainerFromItem(myListBox.SelectedItem));

Now that we have a reference to the ListBoxItem container, we can call its BringIntoView() method.

if (item != null)
{
    item.BringIntoView();
}

Unfortunately this was to no avail as it did not have the desired effect.  In fact, as far as I can tell, it had no effect, but it probably caused the ListBox’s RequestBringIntoView event to fire.  So, on to the next trial.

ScrollIntoView: The method that (almost) worked

Not to be thwarted, I began thumbing through Intellisense and found that ListBox has a method called ScrollIntoView.  For some strange reason, this sounded like it might be helpful, so I figured I’d give it a whirl.

ScrollIntoView (which is a really hard name to read in certain fonts because of the two lowercase ‘l’s followed by an uppercase ‘I’) requires that you pass it the object you want to scroll to, so we’ll pass it the ListBox.SelectedItem object:

myListBox.ScrollIntoView(myListBox.SelectedItem);

Now, if you run the code at first you will be lulled into a false sense of security.  This is the worst kind of bug, because it appears to work.  Change the SelectedItem and the ScrollViewer will adjust as necessary to scroll to the newly selected item… the first time.  What I found in subsequent drops to my ListBox was a very ugly bug.  It appears that the ScrollIntoView method gets caught in some kind of infinite loop, constantly resetting itself and causing the control display to gyrate wildly.  A little searching led me to this bug report at Microsoft.

This does not explain the exact same scenario, but it does exhibit the exact same results: an endless loop caused by a call to ListBox.ScrollIntoView(). Unfortunately, the bug is over a year old with no update or new information since it was accepted by Microsoft.

ScrollToHorizontalOffset: The method that does work

I was feeling pretty daunted at this point, but I decided to check one more thing before I quit for the day.  If I could get a reference to the ScrollViewer object itself, perhaps I could manually move it to the desired location.  I posted the code yesterday that I can use to retrieve the object reference, so all I had to do at this point was figure out what my options were for manually scrolling.

I found it in the ScrollToHorizontalOffset() method.  The method takes a double, which I mistakenly assumed was the number of screen units I wanted to scroll to, so I was trying to do a lot of math like measuring the ListBoxItem and multiplying the index number by the ActualWidth, and other pocket protector-y stuff like that.  Needless to say my first efforts were unrewarding.  I finally stumbled onto the fact the the HorizontalOffset indicates the index number of the item to scroll to – I was making it way too hard!  So all I had to do was pass the Selectedndex to the method and viola, a Scroll is born:

private void myListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (myListBox.SelectedIndex >= 0)
    {
                ScrollViewer scroll = myListBox.GetScrollViewer();
                double scrollPos = myListBox.SelectedIndex;
                scroll.ScrollToHorizontalOffset(scrollPos);
    }
}

Conclusion

I’m far from done with my drag and drop reorderable ListBox, but I’ve learned a lot about the VisualTree, item containers, and obviously the ScrollViewer. This code is certainly not set in stone: if you know another way to do this or have any recommendations for the above code, please share them below in the comments.

Advertisement
Categories: WPF
  1. ChrisC
    June 3, 2009 at 3:28 am

    The reason that BringIntoView and ScrollIntoView don’t work is that they are methods on the ListBox itself. If the ListBox is contained in a scrollviewer as you are doing (and as i am doing on a project similar to yours) as far as Silverlight is concerned, the ListBox has all the space in the world to use, and every pixel of it is visible.

    The ScrollViewer, therefore, acts as a kind of window onto the ListBox, which is blocking some of the listbox content. I, as you did, first tried to use ListBox members to make the selected item visible. Because the ListBox and ScrollViewer are separate controls, the methods are ignored. the ScrollViewer, as you and i both found, is the real villain of the piece, and the one that needs beating with a sharp stick to provide the correct functionality…

  2. ChrisC
    June 3, 2009 at 3:39 am

    (sorry about the dual posts) another point to note is that, even if you dont use a scrollviewer, and enable the listbox to perform it’s own scrolling, ScrollIntoView still doesnt work correctly.

    there are multiple posts on Silverlight.net and connect.microsoft.com. i think the workaround you use above is the best that anyone has found, however this is supposed to work as well: href=”http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=373113

  3. June 3, 2009 at 8:03 am

    Thanks Chris, nice to know I’m not the only one who has a hate/hate relationship with ScrollViewer!

  4. regev
    September 21, 2009 at 4:04 am

    hi,
    i was wondering if you encountered the same problem (the scrolling issue) in the WPF TreeView as well (i did…).
    the above solution should be adjusted since TreeView doesn’t have any SelectedIndex property (what should such potential property return?)

  5. September 21, 2009 at 12:06 pm

    Sorry regev, I have no experience with the WPF TreeView control. I would imagine there is a CurrentNode or SelectedNode property you could use. Let us know what you find.

  6. Amit
    April 24, 2010 at 4:50 am

    listBox1.ScrollIntoView(textBox1.Text);

    this will solve the problem
    no need of index as well

  1. No trackbacks yet.
Comments are closed.
%d bloggers like this: