C# WPF List View Auto Scroll Behavior

C# WPF List View Auto Scroll Behavior

I’m in the middle of building a TCP Server and Messaging Clients, and I wanted my List Views to auto scroll to the bottom or the last message added. So I’m going to show you how to do it with WPF behaviors.

Nuget Packages

This is the only package required but it enables the Behaviors within WPF.

Auto Scroll Behavior Code

using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using Microsoft.Xaml.Behaviors;

namespace TCPChatClient.Behaviors
{
    /// <summary>
    /// A behavior that automatically scrolls a ListView to the bottom when new items are added.
    /// This is particularly useful for chat-like interfaces where the most recent messages
    /// should always be visible.
    /// </summary>
    public class AutoScrollBehavior : Behavior<ListView>
    {
        // The ScrollViewer inside the ListView that we'll use to scroll
        private ScrollViewer _scrollViewer;

        /// <summary>
        /// Called after the behavior is attached to an AssociatedObject.
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();
            // Wait for the ListView to be fully loaded before we try to find its ScrollViewer
            AssociatedObject.Loaded += ListView_Loaded;
        }

        /// <summary>
        /// Called when the behavior is being detached from its AssociatedObject.
        /// </summary>
        protected override void OnDetaching()
        {
            // Clean up event subscriptions to prevent memory leaks
            AssociatedObject.Loaded -= ListView_Loaded;
            if (AssociatedObject.ItemsSource is INotifyCollectionChanged notifyCollection)
            {
                notifyCollection.CollectionChanged -= ItemsSource_CollectionChanged;
            }
            base.OnDetaching();
        }

        /// <summary>
        /// Handles the Loaded event of the ListView.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The event arguments.</param>
        private void ListView_Loaded(object sender, RoutedEventArgs e)
        {
            // Find the ScrollViewer within the ListView
            _scrollViewer = AssociatedObject.FindDescendant<ScrollViewer>();
            if (_scrollViewer != null)
            {
                // If the ItemsSource supports INotifyCollectionChanged, subscribe to its CollectionChanged event
                if (AssociatedObject.ItemsSource is INotifyCollectionChanged notifyCollection)
                {
                    notifyCollection.CollectionChanged += ItemsSource_CollectionChanged;
                }
            }
        }

        /// <summary>
        /// Handles the CollectionChanged event of the ItemsSource.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The event arguments.</param>
        private void ItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            // We're only interested in new items being added
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                // Use the Dispatcher to ensure we're on the UI thread and that the UI has been updated
                _scrollViewer.Dispatcher.InvokeAsync(() =>
                {
                    _scrollViewer.ScrollToBottom();
                }, System.Windows.Threading.DispatcherPriority.Loaded);
            }
        }
    }

    /// <summary>
    /// Extension methods for the VisualTreeHelper to simplify finding elements in the visual tree.
    /// </summary>
    public static class VisualTreeHelperExtensions
    {
        /// <summary>
        /// Finds a descendant of a specified type in the visual tree.
        /// </summary>
        /// <typeparam name="T">The type of the descendant to find.</typeparam>
        /// <param name="d">The root element to start the search from.</param>
        /// <returns>The first descendant of the specified type, or null if none is found.</returns>
        public static T FindDescendant<T>(this DependencyObject d) where T : DependencyObject
        {
            if (d == null) return null;

            var childCount = VisualTreeHelper.GetChildrenCount(d);
            for (var i = 0; i < childCount; i++)
            {
                var child = VisualTreeHelper.GetChild(d, i);
                var result = child as T ?? FindDescendant<T>(child);
                if (result != null) return result;
            }
            return null;
        }
    }
}

Implementing the Behaviour

First we have to add the necessary namespace declarations at the top of your XAML.

        xmlns:behaviors="clr-namespace:TCPServerMessengerCore.Behaviours;assembly=TCPServerMessengerCore"
        xmlns:b="http://schemas.microsoft.com/xaml/behaviors"

Next we can decorate the List View with the behavior.

<!-- Server Messages -->
<ListView ItemsSource="{Binding ServerMessages}" Grid.Row="2" Margin="10" MaxHeight="100">
    <b:Interaction.Behaviors>
        <behaviors:AutoScrollBehavior />
    </b:Interaction.Behaviors>
    <ListView.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding}" TextWrapping="Wrap" Foreground="Blue"/>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>