WPF ListBox Scroll to end automatically
WpfListboxScrollWpf Problem Overview
In my application, I have a ListBox
with items. The application is written in WPF.
How can I scroll automatically to the last added item? I want the ScrollViewer
to be moved to the end of the list when new item has been added.
Is there any event like ItemsChanged
?
(I don't want to use the SelectionChanged
event)
Wpf Solutions
Solution 1 - Wpf
Try this:
lstBox.SelectedIndex = lstBox.Items.Count -1;
lstBox.ScrollIntoView(lstBox.SelectedItem) ;
In your MainWindow, this will select and focus on last item on the list!
Solution 2 - Wpf
The easiest way to do this:
if (VisualTreeHelper.GetChildrenCount(listView) > 0)
{
Border border = (Border)VisualTreeHelper.GetChild(listView, 0);
ScrollViewer scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
scrollViewer.ScrollToBottom();
}
It is always working for ListView and ListBox controls. Attach this code to the listView.Items.SourceCollection.CollectionChanged
event and you have fully automatic auto-scrolling behaviour.
Solution 3 - Wpf
Keep in mind that listBox.ScrollIntoView(listBox.Items[listBox.Items.Count - 1]);
works only if you have no duplicate items. If you have items with the same contents it scrolls down to the first find.
Here is the solution I found:
ListBoxAutomationPeer svAutomation = (ListBoxAutomationPeer)ScrollViewerAutomationPeer.CreatePeerForElement(myListBox);
IScrollProvider scrollInterface = (IScrollProvider)svAutomation.GetPattern(PatternInterface.Scroll);
System.Windows.Automation.ScrollAmount scrollVertical = System.Windows.Automation.ScrollAmount.LargeIncrement;
System.Windows.Automation.ScrollAmount scrollHorizontal = System.Windows.Automation.ScrollAmount.NoAmount;
//If the vertical scroller is not available, the operation cannot be performed, which will raise an exception.
if ( scrollInterface.VerticallyScrollable )
scrollInterface.Scroll(scrollHorizontal, scrollVertical);
Solution 4 - Wpf
The best solution is to use the ItemCollection object inside the ListBox control this collection was specially designed to content viewers. It has a predefined method to select the last item and keep a cursor position reference....
myListBox.Items.MoveCurrentToLast();
myListBox.ScrollIntoView(myListBox.Items.CurrentItem);
Solution 5 - Wpf
A slightly different approach to those presented so far.
You could use the ScrollViewer
ScrollChanged
event and watch for the content of the ScrollViewer
getting larger.
private void ListBox_OnLoaded(object sender, RoutedEventArgs e)
{
var listBox = (ListBox) sender;
var scrollViewer = FindScrollViewer(listBox);
if (scrollViewer != null)
{
scrollViewer.ScrollChanged += (o, args) =>
{
if (args.ExtentHeightChange > 0)
scrollViewer.ScrollToBottom();
};
}
}
This avoids some issues with the binding to the ListBox
ItemsSource
changing.
The ScrollViewer
can also be found without making the assumption that the ListBox
is using the default control template.
// Search for ScrollViewer, breadth-first
private static ScrollViewer FindScrollViewer(DependencyObject root)
{
var queue = new Queue<DependencyObject>(new[] {root});
do
{
var item = queue.Dequeue();
if (item is ScrollViewer)
return (ScrollViewer) item;
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(item); i++)
queue.Enqueue(VisualTreeHelper.GetChild(item, i));
} while (queue.Count > 0);
return null;
}
Then attach this to the ListBox
Loaded
event:
<ListBox Loaded="ListBox_OnLoaded" />
This could be easily modified to be an attached property, to make it more general purpose.
Or yarik's suggestion:
<ListBox ScrollViewer.ScrollChanged="ScrollViewer_OnScrollChanged" />
and in the code behind:
private void ScrollViewer_OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (e.OriginalSource is ScrollViewer scrollViewer &&
Math.Abs(e.ExtentHeightChange) > 0.0)
{
scrollViewer.ScrollToBottom();
}
}
Solution 6 - Wpf
listBox.ScrollIntoView(listBox.Items[listBox.Items.Count - 1]);
Solution 7 - Wpf
None of the answers here did what I needed. So I wrote my own behavior that auto scrolls an items control, and pauses autoscrolling when the user scrolls up, and resumes auto scrolling when the user scrolls down to the bottom.
/// <summary>
/// This will auto scroll a list view to the bottom as items are added.
/// Automatically suspends if the user scrolls up, and recommences when
/// the user scrolls to the end.
/// </summary>
/// <example>
/// <ListView sf:AutoScrollToBottomBehavior="{Binding viewModelAutoScrollFlag}" />
/// </example>
public class AutoScrollToBottomBehavior
{
/// <summary>
/// Enumerated type to keep track of the current auto scroll status
/// </summary>
public enum StatusType
{
NotAutoScrollingToBottom,
AutoScrollingToBottom,
AutoScrollingToBottomButSuppressed
}
public static StatusType GetAutoScrollToBottomStatus(DependencyObject obj)
{
return (StatusType)obj.GetValue(AutoScrollToBottomStatusProperty);
}
public static void SetAutoScrollToBottomStatus(DependencyObject obj, StatusType value)
{
obj.SetValue(AutoScrollToBottomStatusProperty, value);
}
// Using a DependencyProperty as the backing store for AutoScrollToBottomStatus. This enables animation, styling, binding, etc...
public static readonly DependencyProperty AutoScrollToBottomStatusProperty =
DependencyProperty.RegisterAttached(
"AutoScrollToBottomStatus",
typeof(StatusType),
typeof(AutoScrollToBottomBehavior),
new PropertyMetadata(StatusType.NotAutoScrollingToBottom, (s, e) =>
{
if (s is DependencyObject viewer && e.NewValue is StatusType autoScrollToBottomStatus)
{
// Set the AutoScrollToBottom property to mirror this one
bool? autoScrollToBottom = autoScrollToBottomStatus switch
{
StatusType.AutoScrollingToBottom => true,
StatusType.NotAutoScrollingToBottom => false,
StatusType.AutoScrollingToBottomButSuppressed => false,
_ => null
};
if (autoScrollToBottom.HasValue)
{
SetAutoScrollToBottom(viewer, autoScrollToBottom.Value);
}
// Only hook/unhook for cases below, not when suspended
switch(autoScrollToBottomStatus)
{
case StatusType.AutoScrollingToBottom:
HookViewer(viewer);
break;
case StatusType.NotAutoScrollingToBottom:
UnhookViewer(viewer);
break;
}
}
}));
public static bool GetAutoScrollToBottom(DependencyObject obj)
{
return (bool)obj.GetValue(AutoScrollToBottomProperty);
}
public static void SetAutoScrollToBottom(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollToBottomProperty, value);
}
// Using a DependencyProperty as the backing store for AutoScrollToBottom. This enables animation, styling, binding, etc...
public static readonly DependencyProperty AutoScrollToBottomProperty =
DependencyProperty.RegisterAttached(
"AutoScrollToBottom",
typeof(bool),
typeof(AutoScrollToBottomBehavior),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (s, e) =>
{
if (s is DependencyObject viewer && e.NewValue is bool autoScrollToBottom)
{
// Set the AutoScrollToBottomStatus property to mirror this one
if (autoScrollToBottom)
{
SetAutoScrollToBottomStatus(viewer, StatusType.AutoScrollingToBottom);
}
else if (GetAutoScrollToBottomStatus(viewer) == StatusType.AutoScrollingToBottom)
{
SetAutoScrollToBottomStatus(viewer, StatusType.NotAutoScrollingToBottom);
}
// No change if autoScrollToBottom = false && viewer.AutoScrollToBottomStatus = AutoScrollToBottomStatusType.AutoScrollingToBottomButSuppressed;
}
}));
private static Action GetUnhookAction(DependencyObject obj)
{
return (Action)obj.GetValue(UnhookActionProperty);
}
private static void SetUnhookAction(DependencyObject obj, Action value)
{
obj.SetValue(UnhookActionProperty, value);
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
private static readonly DependencyProperty UnhookActionProperty =
DependencyProperty.RegisterAttached("UnhookAction", typeof(Action), typeof(AutoScrollToBottomBehavior), new PropertyMetadata(null));
private static void ItemsControl_Loaded(object sender, RoutedEventArgs e)
{
if (sender is ItemsControl itemsControl)
{
itemsControl.Loaded -= ItemsControl_Loaded;
HookViewer(itemsControl);
}
}
private static void HookViewer(DependencyObject viewer)
{
if (viewer is ItemsControl itemsControl)
{
// If this is triggered the xaml setup then the control won't be loaded yet,
// and so won't have a visual tree which we need to get the scrollviewer,
// so defer this hooking until the items control is loaded.
if (!itemsControl.IsLoaded)
{
itemsControl.Loaded += ItemsControl_Loaded;
return;
}
if (FindScrollViewer(viewer) is ScrollViewer scrollViewer)
{
scrollViewer.ScrollToBottom();
// Scroll to bottom when the item count changes
NotifyCollectionChangedEventHandler itemsCollectionChangedHandler = (s, e) =>
{
if (GetAutoScrollToBottom(viewer))
{
scrollViewer.ScrollToBottom();
}
};
((INotifyCollectionChanged)itemsControl.Items).CollectionChanged += itemsCollectionChangedHandler;
ScrollChangedEventHandler scrollChangedEventHandler = (s, e) =>
{
bool userScrolledToBottom = (e.VerticalOffset + e.ViewportHeight) > (e.ExtentHeight - 1.0);
bool userScrolledUp = e.VerticalChange < 0;
// Check if auto scrolling should be suppressed
if (userScrolledUp && !userScrolledToBottom)
{
if (GetAutoScrollToBottomStatus(viewer) == StatusType.AutoScrollingToBottom)
{
SetAutoScrollToBottomStatus(viewer, StatusType.AutoScrollingToBottomButSuppressed);
}
}
// Check if auto scrolling should be unsuppressed
if (userScrolledToBottom)
{
if (GetAutoScrollToBottomStatus(viewer) == StatusType.AutoScrollingToBottomButSuppressed)
{
SetAutoScrollToBottomStatus(viewer, StatusType.AutoScrollingToBottom);
}
}
};
scrollViewer.ScrollChanged += scrollChangedEventHandler;
Action unhookAction = () =>
{
((INotifyCollectionChanged)itemsControl.Items).CollectionChanged -= itemsCollectionChangedHandler;
scrollViewer.ScrollChanged -= scrollChangedEventHandler;
};
SetUnhookAction(viewer, unhookAction);
}
}
}
/// <summary>
/// Unsubscribes the event listeners on the ItemsControl and ScrollViewer
/// </summary>
/// <param name="viewer"></param>
private static void UnhookViewer(DependencyObject viewer)
{
var unhookAction = GetUnhookAction(viewer);
SetUnhookAction(viewer, null);
unhookAction?.Invoke();
}
/// <summary>
/// A recursive function that drills down a visual tree until a ScrollViewer is found.
/// </summary>
/// <param name="viewer"></param>
/// <returns></returns>
private static ScrollViewer FindScrollViewer(DependencyObject viewer)
{
if (viewer is ScrollViewer scrollViewer)
return scrollViewer;
return Enumerable.Range(0, VisualTreeHelper.GetChildrenCount(viewer))
.Select(i => FindScrollViewer(VisualTreeHelper.GetChild(viewer, i)))
.Where(child => child != null)
.FirstOrDefault();
}
}
Solution 8 - Wpf
For me, the simplest working way was this: (without Binding)
private void WriteMessage(string message, Brush color, ListView lv)
{
Dispatcher.BeginInvoke(new Action(delegate
{
ListViewItem ls = new ListViewItem
{
Foreground = color,
Content = message
};
lv.Items.Add(ls);
lv.ScrollIntoView(lv.Items[lv.Items.Count - 1]);
}));
}
Don't need to create classes or change the xaml, just write the messages with this method and it scroll automatically.
Calling just
myLv.Items.Add(ls);
myLv.ScrollIntoView(lv.Items[lv.Items.Count - 1]);
for exemple, don't work for me.
Solution 9 - Wpf
You could try ListBox.ScrollIntoView() method, although there are some problems in some cases...
Here is an example from Tamir Khason: Auto scroll ListBox in WPF
Solution 10 - Wpf
The most easiest way to achieve autoscrolling is to hook on the CollectionChanged event. Just add that functionality to a custom class which derives from ListBox control:
using System.Collections.Specialized;
using System.Windows.Controls;
using System.Windows.Media;
namespace YourProgram.CustomControls
{
public class AutoScrollListBox : ListBox
{
public AutoScrollListBox()
{
if (Items != null)
{
// Hook to the CollectionChanged event of your ObservableCollection
((INotifyCollectionChanged)Items).CollectionChanged += CollectionChange;
}
}
// Is called whenever the item collection changes
private void CollectionChange(object sender, NotifyCollectionChangedEventArgs e)
{
if (Items.Count > 0)
{
// Get the ScrollViewer object from the ListBox control
Border border = (Border)VisualTreeHelper.GetChild(this, 0);
ScrollViewer SV = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
// Scroll to bottom
SV.ScrollToBottom();
}
}
}
}
Add the namespace of the custom control to your WPF window and use the custom ListBox control:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:YourProgram"
xmlns:cc="clr-namespace:YourProgram.CustomControls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<cc:AutoScrollListBox ItemsSource="{Binding YourObservableCollection}"/>
</Window>
Solution 11 - Wpf
This is the method which 100% worked to me.
Initialization part:
private ObservableCollection<ActionLogData> LogListBind = new ObservableCollection<ActionLogData>();
LogList.ItemsSource = LogListBind;
LogListBind.CollectionChanged += this.OnCollectionChanged;
Delegate binded to CollectionChanged of my ObservableCollection used as items source of my ListView:
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (VisualTreeHelper.GetChildrenCount(LogList) > 0)
{
Decorator border = VisualTreeHelper.GetChild(LogList, 0) as Decorator;
ScrollViewer scrollViewer = border.Child as ScrollViewer;
scrollViewer.ScrollToBottom();
}
}
This solution based on @mateusz-myślak solution but I made some fixes and simplifications.
Solution 12 - Wpf
With .NET 5, from this answer and a combination of everyone's answers, the cleanest way I came up with is :
Subscribe to the event in the constructor of your View (Code behind):
var listViewItemsSource = (INotifyCollectionChanged)MyListView.Items.SourceCollection;
listViewItemsSource.CollectionChanged += MyListViewCollectionChanged;
And in the MyListViewCollectionChanged
delegate, you fetch the ScrollViewer
and you scroll to the end :
private void MyListViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
var border = (Decorator)VisualTreeHelper.GetChild(LoggerListView, 0);
var scrollViewer = (ScrollViewer)border.Child;
scrollViewer.ScrollToEnd();
}
NOTE: You cannot get the scrollviewer in the constructor, because the component isn't initialized.