41
votes

I have a XAML view with a list box:

<control:ListBoxScroll ItemSource="{Binding Path=FooCollection}"
                       SelectedItem="{Binding SelectedFoo, Mode=TwoWay}"
                       ScrollSelectedItem="{Binding SelectedFoo}">
    <!-- data templates, etc. -->
</control:ListBoxScroll>

The selected item is bound to a property in my view. When the user selects an item in the list box my SelectedFoo property in the view model gets updated. When I set the SelectedFoo property in my view model then the correct item is selected in the list box.

The problem is that if the SelectedFoo that is set in code is not currently visible I need to additionally call ScrollIntoView on the list box. Since my ListBox is inside a view and my logic is inside my view model ... I couldn't find a convenient way to do it. So I extended ListBoxScroll:

class ListBoxScroll : ListBox
{
    public static readonly DependencyProperty ScrollSelectedItemProperty = DependencyProperty.Register(
        "ScrollSelectedItem",
        typeof(object),
        typeof(ListBoxScroll),
        new FrameworkPropertyMetadata(
            null,
            FrameworkPropertyMetadataOptions.AffectsRender, 
            new PropertyChangedCallback(onScrollSelectedChanged)));
    public object ScrollSelectedItem
    {
        get { return (object)GetValue(ScrollSelectedItemProperty); }
        set { SetValue(ScrollSelectedItemProperty, value); }
    }

    private static void onScrollSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var listbox = d as ListBoxScroll;
        listbox.ScrollIntoView(e.NewValue);
    }
}

It basically exposes a new dependency property ScrollSelectedItem which I bind to the SelectedFoo property on my view model. I then hook into the property changed callback of the dependent property and scroll the newly selected item into view.

Does anyone else know of an easier way to call functions on user controls on a XAML view that is backed by a view model? It's a bit of a run around to:

  1. create a dependent property
  2. add a callback to the property changed callback
  3. handle function invocation inside the static callback

It would be nice to put the logic right in the ScrollSelectedItem { set { method but the dependency framework seems to sneak around and manages to work without actually calling it.

8
Will be much easier to set SelectedIndex.Anatolii Gabuza
It sounds like a View "concern" rather than a ViewModel one. I've had to do something similar but I left the code in the view. See matthamilton.net/focus-a-virtualized-listboxitemMatt Hamilton
@MattHamilton - this code is technically in the View (inside a control). What code would you write in a View (anywhere) that would accomplish calling ScrollIntoView? Keep in mind that I can't override the set on SelectedItem since it isn't virtual.James Fassett
@anatoliiG - does SelectedIndex cause the view to scroll to the selected item?James Fassett
@JamesFassett For DataGrid - yes. Unfortunately I've not tried it for ListBox.Anatolii Gabuza

8 Answers

58
votes

Have you tried using Behavior... Here is a ScrollInViewBehavior. I have used it for ListView and DataGrid..... I thinks it should work for ListBox......

You have to add a reference to System.Windows.Interactivity to use Behavior<T> class

Behavior

public class ScrollIntoViewForListBox : Behavior<ListBox>
{
    /// <summary>
    ///  When Beahvior is attached
    /// </summary>
    protected override void OnAttached()
    {
        base.OnAttached();
        this.AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
    }

    /// <summary>
    /// On Selection Changed
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void AssociatedObject_SelectionChanged(object sender,
                                           SelectionChangedEventArgs e)
    {
        if (sender is ListBox)
        {
            ListBox listBox = (sender as ListBox);
            if (listBox .SelectedItem != null)
            {
                listBox.Dispatcher.BeginInvoke(
                    (Action) (() =>
                                  {
                                      listBox.UpdateLayout();
                                      if (listBox.SelectedItem !=
                                          null)
                                          listBox.ScrollIntoView(
                                              listBox.SelectedItem);
                                  }));
            }
        }
    }
    /// <summary>
    /// When behavior is detached
    /// </summary>
    protected override void OnDetaching()
    {
        base.OnDetaching();
        this.AssociatedObject.SelectionChanged -=
            AssociatedObject_SelectionChanged;

    }
}

Usage

Add alias to XAML as xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

then in your Control

        <ListBox ItemsSource="{Binding Path=MyList}"
                  SelectedItem="{Binding Path=MyItem,
                                         Mode=TwoWay}"
                  SelectionMode="Single">
            <i:Interaction.Behaviors>
                <Behaviors:ScrollIntoViewForListBox />
            </i:Interaction.Behaviors>
        </ListBox>

Now When ever "MyItem" property is set in ViewModel the List will be scrolled when changes are refelected.

38
votes

After reviewing the answers a common theme came up: external classes listening to the SelectionChanged event of the ListBox. That made me realize that the dependant property approach was overkill and I could just have the sub-class listen to itself:

class ListBoxScroll : ListBox
{
    public ListBoxScroll() : base()
    {
        SelectionChanged += new SelectionChangedEventHandler(ListBoxScroll_SelectionChanged);
    }

    void ListBoxScroll_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        ScrollIntoView(SelectedItem);
    }
}

I feel this is the simplest solution that does what I want.

Honourable mention goes to adcool2007 for bringing up Behaviours. Here are a couple of articles for those interested:

http://blogs.msdn.com/b/johngossman/archive/2008/05/07/the-attached-behavior-pattern.aspx
http://www.codeproject.com/KB/WPF/AttachedBehaviors.aspx

I think for generic behaviours that will be added to several different user controls (e.g. click behaviours, drag behaviours, animation behaviours, etc.) then attached behaviours make a lot of sense. The reason I don't want to use them in this particular case is that the implementation of the behaviour (calling ScrollIntoView) isn't a generic action that can happen to any control other than a ListBox.

23
votes

Because this is strictly a View problem, there's no reason you can't have an event handler in the code behind of your view for this purpose. Listen for ListBox.SelectionChanged and use that to scroll the newly selected item into view.

private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ((ListBox)sender).ScrollIntoView(e.AddedItems[0]);
}

You also don't need a derived ListBox to do this. Just use a standard control and when the ListBox.SelectedItem value changes (as described in your original question), the above handler will be executed and the item will be scrolled into view.

    <ListBox
        ItemsSource="{Binding Path=FooCollection}"
        SelectedItem="{Binding Path=SelectedFoo}"
        SelectionChanged="ListBox_SelectionChanged"
        />

Another approach would be to write an attached property that listens for ICollectionView.CurrentChanged and then invokes ListBox.ScrollIntoView for the new current item. This is a more "reusable" approach if you need this functionality for several list boxes. You can find a good example here to get you started: http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/

13
votes

I know this is an old question, but my recent search for the same problem has brought me to this. I wanted to use the behavior approach, but didn't want a dependency on the Blend SDK just to give me Behavior<T> so here's my solution without it:

public static class ListBoxBehavior
{
    public static bool GetScrollSelectedIntoView(ListBox listBox)
    {
        return (bool)listBox.GetValue(ScrollSelectedIntoViewProperty);
    }

    public static void SetScrollSelectedIntoView(ListBox listBox, bool value)
    {
        listBox.SetValue(ScrollSelectedIntoViewProperty, value);
    }

    public static readonly DependencyProperty ScrollSelectedIntoViewProperty =
        DependencyProperty.RegisterAttached("ScrollSelectedIntoView", typeof (bool), typeof (ListBoxBehavior),
                                            new UIPropertyMetadata(false, OnScrollSelectedIntoViewChanged));

    private static void OnScrollSelectedIntoViewChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var selector = d as Selector;
        if (selector == null) return;

        if (e.NewValue is bool == false)
            return;

        if ((bool) e.NewValue)
        {
            selector.AddHandler(Selector.SelectionChangedEvent, new RoutedEventHandler(ListBoxSelectionChangedHandler));
        }
        else
        {
            selector.RemoveHandler(Selector.SelectionChangedEvent, new RoutedEventHandler(ListBoxSelectionChangedHandler));
        }
    }

    private static void ListBoxSelectionChangedHandler(object sender, RoutedEventArgs e)
    {
        if (!(sender is ListBox)) return;

        var listBox = (sender as ListBox);
        if (listBox.SelectedItem != null)
        {
            listBox.Dispatcher.BeginInvoke(
                (Action)(() =>
                    {
                        listBox.UpdateLayout();
                        if (listBox.SelectedItem !=null)
                            listBox.ScrollIntoView(listBox.SelectedItem);
                    }));
        }
    }
}

and then usage is just

<ListBox ItemsSource="{Binding Path=MyList}"
         SelectedItem="{Binding Path=MyItem, Mode=TwoWay}"
         SelectionMode="Single" 
         behaviors:ListBoxBehavior.ScrollSelectedIntoView="True">
11
votes

I'm using this (in my opinion) clear and easy solution

listView.SelectionChanged += (s, e) => 
    listView.ScrollIntoView(listView.SelectedItem);

where listView is name of ListView control in xaml, SelectedItem is affected from my MVVM and code is inserted in constructor in xaml.cs file.

9
votes

Try this:

private void lstBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    lstBox.ScrollIntoView(lstBox.SelectedItem);
}
3
votes

After tying various methods I found the following to be the simplest and the best

lstbox.Items.MoveCurrentToLast();
lstbox.ScrollIntoView(lstbox.Items.CurrentItem);
2
votes

I took Ankesh's answer and made it not dependent on the blend sdk. The downside of my solution is that it will apply to all listboxes in your app. But the upside is no custom class needed.

When your app is initializing...

    internal static void RegisterFrameworkExtensionEvents()
    {
        EventManager.RegisterClassHandler(typeof(ListBox), ListBox.SelectionChangedEvent, new RoutedEventHandler(ScrollToSelectedItem));
    }

    //avoid "async void" unless used in event handlers (or logical equivalent)
    private static async void ScrollToSelectedItem(object sender, RoutedEventArgs e)
    {
        if (sender is ListBox)
        {
            var lb = sender as ListBox;
            if (lb.SelectedItem != null)
            {
                await lb.Dispatcher.BeginInvoke((Action)delegate
                {
                    lb.UpdateLayout();
                    if (lb.SelectedItem != null)
                        lb.ScrollIntoView(lb.SelectedItem);
                });
            }
        }
    }

This makes all of your listboxes scroll to selected (which I like as a default behavior).