How can I have a ListBox auto-scroll when a new it

2020-01-27 10:12发布

I have a WPF ListBox that is set to scroll horizontally. The ItemsSource is bound to an ObservableCollection in my ViewModel class. Every time a new item is added, I want the ListBox to scroll to the right so that the new item is viewable.

The ListBox is defined in a DataTemplate, so I am unable to access the ListBox by name in my code behind file.

How can I get a ListBox to always scroll to show a latest added item?

I would like a way to know when the ListBox has a new item added to it, but I do not see an event that does this.

11条回答
Animai°情兽
2楼-- · 2020-01-27 10:30

I found a much simpler way which helped me with a similar problem, just a couple of lines of code behind, no need to create custom Behaviors. Check my answer to this question (and follow the link within):

wpf(C#) DataGrid ScrollIntoView - how to scroll to the first row that is not shown?

It works for ListBox, ListView and DataGrid.

查看更多
SAY GOODBYE
3楼-- · 2020-01-27 10:34

solution for Datagrid (the same for ListBox, only substitute DataGrid with ListBox class)

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            int count = AssociatedObject.Items.Count;
            if (count == 0)
                return;

            var item = AssociatedObject.Items[count - 1];

            if (AssociatedObject is DataGrid)
            {
                DataGrid grid = (AssociatedObject as DataGrid);
                grid.Dispatcher.BeginInvoke((Action)(() =>
                {
                    grid.UpdateLayout();
                    grid.ScrollIntoView(item, null);
                }));
            }

        }
    }
查看更多
混吃等死
4楼-- · 2020-01-27 10:36

The most straight-forward way i've found to do this, especially for listbox (or listview) that is bound to a data source is to hook it up with the collection change event. You can do this very easily at DataContextChanged event of the listbox:

    //in xaml <ListView x:Name="LogView" DataContextChanged="LogView_DataContextChanged">
    private void LogView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
      var src = LogView.Items.SourceCollection as INotifyCollectionChanged;
      src.CollectionChanged += (obj, args) => { LogView.Items.MoveCurrentToLast(); LogView.ScrollIntoView(LogView.Items.CurrentItem); };
    }

This is actually just a combination of all the other answers i've found. I feel that this is such a trivial feature that we should not need to spend so much time (and lines of code) doing.

If only there was an Autoscroll = true property. Sigh.

查看更多
欢心
5楼-- · 2020-01-27 10:37

You can extend the behavior of the ListBox by using attached properties. In your case I would define an attached property called ScrollOnNewItem that when set to true hooks into the INotifyCollectionChanged events of the list box items source and upon detecting a new item, scrolls the list box to it.

Example:

class ListBoxBehavior
{
    static readonly Dictionary<ListBox, Capture> Associations =
           new Dictionary<ListBox, Capture>();

    public static bool GetScrollOnNewItem(DependencyObject obj)
    {
        return (bool)obj.GetValue(ScrollOnNewItemProperty);
    }

    public static void SetScrollOnNewItem(DependencyObject obj, bool value)
    {
        obj.SetValue(ScrollOnNewItemProperty, value);
    }

    public static readonly DependencyProperty ScrollOnNewItemProperty =
        DependencyProperty.RegisterAttached(
            "ScrollOnNewItem",
            typeof(bool),
            typeof(ListBoxBehavior),
            new UIPropertyMetadata(false, OnScrollOnNewItemChanged));

    public static void OnScrollOnNewItemChanged(
        DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        var listBox = d as ListBox;
        if (listBox == null) return;
        bool oldValue = (bool)e.OldValue, newValue = (bool)e.NewValue;
        if (newValue == oldValue) return;
        if (newValue)
        {
            listBox.Loaded += ListBox_Loaded;
            listBox.Unloaded += ListBox_Unloaded;
            var itemsSourcePropertyDescriptor = TypeDescriptor.GetProperties(listBox)["ItemsSource"];
            itemsSourcePropertyDescriptor.AddValueChanged(listBox, ListBox_ItemsSourceChanged);
        }
        else
        {
            listBox.Loaded -= ListBox_Loaded;
            listBox.Unloaded -= ListBox_Unloaded;
            if (Associations.ContainsKey(listBox))
                Associations[listBox].Dispose();
            var itemsSourcePropertyDescriptor = TypeDescriptor.GetProperties(listBox)["ItemsSource"];
            itemsSourcePropertyDescriptor.RemoveValueChanged(listBox, ListBox_ItemsSourceChanged);
        }
    }

    private static void ListBox_ItemsSourceChanged(object sender, EventArgs e)
    {
        var listBox = (ListBox)sender;
        if (Associations.ContainsKey(listBox))
            Associations[listBox].Dispose();
        Associations[listBox] = new Capture(listBox);
    }

    static void ListBox_Unloaded(object sender, RoutedEventArgs e)
    {
        var listBox = (ListBox)sender;
        if (Associations.ContainsKey(listBox))
            Associations[listBox].Dispose();
        listBox.Unloaded -= ListBox_Unloaded;
    }

    static void ListBox_Loaded(object sender, RoutedEventArgs e)
    {
        var listBox = (ListBox)sender;
        var incc = listBox.Items as INotifyCollectionChanged;
        if (incc == null) return;
        listBox.Loaded -= ListBox_Loaded;
        Associations[listBox] = new Capture(listBox);
    }

    class Capture : IDisposable
    {
        private readonly ListBox listBox;
        private readonly INotifyCollectionChanged incc;

        public Capture(ListBox listBox)
        {
            this.listBox = listBox;
            incc = listBox.ItemsSource as INotifyCollectionChanged;
            if (incc != null)
            {
                incc.CollectionChanged += incc_CollectionChanged;
            }
        }

        void incc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                listBox.ScrollIntoView(e.NewItems[0]);
                listBox.SelectedItem = e.NewItems[0];
            }
        }

        public void Dispose()
        {
            if (incc != null)
                incc.CollectionChanged -= incc_CollectionChanged;
        }
    }
}

Usage:

<ListBox ItemsSource="{Binding SourceCollection}" 
         lb:ListBoxBehavior.ScrollOnNewItem="true"/>

UPDATE As per Andrej's suggestion in the comments below, I added hooks to detect a change in the ItemsSource of the ListBox.

查看更多
啃猪蹄的小仙女
6楼-- · 2020-01-27 10:42

I found an really slick way to do this, simply update the listbox scrollViewer and set position to the bottom. Call this function in one of the ListBox Events like SelectionChanged for example.

 private void UpdateScrollBar(ListBox listBox)
    {
        if (listBox != null)
        {
            var border = (Border)VisualTreeHelper.GetChild(listBox, 0);
            var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
            scrollViewer.ScrollToBottom();
        }

    }
查看更多
登录 后发表回答