Stellen Sie eine FrageStellen Sie eine Frage
 

BeantwortetTrivial ListBox/ListView Autoscroll

  • Sonntag, 9. November 2008 18:26Borka_ TeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillen
     Enthält Code
    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.
    • BearbeitetBorka_ Sonntag, 9. November 2008 18:27
    •  

Antworten

  • Sonntag, 9. November 2008 22:07Alexander Yudakov TeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillen
     BeantwortetEnthält Code

    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.

  • Montag, 10. November 2008 13:25Tamir Khason TeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillen
     BeantwortetEnthält Code
    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.
    • Als Antwort markiertMarco Zhou Mittwoch, 12. November 2008 10:08
    •  

Alle Antworten

  • Sonntag, 9. November 2008 22:07Alexander Yudakov TeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillen
     BeantwortetEnthält Code

    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.

  • Sonntag, 9. November 2008 23:51Borka_ TeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillen
     
    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.
  • Montag, 10. November 2008 13:25Tamir Khason TeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillen
     BeantwortetEnthält Code
    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.
    • Als Antwort markiertMarco Zhou Mittwoch, 12. November 2008 10:08
    •  
  • Montag, 10. November 2008 13:43Borka_ TeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillen
     Enthält Code
    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.

  • Montag, 10. November 2008 13:47Tamir Khason TeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillen
     
    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.
  • Montag, 10. November 2008 13:51Borka_ TeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillen
     

    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.

  • Dienstag, 2. Dezember 2008 09:54Alesandro Zambonin TeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillen
     
    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

  • Dienstag, 21. April 2009 01:30cvconcrk TeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillen
     Vorgeschlagene Antwort

    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

    • Als Antwort vorgeschlagencvconcrk Dienstag, 21. April 2009 01:33
    •  
  • Mittwoch, 29. April 2009 02:51Jeffrey Knight TeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillenTeilnehmermedaillen
     Enthält Code
    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