WPF ListBox Scroll to end automatically

WpfListboxScroll

Wpf 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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionniaoView Question on Stackoverflow
Solution 1 - WpfOz MaytView Answer on Stackoverflow
Solution 2 - WpfMateusz MyślakView Answer on Stackoverflow
Solution 3 - WpfNovaLogicView Answer on Stackoverflow
Solution 4 - WpfGivanioView Answer on Stackoverflow
Solution 5 - WpfScroog1View Answer on Stackoverflow
Solution 6 - WpfjNaydenView Answer on Stackoverflow
Solution 7 - WpfJohn StewienView Answer on Stackoverflow
Solution 8 - WpfMatheus StumpfView Answer on Stackoverflow
Solution 9 - WpfAnvakaView Answer on Stackoverflow
Solution 10 - WpfBennyView Answer on Stackoverflow
Solution 11 - WpfAlexander BrodilinView Answer on Stackoverflow
Solution 12 - WpfRamenTurismoView Answer on Stackoverflow