How can I use automatic scrolling of a ListBox when adding a new item?

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

The ListBox is defined in the DataTemplate, so I cannot access the ListBox by name in my code behind the file.

How can I make the ListBox always scroll to show the last item added?

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

+54
scroll wpf listbox
Jan 05
source share
11 answers

You can extend the behavior of a ListBox using attached properties. In your case, I would define the attached property ScrollOnNewItem , which when set to true captures the INotifyCollectionChanged events of the source of the list items and scrolls to the list when a new item is detected.

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; } } } 

Using:

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

UPDATE . As suggested by Andrew in the comments below, I added hooks to detect a change in the ItemsSource ListBox .

+63
Jan 05
source share
 <ItemsControl ItemsSource="{Binding SourceCollection}"> <i:Interaction.Behaviors> <Behaviors:ScrollOnNewItem/> </i:Interaction.Behaviors> </ItemsControl> public class ScrollOnNewItem : Behavior<ItemsControl> { protected override void OnAttached() { AssociatedObject.Loaded += OnLoaded; AssociatedObject.Unloaded += OnUnLoaded; } protected override void OnDetaching() { AssociatedObject.Loaded -= OnLoaded; AssociatedObject.Unloaded -= OnUnLoaded; } private void OnLoaded(object sender, RoutedEventArgs e) { var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged; if (incc == null) return; incc.CollectionChanged += OnCollectionChanged; } private void OnUnLoaded(object sender, RoutedEventArgs e) { var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged; if (incc == null) return; incc.CollectionChanged -= OnCollectionChanged; } 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]; var frameworkElement = AssociatedObject.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement; if (frameworkElement == null) return; frameworkElement.BringIntoView(); } } 
+20
Jul 17 '12 at 20:34
source share

I found a really smooth way to do this, just updated the listroll scrollViewer and set the position to the bottom. Call this function in one of ListBox events such as SelectionChanged.

  private void UpdateScrollBar(ListBox listBox) { if (listBox != null) { var border = (Border)VisualTreeHelper.GetChild(listBox, 0); var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0); scrollViewer.ScrollToBottom(); } } 
+19
May 31 '14 at 4:39
source share

I am using this solution: http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/ .

It works even if you bind a Listbox ItemsSource to an ObservableCollection that is processed in a thread other than the UI.

+9
Dec 11 2018-10-11
source share

for Datagrid (same for ListBox, replace DataGrid with ListBox only)

  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); })); } } } 
+2
Sep 12 '13 at 7:15
source share

MVVM style attached behavior

This pinned behavior automatically scrolls the list down when a new item is added.

 <ListBox ItemsSource="{Binding LoggingStream}"> <i:Interaction.Behaviors> <behaviors:ScrollOnNewItemBehavior IsActiveScrollOnNewItem="{Binding IfFollowTail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> </i:Interaction.Behaviors> </ListBox> 

In your ViewModel you can bind to boolean IfFollowTail { get; set; } IfFollowTail { get; set; } IfFollowTail { get; set; } to control the auto scroll activity.

Behavior does everything right:

  • If IfFollowTail=false set in the ViewModel, the ListBox no longer scrolls to the bottom of the new item.
  • Once IfFollowTail=true set to ViewModel, the ListBox instantly scrolls down and continues to do so.
  • It is fast. It scrolls only after a couple of hundred milliseconds of inactivity. The naive implementation will be very slow, as it will scroll with each added addition.
  • It works with duplicate ListBox elements (many other implementations do not work with duplicates - they scroll to the first element and then stop).
  • Ideal for a logging console that deals with continuous inbound items.

Behavior C # Code

 public class ScrollOnNewItemBehavior : Behavior<ListBox> { public static readonly DependencyProperty IsActiveScrollOnNewItemProperty = DependencyProperty.Register( name: "IsActiveScrollOnNewItem", propertyType: typeof(bool), ownerType: typeof(ScrollOnNewItemBehavior), typeMetadata: new PropertyMetadata(defaultValue: true, propertyChangedCallback:PropertyChangedCallback)); private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { // Intent: immediately scroll to the bottom if our dependency property changes. ScrollOnNewItemBehavior behavior = dependencyObject as ScrollOnNewItemBehavior; if (behavior == null) { return; } behavior.IsActiveScrollOnNewItemMirror = (bool)dependencyPropertyChangedEventArgs.NewValue; if (behavior.IsActiveScrollOnNewItemMirror == false) { return; } ListboxScrollToBottom(behavior.ListBox); } public bool IsActiveScrollOnNewItem { get { return (bool)this.GetValue(IsActiveScrollOnNewItemProperty); } set { this.SetValue(IsActiveScrollOnNewItemProperty, value); } } public bool IsActiveScrollOnNewItemMirror { get; set; } = true; protected override void OnAttached() { this.AssociatedObject.Loaded += this.OnLoaded; this.AssociatedObject.Unloaded += this.OnUnLoaded; } protected override void OnDetaching() { this.AssociatedObject.Loaded -= this.OnLoaded; this.AssociatedObject.Unloaded -= this.OnUnLoaded; } private IDisposable rxScrollIntoView; private void OnLoaded(object sender, RoutedEventArgs e) { var changed = this.AssociatedObject.ItemsSource as INotifyCollectionChanged; if (changed == null) { return; } // Intent: If we scroll into view on every single item added, it slows down to a crawl. this.rxScrollIntoView = changed .ToObservable() .ObserveOn(new EventLoopScheduler(ts => new Thread(ts) { IsBackground = true})) .Where(o => this.IsActiveScrollOnNewItemMirror == true) .Where(o => o.NewItems?.Count > 0) .Sample(TimeSpan.FromMilliseconds(180)) .Subscribe(o => { this.Dispatcher.BeginInvoke((Action)(() => { ListboxScrollToBottom(this.ListBox); })); }); } ListBox ListBox => this.AssociatedObject; private void OnUnLoaded(object sender, RoutedEventArgs e) { this.rxScrollIntoView?.Dispose(); } /// <summary> /// Scrolls to the bottom. Unlike other methods, this works even if there are duplicate items in the listbox. /// </summary> private static void ListboxScrollToBottom(ListBox listBox) { if (VisualTreeHelper.GetChildrenCount(listBox) > 0) { Border border = (Border)VisualTreeHelper.GetChild(listBox, 0); ScrollViewer scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0); scrollViewer.ScrollToBottom(); } } } 

Bridge from events to jet extensions

Finally, add this extension method so that we can use all the features of RX:

 public static class ListBoxEventToObservableExtensions { /// <summary>Converts CollectionChanged to an observable sequence.</summary> public static IObservable<NotifyCollectionChangedEventArgs> ToObservable<T>(this T source) where T : INotifyCollectionChanged { return Observable.FromEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>( h => (sender, e) => h(e), h => source.CollectionChanged += h, h => source.CollectionChanged -= h); } } 

Add reactive extensions

You need to add Reactive Extensions to your project. I recommend NuGet .

+1
Mar 14 '17 at 17:45
source share

The most direct way I've found to do this, especially for a listbox (or listview) tied to a data source, is to associate it with a collection change event. You can do this very easily in the DataContextChanged event from the list:

  //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 that I found. I feel that this is such a trivial function that we do not need to spend so much time (and lines of code).

If only the Autoscroll = true property existed. Sigh.

+1
Aug 14 '17 at 8:01
source share

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

wpf (C #) DataGrid ScrollIntoView - how to scroll the first row that doesn't display?

It works for ListBox, ListView and DataGrid.

0
Dec 18
source share

I was not satisfied with the proposed solutions.

  • I did not want to use leaky property descriptors.
  • I did not want to add an Rx dependency and an 8-line query for a seemingly trivial task. Also, I did not want a constantly running timer.
  • I really liked the idea of ​​shawnpfiore, so I built on it an attached behavior that still works well in my case.

That's what I ended up with. Maybe this will save someone time.

 public class AutoScroll : Behavior<ItemsControl> { public static readonly DependencyProperty ModeProperty = DependencyProperty.Register( "Mode", typeof(AutoScrollMode), typeof(AutoScroll), new PropertyMetadata(AutoScrollMode.VerticalWhenInactive)); public AutoScrollMode Mode { get => (AutoScrollMode) GetValue(ModeProperty); set => SetValue(ModeProperty, value); } protected override void OnAttached() { base.OnAttached(); AssociatedObject.Loaded += OnLoaded; AssociatedObject.Unloaded += OnUnloaded; } protected override void OnDetaching() { Clear(); AssociatedObject.Loaded -= OnLoaded; AssociatedObject.Unloaded -= OnUnloaded; base.OnDetaching(); } private static readonly DependencyProperty ItemsCountProperty = DependencyProperty.Register( "ItemsCount", typeof(int), typeof(AutoScroll), new PropertyMetadata(0, (s, e) => ((AutoScroll)s).OnCountChanged())); private ScrollViewer _scroll; private void OnLoaded(object sender, RoutedEventArgs e) { var binding = new Binding("ItemsSource.Count") { Source = AssociatedObject, Mode = BindingMode.OneWay }; BindingOperations.SetBinding(this, ItemsCountProperty, binding); _scroll = AssociatedObject.FindVisualChild<ScrollViewer>() ?? throw new NotSupportedException("ScrollViewer was not found!"); } private void OnUnloaded(object sender, RoutedEventArgs e) { Clear(); } private void Clear() { BindingOperations.ClearBinding(this, ItemsCountProperty); } private void OnCountChanged() { var mode = Mode; if (mode == AutoScrollMode.Vertical) { _scroll.ScrollToBottom(); } else if (mode == AutoScrollMode.Horizontal) { _scroll.ScrollToRightEnd(); } else if (mode == AutoScrollMode.VerticalWhenInactive) { if (_scroll.IsKeyboardFocusWithin) return; _scroll.ScrollToBottom(); } else if (mode == AutoScrollMode.HorizontalWhenInactive) { if (_scroll.IsKeyboardFocusWithin) return; _scroll.ScrollToRightEnd(); } } } public enum AutoScrollMode { /// <summary> /// No auto scroll /// </summary> Disabled, /// <summary> /// Automatically scrolls horizontally, but only if items control has no keyboard focus /// </summary> HorizontalWhenInactive, /// <summary> /// Automatically scrolls vertically, but only if itmes control has no keyboard focus /// </summary> VerticalWhenInactive, /// <summary> /// Automatically scrolls horizontally regardless of where the focus is /// </summary> Horizontal, /// <summary> /// Automatically scrolls vertically regardless of where the focus is /// </summary> Vertical } 
0
Dec 31 '17 at 14:26
source share

So what I read in this topcs is a little harder for a simple action.

So I signed up for the scroll event and then used this code:

 private void TelnetListBox_OnScrollChanged(object sender, ScrollChangedEventArgs e) { var scrollViewer = ((ScrollViewer)e.OriginalSource); scrollViewer.ScrollToEnd(); } 

Bonus:

After that, I checked the box in which I could set when I want to use the auto-scroll function, and I said that sometimes I forgot to uncheck the list box if I saw information that was interesting to me. So I decided that I would like to create an intelligent auto-scroll list that responds to my mouse actions.

 private void TelnetListBox_OnScrollChanged(object sender, ScrollChangedEventArgs e) { var scrollViewer = ((ScrollViewer)e.OriginalSource); scrollViewer.ScrollToEnd(); if (AutoScrollCheckBox.IsChecked != null && (bool)AutoScrollCheckBox.IsChecked) scrollViewer.ScrollToEnd(); if (_isDownMouseMovement) { var verticalOffsetValue = scrollViewer.VerticalOffset; var maxVerticalOffsetValue = scrollViewer.ExtentHeight - scrollViewer.ViewportHeight; if (maxVerticalOffsetValue < 0 || verticalOffsetValue == maxVerticalOffsetValue) { // Scrolled to bottom AutoScrollCheckBox.IsChecked = true; _isDownMouseMovement = false; } else if (verticalOffsetValue == 0) { } } } private bool _isDownMouseMovement = false; private void TelnetListBox_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e) { if (e.Delta > 0) { _isDownMouseMovement = false; AutoScrollCheckBox.IsChecked = false; } if (e.Delta < 0) { _isDownMouseMovement = true; } } 

When I scroll to the lower limit, the checkbox is marked as true and will remain my bottom view, if I scroll with the mouse wheel, the checkbox will be unchecked and you can view the list.

0
04 Oct '18 at 13:36
source share

This solution that I use works, may help someone else;

  statusWindow.SelectedIndex = statusWindow.Items.Count - 1; statusWindow.UpdateLayout(); statusWindow.ScrollIntoView(statusWindow.SelectedItem); statusWindow.UpdateLayout(); 
0
Apr 09 '19 at 0:19
source share



All Articles