使用以下代码实现了所需的列表框行为,感谢 Roel 提供了上面的初始 Behavior 框架。
这是一个包含行为代码的示例项目,以及可用于测试交互性的最小 WPF 窗口。
测试窗口包含一个 ListBox,项目通过后台任务异步添加到其中。该行为的要点如下:
- 列表框会自动滚动以显示异步添加的新项目。
- 用户与列表框的交互会停止自动滚动 - AKA 令人讨厌的行为。
- 一旦完成交互,要继续自动滚动,用户将滚动条拖到底部并松开,或者使用鼠标滚轮或键盘来执行相同操作。这表明用户希望恢复自动滚动。
AutoScrollBehavior.cs:
using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
using System.Windows.Media;
namespace BehaviorTest.Code
{
// List box automatically scrolls to show new items as they are added asynchronously.
// A user interaction with the listbox stops automatic scrolling - AKA obnoxious behavior.
// Once finished interacting, to continue automatic scrolling, drag the scroll bar to
// the bottom and let go, or use the mouse wheel or keyboard to do the same.
// This indicates that the user wants automatic scrolling to resume.
public class AutoScrollBehavior : Behavior<ListBox>
{
private ScrollViewer scrollViewer;
private bool autoScroll = true;
private bool justWheeled = false;
private bool userInteracting = false;
protected override void OnAttached()
{
AssociatedObject.Loaded += AssociatedObjectOnLoaded;
AssociatedObject.Unloaded += AssociatedObjectOnUnloaded;
}
private void AssociatedObjectOnUnloaded(object sender, RoutedEventArgs routedEventArgs)
{
if (scrollViewer != null)
{
scrollViewer.ScrollChanged -= ScrollViewerOnScrollChanged;
}
AssociatedObject.SelectionChanged -= AssociatedObjectOnSelectionChanged;
AssociatedObject.ItemContainerGenerator.ItemsChanged -= ItemContainerGeneratorItemsChanged;
AssociatedObject.GotMouseCapture -= AssociatedObject_GotMouseCapture;
AssociatedObject.LostMouseCapture -= AssociatedObject_LostMouseCapture;
AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
scrollViewer = null;
}
private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
scrollViewer = GetScrollViewer(AssociatedObject);
if (scrollViewer != null)
{
scrollViewer.ScrollChanged += ScrollViewerOnScrollChanged;
AssociatedObject.SelectionChanged += AssociatedObjectOnSelectionChanged;
AssociatedObject.ItemContainerGenerator.ItemsChanged += ItemContainerGeneratorItemsChanged;
AssociatedObject.GotMouseCapture += AssociatedObject_GotMouseCapture;
AssociatedObject.LostMouseCapture += AssociatedObject_LostMouseCapture;
AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel;
}
}
private static ScrollViewer GetScrollViewer(DependencyObject root)
{
int childCount = VisualTreeHelper.GetChildrenCount(root);
for (int i = 0; i < childCount; ++i)
{
DependencyObject child = VisualTreeHelper.GetChild(root, i);
ScrollViewer sv = child as ScrollViewer;
if (sv != null)
return sv;
return GetScrollViewer(child);
}
return null;
}
void AssociatedObject_GotMouseCapture(object sender, System.Windows.Input.MouseEventArgs e)
{
// User is actively interacting with listbox. Do not allow automatic scrolling to interfere with user experience.
userInteracting = true;
autoScroll = false;
}
void AssociatedObject_LostMouseCapture(object sender, System.Windows.Input.MouseEventArgs e)
{
// User is done interacting with control.
userInteracting = false;
}
private void ScrollViewerOnScrollChanged(object sender, ScrollChangedEventArgs e)
{
// diff is exactly zero if the last item in the list is visible. This can occur because of scroll-bar drag, mouse-wheel, or keyboard event.
double diff = (scrollViewer.VerticalOffset - (scrollViewer.ExtentHeight - scrollViewer.ViewportHeight));
// User just wheeled; this event is called immediately afterwards.
if (justWheeled && diff != 0.0)
{
justWheeled = false;
autoScroll = false;
return;
}
if (diff == 0.0)
{
// then assume user has finished with interaction and has indicated through this action that scrolling should continue automatically.
autoScroll = true;
}
}
private void ItemContainerGeneratorItemsChanged(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Reset)
{
// An item was added to the listbox, or listbox was cleared.
if (autoScroll && !userInteracting)
{
// If automatic scrolling is turned on, scroll to the bottom to bring new item into view.
// Do not do this if the user is actively interacting with the listbox.
scrollViewer.ScrollToBottom();
}
}
}
private void AssociatedObjectOnSelectionChanged(object sender, SelectionChangedEventArgs selectionChangedEventArgs)
{
// User selected (clicked) an item, or used the keyboard to select a different item.
// Turn off automatic scrolling.
autoScroll = false;
}
void AssociatedObject_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
// User wheeled the mouse.
// Cannot detect whether scroll viewer right at the bottom, because the scroll event has not occurred at this point.
// Same for bubbling event.
// Just indicated that the user mouse-wheeled, and that the scroll viewer should decide whether or not to stop autoscrolling.
justWheeled = true;
}
}
}
MainWindow.xaml.cs:
using BehaviorTest.Code;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Threading;
namespace BehaviorTest
{
public partial class MainWindow : Window
{
public ObservableCollection<String> data { get; set; }
public MainWindow()
{
InitializeComponent();
data = new ObservableCollection<String>();
DataContext = this;
Interaction.GetBehaviors(listbox).Add(new AutoScrollBehavior());
BeginAddingItems();
}
private async void BeginAddingItems()
{
List<Task> tasks = new List<Task>();
await Task.Factory.StartNew(() =>
{
for (int i = 0; i < Int32.MaxValue; ++i)
{
AddToList("Added Slowly: " + i.ToString());
Thread.Sleep(2000);
if (i % 3 == 0)
{
for (int j = 0; j < 5; ++j)
{
AddToList("Added Quickly: " + j.ToString());
Thread.Sleep(200);
}
}
}
});
}
void AddToList(String item)
{
if (Application.Current == null)
return; // Application is shutting down.
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new Action(() => { data.Add(item); }));
}
private void clearButton_Click(object sender, RoutedEventArgs e)
{
data.Clear();
}
private void listbox_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
MessageBox.Show("Launch a modal dialog. Items are still added to the list in the background.");
}
}
}
MainWindow.xaml.cs:
<Window x:Class="BehaviorTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Test Scrolling"
FontFamily="Verdana"
Width="400" Height="250"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox x:Name="listbox" Grid.Row="0"
ItemsSource="{Binding data}"
MouseDoubleClick="listbox_MouseDoubleClick" >
</ListBox>
<StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Right">
<Button x:Name="startButton" Click="clearButton_Click" MinWidth="80" >Clear</Button>
</StackPanel>
</Grid>
</Window>