Auto-scrolling a WPF ListView when a new item is added
AKA: "How to give a WPF ListView a sticky bottom" but I thought better of making that the blog post title =)
If you've ever tried to auto-scroll a WPF ListView when a new item is added, you will find that as you can't access the ListView's internal ScrollViewer from XAML this is a trickier task than it should be.
Here is an auto-scrolling ListView that will ensure that any new items added will be automatically scrolled into view. This behavior is overridden if the scrollbar position is not currently at the bottom.
public class AutoScrollingListView : ListView { private ScrollViewer _scrollViewer; protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue) { base.OnItemsSourceChanged(oldValue, newValue); if (oldValue as INotifyCollectionChanged != null) (oldValue as INotifyCollectionChanged).CollectionChanged -= ItemsCollectionChanged; if (newValue as INotifyCollectionChanged == null) return; (newValue as INotifyCollectionChanged).CollectionChanged += ItemsCollectionChanged; } public override void OnApplyTemplate() { base.OnApplyTemplate(); // Dig out and store a reference to our internal ScrollViewer _scrollViewer = RecursiveVisualChildFinder<ScrollViewer>(this) as ScrollViewer; } void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (_scrollViewer == null) return; if (!_scrollViewer.VerticalOffset.Equals(_scrollViewer.ScrollableHeight)) return; UpdateLayout(); _scrollViewer.ScrollToBottom(); } private static DependencyObject RecursiveVisualChildFinder<T>(DependencyObject rootObject) { var child = VisualTreeHelper.GetChild(rootObject, 0); if (child == null) return null; return child.GetType() == typeof (T) ? child : RecursiveVisualChildFinder<T>(child); } }
As you can see, we derive from ListView, and do two things:
- When the template is applied, we search the visual tree for the scrollviewer and store its reference.
- Override the OnItemsSourceChanged event handler and hook into the CollectionChanged event on the ItemsSource. This way we know when things have been added to the underlying bound ObservableCollection. When the collection changes, we can then call ScrollToBottom on our ScrollViewer reference.