Поведение в стиле 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; }
, чтобы контролировать, активна ли автопрокрутка или нет.
Поведение делает все правильно:
- Если в ViewModel установлено
IfFollowTail=false
, 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();
}
}
}
Мост из событий в Reactive Extensions
Наконец, добавьте этот метод расширения, чтобы мы могли использовать все качества 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
.