MSDN > Home page del forum > Windows Presentation Foundation (WPF) > Trivial ListBox/ListView Autoscroll
Formula una domandaFormula una domanda
 

Con rispostaTrivial ListBox/ListView Autoscroll

  • domenica 9 novembre 2008 18.26Borka_ Medaglie utenteMedaglie utenteMedaglie utenteMedaglie utenteMedaglie utente
     Contiene codice
    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.
    • ModificatoBorka_ domenica 9 novembre 2008 18.27
    •  

Risposte

  • domenica 9 novembre 2008 22.07Alexander Yudakov Medaglie utenteMedaglie utenteMedaglie utenteMedaglie utenteMedaglie utente
     Con rispostaContiene codice

    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.

  • lunedì 10 novembre 2008 13.25Tamir Khason Medaglie utenteMedaglie utenteMedaglie utenteMedaglie utenteMedaglie utente
     Con rispostaContiene codice
    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.
    • Contrassegnato come rispostaMarco Zhou mercoledì 12 novembre 2008 10.08
    •  

Tutte le risposte

  • domenica 9 novembre 2008 22.07Alexander Yudakov Medaglie utenteMedaglie utenteMedaglie utenteMedaglie utenteMedaglie utente
     Con rispostaContiene codice

    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.

  • domenica 9 novembre 2008 23.51Borka_ Medaglie utenteMedaglie utenteMedaglie utenteMedaglie utenteMedaglie utente
     
    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.
  • lunedì 10 novembre 2008 13.25Tamir Khason Medaglie utenteMedaglie utenteMedaglie utenteMedaglie utenteMedaglie utente
     Con rispostaContiene codice
    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.
    • Contrassegnato come rispostaMarco Zhou mercoledì 12 novembre 2008 10.08
    •  
  • lunedì 10 novembre 2008 13.43Borka_ Medaglie utenteMedaglie utenteMedaglie utenteMedaglie utenteMedaglie utente
     Contiene codice
    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.

  • lunedì 10 novembre 2008 13.47Tamir Khason Medaglie utenteMedaglie utenteMedaglie utenteMedaglie utenteMedaglie utente
     
    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.
  • lunedì 10 novembre 2008 13.51Borka_ Medaglie utenteMedaglie utenteMedaglie utenteMedaglie utenteMedaglie utente
     

    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.

  • martedì 2 dicembre 2008 9.54Alesandro Zambonin Medaglie utenteMedaglie utenteMedaglie utenteMedaglie utenteMedaglie utente
     
    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

  • martedì 21 aprile 2009 1.30cvconcrk Medaglie utenteMedaglie utenteMedaglie utenteMedaglie utenteMedaglie utente
     Risposta suggerita

    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

    • Proposto come rispostacvconcrk martedì 21 aprile 2009 1.33
    •  
  • mercoledì 29 aprile 2009 2.51Jeffrey Knight Medaglie utenteMedaglie utenteMedaglie utenteMedaglie utenteMedaglie utente
     Contiene codice
    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