如何添加一个新的项目时,一个ListBox自动滚动?
我有一个设置为水平滚动的WPF列表框。 ItemsSource绑定到ViewModel类中的ObservableCollection。 每次添加新项目时,我都希望ListBox滚动到右侧,以便新项目可见。
ListBox是在DataTemplate中定义的,所以我无法通过我的代码隐藏文件中的名称来访问ListBox。
我怎样才能得到一个ListBox总是滚动显示最新添加的项目?
我想知道什么时候ListBox添加了一个新的项目,但我没有看到这样做的事件。
您可以通过使用附加的属性来扩展列表框的行为。 在你的情况下,我将定义一个名为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"/>
更新按照Andrej在下面的评论中的build议,我添加了钩子来检测ListBox
的ItemsSource
中的更改。
<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和设置位置底部。 例如在SelectionChanged之类的ListBox事件中调用这个函数。
private void UpdateScrollBar(ListBox listBox) { if (listBox != null) { var border = (Border)VisualTreeHelper.GetChild(listBox, 0); var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0); scrollViewer.ScrollToBottom(); } }
我使用这个解决scheme: http : //michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/ 。
即使将列表框的ItemsSource绑定到在非UI线程中操作的ObservableCollection,它也可以工作。
Datagrid的解决scheme(ListBox相同,只能用ListBox类替代DataGrid)
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); })); } } }
我发现了一个更简单的方法,帮助我解决了类似的问题,后面只有几行代码,不需要创build自定义行为。 检查我对这个问题的答案(并按照内的链接):
wpf(C#)DataGrid ScrollIntoView – 如何滚动到第一行不显示?
它适用于ListBox,ListView和DataGrid。
MVVM风格的附加行为
添加新项目时,此附加行为自动将列表框滚动到底部。
<ListBox ItemsSource="{Binding LoggingStream}"> <i:Interaction.Behaviors> <behaviors:ScrollOnNewItemBehavior IsActiveScrollOnNewItem="{Binding IfFollowTail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> </i:Interaction.Behaviors> </ListBox>
在你的ViewModel
,你可以绑定到布尔值IfFollowTail { get; set; }
IfFollowTail { get; set; }
IfFollowTail { get; set; }
来控制自动滚动是否激活。
行为做所有正确的事情:
- 如果在ViewModel中设置了
IfFollowTail=false
,则列表框不再滚动到新项目的底部。 - 只要在ViewModel中设置了
IfFollowTail=true
,ListBox就会立即滚动到底部,并继续这样做。 - 它很快。 它只在几百毫秒的不活动之后才会滚动。 一个天真的执行会非常缓慢,因为它会滚动添加的每个新项目。
- 它可以处理重复的ListBox项目(很多其他的实现不能使用重复项 – 它们会滚动到第一项,然后停止)。
- 它是处理连续传入项目的日志控制台的理想select。
行为C#代码
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(); } } }
从事件到react native扩展的桥梁
最后,添加这个扩展方法,以便我们可以使用所有的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); } }
添加react native扩展
您需要将Reactive Extensions
添加到您的项目中。 我推荐NuGet
。
我发现最直接的方法,特别是绑定到数据源的列表框(或列表视图),是将其与集合更改事件挂钩。 在列表框的DataContextChanged事件中,您可以非常轻松地执行此操作:
//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); }; }
这实际上只是我find的所有其他答案的组合。 我觉得这是一个微不足道的function,我们不需要花费太多的时间(和代码行)。
如果只有一个Autoscroll = true属性。 叹。