it-swarm.com.de

Wie kann ich eine ListBox automatisch scrollen lassen, wenn ein neues Element hinzugefügt wird?

Ich habe eine WPF-ListBox, die so eingestellt ist, dass sie horizontal scrollt. Die ItemsSource ist an eine ObservableCollection in meiner ViewModel-Klasse gebunden. Jedes Mal, wenn ein neues Element hinzugefügt wird, soll die ListBox nach rechts gescrollt werden, damit das neue Element angezeigt werden kann.

Die ListBox ist in einer DataTemplate definiert, daher kann ich nicht mit dem Namen in meinem Code hinter der Datei auf die ListBox zugreifen.

Wie kann ich eine ListBox so einstellen, dass immer ein Bildlauf erfolgt, um den zuletzt hinzugefügten Artikel anzuzeigen?

Ich möchte gerne wissen, wann der ListBox ein neues Element hinzugefügt wird, aber ich sehe kein Ereignis, das dies tut.

54
Rob Buhler

Sie können das Verhalten der ListBox erweitern, indem Sie angefügte Eigenschaften verwenden. In Ihrem Fall würde ich eine angehängte Eigenschaft mit dem Namen ScrollOnNewItem definieren, die bei Festlegung auf true die INotifyCollectionChanged -Ereignisse der Quellenquelle des Listenfelds einhakt und beim Erkennen eines neuen Elements das Listenfeld darauf blättert.

Beispiel:

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;
        }
    }
}

Verwendungszweck:

<ListBox ItemsSource="{Binding SourceCollection}" 
         lb:ListBoxBehavior.ScrollOnNewItem="true"/>

UPDATEGemäß dem Vorschlag von Andrej in den Kommentaren unten habe ich Hooks hinzugefügt, um eine Änderung in der ItemsSource der ListBox zu erkennen.

63
Aviad P.
<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();
        }
    }
20
denis morozov

Ich habe einen wirklich schnellen Weg gefunden, dies zu tun. Aktualisieren Sie einfach die Listbox scrollViewer und stellen Sie die Position nach unten. Rufen Sie diese Funktion in einem der ListBox-Ereignisse wie beispielsweise SelectionChanged auf. 

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

    }
19
shawnpfiore

Ich verwende diese Lösung: http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/ .

Es funktioniert auch, wenn Sie die ItemsSource der Listbox an eine ObservableCollection binden, die in einem Nicht-UI-Thread bearbeitet wird.

9
Shuo

lösung für Datagrid (dasselbe für ListBox, ersetzt DataGrid nur durch ListBox-Klasse) 

    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);
                }));
            }

        }
    }
2
Jozef Kemenik

Die einfachste Methode, die ich dazu gefunden habe, insbesondere für Listbox (oder Listenansicht), die an eine Datenquelle gebunden ist, besteht darin, sie mit dem Änderungsereignis für die Sammlung zu verbinden. Sie können dies sehr einfach beim DataContextChanged-Ereignis des Listenfelds tun: 

    //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); };
    }

Dies ist eigentlich nur eine Kombination aller anderen Antworten, die ich gefunden habe. Ich denke, dass dies ein so unbedeutendes Merkmal ist, dass wir nicht so viel Zeit (und Codezeilen) damit verbringen müssen. 

Wenn nur eine Autoscroll = true -Eigenschaft vorhanden wäre. Seufzer. 

1
mickeymicks

Angehängtes Verhalten im MVVM-Stil

Dieses angefügte Verhalten rollt das Listenfeld automatisch nach unten, wenn ein neues Element hinzugefügt wird.

<ListBox ItemsSource="{Binding LoggingStream}">
    <i:Interaction.Behaviors>
        <behaviors:ScrollOnNewItemBehavior 
           IsActiveScrollOnNewItem="{Binding IfFollowTail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    </i:Interaction.Behaviors>
</ListBox>

In ViewModel können Sie eine Bindung an den booleschen IfFollowTail { get; set; } erstellen, um zu steuern, ob der automatische Bildlauf aktiv ist oder nicht.

Das Verhalten tut alle richtigen Dinge:

  • Wenn im ViewModel IfFollowTail=false festgelegt ist, wird die ListBox bei einem neuen Element nicht mehr nach unten verschoben.
  • Sobald IfFollowTail=true im ViewModel festgelegt ist, scrollt die ListBox sofort nach unten und fährt damit fort.
  • Es ist schnell. Es scrollt nur nach ein paar hundert Millisekunden Inaktivität. Eine naive Implementierung wäre extrem langsam, da bei jedem neuen Element ein Bildlauf durchgeführt würde.
  • Es funktioniert mit doppelten ListBox-Elementen (viele andere Implementierungen funktionieren nicht mit Duplikaten - sie scrollen zum ersten Element und stoppen dann).
  • Es ist ideal für eine Protokollierungskonsole, die sich mit fortlaufenden eingehenden Artikeln befasst.

Verhalten 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();
        }
    }
}

Brücke von Ereignissen zu reaktiven Erweiterungen

Fügen Sie schließlich diese Erweiterungsmethode hinzu, damit wir die gesamte RX-Güte verwenden können:

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);
    }
}

Reaktive Erweiterungen hinzufügen

Sie müssen Reactive Extensions zu Ihrem Projekt hinzufügen. Ich empfehle NuGet.

1
Contango

Ich fand einen viel einfacheren Weg, der mir bei einem ähnlichen Problem half, nur ein paar Zeilen Code hinter sich, keine Notwendigkeit, benutzerdefinierte Verhalten zu erstellen. Überprüfen Sie meine Antwort auf diese Frage (und folgen Sie dem Link innerhalb):

wpf (C #) DataGrid ScrollIntoView - wie blättern Sie zur ersten Zeile, die nicht angezeigt wird?

Es funktioniert für ListBox, ListView und DataGrid.

0
Hannish

Was ich in diesen Topcs gelesen habe, ist ein bisschen komplex für eine einfache Aktion.

Also habe ich das scrollchanged-Event abonniert und dann diesen Code verwendet:

private void TelnetListBox_OnScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        var scrollViewer = ((ScrollViewer)e.OriginalSource);
        scrollViewer.ScrollToEnd();

    }

Bonus:

Danach habe ich ein Kontrollkästchen gesetzt, in dem ich festlegen konnte, wann ich die Autoscroll-Funktion verwenden möchte. Ich habe vergessen, dass ich das Listenfeld einige Male deaktiviert habe, wenn ich interessante Informationen für mich gefunden habe. Also entschied ich mich, eine intelligente autoscrollte Listbox zu erstellen, die auf meine Mausaktion reagiert.

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;
        } 
    }

Wenn ich zur unteren Seite des Kontrollkästchens geklettert bin, ist das Kontrollkästchen auf "Wahr" gesetzt und meine Ansicht bleibt unten. Wenn ich mit dem Mausrad hochgefahren bin, wird das Kontrollkästchen deaktiviert und Sie können das Listenfeld "Explorer" durchsuchen.

0
Birek

Ich war mit den vorgeschlagenen Lösungen nicht zufrieden.

  • Ich wollte keine "undichten" Eigenschaftsbeschreibungen verwenden.
  • Ich wollte nicht Rx-Abhängigkeit und 8-Zeilen-Abfrage für scheinbar triviale Aufgaben hinzufügen. Ich wollte auch keinen ständig laufenden Timer.
  • Ich mochte die Idee von shawnpfiore jedoch, deshalb habe ich dazu ein Verhalten hinzugefügt, das in meinem Fall bisher gut funktioniert.

Hier ist was ich endete. Vielleicht wird jemand etwas Zeit sparen.

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
}
0
Nikita B

Dies ist die Lösung, die ich verwende und die jemand anderem helfen kann.

 statusWindow.SelectedIndex = statusWindow.Items.Count - 1;
 statusWindow.UpdateLayout();
 statusWindow.ScrollIntoView(statusWindow.SelectedItem);
 statusWindow.UpdateLayout();
0
Madison Courto