How to control the scroll position of a ListBox in a MVVM WPF app How to control the scroll position of a ListBox in a MVVM WPF app wpf wpf

How to control the scroll position of a ListBox in a MVVM WPF app


I typically set IsSynchronizedWithCurrentItem="True" on the ListBox. Then I add a SelectionChanged handler and always bring the selected item into view, with code like this:

    private void BringSelectionIntoView(object sender, SelectionChangedEventArgs e)    {        Selector selector = sender as Selector;        if (selector is ListBox)        {            (selector as ListBox).ScrollIntoView(selector.SelectedItem);        }    }

From my VM I can get the default collection view and use one of the MoveCurrent*() methods to ensure that the item being edited is the current item.

CollectionViewSource.GetDefaultView(_myCollection).MoveCurrentTo(thisItem);

NOTE: Edited to use ListBox.ScrollIntoView() to accomodate virtualization


Using this in MVVM can be easily accomplished via an attached behavior like so:

using System.Windows.Controls;using System.Windows.Interactivity;namespace Jarloo.Sojurn.Behaviors{    public sealed class ScrollIntoViewBehavior : Behavior<ListBox>    {        protected override void OnAttached()        {            base.OnAttached();            AssociatedObject.SelectionChanged += ScrollIntoView;        }        protected override void OnDetaching()        {            AssociatedObject.SelectionChanged -= ScrollIntoView;            base.OnDetaching();        }        private void ScrollIntoView(object o, SelectionChangedEventArgs e)        {            ListBox b = (ListBox) o;            if (b == null)                return;            if (b.SelectedItem == null)                return;            ListBoxItem item = (ListBoxItem) ((ListBox) o).ItemContainerGenerator.ContainerFromItem(((ListBox) o).SelectedItem);            if (item != null) item.BringIntoView();        }    }}

Then in the View ad this reference at the top:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

And do this:

<ListBox ItemsSource="{Binding MyData}" SelectedItem="{Binding MySelectedItem}">         <i:Interaction.Behaviors>             <behaviors:ScrollIntoViewBehavior />         </i:Interaction.Behaviors></ListBox>

Now when the SelectedItem changes the behavior will do the BringIntoView() call for you.


This is the attached property form of the accepted answer:

using System.Windows;using System.Windows.Controls;namespace CommonBehaviors{    public static class ScrollCurrentItemIntoViewBehavior    {        public static readonly DependencyProperty AutoScrollToCurrentItemProperty =            DependencyProperty.RegisterAttached("AutoScrollToCurrentItem",                typeof(bool), typeof(ScrollCurrentItemIntoViewBehavior),                new UIPropertyMetadata(default(bool), OnAutoScrollToCurrentItemChanged));        public static bool GetAutoScrollToCurrentItem(DependencyObject obj)        {            return (bool)obj.GetValue(AutoScrollToCurrentItemProperty);        }        public static void OnAutoScrollToCurrentItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)        {            var listBox = obj as ListBox;            if (listBox == null) return;            var newValue = (bool)e.NewValue;            if (newValue)                listBox.SelectionChanged += listBoxSelectionChanged;            else                listBox.SelectionChanged -= listBoxSelectionChanged;        }        static void listBoxSelectionChanged(object sender, SelectionChangedEventArgs e)        {            var listBox = sender as ListBox;            if (listBox == null || listBox.SelectedItem == null || listBox.Items == null) return;            listBox.Items.MoveCurrentTo(listBox.SelectedItem);            listBox.ScrollIntoView(listBox.SelectedItem);        }        public static void SetAutoScrollToCurrentItem(DependencyObject obj, bool value)        {            obj.SetValue(AutoScrollToCurrentItemProperty, value);        }    }}

Usage:

<ListBox ItemsSource="{Binding}"          IsSynchronizedWithCurrentItem="True"          behaviors:ScrollCurrentItemIntoViewBehavior.AutoScrollToCurrentItem="True">