如何可靠地检测项目在视图外滚动的时间?

时间:2014-01-12 22:58:20

标签: c# wpf itemscontrol ui-virtualization

我有大量项目绑定到ListBoxVirtualizingStackPanel设置为ItemsPanel。当用户滚动并创建项容器时,我做了一些工作来用数据填充项目(使用数据库查询)。如果用户滚动非常快,它会产生大量的请求,这些请求往往会使事情陷入困境。我想要做的是检测项目在视口外滚动的时间,以便取消相应的请求。

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

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

  2. 打开容器回收利用 VirtualizationMode.Recycling。此事件导致该项目 容器的DataContext要更改,但项容器本身是 重复使用。 DataContextChanged事件会立即发生 项目超出了视野,所以在这方面是好的。问题 是容器回收产生了很多副作用,在我的 测试,总体来说是一个小小的车。我宁愿不使用它。

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

3 个答案:

答案 0 :(得分:2)

这是一个粗略的解决方案,我认为可以实现您正在寻找的东西。我通过侦听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>

答案 1 :(得分:0)

在viewmodel端,您可以观看与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很复杂(并且也不会取消数据库查询)。

答案 2 :(得分:-1)

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