DCSIMG
September 2010 - Posts - David Birin's blog

September 2010 - Posts

WPF Auto-scroll ListBox

The WPF’s ListBox is a great control with infinite ways to design the items template, but I found that is lacks one important feature: AutoScroll.
I had to use a control that shows messages which are received in runtime, so I had to add this functionality because the user needs to see the newest messages, I also gave the ability to stop the AutoScroll using a DependencyProperty and using this ability I wrote a trigger that stops the scrolling when the mouse is over the ListBox (imagine trying to scroll to read old messages when new message arrive and the control “jumps” to the new one, not a great UX…)

 

The first step was to write a control which inherits from ListBox and adds the AutoScroll capabilities, the catch is to hook into the items CollectionChanged event:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Collections.Specialized;
 
namespace SampleAutoScrollListBox.Helpers
{
    class AutoScrollListBox : ListBox
    {
 
        public bool AutoScroll
        {
            get { return (bool)GetValue(AutoScrollProperty); }
            set { SetValue(AutoScrollProperty, value); }
        }
 
        // Using a DependencyProperty as the backing store for AutoScoll.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AutoScrollProperty =
            DependencyProperty.Register("AutoScroll", typeof(bool), typeof(AutoScrollListBox), new UIPropertyMetadata(default(bool), OnAutoScrollChanged));
 
        public static void OnAutoScrollChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
        {
            AutoScrollListBox thisLb = (AutoScrollListBox)s;
 
            // Add the event handler in case that the property is set to true
            if ((bool)e.NewValue == true && (bool)e.OldValue == false)
            {
                var ic = thisLb.Items as INotifyCollectionChanged;
                if (ic == null)
                {
                    return;
                }
                ic.CollectionChanged += new NotifyCollectionChangedEventHandler(thisLb.ic_CollectionChanged);
            }
            // Remove the event handel in case the property is set to false
            if ((bool)e.NewValue == false && (bool)e.OldValue == true)
            {
                var ic = thisLb.Items as INotifyCollectionChanged;
                if (ic == null)
                {
                    return;
                }
                ic.CollectionChanged -= new NotifyCollectionChangedEventHandler(thisLb.ic_CollectionChanged);
            }
        }
 
        void ic_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            var ic = sender as ItemCollection;
            if (ic != null)
            {
                //Scroll into the last item
                if (ic.Count > 1)
                {
                    this.ScrollIntoView(ic[ic.Count - 1]);
                }
            }
        }
    }
}

So all I had to do now is to add the control to the page and write a style which disables the AutoScroll when the mouse is over the control:

<Window x:Class="SampleAutoScrollListBox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Helpers="clr-namespace:SampleAutoScrollListBox.Helpers"
        xmlns:local="clr-namespace:SampleAutoScrollListBox"
        Title="Sample WPF AutoScrollListBox" Height="150" Width="250">
    <Window.Resources>
        <local:SampleDataSource x:Key="listboxDataSource"/>
        
        <Style x:Key="autoScrollLBStyle" TargetType="Helpers:AutoScrollListBox">
            <Style.Triggers>
                <Trigger Property="IsMouseOver" Value="true">
                    <Setter Property="AutoScroll" Value="false"/>
                </Trigger>
                <Trigger Property="IsMouseOver" Value="false">
                    <Setter Property="AutoScroll" Value="true"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
        <Grid>
        <Helpers:AutoScrollListBox Style="{StaticResource autoScrollLBStyle}"  ItemsSource="{Binding Source={StaticResource listboxDataSource}, Path=Messages}"/>
    </Grid>
</Window>

I wrote a sample data source for the control which adds a message on every second (just for demo purposes):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Threading;
using System.Collections.ObjectModel;
 
namespace SampleAutoScrollListBox
{
    class SampleDataSource
    {
 
        ObservableCollection<string> _messages;
 
        DispatcherTimer dt;
 
        public SampleDataSource()
        {
            _messages = new ObservableCollection<string>();
            dt = new DispatcherTimer();
            dt.Interval = new TimeSpan(0,0,1);
            dt.Tick += new EventHandler(dt_Tick);
            dt.Start();
        }
 
        void dt_Tick(object sender, EventArgs e)
        {
            _messages.Add(String.Format("New message received at {0}",DateTime.Now.ToLongTimeString()));
        }
 
        public ObservableCollection<string> Messages
        {
            get
            {
                return _messages;
            }
        }
    }
}

The final result is:

Messages added on runtime (AutoScroll): Mouse over (no AutoScroll):
image image

The sample project can be downloaded from here (tested in .NET 3.5 and 4)