none
Trivial ListBox/ListView Autoscroll

    Question

  • Hello,
     
    I created a simple ListBox and bound it to some ObservableCollection and I'd like the listbox to autoscroll when I'm adding item to the source collection. Since I'm really into WPF UI/data separation, my data source object doesn't know anything about the ListBox (so it cannot call ListBox.ScrollIntoView on each item add).

    The easiest way I could come to seems to be extremally tricky for such a trivial task. Something like this:

     


    <ListBox Name="lb" ItemsSource="{Binding Path=Data}" VirtualizingStackPanel.IsVirtualizing="False"/>  
    lb.IsSynchronizedWithCurrentItem = true;  
    lb.SelectionChanged += (s, e1) => { lb.ScrollIntoView(lb.Items[lb.Items.Count-1]); };  
     
    // on every Add to the source collection:  
    ..Data.Add(“blablabla”);  
    CollectionViewSource.GetDefaultView(..Data).MoveCurrentToLast();  
     


    Another way would be to "listen" to ItemSource changes for my ListBox/ListView but this is as tricky as the sample above (and would also bring UI/data separation because this way ListBox should know some stuff about its data source)

    There are several other ways to achieve the same thing, but they are even more tricky (e.g. sit on ItemContainerGenerator events, or find ScrollViewer in the visual tree and play with its scroll offset etc.).

    Am I missing something or there's really no straightforward way to do such a common task in WPF?

    Thanks,
      Boris.
    • Edited by Borka_ Sunday, November 09, 2008 6:27 PM
    Sunday, November 09, 2008 6:26 PM

Answers

  • Interesting task.  Well, let`s solve it:  we keep UI/data separation principle and move all the implementation stuff into the separate code file.

    In the example window XAML we just declare: ListBox have to auto-scroll:


    <
    Window x:Class="ListBoxAutoScrollTest.Window1" 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
            xmlns:w="clr-namespace:System.Windows.Workarounds" 
            Title="Window1" Height="150" Width="200">  
        <Grid> 
            <Grid.RowDefinitions> 
                <RowDefinition Height="*"/>  
                <RowDefinition Height="Auto"/>  
            </Grid.RowDefinitions> 
            <ListBox ItemsSource="{Binding}" 
                     w:ListBox.AutoScroll="True" /> 
            <Button Grid.Row="1" 
                    Click="Add_Click">Add item</Button> 
        </Grid> 
    </Window> 
     


    Corresponding C# code-behind is trivial. It provides example implementation and have no mention about auto-scrolling:


    using
     System.Collections.ObjectModel;  
    using System.Windows;  
     
    namespace ListBoxAutoScrollTest  
    {  
        public partial class Window1 : Window  
        {  
            ObservableCollection<Person> People;  
            public Window1()  
            {  
     
                InitializeComponent();  
     
                People = new ObservableCollection<Person>();  
                People.Add(new Person("Jack"));  
                People.Add(new Person("Michael"));  
                People.Add(new Person("Leonard"));  
                People.Add(new Person("Arthur"));  
                People.Add(new Person("Robert"));  
     
                DataContext = People;  
     
            }  
     
            private void Add_Click(object sender, RoutedEventArgs e)  
            {  
                People.Add(new Person("Person # " + (People.Count + 1)));  
            }  
     
        }  
     
        public class Person  
        {  
            public string Name { getprivate set; }  
            public Person(string name)  
            {  
                Name = name;  
            }  
            public override string ToString()  
            {  
                return Name;  
            }  
        }  
     
    }
     


    All the auto-scroll implementation stuff we put into separate application code file.

    Here we listen to ListBox.ItemsSource property changes, subscribe to NotifyCollectionChanged event of underlying collection and call ListBox.ScrollIntoView() method when new items are added: 


    using
     System.Collections;  
    using System.Collections.Generic;  
    using System.Collections.ObjectModel;  
    using System.Collections.Specialized;  
    using System.Windows.Data;  
     
    namespace System.Windows.Workarounds  
    {  
     
        public static class ListBox  
        {  
     
            public static readonly DependencyProperty AutoScrollProperty =  
                DependencyProperty.RegisterAttached("AutoScroll"typeof(bool), typeof(System.Windows.Controls.ListBox),  
                new PropertyMetadata(false));  
     
            public static readonly DependencyProperty AutoScrollHandlerProperty =  
                DependencyProperty.RegisterAttached("AutoScrollHandler"typeof(AutoScrollHandler), typeof(System.Windows.Controls.ListBox));  
     
            public static bool GetAutoScroll(System.Windows.Controls.ListBox instance)  
            {  
                return (bool)instance.GetValue(AutoScrollProperty);  
            }  
     
            public static void SetAutoScroll(System.Windows.Controls.ListBox instance, bool value)  
            {  
                AutoScrollHandler OldHandler = (AutoScrollHandler)instance.GetValue(AutoScrollHandlerProperty);  
                if (OldHandler != null)  
                {  
                    OldHandler.Dispose();  
                    instance.SetValue(AutoScrollHandlerProperty, null);  
                }  
                instance.SetValue(AutoScrollProperty, value);  
                if (value)  
                    instance.SetValue(AutoScrollHandlerProperty, new AutoScrollHandler(instance));  
            }  
     
        }  
     
        public class AutoScrollHandler : DependencyObjectIDisposable  
        {  
     
            public static readonly DependencyProperty ItemsSourceProperty =  
                DependencyProperty.Register("ItemsSource"typeof(IEnumerable),  
                typeof(AutoScrollHandler), new FrameworkPropertyMetadata(nullFrameworkPropertyMetadataOptions.None,  
                    new PropertyChangedCallback(ItemsSourcePropertyChanged)));  
     
            private System.Windows.Controls.ListBox Target;  
     
            public AutoScrollHandler(System.Windows.Controls.ListBox target)  
            {  
                Target = target;  
                Binding B = new Binding("ItemsSource");  
                B.Source = Target;  
                BindingOperations.SetBinding(this, ItemsSourceProperty, B);  
            }  
     
            public void Dispose()  
            {  
                BindingOperations.ClearBinding(this, ItemsSourceProperty);  
            }  
     
            public IEnumerable ItemsSource  
            {  
                get { return (IEnumerable)GetValue(ItemsSourceProperty); }  
                set { SetValue(ItemsSourceProperty, value); }  
            }  
     
            static void ItemsSourcePropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)  
            {  
                ((AutoScrollHandler)o).ItemsSourceChanged((IEnumerable)e.OldValue, (IEnumerable)e.NewValue);  
            }  
     
            void ItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)  
            {  
                INotifyCollectionChanged Collection = oldValue as INotifyCollectionChanged;  
                if (Collection != null)  
                    Collection.CollectionChanged -= new NotifyCollectionChangedEventHandler(Collection_CollectionChanged);  
                Collection = newValue as INotifyCollectionChanged;  
                if (Collection != null)  
                    Collection.CollectionChanged += new NotifyCollectionChangedEventHandler(Collection_CollectionChanged);  
            }  
     
            void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)  
            {  
                if (e.Action != NotifyCollectionChangedAction.Add || e.NewItems == null || e.NewItems.Count < 1)  
                    return;  
                Target.ScrollIntoView(e.NewItems[e.NewItems.Count - 1]);  
            }  
     
        }  
     


    Hope, this helps.

    • Proposed as answer by Alexander Yudakov Sunday, November 09, 2008 10:07 PM
    • Edited by Alexander Yudakov Sunday, November 09, 2008 10:11 PM
    • Unproposed as answer by Borka_ Sunday, November 09, 2008 11:27 PM
    • Marked as answer by Marco Zhou Wednesday, November 12, 2008 10:09 AM
    Sunday, November 09, 2008 10:07 PM
  • I can propose similar, but smaller solution

    Use this inside OnAutoScrollChanged handler

    var val = (bool)e.NewValue; 
                var lb = s as ListBox; 
                var ic = lb.Items; 
                var data = ic.SourceCollection as INotifyCollectionChanged; 
     
                var autoscroller = new System.Collections.Specialized.NotifyCollectionChangedEventHandler( 
                    (s1, e1) => { 
                        object selectedItem = default(object); 
                        switch (e1.Action) { 
                            case NotifyCollectionChangedAction.Add: 
                            case NotifyCollectionChangedAction.Move: selectedItem = e1.NewItems[e1.NewItems.Count - 1]; break
                            case NotifyCollectionChangedAction.Remove: if (ic.Count < e1.OldStartingIndex) { selectedItem = ic[e1.OldStartingIndex - 1]; } else if (ic.Count > 0) selectedItem = ic[0]; break
                            case NotifyCollectionChangedAction.Reset: if (ic.Count > 0) selectedItem = ic[0]; break
                        } 
     
                        if (selectedItem != default(object)) { 
                            ic.MoveCurrentTo(selectedItem); 
                            lb.ScrollIntoView(selectedItem); 
                        } 
                    }); 
     
                if (val) data.CollectionChanged += autoscroller;   
                else  data.CollectionChanged -= autoscroller;  
     
            } 


    Tamir http://blogs.microsoft.co.il/blogs/tamir
    If your question was answered, please mark it.
    • Marked as answer by Marco Zhou Wednesday, November 12, 2008 10:08 AM
    Monday, November 10, 2008 1:25 PM

All replies

  • Interesting task.  Well, let`s solve it:  we keep UI/data separation principle and move all the implementation stuff into the separate code file.

    In the example window XAML we just declare: ListBox have to auto-scroll:


    <
    Window x:Class="ListBoxAutoScrollTest.Window1" 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
            xmlns:w="clr-namespace:System.Windows.Workarounds" 
            Title="Window1" Height="150" Width="200">  
        <Grid> 
            <Grid.RowDefinitions> 
                <RowDefinition Height="*"/>  
                <RowDefinition Height="Auto"/>  
            </Grid.RowDefinitions> 
            <ListBox ItemsSource="{Binding}" 
                     w:ListBox.AutoScroll="True" /> 
            <Button Grid.Row="1" 
                    Click="Add_Click">Add item</Button> 
        </Grid> 
    </Window> 
     


    Corresponding C# code-behind is trivial. It provides example implementation and have no mention about auto-scrolling:


    using
     System.Collections.ObjectModel;  
    using System.Windows;  
     
    namespace ListBoxAutoScrollTest  
    {  
        public partial class Window1 : Window  
        {  
            ObservableCollection<Person> People;  
            public Window1()  
            {  
     
                InitializeComponent();  
     
                People = new ObservableCollection<Person>();  
                People.Add(new Person("Jack"));  
                People.Add(new Person("Michael"));  
                People.Add(new Person("Leonard"));  
                People.Add(new Person("Arthur"));  
                People.Add(new Person("Robert"));  
     
                DataContext = People;  
     
            }  
     
            private void Add_Click(object sender, RoutedEventArgs e)  
            {  
                People.Add(new Person("Person # " + (People.Count + 1)));  
            }  
     
        }  
     
        public class Person  
        {  
            public string Name { getprivate set; }  
            public Person(string name)  
            {  
                Name = name;  
            }  
            public override string ToString()  
            {  
                return Name;  
            }  
        }  
     
    }
     


    All the auto-scroll implementation stuff we put into separate application code file.

    Here we listen to ListBox.ItemsSource property changes, subscribe to NotifyCollectionChanged event of underlying collection and call ListBox.ScrollIntoView() method when new items are added: 


    using
     System.Collections;  
    using System.Collections.Generic;  
    using System.Collections.ObjectModel;  
    using System.Collections.Specialized;  
    using System.Windows.Data;  
     
    namespace System.Windows.Workarounds  
    {  
     
        public static class ListBox  
        {  
     
            public static readonly DependencyProperty AutoScrollProperty =  
                DependencyProperty.RegisterAttached("AutoScroll"typeof(bool), typeof(System.Windows.Controls.ListBox),  
                new PropertyMetadata(false));  
     
            public static readonly DependencyProperty AutoScrollHandlerProperty =  
                DependencyProperty.RegisterAttached("AutoScrollHandler"typeof(AutoScrollHandler), typeof(System.Windows.Controls.ListBox));  
     
            public static bool GetAutoScroll(System.Windows.Controls.ListBox instance)  
            {  
                return (bool)instance.GetValue(AutoScrollProperty);  
            }  
     
            public static void SetAutoScroll(System.Windows.Controls.ListBox instance, bool value)  
            {  
                AutoScrollHandler OldHandler = (AutoScrollHandler)instance.GetValue(AutoScrollHandlerProperty);  
                if (OldHandler != null)  
                {  
                    OldHandler.Dispose();  
                    instance.SetValue(AutoScrollHandlerProperty, null);  
                }  
                instance.SetValue(AutoScrollProperty, value);  
                if (value)  
                    instance.SetValue(AutoScrollHandlerProperty, new AutoScrollHandler(instance));  
            }  
     
        }  
     
        public class AutoScrollHandler : DependencyObjectIDisposable  
        {  
     
            public static readonly DependencyProperty ItemsSourceProperty =  
                DependencyProperty.Register("ItemsSource"typeof(IEnumerable),  
                typeof(AutoScrollHandler), new FrameworkPropertyMetadata(nullFrameworkPropertyMetadataOptions.None,  
                    new PropertyChangedCallback(ItemsSourcePropertyChanged)));  
     
            private System.Windows.Controls.ListBox Target;  
     
            public AutoScrollHandler(System.Windows.Controls.ListBox target)  
            {  
                Target = target;  
                Binding B = new Binding("ItemsSource");  
                B.Source = Target;  
                BindingOperations.SetBinding(this, ItemsSourceProperty, B);  
            }  
     
            public void Dispose()  
            {  
                BindingOperations.ClearBinding(this, ItemsSourceProperty);  
            }  
     
            public IEnumerable ItemsSource  
            {  
                get { return (IEnumerable)GetValue(ItemsSourceProperty); }  
                set { SetValue(ItemsSourceProperty, value); }  
            }  
     
            static void ItemsSourcePropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)  
            {  
                ((AutoScrollHandler)o).ItemsSourceChanged((IEnumerable)e.OldValue, (IEnumerable)e.NewValue);  
            }  
     
            void ItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)  
            {  
                INotifyCollectionChanged Collection = oldValue as INotifyCollectionChanged;  
                if (Collection != null)  
                    Collection.CollectionChanged -= new NotifyCollectionChangedEventHandler(Collection_CollectionChanged);  
                Collection = newValue as INotifyCollectionChanged;  
                if (Collection != null)  
                    Collection.CollectionChanged += new NotifyCollectionChangedEventHandler(Collection_CollectionChanged);  
            }  
     
            void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)  
            {  
                if (e.Action != NotifyCollectionChangedAction.Add || e.NewItems == null || e.NewItems.Count < 1)  
                    return;  
                Target.ScrollIntoView(e.NewItems[e.NewItems.Count - 1]);  
            }  
     
        }  
     


    Hope, this helps.

    • Proposed as answer by Alexander Yudakov Sunday, November 09, 2008 10:07 PM
    • Edited by Alexander Yudakov Sunday, November 09, 2008 10:11 PM
    • Unproposed as answer by Borka_ Sunday, November 09, 2008 11:27 PM
    • Marked as answer by Marco Zhou Wednesday, November 12, 2008 10:09 AM
    Sunday, November 09, 2008 10:07 PM
  • Well, this could work, just like other similar solutions I mentioned. The only problem is that I was looking for some really trivial solution, while the idea above demands writing 2 extra classes(!) with ~60(!) lines of code for something I would expect to look like: "<ListBox AutoScroll="true" />"
     
    Thanks anyway.
    Sunday, November 09, 2008 11:51 PM
  • I can propose similar, but smaller solution

    Use this inside OnAutoScrollChanged handler

    var val = (bool)e.NewValue; 
                var lb = s as ListBox; 
                var ic = lb.Items; 
                var data = ic.SourceCollection as INotifyCollectionChanged; 
     
                var autoscroller = new System.Collections.Specialized.NotifyCollectionChangedEventHandler( 
                    (s1, e1) => { 
                        object selectedItem = default(object); 
                        switch (e1.Action) { 
                            case NotifyCollectionChangedAction.Add: 
                            case NotifyCollectionChangedAction.Move: selectedItem = e1.NewItems[e1.NewItems.Count - 1]; break
                            case NotifyCollectionChangedAction.Remove: if (ic.Count < e1.OldStartingIndex) { selectedItem = ic[e1.OldStartingIndex - 1]; } else if (ic.Count > 0) selectedItem = ic[0]; break
                            case NotifyCollectionChangedAction.Reset: if (ic.Count > 0) selectedItem = ic[0]; break
                        } 
     
                        if (selectedItem != default(object)) { 
                            ic.MoveCurrentTo(selectedItem); 
                            lb.ScrollIntoView(selectedItem); 
                        } 
                    }); 
     
                if (val) data.CollectionChanged += autoscroller;   
                else  data.CollectionChanged -= autoscroller;  
     
            } 


    Tamir http://blogs.microsoft.co.il/blogs/tamir
    If your question was answered, please mark it.
    • Marked as answer by Marco Zhou Wednesday, November 12, 2008 10:08 AM
    Monday, November 10, 2008 1:25 PM
  • Guys, please look at the title of my question - I was NOT asking HOW to do autoscroll - I know how to do it and I wrote how, for example, this can be done. I was looking for a TRIVIAL way to do it, and if there's no trivial way - a "no" is a perfectly good answer.

    Meanwhile, the shortest, the simplest and pretty safe way I found is like this (a ~5 line, no extra classes solution):

    lb.Items.SourceCollection.CollectionChanged += (s, e) => {  
       var sv = /* find ScrollViewer in lb's visual tree */ as ScrollViewer;  
       sv.ScrollToEnd();  
    };  
     


    Thanks a lot,
      Boris.

    Monday, November 10, 2008 1:43 PM
  • You solution will work only for scroll to the end. In my solution you can scroll into newly added or last removed items. Also /* find ScrollViewer in lb's visual tree */ it a bit more, then ~5 lines of extra scroll and for sure at least one extra method (you're digging visual tree also, which is not very fast and efficient query)
    Tamir http://blogs.microsoft.co.il/blogs/tamir
    If your question was answered, please mark it.
    Monday, November 10, 2008 1:47 PM
  • In my question, I only want it to scroll to the end - that's generally the common meaning of autoscroll. I do not want it to do anything else. Moreover, I want it NOT to do anything else.

    Anyway, it seems that nobody knows some really simple solution - I hope WPF will eventually have such trivial common tasks built-in.

    Boris.

    Monday, November 10, 2008 1:51 PM
  • Hi,
    i have resolved with this simple line of code.

    ...
    mylistbox.SelectIndex(n); // Or SelectItem ...

    mylistbox.ScrollIntoView( myobject );

    After this the listbox show my new selection.

    Alessandro

    Tuesday, December 02, 2008 9:54 AM
  • Hello, I was just working with a ListBox that I dynamically add items (status text) to during a file import procedure.  I tried using ScrollIntoView(object item) to "autoscroll" down to the newly added item, but my display would end up lagging/freezing when displaying a large number of status messages (~50,000).  Not too surprising, considering it takes in an object instead of an index as a parameter.  This is the solution I came up with:

    xaml:

    <ListBox x:Name="m_cStatusList" ScrollViewer.ScrollChanged="m_cStatusList_ScrollChanged"></ListBox>
    
    

    cs:

    private void m_cStatusList_ScrollChanged(object sender,ScrollChangedEventArgs e)
    {
         if (e.ExtentHeightChange>0.0)
              ((ScrollViewer)e.OriginalSource).ScrollToEnd();
    }
    

    I haven't tried it with a bound source, but I'm assuming it would still work.

    -Al

    • Proposed as answer by cvconcrk Tuesday, April 21, 2009 1:33 AM
    Tuesday, April 21, 2009 1:30 AM
  • Try this:

    rather than Adding to your ObservableCollection, Insert(0, ...) instead.

    Combine that with  FlowDirection and see if it doesn't give you the effect you're looking for.


    Jeffrey Knight | MCTS .NET 2.0 | RHCT | http://www.rootsilver.com
    Wednesday, April 29, 2009 2:51 AM
  • Try this:

    rather than Adding to your ObservableCollection, Insert(0, ...) instead.

    Combine that with  FlowDirection and see if it doesn't give you the effect you're looking for.


    Jeffrey Knight | MCTS .NET 2.0 | RHCT | http://www.rootsilver.com
    This worked for me, and required no code behind. 

    I am using a horizontal Listbox (I replaced the ListBox ItemsPanelTemplate with a horizontal VirtualizingStackPanel) bound to an observable collection of images, and I wanted the last added image to show on the far right of the ListBox. I changed my ViewModel code to use Insert(0,..) on the ObservableCollection, and set the ListBox FlowDirection to "RightToLeft" in the XAML, and Voila, the ListBox 'autoscrolls' to the 'end' (right side, in my case). 

    Thanks Jeffrey - very elegant.
    Monday, December 28, 2009 9:58 PM
  • You solution will work only for scroll to the end. In my solution you can scroll into newly added or last removed items. Also /* find ScrollViewer in lb's visual tree */ it a bit more, then ~5 lines of extra scroll and for sure at least one extra method (you're digging visual tree also, which is not very fast and efficient query)
    Tamir http://blogs.microsoft.co.il/blogs/tamir
    If your question was answered, please mark it.

    I'm sure I found the simpliest solution for this case. Just this:  

                listbox1.ScrollIntoView(listbox1.Items.GetItemAt(listbox1.Count - 1));
    
    

    Hope it helps!

    Fernando Vieira

     

     

    Monday, March 29, 2010 5:24 AM
  • Hi Alexander, I want separation of UI/data separation that you provided in your solution.

    I used this solution in my project but it gives me design time error in Blend 3 that AutoScroll is already registered by ListBox.

    I don't know how to get rid of it.

    Sunday, May 09, 2010 6:43 PM
  • Thanks, This work great! All I had to do was cut-and-paste the ListBox class into my Solution and then add the attribute to the bound ListBox that I wanted to AutoScroll.
    Wednesday, August 14, 2013 4:20 PM