【问题标题】:How to reliably detect when an item is scrolled outside of view?如何可靠地检测项目何时滚动到视图之外?
【发布时间】:2014-01-31 14:45:58
【问题描述】:

我有大量绑定到ListBox 的项目集合,其中VirtualizingStackPanel 设置为其ItemsPanel。随着用户滚动和项目容器的创建,我做了一些工作来用数据填充项目(使用数据库查询)。如果用户滚动得非常快,它会产生大量请求,从而使事情陷入困境。我想做的是检测项目何时滚动到视口之外,因此我可以取消其相应的请求。

以下是我迄今为止尝试过的方法,以及为什么它们没有奏效:

  1. 覆盖VirtualizingStackPanel.OnCleanUpVirtualizedItem。这 问题是这个方法似乎是在很久以后调用的 比项目实际离开屏幕时。取消我的请求 在这种方法中并没有多大用处,因为它发生得太晚了。

  2. 开启容器回收 VirtualizationMode.Recycling。此事件导致项目 容器的DataContext 要更改,但项目容器本身是 重复使用。 DataContextChanged 事件立即发生 项目不在视野范围内,因此在这方面很好。问题 是容器回收会产生很多副作用吗? 测试,总体来说有点毛病。我宁愿不使用它。

是否有一个好的低级方法,例如挂钩到布局事件,可以在项目何时超出视图时给我一个确定性的答案?也许在ScrollViewer 级别?

【问题讨论】:

    标签: c# wpf itemscontrol ui-virtualization


    【解决方案1】:

    这是一个粗略的解决方案,我认为可以满足您的需求。我通过侦听 XAML 中的加载事件来获取虚拟化堆栈面板。如果我在生产代码中这样做,我可能会将其纳入可重用的附加行为,而不是在代码隐藏中抛出一堆代码。

    public partial class MainWindow
    {
        private VirtualizingStackPanel _panel;
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MyViewModel();
        }
    
        private IList<ChildViewModel> _snapshot = new List<ChildViewModel>();
    
        private void OnPanelLoaded(object sender, RoutedEventArgs eventArgs)
        {
            _panel = (VirtualizingStackPanel)sender;
            UpdateSnapshot();
            _panel.ScrollOwner.ScrollChanged += (s,e) => UpdateSnapshot();
        }
    
        private void UpdateSnapshot()
        {
            var layoutBounds = LayoutInformation.GetLayoutSlot(_panel);
    
            var onScreenChildren = 
                (from visualChild in _panel.GetChildren()
                 let childBounds = LayoutInformation.GetLayoutSlot(visualChild)
                 where layoutBounds.Contains(childBounds) || layoutBounds.IntersectsWith(childBounds)
                 select visualChild.DataContext).Cast<ChildViewModel>().ToList();            
    
            foreach (var removed in _snapshot.Except(onScreenChildren))
            {
                // TODO: Cancel pending calculations.
                Console.WriteLine("{0} was removed.", removed.Value);
            }
            _snapshot = onScreenChildren;
        }
    }
    

    请注意,这里并没有真正可以用来查找屏幕上的子级的属性,因此我们查看父级与子级的布局边界,以确定哪些子级在屏幕上。 该代码使用扩展方法来获取可视树中项目的可视子项,包括如下:

    public static class MyVisualTreeHelpers
    {
        public static IEnumerable<FrameworkElement> GetChildren(this DependencyObject dependencyObject)
        {
            var numberOfChildren = VisualTreeHelper.GetChildrenCount(dependencyObject);
            return (from index in Enumerable.Range(0, numberOfChildren)
                    select VisualTreeHelper.GetChild(dependencyObject, index)).Cast<FrameworkElement>();
        }
    }
    

    此代码使用我创建的一个非常基本的视图模型层次结构来测试它。我会包含它以防万一它有助于理解其他代码:

    public class MyViewModel
    {
        public MyViewModel()
        {
            Children = new ObservableCollection<ChildViewModel>(GenerateChildren());
        }
    
        public ObservableCollection<ChildViewModel> Children { get; set; }
    
        private static IEnumerable<ChildViewModel> GenerateChildren()
        {
            return from value in Enumerable.Range(1, 1000)
                   select new ChildViewModel {Value = value};
        }
    }
    
    public class ChildViewModel
    {
        public int Value { get; set; }
    }
    

    XAML:

    <Window x:Class="WpfTest.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:wpfTest="clr-namespace:WpfTest"
            Title="MainWindow" Height="500" Width="500">
        <ListBox ItemsSource="{Binding Children}">
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel Loaded="OnPanelLoaded" />
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
    
            <ListBox.ItemTemplate>
                <DataTemplate DataType="wpfTest:ChildViewModel">
                    <TextBlock Text="{Binding Value}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Window>
    

    【讨论】:

    • 谢谢蒂姆,这是一个很好的答案。我实际上只是从 VirtualizingStackPanel 继承并将代码移到那里。然后让孩子们输入 IDisposable,所以现在我需要做的就是在我的 VM 上实现 IDisposable 以使其正常工作。
    • 上面的实现对于没有分组的列表视图正常工作。但是,在启用分组的情况下,我无法为列表视图获得正确的屏幕上的子项。列表视图的数据源是启用组描述的 ICollectionView 类型。 ScrollViewer 也不属于面板,必须从列表视图的可视子项中找出。
    【解决方案2】:

    在视图模型方面,您可以观看附加和分离 INotifyPropertyChanged 事件:

    public event PropertyChangedEventHandler PropertyChanged
    {
        add
        {
            if(this.InternalPropertyChanged == null)
                Console.WriteLine("COMING INTO VIEW");
            this.InternalPropertyChanged += value;
        }
    
        remove
        {
            this.InternalPropertyChanged -= value;
            if(this.InternalPropertyChanged == null)
                Console.WriteLine("OUT OF VIEW");
        }
    }
    
    private event PropertyChangedEventHandler InternalPropertyChanged;
    

    注意:如果没有VirtualizationMode.Recycling,ListBox 可能会推迟容器销毁(因此分离),直到用户停止滚动。这会大量增加内存消耗,尤其是在 ItemTemplate 很复杂的情况下(也不会取消您的数据库查询)。

    【讨论】:

      【解决方案3】:

      您可以尝试添加。
      scrollviewer.veriticalscroll = "自动" 这将导致它只滚动列表框中已有的项目数量

      【讨论】:

      • 我想你误解了我的问题。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-01-11
      • 1970-01-01
      • 2015-01-15
      • 1970-01-01
      • 2014-12-26
      • 2016-12-12
      • 2020-03-08
      相关资源
      最近更新 更多