22

I have ListView (virtualization is on by default), which ItemsSource is bound to ObservableCollection<Item> property.

When data are populated (property is set and notification is rised) I see 2 layout spikes in profiler, second one happens after call listView.ScrollIntoView().

My understanding is:

  1. ListView loads data via binding and creates ListViewItem for items on screen, starting from index 0.
  2. Then I call listView.ScrollIntoView().
  3. And now ListView does that second time (creating ListViewItems).

How do I prevent that de-virtualization from happening twice (I don't want one before ScrollIntoView to occur)?


I tried to make a repro using 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());
    }
}

Debug - Performance Profiler - Application Timeline... wait a bit, click button, wait a bit, close window. You will see 2 layout passes with VirtualizingStackPanel. My aim is to have just one and I don't know how.

The problem with repro is to simulate load (when creating ListViewItem is expensive), but I hope it's more clearly demonstrate the problem now.

Community
  • 1
  • 1
Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • Are you always wanting to bring the last item into view once the collection of items has been added to the UI? It doesn't solve the issue of the two layout passes but if that is the case you could potentially change how the items are sorted before adding them to the UI so that the item you want to bring into view is the first item in the collection – Bijington Jul 20 '16 at 06:52
  • @Bijington, collection is already sorted (by date, not shown in mcve), I shouldn't change order. It can be any item (in real project it's multi-select `ListView`, scrolling to the last selected item). Ideally I am seeking for the way to store/restore `ListView` state in MVVM application (selection is handled, but scroll position is not, it's *somehow* handled with `ScrollIntoView`), that double de-virtualization is kind of XY-problem (but I am ok with `ScrollIntoView`, only de-virtualization two times is the problem). – Sinatr Jul 20 '16 at 07:01
  • I assumed as much but I thought it was worth an ask. This certainly sounds like an interesting problem to solve. You need to tell the `ListView` it's scroll position before loading it's content somehow. Could you somehow subclass `ListView` and prevent rendering until you have finished the load pass (perhaps an IsLoadingContent flag) so that you can assign the items and also mark which item needs to be selected and brought into view? – Bijington Jul 20 '16 at 07:35
  • Would it solve the problem if you were to set the visibility of the ListBox to Hidden, then make it visible after ScrollIntoView is called? – user3308241 Sep 30 '16 at 20:03

2 Answers2

0

This is what i found in chatgpt,hope can help.

The behavior you are observing is due to the virtualization mechanism of the ListBox control. By default, when you call ScrollIntoView, the ListBox needs to ensure that the requested item is visible on the screen. This may cause the generation of additional ListBoxItems and trigger a second layout pass.

To prevent this behavior and have only one layout pass, you can disable virtualization temporarily before populating the ListBox and re-enable it afterward. Here's an updated version of your code that incorporates this approach:

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());
    }
}

By setting the VirtualizingStackPanel.IsVirtualizing attached property to false before populating the ListBox, you effectively disable virtualization for that specific operation. After populating the ListBox, you re-enable virtualization by setting IsVirtualizing back to true. This way, you ensure that only one layout pass occurs during the population, and the subsequent ScrollIntoView will not trigger an additional layout pass.

CkyRoomee
  • 53
  • 5
  • *"disable virtualization temporarily before populating the ListBox and re-enable it afterward"* - why enable virtualization at all then? I am trying to avoid loading a small chunk of items at start. Loading whole list would be a disaster. Add few milliseconds delay inside the loop when creating items to experience my isssue. – Sinatr Jun 21 '23 at 08:57
-1

The Scroll methods generally don't work very well on a VirtualizingStackPanel. To work around that I use the following solution.

  1. Ditch the VirtualizingStackPanel. Go with a normal StackPanel for the panel template.
  2. Make the outer layer of your DataTemplate be the LazyControl from here: http://blog.angeloflogic.com/2014/08/lazycontrol-in-junglecontrols.html
  3. Make sure you set the height on that LazyControl.

I generally get good performance out of that approach. To make it do exactly what you are asking, you may need to add some additional logic to LazyControl to wait for some flag to be set (after you call the scroll method).

Brannon
  • 5,324
  • 4
  • 35
  • 83