ScrollIntoView and ListView with virtualization
Asked Answered
Q

3

25

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.

Quieten answered 11/7, 2016 at 9:14 Comment(4)
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 collectionLifeanddeath
@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).Quieten
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?Lifeanddeath
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?Coherent
O
0

The answer is in your last statement:

when creating ListViewItem is expensive

You can recycle the layout of items (as long they have the same layout). Moreover Pixel scrolling makes the scroll smother than Unit scroll.

<ListBox VirtualizingPanel.VirtualizationMode="Recycling"
         VirtualizingPanel.ScrollUnit="Pixel">

I guess you set Button_Click function for the sake of example; Note that if you would add many items into an ObservableCollection while it is actively used by the ListBox control, it would destroy the performance; Unfortunately the class does not have AddRange function so you need to improvise; Check this thread.

Ouachita answered 7/12, 2023 at 10:26 Comment(0)
S
0

Add Delay in the button Click Event maybe resolve the issue.and make the button click event as Async

await Task.Delay(500);
Seftton answered 23/7, 2024 at 7:4 Comment(0)
C
-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).

Cornwallis answered 22/11, 2016 at 5:28 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.