Trivial ListBox/ListView Autoscroll
- 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.- UpravenýBorka_ 9. listopadu 2008 18:27
Odpovědi
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 { get; private 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 : DependencyObject, IDisposable { public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(AutoScrollHandler), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.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.- Navržen jako odpověďAlexander Yudakov 9. listopadu 2008 22:07
- UpravenýAlexander Yudakov 9. listopadu 2008 22:09
- UpravenýAlexander Yudakov 9. listopadu 2008 22:11
- Zrušeno navržení jako odpověďBorka_ 9. listopadu 2008 23:27
- Označen jako odpověďMarco Zhou 12. listopadu 2008 10:09
- I can propose similar, but smaller solution
Use this inside OnAutoScrollChanged handlervar 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.- Označen jako odpověďMarco Zhou 12. listopadu 2008 10:08
Všechny reakce
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 { get; private 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 : DependencyObject, IDisposable { public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(AutoScrollHandler), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.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.- Navržen jako odpověďAlexander Yudakov 9. listopadu 2008 22:07
- UpravenýAlexander Yudakov 9. listopadu 2008 22:09
- UpravenýAlexander Yudakov 9. listopadu 2008 22:11
- Zrušeno navržení jako odpověďBorka_ 9. listopadu 2008 23:27
- Označen jako odpověďMarco Zhou 12. listopadu 2008 10:09
- 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. - I can propose similar, but smaller solution
Use this inside OnAutoScrollChanged handlervar 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.- Označen jako odpověďMarco Zhou 12. listopadu 2008 10:08
- 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. - 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. 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.- 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 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- Navržen jako odpověďcvconcrk 21. dubna 2009 1:33
- 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

