滚动到视图并使用虚拟化的列表视图

24
我有一个 ListView(默认情况下启用虚拟化),它的 ItemsSource 绑定到 ObservableCollection<Item> 属性。

当数据填充时(属性被设置并发出通知),我在分析器中看到 2 次布局峰值,第二次在调用 listView.ScrollIntoView() 后发生。
我的理解是:
  1. ListView 通过绑定加载数据,并为屏幕上的项创建 ListViewItem,从索引 0 开始。
  2. 然后我调用了 listView.ScrollIntoView()
  3. 现在 ListView 第二次执行此操作(创建 ListViewItems)。
如何防止这种取消虚拟化两次发生(我不希望在 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());
    }
}

调试 - 性能分析器 - 应用程序时间轴...等待一会儿,点击按钮,再等一会儿,关闭窗口。您将看到使用 VirtualizingStackPanel 的 2 次布局传递。我的目标是只有一次,但我不知道如何做。

重现问题的困难之处在于模拟负载(当创建 ListViewItem昂贵的操作),但我希望现在更清楚地展示了问题。


你是否总是希望在将项目集添加到用户界面后,将最后一个项目显示出来?这并不能解决两次布局传递的问题,但如果是这种情况,你可以在将项目添加到用户界面之前改变它们的排序方式,使你想要显示的项目成为集合中的第一个项目。 - Bijington
@Bijington,集合已经按日期排序(在mcve中未显示),我不应该改变顺序。它可以是任何项目(在实际项目中是多选的ListView,滚动到最后选择的项目)。理想情况下,我正在寻找在MVVM应用程序中存储/恢复ListView状态的方法(选择已处理,但滚动位置没有,它是通过ScrollIntoView 某种方式 处理的),这种双重去虚拟化是一种XY问题(但我对ScrollIntoView没问题,只是两次去虚拟化是个问题)。 - Sinatr
我也是这么想的,但我觉得问一下还是值得的。这个问题确实听起来很有趣,需要解决。你需要在加载ListView内容之前告诉它滚动位置。你可以尝试通过子类化ListView,在完成加载过程之前阻止渲染(可能需要一个IsLoadingContent标志),这样你就可以分配项目,并标记哪个项目需要被选中并显示出来。 - Bijington
如果您将ListBox的可见性设置为Hidden,然后在调用ScrollIntoView之后再将其设置为可见,是否可以解决问题? - user3308241
2个回答

0
这是我在chatgpt中找到的,希望能帮到你。
你观察到的行为是由于ListBox控件的虚拟化机制引起的。默认情况下,当你调用ScrollIntoView时,ListBox需要确保请求的项在屏幕上可见。这可能会导致生成额外的ListBoxItems并触发第二次布局。
为了防止这种行为并只进行一次布局,你可以在填充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,您可以有效地禁用该特定操作的虚拟化。在填充ListBox之后,通过将IsVirtualizing重新设置为true来重新启用虚拟化。这样,您可以确保在填充过程中只进行一次布局传递,并且随后的ScrollIntoView不会触发额外的布局传递。

在填充列表框之前,暂时禁用虚拟化,并在之后重新启用它 - 那为什么要启用虚拟化呢?我试图避免在开始时加载一小部分项目。加载整个列表将是一场灾难。在创建项目时,在循环内添加几毫秒的延迟以体验我的问题。 - Sinatr

-1

VirtualizingStackPanel上,滚动方法通常效果不佳。为了解决这个问题,我使用以下解决方案。

  1. 放弃VirtualizingStackPanel,改用普通的StackPanel作为面板模板。
  2. 将DataTemplate的外层设置为来自此处的LazyControl:http://blog.angeloflogic.com/2014/08/lazycontrol-in-junglecontrols.html
  3. 确保在该LazyControl上设置高度。

通常情况下,我通过这种方法获得良好的性能。要使其完全符合您的要求,您可能需要在LazyControl中添加一些额外的逻辑,以等待某个标志被设置(在调用滚动方法之后)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接