sbaeumlisberger / VirtualizingWrapPanel

Implementation of a comprehensive VirtualisingWrapPanel for WPF
MIT License
256 stars 35 forks source link

Application Freezes When there are too many items in Listbox #67

Closed xDreamms closed 2 weeks ago

xDreamms commented 2 weeks ago

Describe your issue After scrolling a lot the application freezes. Error: not enough memory resources to process this command.

      <ListBox  VerticalAlignment="Top" ScrollViewer.CanContentScroll="True" 
VirtualizingPanel.IsVirtualizing="true" 
VirtualizingPanel.VirtualizationMode="Recycling"  Grid.Row="1" Grid.Column="1"  SelectionChanged="MoviesDisplay_OnSelectionChanged" ScrollViewer.ScrollChanged="MoviesDisplay_OnScrollChanged"  ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Visible" Background="Transparent" x:Name="MoviesDisplay">

          <ItemsControl.Resources>
              <Style x:Key="ScaleStyle" TargetType="Image">
                  <Style.Triggers>
                      <Trigger Property="IsMouseOver" Value="True">
                          <Setter Property="Grid.ZIndex" Value="1"/>
                          <Setter Property="RenderTransform">
                              <Setter.Value>
                                  <ScaleTransform ScaleX="1.1" ScaleY="1.1"/>
                              </Setter.Value>
                          </Setter>
                      </Trigger>
                  </Style.Triggers>
              </Style>
          </ItemsControl.Resources>
          <ListBox.ItemContainerStyle>
              <Style TargetType="ListBoxItem">
                  <Setter Property="IsSelected" Value="{Binding Content.IsSelected, Mode=TwoWay, RelativeSource={RelativeSource Self}}"/>
                  <Setter Property="Template">
                      <Setter.Value>
                          <ControlTemplate TargetType="ListBoxItem">
                              <ContentPresenter/>
                          </ControlTemplate>
                      </Setter.Value>
                  </Setter>
              </Style>
          </ListBox.ItemContainerStyle>
          <ListBox.ItemTemplate>
              <DataTemplate>
                  <StackPanel  Cursor="Hand" Margin="0 20 20 0" Orientation="Vertical">
                      <Image  VerticalAlignment="Top" Height="375" Width="250" HorizontalAlignment="Left" Grid.Row="0" Grid.Column="0" RenderTransformOrigin="0.5,0.5" Style="{StaticResource ScaleStyle}"  Source="{Binding Bitmap}"></Image>

                      <TextBlock VerticalAlignment="Top" Margin="0 20 0 0" FontSize="18" MaxWidth="200" HorizontalAlignment="Left" Grid.Row="1"  TextWrapping="Wrap" Text="{Binding Name}" Foreground="White"></TextBlock>

                      <StackPanel Margin="-5 1 0 0" Orientation="Horizontal">

                          <materialDesign:RatingBar  HorizontalAlignment="Left"
                                                     x:Name="ReadOnlyRatingBar"
                                                     IsReadOnly="True"
                                                     Value="{Binding Rating}"></materialDesign:RatingBar>

                          <TextBlock FontSize="15" Margin="15 3.5 0 0" Grid.Column="1" HorizontalAlignment="Left" Text="{Binding RatingNumber}" Foreground="#767676"></TextBlock>
                      </StackPanel>
                  </StackPanel>
              </DataTemplate>
          </ListBox.ItemTemplate>
          <ListBox.ItemsPanel>
              <ItemsPanelTemplate>
                  <controls:VirtualizingWrapPanel VirtualizingStackPanel.IsVirtualizing="True"  VirtualizingStackPanel.VirtualizationMode="Recycling" IsItemsHost="True" Orientation="Horizontal"  />
              </ItemsPanelTemplate>
          </ListBox.ItemsPanel>
      </ListBox>
private async void MoviesDisplay_OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
    if (e.VerticalOffset == e.ExtentHeight - e.ViewportHeight)
    {
            await GetMorePageForPopularMovies("en");
    }
}

I use this https://github.com/jellyfin/TMDbLib

  private ObservableCollection<Movie> newMovieList = new ObservableCollection<Movie>();

 private async Task GetMorePageForPopularMovies(string language)
 {
     var popularList = await Service.client.GetMoviePopularListAsync(language, page);

         foreach (var popularMovie in popularList.Results)
         {
             if (!String.IsNullOrWhiteSpace(popularMovie.PosterPath))
             {
                 Movie mov = new Movie()
                 {
                     Name = popularMovie.Title,
                     Id = popularMovie.Id,
                     Rating = popularMovie.VoteAverage,
                     ShowType = ShowType.Movie
                 };

                 var url = Service.client.GetImageUrl("w500", popularMovie.PosterPath);
                 mov.Bitmap = Service.GetImage(url.AbsoluteUri, ImageType.Poster);
                 if (newMovieList.Any(x => x.Id == popularMovie.Id))
                 {
                 }
                 else
                 {
                     newMovieList.Add(mov);
                 }

         }
     }
     page++;
 }

Version Info Package Version 2.0.12 .NET Version: NET 8.0 OS Version: Windows 10 22H2 Build 19045.5011

sbaeumlisberger commented 2 weeks ago

The problem I see here is that you load a bitmap for each movie you load, but you never free the bitmaps. The VirtualizingWrapPanel only virtualizes the UI elements. The data you are loading has to be freed by your code when it is no longer needed.

xDreamms commented 2 weeks ago

The problem I see here is that you load a bitmap for each movie you load, but you never free the bitmaps. The VirtualizingWrapPanel only virtualizes the UI elements. The data you are loading has to be freed by your code when it is no longer needed.

How can I do that?

sbaeumlisberger commented 2 weeks ago

Basically, you need to make sure that you dispose the bitmaps that are no longer needed/visible (data virtualization). You can do this, for example, in the ScrollChanged event on a per-page basis, such as in your loading code, or in the DataContextChanged event of the individual elements. Alternatively, you could use some sort of bitmap "pool" with an appropriate maximum size.

RayCarrot commented 1 week ago

I have the same issue on my end, but for me it's caused by using a SharedSizeGroup on a Grid inside of the item template. This works fine most of the time, but when there are several hundred items in the ListBox it causes the app to freeze and I can see in the Visual Studio debugger how the app memory keeps increasing until it runs out of memory. I should also note that this does not happen when using a normal non-virtualized WrapPanel as the panel of the ListBox.

I do realize that a SharedSizeGroup can be expensive, and the reason I use it is to ensure the height of the items is the same. Is there another approach I can take instead? If I don't do this then the items height appears to be determined by the first item in the list. This causes the bigger items to be clipped so they don't fully display. I know I can set AllowDifferentSizedItems to true, but then the items end of having different heights, while what I want is to have each item have the same height as the biggest item. Is there another solution to this?

sbaeumlisberger commented 1 week ago

@RayCarrot I don't know how SharedSizeGroup is implemented, but I can't imagine it's such a big problem. Have you analyzed your memory usage? If you are sure that SharedSizeGroup is the cause of the freeze, please open a new issue with steps to reproduce or an sample project. Another approach would be to keep track of the max item height yourself (e.g. using the loaded or size changed event of the root element of your item template) and set the ItemSize property of the VirtualizingWrapPanel accordingly.