Search code examples
.netwpfperformancelistviewstyling

Terrible Performance on ListView after styling ScrollViewer


I am working on an application that gets data from the Steam API. I am taking a huge performance hit to my ListView of games when applying a style to the ScrollViewer and ScrollBars.

Import Times


Without Using a Style on ScrollViewer/ScrollBar

When not using a style, it typically takes around 1.5 minutes to fully load all 86,000 games from the steam store. The memory usage stay right around 200mb.

Total Time: 1 minute and 23 second

Diagnostic Image without using a style

While Using a Style on ScrollViewer/ScrollBar

This is 1 minute and 40 seconds into importing while using a style. As you can see I only got 10,000 objects into the ListView while using the styling. If i am not using the styling this would by done by this time. Instead I am getting some kind of memory leak, at 50+ minutes in I was around 60,000 objects and around 1,800MB of Memory.

Total Time: 50+ minutes

Diagnostic Image while using a style

Background Worker

I am doing the work on a background thread and calling back to the UI thread to update the ObservableCollection.

Here is my background worker.

public void Import_Steam_Games_Worker_StartWork()
        {
            steamAppWorker.WorkerSupportsCancellation = false;
            steamAppWorker.DoWork += Import_Steam_Games_Worker_DoWork;
            steamAppWorker.RunWorkerCompleted += Import_Steam_Games_Worker_RunWorkerCompleted;
            steamAppWorker.RunWorkerAsync();
            Console.WriteLine("STARTUP", "Start import of all of steams games");
            TextBoxSearchSteamGames.IsEnabled = false;
            ButtonSearchSteamGames.IsEnabled = false;
        }
        private void Import_Steam_Games_Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            Console.WriteLine("Finished importing all steam games from API");
            TextBoxSearchSteamGames.IsEnabled = true;
            ButtonSearchSteamGames.IsEnabled = true;
        }
        private void Import_Steam_Games_Worker_DoWork(object sender, DoWorkEventArgs e)
        {
            SteamApiClient client = new SteamApiClient();
            SteamAppChunkResponse appChunk = new SteamAppChunkResponse();

            //continue pulling chunks until more results = false;
            bool moreResults = true;
            int lastAppId = 0;
            int count = 0;
            while (moreResults)
            {
                //Pulls 10,000 results from the API
                appChunk = client.GetSteamAppChunk(lastAppId);
                //For each game it finds, add it to the steamGames Observable Collection
                foreach (var app in appChunk.apps)
                {
                    //counter to see how many games have been imported
                    count++;
                    //Dispatcher to call back to the UI thread
                    Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate
                    {
                        steamGames.Add(app);
                        TextBlockSteamImportGameCount.Text = "Games Imported: " + count;
                    }));
                }
                //If response indicated there is no more results, break out of the while loop.
                if (!appChunk.have_more_results)
                {
                    moreResults = false;
                }
                //If the response indicates there is more results, then set the lastAppId to the last app returned from the previous call.
                else
                {
                    lastAppId = appChunk.last_appid;
                }
            }
        }

ListView XAML

Here I am binding the the name prop of the SteamAppChunk class to the ListViews Text.

<ListView x:Name="ListViewSteamGames" Foreground="White" FontSize="12" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="4" Grid.RowSpan="9" Margin="10,0,10,10" Background="#131821" SelectionChanged="ListViewSteamGames_SelectionChanged" BorderThickness="0">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Foreground="#FFA2BADE" FontSize="15" Text="{Binding name}"></TextBlock>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>

SteamAppChunk Class

Objects derived from this class are added to the ObservableCollection();

public class SteamAppChunk
    {
        public int appid { get; set; }
        public string name { get; set; }
        public int last_modified { get; set; }
        public int price_change_number { get; set; }
    }

ScrollViewer/ScrollBar Style

The only difference between the 2 images above is that While using styling this code applies, While not using styling I comment this block of code out. This resides in my App.xaml

<!--All the following is required to Re-Style the ScrollViewer, see 
             http://msdn2.microsoft.com/en-us/library/aa970847(VS.85).aspx
             http://msdn2.microsoft.com/en-us/library/ms742173(VS.85).aspx
             for the default Styles that the ScrollViewer has out of the box-->


        <!--Brushes used in Styling of ScrollViewer-->
        <SolidColorBrush x:Key="StandardBorderBrush" Color="#191d25" />
        <SolidColorBrush x:Key="StandardBackgroundBrush" Color="#191d25" />
        <SolidColorBrush x:Key="HoverBorderBrush" Color="#191d25" />
        <SolidColorBrush x:Key="SelectedBackgroundBrush" Color="#191d25" />
        <SolidColorBrush x:Key="SelectedForegroundBrush" Color="blue" />
        <SolidColorBrush x:Key="DisabledForegroundBrush" Color="blue" />
        <SolidColorBrush x:Key="NormalBrush" Color="#191d25"/>
        <SolidColorBrush x:Key="NormalBorderBrush" Color="Transparent" />
        <SolidColorBrush x:Key="HorizontalNormalBrush" Color="#191d25" />
        <SolidColorBrush x:Key="HorizontalNormalBorderBrush" Color="#191d25" />
        <LinearGradientBrush x:Key="ListBoxBackgroundBrush"
            StartPoint="0,0" EndPoint="1,0.001">
            <GradientBrush.GradientStops>
                <GradientStopCollection>
                    <GradientStop Color="#191d25" Offset="0.0" />
                    <GradientStop Color="#191d25" Offset="0.6" />
                    <GradientStop Color="#191d25" Offset="1.2"/>
                </GradientStopCollection>
            </GradientBrush.GradientStops>
        </LinearGradientBrush>
        <LinearGradientBrush x:Key="StandardBrush"
            StartPoint="0,0" EndPoint="0,1">
            <GradientBrush.GradientStops>
                <GradientStopCollection>
                    <GradientStop Color="#131821" Offset="0.0"/>
                    <GradientStop Color="#131821" Offset="1.0"/>
                </GradientStopCollection>
            </GradientBrush.GradientStops>
        </LinearGradientBrush>
        <SolidColorBrush x:Key="GlyphBrush" Color="#569dfb" />
        <LinearGradientBrush x:Key="PressedBrush"
            StartPoint="0,0" EndPoint="0,1">
            <GradientBrush.GradientStops>
                <GradientStopCollection>
                    <GradientStop Color="#131821" Offset="0.0"/>
                    <GradientStop Color="#131821" Offset="0.1"/>
                    <GradientStop Color="#131821" Offset="0.9"/>
                    <GradientStop Color="#131821" Offset="1.0"/>
                </GradientStopCollection>
            </GradientBrush.GradientStops>
        </LinearGradientBrush>

        <!--SrollViewer ScrollBar Repeat Buttons (at each end)-->
        <Style x:Key="ScrollBarLineButton" TargetType="{x:Type RepeatButton}">
            <Setter Property="SnapsToDevicePixels" Value="True"/>
            <Setter Property="OverridesDefaultStyle" Value="true"/>
            <Setter Property="Focusable" Value="false"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type RepeatButton}">
                        <Border
          Name="Border"
          Margin="1"
          CornerRadius="2"
          Background="{StaticResource NormalBrush}"
          BorderBrush="{StaticResource NormalBorderBrush}"
          BorderThickness="1">
                            <Path
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Fill="{StaticResource GlyphBrush}"
            Data="{Binding Path=Content,
                RelativeSource={RelativeSource TemplatedParent}}" />
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsPressed" Value="true">
                                <Setter TargetName="Border" Property="Background"
                                Value="{StaticResource PressedBrush}" />
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Foreground"
                                Value="{StaticResource DisabledForegroundBrush}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <!--SrollViewer ScrollBar Repeat Buttons (The part in the middle, 
             not the thumb the long area between the buttons )-->
        <Style x:Key="ScrollBarPageButton" TargetType="{x:Type RepeatButton}">
            <Setter Property="SnapsToDevicePixels" Value="True"/>
            <Setter Property="OverridesDefaultStyle" Value="true"/>
            <Setter Property="IsTabStop" Value="false"/>
            <Setter Property="Focusable" Value="false"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type RepeatButton}">
                        <Border Background="#191d25" />
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <!--ScrollViewer ScrollBar Thumb, that part that can be dragged
            up/down or left/right Buttons-->
        <Style x:Key="ScrollBarThumb" TargetType="{x:Type Thumb}">
            <Setter Property="SnapsToDevicePixels" Value="True"/>
            <Setter Property="OverridesDefaultStyle" Value="true"/>
            <Setter Property="IsTabStop" Value="false"/>
            <Setter Property="Focusable" Value="false"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Thumb}">
                        <Border
          CornerRadius="2"
          Background="{TemplateBinding Background}"
          BorderBrush="{TemplateBinding BorderBrush}"
          BorderThickness="1" />
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <ControlTemplate x:Key="VerticalScrollBar"
            TargetType="{x:Type ScrollBar}">
            <Grid >
                <Grid.RowDefinitions>
                    <RowDefinition MaxHeight="18"/>
                    <RowDefinition Height="0.00001*"/>
                    <RowDefinition MaxHeight="18"/>
                </Grid.RowDefinitions>
                <Border
      Grid.RowSpan="3"
      CornerRadius="2"
      Background="#191d25" />
                <RepeatButton
      Grid.Row="0"
      Style="{StaticResource ScrollBarLineButton}"
      Height="18"
      Command="ScrollBar.LineUpCommand"
      Content="M 0 4 L 8 4 L 4 0 Z" />
                <Track
      Name="PART_Track"
      Grid.Row="1"
      IsDirectionReversed="true">
                    <Track.DecreaseRepeatButton>
                        <RepeatButton
          Style="{StaticResource ScrollBarPageButton}"
          Command="ScrollBar.PageUpCommand" />
                    </Track.DecreaseRepeatButton>
                    <Track.Thumb>
                        <Thumb
          Style="{StaticResource ScrollBarThumb}"
          Margin="1,0,1,0"
          Background="#569dfb"
          BorderBrush="Transparent" />
                    </Track.Thumb>
                    <Track.IncreaseRepeatButton>
                        <RepeatButton
          Style="{StaticResource ScrollBarPageButton}"
          Command="ScrollBar.PageDownCommand" />
                    </Track.IncreaseRepeatButton>
                </Track>
                <RepeatButton
      Grid.Row="3"
      Style="{StaticResource ScrollBarLineButton}"
      Height="18"
      Command="ScrollBar.LineDownCommand"
      Content="M 0 0 L 4 4 L 8 0 Z"/>
            </Grid>
        </ControlTemplate>
        <!--HorizontalScrollBar Template using the previously created Templates-->
        <ControlTemplate x:Key="HorizontalScrollBar"
            TargetType="{x:Type ScrollBar}">
            <Grid >
                <Grid.ColumnDefinitions>
                    <ColumnDefinition MaxWidth="18"/>
                    <ColumnDefinition Width="0.00001*"/>
                    <ColumnDefinition MaxWidth="18"/>
                </Grid.ColumnDefinitions>
                <Border
      Grid.ColumnSpan="3"
      CornerRadius="2"
      Background="#191d25" />
                <RepeatButton 
      Grid.Column="0"
      Style="{StaticResource ScrollBarLineButton}"
      Width="18"
      Command="ScrollBar.LineLeftCommand"
      Content="M 4 0 L 4 8 L 0 4 Z" />
                <Track
      Name="PART_Track"
      Grid.Column="1"
      IsDirectionReversed="False">
                    <Track.DecreaseRepeatButton>
                        <RepeatButton
          Style="{StaticResource ScrollBarPageButton}"
          Command="ScrollBar.PageLeftCommand" />
                    </Track.DecreaseRepeatButton>
                    <Track.Thumb>
                        <Thumb
          Style="{StaticResource ScrollBarThumb}"
          Margin="0,1,0,1"
          Background="#569dfb"
          BorderBrush="Transparent" />
                    </Track.Thumb>
                    <Track.IncreaseRepeatButton>
                        <RepeatButton
          Style="{StaticResource ScrollBarPageButton}"
          Command="ScrollBar.PageRightCommand" />
                    </Track.IncreaseRepeatButton>
                </Track>
                <RepeatButton
      Grid.Column="3"
      Style="{StaticResource ScrollBarLineButton}"
      Width="18"
      Command="ScrollBar.LineRightCommand"
      Content="M 0 0 L 4 4 L 0 8 Z"/>
            </Grid>
        </ControlTemplate>
        <!--Style for overall  ScrollBar-->
        <Style x:Key="{x:Type ScrollBar}" TargetType="{x:Type ScrollBar}">
            <Setter Property="SnapsToDevicePixels" Value="True"/>
            <Setter Property="OverridesDefaultStyle" Value="true"/>
            <Style.Triggers>
                <Trigger Property="Orientation" Value="Horizontal">
                    <Setter Property="Width" Value="Auto"/>
                    <Setter Property="Height" Value="18" />
                    <Setter Property="Template"
                        Value="{StaticResource HorizontalScrollBar}" />
                </Trigger>
                <Trigger Property="Orientation" Value="Vertical">
                    <Setter Property="Width" Value="18"/>
                    <Setter Property="Height" Value="Auto" />
                    <Setter Property="Template"
                        Value="{StaticResource VerticalScrollBar}" />
                </Trigger>
            </Style.Triggers>
        </Style>
        
        <!--Style for ScrollViewer-->
        <Style  TargetType="{x:Type ScrollViewer}">
            <Setter Property="OverridesDefaultStyle" Value="False"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ScrollViewer}">
                        <Grid Background="#33000000">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition/>
                                <RowDefinition Height="Auto"/>
                            </Grid.RowDefinitions>

                            <ScrollContentPresenter Grid.Column="0"/>

                            <ScrollBar Name="PART_VerticalScrollBar"
                                                       Grid.Column="1"
                                                       Value="{TemplateBinding VerticalOffset}"
                                                       Maximum="{TemplateBinding ScrollableHeight}"
                                                       ViewportSize="{TemplateBinding ViewportHeight}"
                                                       Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>
                            <ScrollBar Name="PART_HorizontalScrollBar"
                                                       Orientation="Horizontal"
                                                       Grid.Row="1"
                                                       Grid.Column="0"
                                                       Value="{TemplateBinding HorizontalOffset}"
                                                       Maximum="{TemplateBinding ScrollableWidth}"
                                                       ViewportSize="{TemplateBinding ViewportWidth}"
                                                       Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"/>

                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>


Summary/Question

For some reason I am getting a huge performance hit when I enable this style for ScrollViewer and the ScrollBars.

What is the best way to go about pulling 80,000 entries and adding them to a list asynchronously?

I really do not want to get rid of the styling on the ScrollViewer/ScrollBars as the default scroll bars look hideous with my Skin.

Any help is appreciated, if you need more clarification on the problem please ask.


Solution

  • Editing the ControlTemplate can lead to virtualization being disabled. Setting those two Properties on the ScrollContentPresenter might fix your problem:

    ScrollContentPresenter: CanContentScroll="True" VirtualizingPanel.IsVirtualizing="True"