Как я могу иметь автоматическую прокрутку списка при добавлении нового элемента?
У меня есть список WPF, который установлен для прокрутки по горизонтали. ItemsSource привязан к ObservableCollection в моем классе ViewModel. Каждый раз, когда добавляется новый элемент, я хочу, чтобы список прокручивался вправо, чтобы новый элемент был доступен для просмотра.
список определяется в DataTemplate, поэтому я не могу получить доступ к списку по имени в моем коде позади файла.
Как я могу получить список, чтобы всегда прокручивать, чтобы показать последний добавленный элемент?
Я хотел бы способ узнать, когда в ListBox добавлен новый элемент, но я не вижу события, которое это делает.
10 ответов:
вы можете расширить поведение списка с помощью вложенных свойств. В вашем случае я бы определил вложенное свойство под названием
ScrollOnNewItemчто при значенииtrueкрючки вINotifyCollectionChangedсобытия источника элементов списка и при обнаружении нового элемента прокрутите список до него.пример:
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; } } }использование:
<ListBox ItemsSource="{Binding SourceCollection}" lb:ListBoxBehavior.ScrollOnNewItem="true"/>обновление согласно предложению Андрея в комментариях ниже, я добавил крючки, чтобы обнаружить изменение
ItemsSourceна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(); } }
Я нашел действительно гладкий способ сделать это, просто обновите список 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/.
Это работает, даже если вы привязываете ItemsSource listbox к ObservableCollection, который управляется в потоке без пользовательского интерфейса.
решение для Datagrid (то же самое для ListBox, только заменить DataGrid с классом 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-стиль прилагается поведение
это прикрепленное поведение автоматически прокручивает список в нижней части при добавлении нового элемента.
<ListBox ItemsSource="{Binding LoggingStream}"> <i:Interaction.Behaviors> <behaviors:ScrollOnNewItemBehavior IsActiveScrollOnNewItem="{Binding IfFollowTail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> </i:Interaction.Behaviors> </ListBox>в своем
ViewModel, вы можете привязать к booleanIfFollowTail { get; set; }для управления активностью автоматической прокрутки.поведение делает все правильно:
- если
IfFollowTail=falseустанавливается в ViewModel, список больше не прокручивается вниз по новому элементу.- как только как
IfFollowTail=trueустанавливается в ViewModel, список мгновенно прокручивается вниз и продолжает это делать.- это быстро. Он прокручивается только через пару сотен миллисекунд бездействия. Наивная реализация будет чрезвычайно медленной, так как она будет прокручиваться на каждом новом добавленном элементе.
- он работает с повторяющимися элементами списка (многие другие реализации не работают с дубликатами - они прокручиваются до первого элемента, а затем останавливаются).
- это идеально подходит для консоль ведения журнала, которая работает с непрерывными входящими элементами.
Поведение C# Code
public class ScrollOnNewItemBehavior : Behavior<ListBox> { 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(); } /// <summary> /// Scrolls to the bottom. Unlike other methods, this works even if there are duplicate items in the listbox. /// </summary> 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 { /// <summary>Converts CollectionChanged to an observable sequence.</summary> public static IObservable<NotifyCollectionChangedEventArgs> ToObservable<T>(this T source) where T : INotifyCollectionChanged { return Observable.FromEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>( h => (sender, e) => h(e), h => source.CollectionChanged += h, h => source.CollectionChanged -= h); } }Добавить Реактивные Расширения
вам нужно будет добавить
Reactive Extensionsв проект. Я рекомендуюNuGet.
самый простой способ, который я нашел для этого, особенно для listbox (или listview), который привязан к источнику данных, - это подключить его к событию изменения коллекции. Вы можете сделать это очень легко в DataContextChanged событие listbox:
//in xaml <ListView x:Name="LogView" DataContextChanged="LogView_DataContextChanged"> 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.
Я не был доволен предлагаемым решениям.
- Я не хотел использовать "дырявые" дескрипторы свойств.
- Я не хотел добавлять зависимость Rx и 8-строчный запрос для, казалось бы, тривиальной задачи. Я также не хотел постоянно работающего таймера.
- мне понравилась идея shawnpfiore, поэтому я построил прикрепленное поведение поверх него, которое до сих пор хорошо работает в моем случае.
вот что у меня получилось. Может быть, это кого-то спасет время.
public class AutoScroll : Behavior<ItemsControl> { 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<ScrollViewer>() ?? 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 { /// <summary> /// No auto scroll /// </summary> Disabled, /// <summary> /// Automatically scrolls horizontally, but only if items control has no keyboard focus /// </summary> HorizontalWhenInactive, /// <summary> /// Automatically scrolls vertically, but only if itmes control has no keyboard focus /// </summary> VerticalWhenInactive, /// <summary> /// Automatically scrolls horizontally regardless of where the focus is /// </summary> Horizontal, /// <summary> /// Automatically scrolls vertically regardless of where the focus is /// </summary> Vertical }
Итак, то, что я прочитал в этом topcs, немного сложно для простого действия.
поэтому я подписался на scrollchanged событие, а затем я использовал этот код:
private void TelnetListBox_OnScrollChanged(object sender, ScrollChangedEventArgs e) { var scrollViewer = ((ScrollViewer)e.OriginalSource); scrollViewer.ScrollToEnd(); }бонус:
после этого я сделал флажок, где я мог бы установить, когда я хочу использовать функцию автопрокрутки, и я перезапустил я забыл несколько раз снять флажок со списка, если я увидел какую-то интересную информацию для меня. Поэтому я решил, что хотел бы создать интеллектуальный автопрокрученный список, который реагирует на мой действие мыши.
private void TelnetListBox_OnScrollChanged(object sender, ScrollChangedEventArgs e) { var scrollViewer = ((ScrollViewer)e.OriginalSource); scrollViewer.ScrollToEnd(); if (AutoScrollCheckBox.IsChecked != null && (bool)AutoScrollCheckBox.IsChecked) scrollViewer.ScrollToEnd(); if (_isDownMouseMovement) { var verticalOffsetValue = scrollViewer.VerticalOffset; var maxVerticalOffsetValue = scrollViewer.ExtentHeight - scrollViewer.ViewportHeight; if (maxVerticalOffsetValue < 0 || verticalOffsetValue == maxVerticalOffsetValue) { // Scrolled to bottom AutoScrollCheckBox.IsChecked = true; _isDownMouseMovement = false; } else if (verticalOffsetValue == 0) { } } } private bool _isDownMouseMovement = false; private void TelnetListBox_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e) { if (e.Delta > 0) { _isDownMouseMovement = false; AutoScrollCheckBox.IsChecked = false; } if (e.Delta < 0) { _isDownMouseMovement = true; } }когда я ругал Боттон флажок установлен true и остаться мой взгляд на дне, если я скрутился с колесом мыши checkox будет снят, и вы можете explorer вы listbox.
Comments