WinRT XAML: Automatically Scrolling ListView to Bottom and Detecting When ListView is Scrolled
One of our Windows 8 WinRT apps requires a ListView with two features:
- "Auto-stick". The ListView must automatically scroll to the bottom when a new message is added to it.
- "Scroll detection". User can disable the "Auto-stick" by manually scrolling the ListView. If the user scrolls the ListView to bottom, the "Auto-stick" is enabled, otherwise it's disabled.
Here's some guidance on how you can achieve these features in a WinRT app using C# and XAML.
Automatically scrolling ListView to bottom
The ListView can be programmatically scrolled using the ScrollIntoView-method. You can pass in the item and the ListView jumps to show it. Here's how you can automatically scroll the ListView to show the last item when the list's ItemsSource is an ObservableCollection and a new item is added to the collection:
1. Register a listener to collection's CollectionChanged-event:
private readonly ObservableCollection<string> items = new ObservableCollection<string>(); ... private void MyListviewLoaded(object sender, RoutedEventArgs e) { this.MyListView.ItemsSource = items; items.CollectionChanged += (s, args) => ScrollToBottom(); }
2. In the ScrollToBottom-method, set the ListView's SelectedIndex to the last item in the collection, then use ScrollIntoView to scroll to the SelectedItem:
private void ScrollToBottom() { var selectedIndex = MyListView.Items.Count - 1; if (selectedIndex < 0) return; MyListView.SelectedIndex = selectedIndex; MyListView.UpdateLayout(); MyListView.ScrollIntoView(MyListView.SelectedItem); }
Another option (used by the WinRT XAML Toolkit) is to get the ScrollViewer-control from inside the ListView, and call its ScrollToVerticalOffset-method:
private void ScrollToBottom() { var scrollViewer = MyListView.GetFirstDescendantOfType<ScrollViewer>(); scrollViewer.ScrollToVerticalOffset(scrollViewer.ScrollableHeight); }
GetFirstDescendantOfType is part of the WinRT XAML toolkit. Here's the VisualTreeHelperExtensions -class from the toolkit which includes this extension method:
public static class VisualTreeHelperExtensions { public static T GetFirstDescendantOfType<T>(this DependencyObject start) where T : DependencyObject { return start.GetDescendantsOfType<T>().FirstOrDefault(); } public static IEnumerable<T> GetDescendantsOfType<T>(this DependencyObject start) where T : DependencyObject { return start.GetDescendants().OfType<T>(); } public static IEnumerable<DependencyObject> GetDescendants(this DependencyObject start) { var queue = new Queue<DependencyObject>(); var count = VisualTreeHelper.GetChildrenCount(start); for (int i = 0; i < count; i++) { var child = VisualTreeHelper.GetChild(start, i); yield return child; queue.Enqueue(child); } while (queue.Count > 0) { var parent = queue.Dequeue(); var count2 = VisualTreeHelper.GetChildrenCount(parent); for (int i = 0; i < count2; i++) { var child = VisualTreeHelper.GetChild(parent, i); yield return child; queue.Enqueue(child); } } } public static T GetFirstAncestorOfType<T>(this DependencyObject start) where T : DependencyObject { return start.GetAncestorsOfType<T>().FirstOrDefault(); } public static IEnumerable<T> GetAncestorsOfType<T>(this DependencyObject start) where T : DependencyObject { return start.GetAncestors().OfType<T>(); } public static IEnumerable<DependencyObject> GetAncestors(this DependencyObject start) { var parent = VisualTreeHelper.GetParent(start); while (parent != null) { yield return parent; parent = VisualTreeHelper.GetParent(parent); } } public static bool IsInVisualTree(this DependencyObject dob) { return Window.Current.Content != null && dob.GetAncestors().Contains(Window.Current.Content); } public static Rect GetBoundingRect(this FrameworkElement dob, FrameworkElement relativeTo = null) { if (relativeTo == null) { relativeTo = Window.Current.Content as FrameworkElement; } if (relativeTo == null) { throw new InvalidOperationException("Element not in visual tree."); } if (dob == relativeTo) return new Rect(0, 0, relativeTo.ActualWidth, relativeTo.ActualHeight); var ancestors = dob.GetAncestors().ToArray(); if (!ancestors.Contains(relativeTo)) { throw new InvalidOperationException("Element not in visual tree."); } var pos = dob .TransformToVisual(relativeTo) .TransformPoint(new Point()); var pos2 = dob .TransformToVisual(relativeTo) .TransformPoint( new Point( dob.ActualWidth, dob.ActualHeight)); return new Rect(pos, pos2); } }
Detecting when ListView is scrolled to the bottom
In our case the auto-stick is enabled or disabled based on where the user scrolls the ListView. This requires that we can detect when the ListView is scrolled (or actually, when the scrolling ends) and also we need to know if the ListView is scrolled to the bottom. In order to achieve these, we first need to access the ListView's ScrollViewer and then the ScrollViewer's vertical ScrollBar. We can use the GetFirstDescendantOfType and GetDescendantsOfType -extension methods from WinRT XAML Toolkit (shown above) to get these controls:
var scrollViewer = MyListView.GetFirstDescendantOfType<ScrollViewer>(); var scrollbars = scrollViewer.GetDescendantsOfType<ScrollBar>().ToList(); var verticalBar = scrollbars.FirstOrDefault(x => x.Orientation == Orientation.Vertical);
Now that we have the vertical scroll bar, we can register an event handler to its Scroll-event:
if (verticalBar != null) verticalBar.Scroll += BarScroll;
In the BarScroll-method we receive and event argument of type ScrollEventargs. This argument tells us when the scrolling has ended. After making sure that we have received the event we need, we can use the argument's NewValue-property to make sure if we have scrolled to the bottom:
void BarScroll(object sender, ScrollEventArgs e) { if (e.ScrollEventType != ScrollEventType.EndScroll) return; var bar = sender as ScrollBar; if (bar == null) return; System.Diagnostics.Debug.WriteLine("Scrolling ended"); if (e.NewValue >= bar.Maximum) { System.Diagnostics.Debug.WriteLine("We are at the bottom"); LockToBottom = true; } else { System.Diagnostics.Debug.WriteLine("We are away from the bottom"); LockToBottom = false; } }
Now we just need to the edit the ScrollToBottom-method so that the automatic scrolling happens only if the LockToBottom-property is set to true:
private void ScrollToBottom() { if (!LockToBottom) return; var selectedIndex = MyListView.Items.Count - 1; if (selectedIndex < 0) return; MyListView.SelectedIndex = selectedIndex; MyListView.UpdateLayout(); MyListView.ScrollIntoView(MyListView.SelectedItem); }
Sample:
A sample app if available from the GitHub.
Add few messages to the list to see how it auto scrolls, because the “Auto Stick” is enabled by default. Then scroll the list up and auto stick is disabled.