Home > WPF > Fun with the WPF ScrollViewer

Fun with the WPF ScrollViewer

February 16, 2009

In my current project, I have a horizontal ListBox wrapped in a ScrollViewer.  As you might expect, this indicates that I intend to have more content than my ListBox can display at any given time.  I am also implementing Drag and Drop reordering on this same ListBox.  This seemingly average task has turned out to be anything but, and I have spent the last couple of days fighting many issues, a couple of which are ScrollViewer specific.

Dragging and the scroll bar

The first problem I ran into was in initiating the Drag on the ListBox when the scrollbar was visible.  The problem is that the ScrollViewer is part of the default ListBox template, and the PreviewMouseLeftButtonDown event is wired to the ListBox.  This meant that if I tried to use the scroll bar, PreviewMouseLeftButtonDown would interpret it as a drag attempt and react accordingly.

In order to solve this, I needed to gain access to the ScrollViewer object that was part of the ListBox template, but there is no property for such an animal because the ListBox is completely unaware of the ScrollViewer.  And I could not access it directly because the ScrollViewer is not a named object within the Window, but rather it is part of the template.  So what’s a WPF developer to do?  The answer lies in navigating the VisualTree.

Navigating the VisualTree

In the code above, our XAML hierarchy ends at the ListBox:

Window
    LayoutRoot
        StackPanel
            ListBox

But the ScrollViewer lives inside the template definition of the ListBox.  So to access it at run time,we need to use the static VisualTreeHelper class.  To do that, we need to know what the template contains.  I used Blend to open a copy of the default ListBox Template which shows us the following hierarchy:

Template
    Border
        ScrollViewer
            ItemsPresenter

What we see is that the default ListBox template consists of a Border that holds a ScrollViewer that holds an ItemsPresenter.  This is important to visualize, because the ScrollViewer object we want is two levels into our ListBox, so we have to dig a little to get to it.  Our shovel for that digging is the VisualTreeHelper.GetChild() method.  I found this code posted by Matt Hohn at MSDN forums:

private ScrollViewer FindScroll()
{
    Border scroll_border = VisualTreeHelper.GetChild(PhotosListBox, 0) as Border;
    if (scroll_border is Border)
    {
        ScrollViewer scroll = scroll_border.Child as ScrollViewer;
        if (scroll is ScrollViewer)
        {
            return scroll;
        }
        else
        {
            return null;
        }
    }
    else
    {
        return null;
    }
}

In this code, Matt is using GetChild() to get the Border out of the Template.  The second time he uses the Child property to get the ScrollViewer from the Border.  I ended up moving this to an Extension Method so I could reuse it easily:

public static ScrollViewer GetScrollViewer(this ListBox listBox)
{
    Border scroll_border = VisualTreeHelper.GetChild(listBox, 0) as Border;
    if (scroll_border is Border)
    {
        ScrollViewer scroll = scroll_border.Child as ScrollViewer;
        if (scroll is ScrollViewer)
        {
            return scroll;
        }
        else
        {
            return null;
        }
    }
    else
    {
        return null;
    }
}

As you will see later, we will also need access to the ItemsPresenter within the ScrollViewer, so another simple Extension Method will do the trick:

public static ItemsPresenter GetItemsPresenter(this ListBox listBox)
{
    ScrollViewer scroll_viewer = listBox.GetScrollViewer();
    if (scroll_viewer is ScrollViewer)
    {
        ItemsPresenter list = scroll_viewer.Content as ItemsPresenter;
        if (list is ItemsPresenter)
        {
            return list;
        }
        else
        {
            return null;
        }
    }
    else
    {
        return null;
    }
}

Meanwhile, back at the Drag event...

So now that we can get a reference to the ScrollViewer, we have to determine whether or not the user is trying to scroll or drag an item within our ListBox.  To do this, we are going to start working in the ListBox_PreviewMouseLeftButtonDown event.  Remember that this event applies to the entire ListBox, so it will fire when the Scroll bar OR the ItemsPresenter are clicked.  To figure this out, we are going to check IsMouseOver to try to determine if the user clicked on the scroll bar or the actual items.  This gets a little interesting because the template hierarchy, and the inherent bubbling that occurs, means that when you click on the ItemsPresenter, both it AND the ScrollViewer will report IsMouseOver as true.  But if only the ScrollViewer is hovered over, then ItemsPresenter.IsMouseOver will report as false.

So we need references to both, and if ScrollViewer.IsMouseOver is true, but ItemsPresenter.IsMouseOver is false, then we can assume the user clicked on the scroll bar:

private bool IsScrolling { get; set; }
private void ImageListBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    IsScrolling = false;

    ScrollViewer scroll = ImageListBox.GetScrollViewer();
    ItemsPresenter presenter = ImageListBox.GetItemsPresenter();
    if (presenter != null && scroll != null)
    {
        IsScrolling = (scroll.IsMouseOver && !presenter.IsMouseOver);
    }
}

We are storing the IsScrolling variable outside our method, because we are going to use it in the ListBox_PreviewMouseMove event to determine whether we are Scrolling or Dragging:

private void ImageListBox_PreviewMouseMove(object sender, MouseEventArgs e)
{
    if (e.LeftButton == MouseButtonState.Pressed && !IsDragging && !IsScrolling)
    {
        // Initiate drag code
    }
}

Enough fun for now

In my next post, I will discuss how to force the ScrollViewer to scroll to my SelectedItem when it is programmatically selected.

About these ads
Categories: WPF
  1. Fuzzy
    September 14, 2009 at 3:20 pm

    The following code will not work if the visual effects on the computer have been adjusted for “best performance” (System -> Advanced -> Performance Settings -> Visual Effects):

    Border scroll_border = VisualTreeHelper.GetChild(PhotosListBox, 0) as Border;

    The cast to a Border object returns null because the child is actually of type Microsoft.Windows.Themes.ClassicBorderDecorator.

    Instead, cast to a System.Windows.Controls.Decorator object, which both types derive from.

  2. September 14, 2009 at 3:22 pm

    Thanks for the tip Fuzzy!

  3. Morten Aune Lyrstad
    October 21, 2009 at 3:01 am

    I know it’s kinda late to be adding a comment to this topic, but wouldn’t these methods also fail if the visual style of the ListBox has been changed? We use a search method which iterates through the control hierarchy to find the desired component.

  4. October 21, 2009 at 9:14 am

    No doubt that if the Template is different, the above code would fail. It is brittle in that fashion, so if you were going to use this, you may need to enhance it some. Perhaps a recursive search through the VisualTree until you find a ScrollViewer?

  5. Theo Zographos
    December 10, 2009 at 12:55 pm

    On Windows XP with classic theme, the control template is:
    ClassicBorderDecorator > ScrollViewer > ItemsPresenter
    while on Windows XP with XP theme, Windows Vista and Windows 7 is:
    Border > ScrollViewer > ItemsPresenter

    It’s better to use Decorator since (ClassicBorderDecorator and Border both inherit from Decorator) because else it won’t work in all cases…

  6. Arash
    January 9, 2010 at 3:12 am

    This doesn’t seem to work at all – “presenter.IsMouseOver” always returns false for me, regardless of whether I click on the presenter area or the scrollbar area, either way, it is always false.

  7. Arash
    January 9, 2010 at 4:42 am

    Ah, I found the answer – its a HitTest problem.

    The solution is to do this instead:

    var pos = e.GetPosition(this);
    IsScrolling = pos.X > presenter.ActualWidth;

    That is to test for the vertical scrollbar. Use ActualHeight to test for the horizontal scrollbar.

  8. January 26, 2010 at 3:20 pm

    http://msdn.microsoft.com/en-us/library/cc278062(VS.95).aspx

    ScrollViewer is a mandatory part of a ListBox template and so should dependably be there for Silverlight and *probably* WPF when this model comes over to .NET 4.0.

  9. Bill
    February 26, 2012 at 9:21 am

    Great info, which I adapted to ListView. In GetScrollViewer(), it’s best to also check that visual children currently exist. Something like -

    public static class extensions
    {
    public static ScrollViewer get_scrollviewer( this ListView lv )
    {
    if ( VisualTreeHelper.GetChildrenCount( lv ) == 0 )
    {
    return null;
    }

    Border b = VisualTreeHelper.GetChild( lv, 0 ) as Border;
    if ( b == null )
    {
    return null;
    }

    ScrollViewer sv = b.Child as ScrollViewer;
    if ( sv == null )
    {
    return null;
    }

    return sv;
    }
    }

  1. No trackbacks yet.
Comments are closed.
Follow

Get every new post delivered to your Inbox.

%d bloggers like this: