dotnet / wpf

WPF is a .NET Core UI framework for building Windows desktop applications.
MIT License
7.07k stars 1.16k forks source link

ListView/GridView scroll performance is O(n) when automation peers are created #9181

Closed h3xds1nz closed 1 month ago

h3xds1nz commented 5 months ago

Description

I was running some tests with ListView (using GridView) and when I loaded 1 000 000 items (yes, an obscure use case but that's why we got virtualization, no?), the scrolling performance would degrade and keyboard scrolling felt almost unusable.

During a quick rundown, I've discovered the culprit is in override of GetChildrenCore of GridViewItemAutomationPeer, which performs IndexOf (linear) search in the Items collection of the underlying ItemsSource to find the respective item's Row.

Reproduction Steps

The easiest step to reproduce is to add a ListView with GridView plus a ComboBox, after opening the ComboBox's dropdown (clicking it), Accessibility.dll gets loaded and the party gets started. You can find a demo repository on Github - here.

Behavior

The scrolling performance becomes worse to unusable with increasing number of items.

Known Workarounds

The "workaround" is to subclass GridView, overriding GetAutomationPeer to return NULL. Not really a solution though.

Other information

I have created a pull request at the same time, fixing the respective issue. #9182

huiliuss commented 5 months ago

Displaying large amounts of data in WPF:

Method 1: Simple style (important) + virtualization

Method 2: Paged loading

According to my test, your virtualization does not seem to be enabled successfully. Please make sure your virtualization is correct

The following example uses ListView to load 2,000,000 pieces of data without lag when sliding.


<Grid>

    <Grid.DataContext>

        <vm:MainViewModel x:Name="MyData"></vm:MainViewModel>

    </Grid.DataContext>

    <Grid.Resources>

        <DataTemplate x:Key="MyListBoxItem">

            <StackPanel Orientation="Horizontal">

                <TextBlock Text="{Binding Path=Id}" Margin="20,0,20,0"></TextBlock>

                <TextBlock Text="{Binding Path=Name}" Margin="20,0,20,0"></TextBlock>

                <TextBlock Text="{Binding Path=Email}" Margin="20,0,20,0"></TextBlock>

            </StackPanel>

        </DataTemplate>

    </Grid.Resources>

    <StackPanel Height="400">

        <!--<ListBox VirtualizingPanel.IsVirtualizing="True"  VirtualizingPanel.VirtualizationMode="Recycling" ItemsSource="{Binding Path=Users}" ItemTemplate="{StaticResource MyListBoxItem}" Height="400">

        </ListBox>-->

        <ListView  VirtualizingPanel.IsVirtualizing="True"  VirtualizingPanel.VirtualizationMode="Recycling" ItemsSource="{Binding Path=Users}" ItemTemplate="{StaticResource MyListBoxItem}" Height="400"></ListView>

    </StackPanel>

</Grid>

ViewModel


public class MainViewModel : INotifyPropertyChanged

{

    private ObservableCollection<User> users;

    public ObservableCollection<User> Users {  get { return users; }  set { users = value;OnPropertyChanged("Users"); } }

    public MainViewModel() {

        Users = GetUsers(2000000);

        MessageBox.Show("data create success");

    }

    public ObservableCollection<User> GetUsers(int Count)

    {

        ObservableCollection<User> Users = new ObservableCollection<User>();

        for(int i = 0; i < Count; i++)

        {

            Users.Add(new User() { Id = i,Name="AAAA_"+i,Email="BBBB_"+i }); ; ;

        }     

        return Users;

    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string name)

    {

        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

    }

}

Model


public class User:INotifyPropertyChanged

{

    private int id;

    public int Id { get { return id; } set { id = value;OnPropertyChanged("Id"); } }

    private string name;

    public string Name { get { return name; } set { name = value; OnPropertyChanged("Name"); } }

    private string email;  

    public string Email { get { return email; } set { email = value; OnPropertyChanged("Email"); } }

    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string name)

    {

        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

    }

}
h3xds1nz commented 5 months ago

@huiliuss Thank you for your comment, should you check the reproduction project and the steps included to reproduce the issue reported, you will notice your example is lacking necessary steps to successfully reproduce it. That's why I've included one in the first place.

1) You do not need to enable virtualization explicitly by setting the attached property of VirtualizingPanel for ListView (GridView) and ListBox in WPF as it uses VirtualizingStackPanel and it is set to true by default, so unless you change the template to include a different ItemsControl/ItemsPanel for example, you're fine (none of those things are done by my repro project).

2) The problem doesn't exist until automation peers are being created, which can be easily triggered in common scenarios by using a ComboBox and displaying the dropdown. Again, something your example project does not do.

3) If I really didn't use virtualization, I'm looking at at least 16 GB of item containers in memory and wonderful amount of time to create those spend both in GC and actual creation time (over 15 minutes at least). Believe me, I didn't do that.

hongruiyu commented 4 months ago

The equivalent item you mentioned is not specifically used in ComboBox. Can you provide the corresponding usage code so that I can further confirm the effect you want to achieve?

brswan commented 1 month ago

It appears I was able to solve my data grid sluggish issue by making my own DataGrid class and returning null in OnCreateAutomationPeer. I'm not sure what ramifications this may have but it appears to work,.

    public class MyDataGrid : DataGrid
    {
        public MyDataGrid()
        {
        }

        protected override AutomationPeer OnCreateAutomationPeer()
        {
            return null;
            //return base.OnCreateAutomationPeer();
        }
    }
h3xds1nz commented 1 month ago

@brswan Basically entire UI Automation won't work for that control. Which means any accessibility tools.

brswan commented 1 month ago

@brswan Basically entire UI Automation won't work for that control. Which means any accessibility tools.

After more testing it doesn't seem to help datagrids with any sort of complexity to them. It's still an issue for me.

brswan commented 1 month ago

I found a solution that works for my problem. I had to override the Windows class. From there you override the OnCreateAutomationPeer() method.

 public class CustomWindowAutomationPeer : FrameworkElementAutomationPeer
 {
     public CustomWindowAutomationPeer(FrameworkElement owner) : base(owner) { }

     protected override string GetNameCore()
     {
         return "CustomWindowAutomationPeer";
     }

     protected override AutomationControlType GetAutomationControlTypeCore()
     {
         return AutomationControlType.Window;
     }

     protected override List<AutomationPeer> GetChildrenCore()
     {
         return new List<AutomationPeer>();
     }
 }

In your custom Window class add

 protected override System.Windows.Automation.Peers.AutomationPeer OnCreateAutomationPeer()
   {
       return new CustomWindowAutomationPeer(this);
   }