滚动到具有虚拟化功能的视图和列表视图

本文关键字:视图 列表 功能 虚拟化 滚动 | 更新日期: 2023-09-27 17:56:22

我有ListView(虚拟化默认打开),ItemsSource绑定到ObservableCollection<Item>属性。

当填充数据(设置属性并发出通知)时,我在探查器中看到 2 个布局峰值,第二个发生在调用 listView.ScrollIntoView() 之后。

我的理解是:

  1. ListView通过绑定加载数据,并为屏幕上的项目创建ListViewItem,从索引 0 开始。
  2. 然后我打电话给listView.ScrollIntoView().
  3. 现在ListView第二次这样做(创建ListViewItem)。

如何防止去虚拟化发生两次(我不希望在ScrollIntoView发生之前发生一次)?


我试图使用 ListBox 进行复制

XAML:

<Grid>
    <ListBox x:Name="listBox" ItemsSource="{Binding Items}">
        <ListBox.ItemContainerStyle>
            <Style TargetType="ListBoxItem">
                <Setter Property="IsSelected" Value="{Binding IsSelected}" />
            </Style>
        </ListBox.ItemContainerStyle>
    </ListBox>
    <Button Content="Fill" VerticalAlignment="Top" HorizontalAlignment="Center" Click="Button_Click" />
</Grid>

.cs:

public class NotifyPropertyChanged : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName] string property = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
public class ViewModel : NotifyPropertyChanged
{
    public class Item : NotifyPropertyChanged
    {
        bool _isSelected;
        public bool IsSelected
        {
            get { return _isSelected; }
            set
            {
                _isSelected = value;
                OnPropertyChanged();
            }
        }
    }
    ObservableCollection<Item> _items = new ObservableCollection<Item>();
    public ObservableCollection<Item> Items
    {
        get { return _items; }
        set
        {
            _items = value;
            OnPropertyChanged();
        }
    }
}
public partial class MainWindow : Window
{
    ViewModel _vm = new ViewModel();
    public MainWindow()
    {
        InitializeComponent();
        DataContext = _vm;
    }
    void Button_Click(object sender, RoutedEventArgs e)
    {
        var list = new List<ViewModel.Item>(1234567);
        for (int i = 0; i < 1234567; i++)
            list.Add(new ViewModel.Item());
        list.Last().IsSelected = true;
        _vm.Items = new ObservableCollection<ViewModel.Item>(list);
        listBox.ScrollIntoView(list.Last());
    }
}

调试 - 性能探查器 - 应用程序时间线...稍等片刻,单击按钮,稍等片刻,关闭窗口。您将看到 2 个带有 VirtualizingStackPanel 的布局通行证。我的目标是只有一个,我不知道怎么做。

重现的问题是模拟负载(当创建ListViewItem很昂贵时),但我希望它现在更清楚地展示了问题。

滚动到具有虚拟化功能的视图和列表视图

这是我在chatgpt中找到的,希望可以提供帮助。

您观察到的行为是由于 ListBox 控件的虚拟化机制。默认情况下,当您调用 ScrollIntoView 时,列表框需要确保请求的项在屏幕上可见。这可能会导致生成其他列表框项并触发第二个布局传递。

若要防止此行为并且只有一个布局阶段,可以在填充 ListBox 之前暂时禁用虚拟化,然后在之后重新启用它。下面是包含此方法的代码的更新版本:

public partial class MainWindow : Window
{
  ViewModel _vm = new ViewModel();
   public MainWindow()
   {
    InitializeComponent();
    DataContext = _vm;
   }
   void Button_Click(object sender, RoutedEventArgs e)
   {
      // Disable virtualization
      listBox.SetValue(VirtualizingStackPanel.IsVirtualizingProperty, false);
      var list = new List<ViewModel.Item>(1234567);
      for (int i = 0; i < 1234567; i++)
      list.Add(new ViewModel.Item());
      list.Last().IsSelected = true;
      _vm.Items = new ObservableCollection<ViewModel.Item>(list);
      // Enable virtualization after populating the ListBox
      listBox.SetValue(VirtualizingStackPanel.IsVirtualizingProperty, true);
      listBox.ScrollIntoView(list.Last());
    }
}

通过在填充 ListBox 之前将VirtualizingStackPanel.IsVirtualizing附加属性设置为 false,可以有效地禁用该特定操作的虚拟化。填充列表框后,通过将IsVirtualizing设置回 true 来重新启用虚拟化。这样,您可以确保在填充期间仅发生一次布局传递,并且后续的 ScrollIntoView 不会触发其他布局传递。

滚动方法通常不能很好地处理VirtualizingStackPanel。为了解决这个问题,我使用以下解决方案。

  1. 抛弃VirtualizingStackPanel.使用普通的StackPanel作为面板模板。
  2. 从这里开始,将 DataTemplate 的外层设为 LazyControl:http://blog.angeloflogic.com/2014/08/lazycontrol-in-junglecontrols.html
  3. 确保在该 LazyControl 上设置高度。

我通常从这种方法中获得良好的表现。要使其完全按照您的要求执行,您可能需要向 LazyControl 添加一些额外的逻辑以等待设置某个标志(在调用滚动方法之后)。