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.
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
.
<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(); } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With