26

I have got a big ListBox with vertical scrolling enabled, my MVVM has New and Edit ICommands. I am adding new item to the end of the collection but I want the scrollbar also to auto position to the End when I call my MVVM-AddCommand. I am also making an item editable(By calling EditCommand with a particular row item) from some other part of the application so that my ListBoxItem getting in to edit mode using DataTrigger, but how will I bring that particular row(ListBoxItem) to the view by adjusting the scroll position.

If I am doing it in the View side I can call listBox.ScrollInToView(lstBoxItem). But what is the best way to solve this common Scroll issue from an MVVM perspective.

Jacob
  • 3,598
  • 4
  • 35
  • 56
Jobi Joy
  • 49,102
  • 20
  • 108
  • 119
  • 1
    Using the ListBox SelectionChanged event and ScrollIntoView method does not break MVVM. This is entirely view functionality and should be handled by the view. The view model shouldn't even know the ListBox exists or have any control over where an object is located in the view. The only thing the view model should have to do is change the SelectedItem, which should be a bound property of the ListBox to the view model. – Tim Apr 27 '17 at 19:36

4 Answers4

31

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

Noctis
  • 11,507
  • 3
  • 43
  • 82
Dr. WPF
  • 636
  • 5
  • 4
8

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.

LightBulb
  • 964
  • 1
  • 11
  • 27
Kelly
  • 6,992
  • 12
  • 59
  • 76
5

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">
VahidN
  • 18,457
  • 8
  • 73
  • 117
1

If the above code doesn't work for you, give this a try

public class ListBoxExtenders : DependencyObject
{
    public static readonly DependencyProperty AutoScrollToCurrentItemProperty = DependencyProperty.RegisterAttached("AutoScrollToCurrentItem", typeof(bool), typeof(ListBoxExtenders), new UIPropertyMetadata(default(bool), OnAutoScrollToCurrentItemChanged));

    public static bool GetAutoScrollToCurrentItem(DependencyObject obj)
    {
        return (bool)obj.GetValue(AutoScrollToSelectedItemProperty);
    }

    public static void SetAutoScrollToCurrentItem(DependencyObject obj, bool value)
    {
        obj.SetValue(AutoScrollToSelectedItemProperty, value);
    }

    public static void OnAutoScrollToCurrentItemChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
    {
        var listBox = s as ListBox;
        if (listBox != null)
        {
            var listBoxItems = listBox.Items;
            if (listBoxItems != null)
            {
                var newValue = (bool)e.NewValue;

                var autoScrollToCurrentItemWorker = new EventHandler((s1, e2) => OnAutoScrollToCurrentItem(listBox, listBox.Items.CurrentPosition));

                if (newValue)
                    listBoxItems.CurrentChanged += autoScrollToCurrentItemWorker;
                else
                    listBoxItems.CurrentChanged -= autoScrollToCurrentItemWorker;
            }
        }
    }

    public static void OnAutoScrollToCurrentItem(ListBox listBox, int index)
    {
        if (listBox != null && listBox.Items != null && listBox.Items.Count > index && index >= 0)
            listBox.ScrollIntoView(listBox.Items[index]);
    }

}

Usage in XAML

<ListBox IsSynchronizedWithCurrentItem="True" extenders:ListBoxExtenders.AutoScrollToCurrentItem="True" ..../>
D.Kempkes
  • 345
  • 3
  • 5