Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I have a ListBox auto-scroll when a new item is added?

I have a WPF ListBox that is set to scroll horizontally. The ItemsSource is bound to an ObservableCollection in my ViewModel class. Every time a new item is added, I want the ListBox to scroll to the right so that the new item is viewable.

The ListBox is defined in a DataTemplate, so I am unable to access the ListBox by name in my code behind file.

How can I get a ListBox to always scroll to show a latest added item?

I would like a way to know when the ListBox has a new item added to it, but I do not see an event that does this.

like image 534
Rob Buhler Avatar asked Jan 05 '10 14:01

Rob Buhler


2 Answers

You can extend the behavior of the ListBox by using attached properties. In your case I would define an attached property called ScrollOnNewItem that when set to true hooks into the INotifyCollectionChanged events of the list box items source and upon detecting a new item, scrolls the list box to it.

Example:

class ListBoxBehavior {     static readonly Dictionary<ListBox, Capture> Associations =            new Dictionary<ListBox, Capture>();      public static bool GetScrollOnNewItem(DependencyObject obj)     {         return (bool)obj.GetValue(ScrollOnNewItemProperty);     }      public static void SetScrollOnNewItem(DependencyObject obj, bool value)     {         obj.SetValue(ScrollOnNewItemProperty, value);     }      public static readonly DependencyProperty ScrollOnNewItemProperty =         DependencyProperty.RegisterAttached(             "ScrollOnNewItem",             typeof(bool),             typeof(ListBoxBehavior),             new UIPropertyMetadata(false, OnScrollOnNewItemChanged));      public static void OnScrollOnNewItemChanged(         DependencyObject d,         DependencyPropertyChangedEventArgs e)     {         var listBox = d as ListBox;         if (listBox == null) return;         bool oldValue = (bool)e.OldValue, newValue = (bool)e.NewValue;         if (newValue == oldValue) return;         if (newValue)         {             listBox.Loaded += ListBox_Loaded;             listBox.Unloaded += ListBox_Unloaded;             var itemsSourcePropertyDescriptor = TypeDescriptor.GetProperties(listBox)["ItemsSource"];             itemsSourcePropertyDescriptor.AddValueChanged(listBox, ListBox_ItemsSourceChanged);         }         else         {             listBox.Loaded -= ListBox_Loaded;             listBox.Unloaded -= ListBox_Unloaded;             if (Associations.ContainsKey(listBox))                 Associations[listBox].Dispose();             var itemsSourcePropertyDescriptor = TypeDescriptor.GetProperties(listBox)["ItemsSource"];             itemsSourcePropertyDescriptor.RemoveValueChanged(listBox, ListBox_ItemsSourceChanged);         }     }      private static void ListBox_ItemsSourceChanged(object sender, EventArgs e)     {         var listBox = (ListBox)sender;         if (Associations.ContainsKey(listBox))             Associations[listBox].Dispose();         Associations[listBox] = new Capture(listBox);     }      static void ListBox_Unloaded(object sender, RoutedEventArgs e)     {         var listBox = (ListBox)sender;         if (Associations.ContainsKey(listBox))             Associations[listBox].Dispose();         listBox.Unloaded -= ListBox_Unloaded;     }      static void ListBox_Loaded(object sender, RoutedEventArgs e)     {         var listBox = (ListBox)sender;         var incc = listBox.Items as INotifyCollectionChanged;         if (incc == null) return;         listBox.Loaded -= ListBox_Loaded;         Associations[listBox] = new Capture(listBox);     }      class Capture : IDisposable     {         private readonly ListBox listBox;         private readonly INotifyCollectionChanged incc;          public Capture(ListBox listBox)         {             this.listBox = listBox;             incc = listBox.ItemsSource as INotifyCollectionChanged;             if (incc != null)             {                 incc.CollectionChanged += incc_CollectionChanged;             }         }          void incc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)         {             if (e.Action == NotifyCollectionChangedAction.Add)             {                 listBox.ScrollIntoView(e.NewItems[0]);                 listBox.SelectedItem = e.NewItems[0];             }         }          public void Dispose()         {             if (incc != null)                 incc.CollectionChanged -= incc_CollectionChanged;         }     } } 

Usage:

<ListBox ItemsSource="{Binding SourceCollection}"           lb:ListBoxBehavior.ScrollOnNewItem="true"/> 

UPDATE As per Andrej's suggestion in the comments below, I added hooks to detect a change in the ItemsSource of the ListBox.

like image 98
Aviad P. Avatar answered Sep 26 '22 07:09

Aviad P.


<ItemsControl ItemsSource="{Binding SourceCollection}">     <i:Interaction.Behaviors>         <Behaviors:ScrollOnNewItem/>     </i:Interaction.Behaviors>               </ItemsControl>  public class ScrollOnNewItem : Behavior<ItemsControl> {     protected override void OnAttached()     {         AssociatedObject.Loaded += OnLoaded;         AssociatedObject.Unloaded += OnUnLoaded;     }      protected override void OnDetaching()     {         AssociatedObject.Loaded -= OnLoaded;         AssociatedObject.Unloaded -= OnUnLoaded;     }      private void OnLoaded(object sender, RoutedEventArgs e)     {         var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;         if (incc == null) return;          incc.CollectionChanged += OnCollectionChanged;     }      private void OnUnLoaded(object sender, RoutedEventArgs e)     {         var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;         if (incc == null) return;          incc.CollectionChanged -= OnCollectionChanged;     }      private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)     {         if(e.Action == NotifyCollectionChangedAction.Add)         {             int count = AssociatedObject.Items.Count;             if (count == 0)                  return;               var item = AssociatedObject.Items[count - 1];              var frameworkElement = AssociatedObject.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;             if (frameworkElement == null) return;              frameworkElement.BringIntoView();         }     } 
like image 34
denis morozov Avatar answered Sep 24 '22 07:09

denis morozov