58

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,741
  • 12
  • 49
  • 72
niao
  • 4,972
  • 19
  • 66
  • 114

15 Answers15

55

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
  • 349
  • 4
  • 12
Oz Mayt
  • 559
  • 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 ? – Emy Blacksmith Oct 22 '21 at 11:11
54

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
  • 775
  • 6
  • 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
  • 6
    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
34

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);
Boppity Bop
  • 9,613
  • 13
  • 72
  • 151
NovaLogic
  • 658
  • 13
  • 21
31

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
  • 311
  • 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
15

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,539
  • 21
  • 26
  • 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
6

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

jNayden
  • 1,592
  • 2
  • 17
  • 29
6

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,046
  • 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
  • @JohnStewien This works great, as long as there is only one control on the screen with this property. If there are two or more, only one of them gets hooked with the ItemsControl_Loaded property. Any ideas on how to modify so that more than one control can utilize this code? – Alexa Kirk Aug 07 '23 at 21:10
2

The easiest way to achieve autoscrolling is to hook to 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>
Gudarzi
  • 486
  • 3
  • 7
  • 22
Benny
  • 21
  • 1
  • 1
    You should also test against VisualChildrenCount. If the list is Collapsed, it will crash your program. – Paul Jon Dec 07 '22 at 01:15
  • I use this code in the delegate added to a collection's CollectionChanged event : gridView.ScrollIntoView(gridView.Items.Count > 0 ? gridView.Items [gridView.Items.Count - 1] : null); and when the list is updated, sometimes I have a row of the table that is drawn twice on the screen (duplicated row) and if I increase the frequency of updating the list and its items, I get an exception, so be careful! – Aminos May 10 '23 at 17:19
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
  • 4,091
  • 21
  • 32
Anvaka
  • 15,658
  • 2
  • 47
  • 56
  • 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
  • I use this code in the delegate added to a collection's CollectionChanged event : gridView.ScrollIntoView(gridView.Items.Count > 0 ? gridView.Items [gridView.Items.Count - 1] : null); and when the list is updated, sometimes I have a row of the table that is drawn twice on the screen (duplicated row) and if I increase the frequency of updating the list and its items, I get an exception, so be careful! – Aminos May 10 '23 at 17:19
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.

  • I use this code in the delegate: gridView.ScrollIntoView(gridView.Items.Count > 0 ? gridView.Items [gridView.Items.Count - 1] : null); and when the list is updated, sometimes I have a row of the table that is drawn twice on the screen (duplicated row) and if I increase the frequency of updating the list and its items, I get an exception, so be careful! – Aminos May 10 '23 at 17:17
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.

Emy Blacksmith
  • 755
  • 1
  • 8
  • 27
  • I use this code in the delegate: gridView.ScrollIntoView(gridView.Items.Count > 0 ? gridView.Items [gridView.Items.Count - 1] : null); and when the list is updated, sometimes I have a row of the table that is drawn twice on the screen (duplicated row) and if I increase the frequency of updating the list and its items, I get an exception, so be careful! – Aminos May 10 '23 at 17:18
0

Only scroll to bottom if the last item added is the last in the list.

if (lstBox.SelectedIndex == lstBox.Items.Count - 1)
{
    // Scroll to bottom
    lstBox.ScrollIntoView(lstBox.SelectedItem);
}
Jack
  • 151
  • 4
  • 12
0

If you use a code that uses ObservableCollection or BindableCollection (Caliburn)'s CollectionChanged event and use this kind of code as a delegate attached to that event : gridView.ScrollIntoView(gridView.Items.Count > 0 ? gridView.Items [gridView.Items.Count - 1] : null);

when the list is updated, sometimes you may have a row of the table that is drawn twice on the screen (duplicated row) and if you increase the frequency of updating the list and its items, you will get an exception, so be careful !

This bug made me lose a lot of time that's why I share information about it.

By the way, I use .NET framework 4.6.1, maybe with more recent versions ListView is less buggy

Aminos
  • 754
  • 1
  • 20
  • 40
0

I'm using MVVM and have a Listview and although @JohnStewien's answer is good, it does not work if you have more than one control on the page/tab that needs autoscrolling. I used a combination of his answer with the answer from @Benny to create a custom class that supresses scrolling if the user scrolls up.

Class:

using System;
using System.Collections.Specialized;
using System.Windows.Controls;
using System.Windows.Media;

namespace TestProgram.Utils
{
    public class AutoScrollListView : ListView
    {

        /// <summary>
        /// Enumerated type to keep track of the current auto scroll status
        /// </summary>
        public enum StatusType
        {
            NotAutoScrollingToBottom,
            AutoScrollingToBottom,
            AutoScrollingToBottomButSuppressed
        }

        ScrollViewer _scrollViewer;
        StatusType _scrollStatus;

        public AutoScrollListView()
        {
            _scrollStatus = StatusType.AutoScrollingToBottom;
            if (Items != null)
            {
                // Hook to the CollectionChanged event of your ObservableCollection
                ((INotifyCollectionChanged)Items).CollectionChanged += CollectionChange;
            }
        }

        /// <summary>
        /// Called when the items collection changes and scrolls to the bottom.
        /// </summary>
        /// <param name="sender">Sender, not used.</param>
        /// <param name="e">Args, not used.</param>
        void CollectionChange(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (Items.Count > 0)
            {
                try
                {
                    if (_scrollViewer == null && VisualTreeHelper.GetChildrenCount(this) > 0)
                    {
                        Border border = (Border)VisualTreeHelper.GetChild(this, 0);
                        _scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
                        _scrollViewer.ScrollChanged += OnScrollChanged;
                    }
                    else if (_scrollViewer != null && _scrollStatus == StatusType.AutoScrollingToBottom)
                    {
                        _scrollViewer.ScrollToBottom();
                    }
                }
                catch (Exception ex)
                { 
                    Logger.Log(LogWindow.STATUS, LogLevel.WARNING, $"Error in AutoscrollingListView: {ex.GetType()} : {ex.Message}");
                }
            }
        }

        /// <summary>
        /// Changes the scroll status if the user scolls up or down to bottom.
        /// </summary>
        /// <param name="sender">Sender, not used.</param>
        /// <param name="e">ScrollChangedEventArgs, used to determine where the user scrolled.</param>
        void OnScrollChanged(object sender, ScrollChangedEventArgs 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 (_scrollStatus == StatusType.AutoScrollingToBottom)
                {
                    _scrollStatus = StatusType.AutoScrollingToBottomButSuppressed;
                }
            }
            else if (userScrolledToBottom) // Check if auto scrolling should be unsuppressed
            {
                if (_scrollStatus == StatusType.AutoScrollingToBottomButSuppressed)
                {
                    _scrollStatus = StatusType.AutoScrollingToBottom;
                }
            }
        }
    }
}

XAML Just use the custom AutoScrollListView instead of a ListView

<Window x:Class="TestProgram.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:utils="clr-namespace:TestProgram.Utils"
        WindowStyle="SingleBorderWindow">

    <utils:AutoScrollListView x:Name="Status" ItemsSource="{Binding StatusLog}" VirtualizingStackPanel.IsVirtualizing="True" />
</Window>
Alexa Kirk
  • 152
  • 10