57

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)

Dave Clemmer
  • 3,777
  • 12
  • 48
  • 72
niao
  • 4,872
  • 19
  • 64
  • 114

12 Answers12

51

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!

izbrannick
  • 351
  • 4
  • 12
Oz Mayt
  • 519
  • 4
  • 6
  • This is only a valid option, if the last item added is the last in the list. But the last item added might be added at position 0. – 0xBADF00D Jul 02 '15 at 06:27
  • 5
    This answer should be accepted! @0xBADF00D If that's the case, you should just do `lstBox.SelectedIndex = 0` ;) – DerpyNerd Mar 23 '16 at 07:37
  • 1
    Doesn't work with primitive value, `struct` or `record` (Which implements a comparer that compares the value and not the reference). Also, the question is half answered : In which event are you going to do it ? – RamenTurismo Oct 22 '21 at 11:11
48

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.

Mateusz Myślak
  • 705
  • 5
  • 16
  • 1
    The other solutions simply didn't work for me at all. Code was executed (proved in debugging), but it had no effect on the state of the control. This worked perfectly first time around. – Mark W Jan 26 '16 at 21:59
  • 2
    This might not work if you use a custom template for the `ListBox` so be careful. – Drew Noakes Aug 16 '16 at 07:44
  • 5
    For anybody who wants to know how to attach to CollectionChanged to your listbox: After `InitializeComponent();` you have to add `((INotifyCollectionChanged).Items).CollectionChanged += YourListboxCollectionChanged;` – MarkusEgle Feb 12 '18 at 13:37
  • 1
    The first child was a `ListBoxChrome` for me. Changed the casting from `Border` to `FrameworkElement` and it works perfectly, thanks! – Alfie May 10 '19 at 15:25
  • I confirm what @Alfie wrote above. So, `Border border = (Border)...` must be changed to `FrameworkElement border = (FrameworkElement)...`. – Mike Nakis Jul 23 '21 at 09:24
  • 1
    This is great, but you can save a little effort by saving the handle to the ScrollViewer once it is found. – Chuck Savage Oct 28 '21 at 04:58
33

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);
NovaLogic
  • 650
  • 14
  • 20
28

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);
Givanio
  • 281
  • 3
  • 3
  • Yes, agree with @Givanio, after set the SelectedItem my mouse cursor will not work anymore in listview. Thanks! – yancyn Oct 30 '18 at 05:15
13

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();
    }
}
Scroog1
  • 3,399
  • 21
  • 25
  • It's a nice working solution, but most of this code isn't necessary thanks to WPF routed events that are bubbling up the element tree: ``. – Yarik Jun 26 '19 at 23:05
  • You have to be a little careful with that, as the `ListBox` might not have a `ScrollViewer` if it has a custom template. – Scroog1 Jun 27 '19 at 06:25
  • If it does not have a `ScrollViewer`, then there is nothing to scroll, and the event simply will not be raised. – Yarik Jul 09 '19 at 14:12
  • My bad. I assumed that the `ScrollViewer` property wouldn't be available if the template was changed. You do still have the downside of having to implement a separated event handler for every `ListBox` (or at least one handler per control containing listboxes) with this approach though. Whereas an attached property would only require one implementation. It's a shame you can't call static method event handlers. – Scroog1 Jul 11 '19 at 07:01
5

listBox.ScrollIntoView(listBox.Items[listBox.Items.Count - 1]);

jNayden
  • 1,562
  • 1
  • 17
  • 29
5

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();
  }
}
John Stewien
  • 1,036
  • 9
  • 5
  • Nice, just what I needed. Had to make some adjustments: FindScrollViewer now searches also up the tree, (my ItemsControl was wrapped in a ScrollViewer); switch-assignment to switch-case (still on .net 4.6); and usage `AutoScrollToBottomBehavior.AutoScrollToBottomStatus="AutoScrollingToBottom"` – MHolzmayr May 26 '21 at 07:44
1

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.

0

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

Russ
  • 3,983
  • 20
  • 31
Anvaka
  • 15,308
  • 2
  • 44
  • 54
  • two of the three links here are defunct (and they are the only two with any potential for adding something useful to the question) – Peter Duniho May 18 '16 at 05:15
0

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>
Benny
  • 1
0

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.

0

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.

RamenTurismo
  • 628
  • 1
  • 8
  • 24