如何在添加新项目时自动滚动ListBox?

Rob*_*ler 56 wpf scroll listbox

我有一个WPF ListBox设置为水平滚动.ItemsSource绑定到我的ViewModel类中的ObservableCollection.每次添加新项目时,我都希望ListBox向右滚动,以便新项目可见.

ListBox在DataTemplate中定义,因此我无法在代码隐藏文件中按名称访问ListBox.

如何让ListBox始终滚动以显示最新添加的项目?

我想知道ListBox何时添加了一个新项目,但是我没有看到这样做的事件.

Avi*_* P. 65

您可以使用附加属性扩展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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

<ListBox ItemsSource="{Binding SourceCollection}" 
         lb:ListBoxBehavior.ScrollOnNewItem="true"/>
Run Code Online (Sandbox Code Playgroud)

UPDATE按安德烈在下面的意见建议,我加了钩子,以检测改变ItemsSourceListBox.

  • 更新:上面的代码对我大多数时间都有效-有时添加了列表框项目,并且列表框不滚动。 (2认同)
  • 现在正在工作!问题是我多次添加相同的字符串进行测试,但是`ScrollIntoView`方法和`SelectedItem`属性只获取第一个对象,所以它总是在顶部,只是当我添加不同的字符串时它会向下滚动。我通过添加时间戳来防止这种行为。字符串的毫秒数:) (2认同)

den*_*zov 21

<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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

  • 非常好,我根本不知道`行为(Of T)`类!看起来更简洁,更易读. (4认同)
  • 此外,还有这种演变,http://stackoverflow.com/questions/12255055/how-to-get-itemscontrol-scrollbar-position-programmatically 应该在用户向上滚动时停止滚动。我仍然无法让行为发挥作用。在这两种情况下,项目容器似乎都不存在。 (2认同)

小智 19

我找到了一个非常灵活的方法,只需更新列表框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();
        }

    }
Run Code Online (Sandbox Code Playgroud)


Shu*_*huo 9

我使用这个解决方案:http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/.

即使将listbox的ItemsSource绑定到在非UI线程中操作的ObservableCollection,它也能工作.


Con*_*ngo 6

MVVM 风格的附加行为

当添加新项目时,此附加行为会自动将列表框滚动到底部。

<ListBox ItemsSource="{Binding LoggingStream}">
    <i:Interaction.Behaviors>
        <behaviors:ScrollOnNewItemBehavior 
           IsActiveScrollOnNewItem="{Binding IfFollowTail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    </i:Interaction.Behaviors>
</ListBox>
Run Code Online (Sandbox Code Playgroud)

在您的 中ViewModel,您可以绑定到布尔值IfFollowTail { get; set; }来控制自动滚动是否处于活动状态。

该行为做了所有正确的事情:

  • 如果IfFollowTail=false在 ViewModel 中设置,ListBox 将不再滚动到新项目的底部。
  • 一旦IfFollowTail=true在 ViewModel 中设置,ListBox 就会立即滚动到底部,并继续这样做。
  • 它很快。它仅在数百毫秒不活动后才会滚动。一个幼稚的实现会非常慢,因为它会在添加的每个新项目上滚动。
  • 它适用于重复的 ListBox 项目(许多其他实现不适用于重复项 - 它们滚动到第一个项目,然后停止)。
  • 它非常适合处理连续传入项目的日志控制台。

行为 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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

从事件到反应式扩展的桥梁

最后,添加此扩展方法,以便我们可以利用所有 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

添加反应式扩展

您需要添加Reactive Extensions到您的项目中。我建议NuGet