Come posso avere uno scorrimento automatico di ListBox quando viene aggiunto un nuovo elemento?

Ho un ListBox WPF che è impostato per scorrere orizzontalmente. ItemsSource è associato a ObservableCollection nella mia class ViewModel. Ogni volta che viene aggiunto un nuovo elemento, voglio che il ListBox scorra verso destra in modo che il nuovo elemento sia visualizzabile.

Il ListBox è definito in un DataTemplate, quindi non posso accedere al ListBox per nome nel mio codice dietro il file.

Come posso far scorrere un ListBox per mostrare sempre un ultimo elemento aggiunto?

Vorrei un modo per sapere quando al ListBox è stato aggiunto un nuovo elemento, ma non vedo un evento che faccia questo.

È ansible estendere il comportamento di ListBox utilizzando le proprietà associate. Nel tuo caso ScrollOnNewItem una proprietà allegata chiamata ScrollOnNewItem che, quando si imposta su true hook negli eventi INotifyCollectionChanged dell’elenco degli elementi della casella di riepilogo e al rilevamento di un nuovo elemento, fa scorrere la casella di riepilogo su di essa.

Esempio:

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

Uso:

  

AGGIORNAMENTO Come suggerito da Andrej nei commenti qui sotto, ho aggiunto degli hook per rilevare una modifica ItemsSource del 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(); } } 

Ho trovato un modo davvero intelligente per farlo, basta aggiornare la listbox scrollViewer e impostare la posizione verso il basso. Chiama questa funzione in uno degli eventi ListBox come SelectionChanged ad esempio.

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

Io uso questa soluzione: http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/ .

Funziona anche se si associa ItemsSource di listbox a ObservableCollection che viene manipolato in un thread non UI.

soluzione per Datagrid (lo stesso per ListBox, solo DataGrid sostitutivo con 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); })); } } } 

Comportamento allegato in stile MVVM

Questo comportamento associato scorre automaticamente la casella di riepilogo verso il basso quando viene aggiunto un nuovo elemento.

      

Nel tuo ViewModel , puoi associare a booleano IfFollowTail { get; set; } IfFollowTail { get; set; } IfFollowTail { get; set; } per controllare se lo scorrimento automatico è attivo o meno.

Il comportamento fa tutte le cose giuste:

  • Se IfFollowTail=false è impostato nel ViewModel, il ListBox non scorre più in basso su un nuovo object.
  • Non appena IfFollowTail=true viene impostato nel ViewModel, il ListBox scorre istantaneamente verso il basso e continua a farlo.
  • È veloce. Scorre solo dopo un paio di centinaia di millisecondi di inattività. Un’implementazione ingenua sarebbe estremamente lenta, in quanto sarebbe scorrere su ogni nuovo elemento aggiunto.
  • Funziona con elementi ListBox duplicati (molte altre implementazioni non funzionano con i duplicati: scorrono fino al primo elemento, quindi si fermano).
  • È ideale per una console di registrazione che si occupa di articoli in entrata continui.

Codice di comportamento 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(); } } } 

Bridge dagli eventi alle Reactive Extensions

Infine, aggiungi questo metodo di estensione in modo da poter utilizzare tutta la bontà 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); } } 

Aggiungi estensioni reattive

Sarà necessario aggiungere Reactive Extensions al progetto. Raccomando NuGet .

Il modo più diretto in cui ho trovato di farlo, specialmente per listbox (o listview) associato a un’origine dati, è di collegarlo all’evento di modifica della raccolta. Puoi farlo facilmente con l’evento DataContextChanged della listbox:

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

Questa è in realtà solo una combinazione di tutte le altre risposte che ho trovato. Ritengo che questa sia una caratteristica così banale che non dovremmo aver bisogno di dedicare così tanto tempo (e linee di codice) a fare.

Se solo ci fosse un autoscroll = proprietà true. Sospiro.

Ho trovato un modo molto più semplice che mi ha aiutato con un problema simile, solo un paio di righe di codice dietro, non c’è bisogno di creare comportamenti personalizzati. Controlla la mia risposta a questa domanda (e segui il link all’interno):

wpf (C #) DataGrid ScrollIntoView – come scorrere fino alla prima riga che non viene mostrata?

Funziona per ListBox, ListView e DataGrid.

Non ero soddisfatto delle soluzioni proposte.

  • Non volevo usare descrittori di proprietà “leaky”.
  • Non volevo aggiungere la dipendenza Rx e la query a 8 righe per un’attività apparentemente banale. Nemmeno io volevo un timer costantemente funzionante.
  • Mi è piaciuta l’idea di shawnpfiore, quindi ho costruito un comportamento allegato su di esso, che finora funziona bene nel mio caso.

Ecco cosa ho finito con. Forse salverà qualcuno un po ‘di tempo.

 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 }