Как я могу использовать автоматическую прокрутку ListBox при добавлении нового элемента?

У меня есть ListBox WPF, который настроен на прокрутку по горизонтали. Элемент ItemsSource привязан к ObservableCollection в моем classе ViewModel. Каждый раз, когда добавляется новый элемент, я хочу, чтобы ListBox прокручивался вправо, чтобы новый элемент был доступен для просмотра.

ListBox определен в DataTemplate, поэтому я не могу получить доступ к ListBox по имени в моем коде за файлом.

Как я могу заставить ListBox всегда прокручивать, чтобы показать последний добавленный элемент?

Мне бы хотелось узнать, когда в ListBox добавлен новый элемент, но я не вижу события, которое это делает.

Вы можете расширить поведение ListBox, используя прикрепленные свойства. В вашем случае я бы определил nested свойство ScrollOnNewItem которое, когда установлено значение true hooks в событиях INotifyCollectionChanged источника элементов списка и после обнаружения нового элемента, прокручивает к нему список.

Пример:

 class ListBoxBehavior { static readonly Dictionary Associations = new Dictionary(); 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; } } } 

Применение:

  

ОБНОВЛЕНИЕ Согласно предложению Андрея в комментариях ниже, я добавил крючки, чтобы обнаружить изменение в ItemsSource ListBox .

      public class ScrollOnNewItem : Behavior { 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(); } } 

Я нашел действительно скользкий способ сделать это, просто обновить listroll scrollViewer и установить позицию на дно. Вызовите эту функцию в одном из таких событий ListBox, как SelectionChanged.

  private void UpdateScrollBar(ListBox listBox) { if (listBox != null) { var border = (Border)VisualTreeHelper.GetChild(listBox, 0); var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0); scrollViewer.ScrollToBottom(); } } 

Я использую это решение: http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/ .

Он работает, даже если вы привязываете ItemSource списка к ObservableCollection, который обрабатывается в streamе, отличном от UI.

решение для Datagrid (то же самое для ListBox, замените DataGrid только classом ListBox)

  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]; if (AssociatedObject is DataGrid) { DataGrid grid = (AssociatedObject as DataGrid); grid.Dispatcher.BeginInvoke((Action)(() => { grid.UpdateLayout(); grid.ScrollIntoView(item, null); })); } } } 

Приведенное поведение в стиле MVVM

Это прикрепленное поведение автоматически прокручивает список вниз, когда добавляется новый элемент.

      

В вашей ViewModel вы можете привязываться к boolean IfFollowTail { get; set; } IfFollowTail { get; set; } IfFollowTail { get; set; } чтобы контролировать, активна ли автоматическая прокрутка.

Поведение делает все правильно:

  • Если IfFollowTail=false задано в ViewModel, ListBox больше не прокручивается в нижнюю часть нового элемента.
  • Как только в IfFollowTail=true , ListBox мгновенно прокручивается вниз и продолжает это делать.
  • Это быстро. Он прокручивается только через пару сотен миллисекунд бездействия. Наивная реализация будет очень медленной, так как она будет прокручиваться при каждом добавленном новом элементе.
  • Он работает с дублирующимися элементами ListBox (многие другие реализации не работают с дубликатами – они прокручиваются до первого элемента, а затем останавливаются).
  • Он идеально подходит для консоли ведения журнала, которая занимается непрерывными входящими элементами.

Код поведения C #

 public class ScrollOnNewItemBehavior : Behavior { public static readonly DependencyProperty IsActiveScrollOnNewItemProperty = DependencyProperty.Register( name: "IsActiveScrollOnNewItem", propertyType: typeof(bool), ownerType: typeof(ScrollOnNewItemBehavior), typeMetadata: new PropertyMetadata(defaultValue: true, propertyChangedCallback:PropertyChangedCallback)); private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { // Intent: immediately scroll to the bottom if our dependency property changes. ScrollOnNewItemBehavior behavior = dependencyObject as ScrollOnNewItemBehavior; if (behavior == null) { return; } behavior.IsActiveScrollOnNewItemMirror = (bool)dependencyPropertyChangedEventArgs.NewValue; if (behavior.IsActiveScrollOnNewItemMirror == false) { return; } ListboxScrollToBottom(behavior.ListBox); } public bool IsActiveScrollOnNewItem { get { return (bool)this.GetValue(IsActiveScrollOnNewItemProperty); } set { this.SetValue(IsActiveScrollOnNewItemProperty, value); } } public bool IsActiveScrollOnNewItemMirror { get; set; } = true; protected override void OnAttached() { this.AssociatedObject.Loaded += this.OnLoaded; this.AssociatedObject.Unloaded += this.OnUnLoaded; } protected override void OnDetaching() { this.AssociatedObject.Loaded -= this.OnLoaded; this.AssociatedObject.Unloaded -= this.OnUnLoaded; } private IDisposable rxScrollIntoView; private void OnLoaded(object sender, RoutedEventArgs e) { var changed = this.AssociatedObject.ItemsSource as INotifyCollectionChanged; if (changed == null) { return; } // Intent: If we scroll into view on every single item added, it slows down to a crawl. this.rxScrollIntoView = changed .ToObservable() .ObserveOn(new EventLoopScheduler(ts => new Thread(ts) { IsBackground = true})) .Where(o => this.IsActiveScrollOnNewItemMirror == true) .Where(o => o.NewItems?.Count > 0) .Sample(TimeSpan.FromMilliseconds(180)) .Subscribe(o => { this.Dispatcher.BeginInvoke((Action)(() => { ListboxScrollToBottom(this.ListBox); })); }); } ListBox ListBox => this.AssociatedObject; private void OnUnLoaded(object sender, RoutedEventArgs e) { this.rxScrollIntoView?.Dispose(); } ///  /// Scrolls to the bottom. Unlike other methods, this works even if there are duplicate items in the listbox. ///  private static void ListboxScrollToBottom(ListBox listBox) { if (VisualTreeHelper.GetChildrenCount(listBox) > 0) { Border border = (Border)VisualTreeHelper.GetChild(listBox, 0); ScrollViewer scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0); scrollViewer.ScrollToBottom(); } } } 

Мост от событий до реактивных расширений

Наконец, добавьте этот метод расширения, чтобы мы могли использовать все возможности RX:

 public static class ListBoxEventToObservableExtensions { /// Converts CollectionChanged to an observable sequence. public static IObservable ToObservable(this T source) where T : INotifyCollectionChanged { return Observable.FromEvent( h => (sender, e) => h(e), h => source.CollectionChanged += h, h => source.CollectionChanged -= h); } } 

Добавить реактивные расширения

Вам нужно будет добавить в проект Reactive Extensions . Я рекомендую NuGet .

Самый прямой способ, который я нашел для этого, особенно для listbox (или listview), привязанного к источнику данных, – это связать его с событием изменения коллекции. Вы можете сделать это очень легко в событии DataContextChanged из списка:

  //in xaml  private void LogView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { var src = LogView.Items.SourceCollection as INotifyCollectionChanged; src.CollectionChanged += (obj, args) => { LogView.Items.MoveCurrentToLast(); LogView.ScrollIntoView(LogView.Items.CurrentItem); }; } 

На самом деле это всего лишь комбинация всех других ответов, которые я нашел. Я чувствую, что это такая тривиальная функция, что нам не нужно тратить так много времени (и строк кода).

Если бы существовало свойство Autoscroll = true. Вздох.

Я нашел гораздо более простой способ, который помог мне с аналогичной проблемой, всего лишь пару строк кода, не нужно создавать пользовательские поведенья. Проверьте мой ответ на этот вопрос (и перейдите по ссылке внутри):

wpf (C #) DataGrid ScrollIntoView – как прокручивать первую строку, которая не отображается?

Он работает для ListBox, ListView и DataGrid.

Я не был доволен предлагаемыми решениями.

  • Я не хотел использовать дескрипторы свойств «leaky».
  • Я не хотел добавлять зависимость Rx и 8-строчный запрос для кажущейся тривиальной задачи. Мне тоже не нужен постоянно работающий таймер.
  • Однако мне понравилась идея shawnpfiore, поэтому я построил над ним надёжное поведение, которое до сих пор хорошо работает в моем случае.

Вот что я закончил. Может быть, это кое-что спасет.

 public class AutoScroll : Behavior { public static readonly DependencyProperty ModeProperty = DependencyProperty.Register( "Mode", typeof(AutoScrollMode), typeof(AutoScroll), new PropertyMetadata(AutoScrollMode.VerticalWhenInactive)); public AutoScrollMode Mode { get => (AutoScrollMode) GetValue(ModeProperty); set => SetValue(ModeProperty, value); } protected override void OnAttached() { base.OnAttached(); AssociatedObject.Loaded += OnLoaded; AssociatedObject.Unloaded += OnUnloaded; } protected override void OnDetaching() { Clear(); AssociatedObject.Loaded -= OnLoaded; AssociatedObject.Unloaded -= OnUnloaded; base.OnDetaching(); } private static readonly DependencyProperty ItemsCountProperty = DependencyProperty.Register( "ItemsCount", typeof(int), typeof(AutoScroll), new PropertyMetadata(0, (s, e) => ((AutoScroll)s).OnCountChanged())); private ScrollViewer _scroll; private void OnLoaded(object sender, RoutedEventArgs e) { var binding = new Binding("ItemsSource.Count") { Source = AssociatedObject, Mode = BindingMode.OneWay }; BindingOperations.SetBinding(this, ItemsCountProperty, binding); _scroll = AssociatedObject.FindVisualChild() ?? throw new NotSupportedException("ScrollViewer was not found!"); } private void OnUnloaded(object sender, RoutedEventArgs e) { Clear(); } private void Clear() { BindingOperations.ClearBinding(this, ItemsCountProperty); } private void OnCountChanged() { var mode = Mode; if (mode == AutoScrollMode.Vertical) { _scroll.ScrollToBottom(); } else if (mode == AutoScrollMode.Horizontal) { _scroll.ScrollToRightEnd(); } else if (mode == AutoScrollMode.VerticalWhenInactive) { if (_scroll.IsKeyboardFocusWithin) return; _scroll.ScrollToBottom(); } else if (mode == AutoScrollMode.HorizontalWhenInactive) { if (_scroll.IsKeyboardFocusWithin) return; _scroll.ScrollToRightEnd(); } } } public enum AutoScrollMode { ///  /// No auto scroll ///  Disabled, ///  /// Automatically scrolls horizontally, but only if items control has no keyboard focus ///  HorizontalWhenInactive, ///  /// Automatically scrolls vertically, but only if itmes control has no keyboard focus ///  VerticalWhenInactive, ///  /// Automatically scrolls horizontally regardless of where the focus is ///  Horizontal, ///  /// Automatically scrolls vertically regardless of where the focus is ///  Vertical } 
  • Как преобразовать строку ключа, разделенного пробелами, пары значений уникальных слов в dict
  • Как настроить переключатель предпочтений списка
  • Как преобразовать список корневых ключей в словарь?
  • В чем разница между Set и List?
  • Сортировка одного списка другим
  • Преобразование списка в * args при вызове функции
  • Как массивы на C # частично реализуют IList ?
  • Пересечение и объединение ArrayLists в Java
  • Что такое Haskell's Stream Fusion
  • Как удалить элемент из списка?
  • Что именно делает метод .join ()?
  • Давайте будем гением компьютера.