ListView Resize Columns Performance Issues (Grouping)
c#, listview, performance, wpf, xaml
Solution
Your performance issue is due to not using `IsVirtualizingWhenGrouping`
You mentionned that using `IsVirtualizingWhenGrouping` was locking your application, and this is a known WPF issue (see : http://connect.microsoft.com/VisualStudio/feedback/details/780146/freeze-when-using-virtualizingpanel-isvirtualizingwhengrouping for more information on this bug happening when having a custom GroupStyle and `IsVirtualizingWhenGrouping` set to true)
Here is a quick workaroung to solve your issue : You just have to add an expander in the GroupStyle ControlTemplate. You'll then be able to use `IsVirtualizingWhenGrouping` and then have good performances when scrolling/Resizing columns.
Here is the code that works on my machine : (I put everything directly in MainWindow to simplify a bit)
<Window x:Class="WpfApplication21.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style x:Key="lvStyle" TargetType="{x:Type ListView}" >
<Setter Property="ListView.ItemsSource" Value="{Binding}"/>
<Setter Property="ListView.View">
<Setter.Value>
<GridView>
<GridViewColumn Header="Name">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Date">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Date}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Desc">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Desc}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<ListView x:Name="ListView1"
Style="{DynamicResource lvStyle}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.IsVirtualizingWhenGrouping="True">
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander IsExpanded="True">
<DockPanel>
<Border DockPanel.Dock="Top">
<TextBlock x:Name="groupItem"
Text="{Binding ItemCount, StringFormat={}({0} Results)}"></TextBlock>
</Border>
<ItemsPresenter x:Name="groupItemPresenter" DockPanel.Dock="Bottom"></ItemsPresenter>
</DockPanel>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
</Grid>
</Window>
EDIT: Here is a ControlTemplate that "hides" the expander. It's the original one from which I removed the unnecessary parts :
<ControlTemplate x:Key="CustomExpanderControlTemplate" TargetType="{x:Type Expander}">
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="3" SnapsToDevicePixels="True">
<DockPanel>
<ToggleButton x:Name="HeaderSite" ContentTemplate="{TemplateBinding HeaderTemplate}" Content="{TemplateBinding Header}" DockPanel.Dock="Top" Foreground="{TemplateBinding Foreground}" FontWeight="{TemplateBinding FontWeight}" FontStyle="{TemplateBinding FontStyle}" FontStretch="{TemplateBinding FontStretch}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Margin="1" MinWidth="0" MinHeight="0" Padding="{TemplateBinding Padding}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}">
<ToggleButton.FocusVisualStyle>
<Style>
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Border>
<Rectangle Margin="0" SnapsToDevicePixels="True" Stroke="Black" StrokeThickness="1" StrokeDashArray="1 2"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ToggleButton.FocusVisualStyle>
<ToggleButton.Style>
<Style TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Padding="{TemplateBinding Padding}">
<Grid Background="Transparent" SnapsToDevicePixels="False">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Grid.Column="0" ContentStringFormat="{TemplateBinding ContentStringFormat}" HorizontalAlignment="Left" Margin="4,0,0,0" RecognizesAccessKey="True" SnapsToDevicePixels="True" VerticalAlignment="Center"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ToggleButton.Style>
</ToggleButton>
<ContentPresenter x:Name="ExpandSite" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}" DockPanel.Dock="Bottom" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" Visibility="Collapsed" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</DockPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Setter Property="Visibility" TargetName="ExpandSite" Value="Visible"/>
</Trigger>
<Trigger Property="ExpandDirection" Value="Right">
<Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Right"/>
<Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Left"/>
<Setter Property="Style" TargetName="HeaderSite">
<Setter.Value>
<Style TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Padding="{TemplateBinding Padding}">
<Grid Background="Transparent" SnapsToDevicePixels="False">
<Grid.RowDefinitions>
<RowDefinition Height="19"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid>
<Grid.LayoutTransform>
<TransformGroup>
<RotateTransform Angle="-90"/>
</TransformGroup>
</Grid.LayoutTransform>
<Ellipse x:Name="circle" Fill="Transparent" HorizontalAlignment="Center" Height="19" Stroke="DarkGray" VerticalAlignment="Center" Width="19"/>
<Path x:Name="arrow" Data="M1,1.5L4.5,5 8,1.5" HorizontalAlignment="Center" SnapsToDevicePixels="False" Stroke="#FF666666" StrokeThickness="2" VerticalAlignment="Center"/>
</Grid>
<ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}" HorizontalAlignment="Center" Margin="0,4,0,0" Grid.Row="1" RecognizesAccessKey="True" SnapsToDevicePixels="True" VerticalAlignment="Top"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Data" TargetName="arrow" Value="M1,4.5L4.5,1 8,4.5"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Stroke" TargetName="circle" Value="#FF3C7FB1"/>
<Setter Property="Fill" TargetName="circle" Value="Transparent"/>
<Setter Property="Stroke" TargetName="arrow" Value="#FF222222"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Stroke" TargetName="circle" Value="#FF526C7B"/>
<Setter Property="StrokeThickness" TargetName="circle" Value="1.5"/>
<Setter Property="Fill" TargetName="circle" Value="Transparent"/>
<Setter Property="Stroke" TargetName="arrow" Value="#FF003366"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Stroke" TargetName="circle" Value="DarkGray"/>
<Setter Property="Fill" TargetName="circle" Value="Transparent"/>
<Setter Property="Stroke" TargetName="arrow" Value="#FF666666"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="ExpandDirection" Value="Up">
<Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Top"/>
<Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Bottom"/>
<Setter Property="Style" TargetName="HeaderSite">
<Setter.Value>
<Style TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Padding="{TemplateBinding Padding}">
<Grid Background="Transparent" SnapsToDevicePixels="False">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="19"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid>
<Grid.LayoutTransform>
<TransformGroup>
<RotateTransform Angle="180"/>
</TransformGroup>
</Grid.LayoutTransform>
<Ellipse x:Name="circle" Fill="Transparent" HorizontalAlignment="Center" Height="19" Stroke="DarkGray" VerticalAlignment="Center" Width="19"/>
<Path x:Name="arrow" Data="M1,1.5L4.5,5 8,1.5" HorizontalAlignment="Center" SnapsToDevicePixels="False" Stroke="#FF666666" StrokeThickness="2" VerticalAlignment="Center"/>
</Grid>
<ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Grid.Column="1" ContentStringFormat="{TemplateBinding ContentStringFormat}" HorizontalAlignment="Left" Margin="4,0,0,0" RecognizesAccessKey="True" SnapsToDevicePixels="True" VerticalAlignment="Center"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Data" TargetName="arrow" Value="M1,4.5L4.5,1 8,4.5"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Stroke" TargetName="circle" Value="#FF3C7FB1"/>
<Setter Property="Fill" TargetName="circle" Value="Transparent"/>
<Setter Property="Stroke" TargetName="arrow" Value="#FF222222"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Stroke" TargetName="circle" Value="#FF526C7B"/>
<Setter Property="StrokeThickness" TargetName="circle" Value="1.5"/>
<Setter Property="Fill" TargetName="circle" Value="Transparent"/>
<Setter Property="Stroke" TargetName="arrow" Value="#FF003366"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Stroke" TargetName="circle" Value="DarkGray"/>
<Setter Property="Fill" TargetName="circle" Value="Transparent"/>
<Setter Property="Stroke" TargetName="arrow" Value="#FF666666"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="ExpandDirection" Value="Left">
<Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Left"/>
<Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Right"/>
<Setter Property="Style" TargetName="HeaderSite">
<Setter.Value>
<Style TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Padding="{TemplateBinding Padding}">
<Grid Background="Transparent" SnapsToDevicePixels="False">
<Grid.RowDefinitions>
<RowDefinition Height="19"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid>
<Grid.LayoutTransform>
<TransformGroup>
<RotateTransform Angle="90"/>
</TransformGroup>
</Grid.LayoutTransform>
<Ellipse x:Name="circle" Fill="Transparent" HorizontalAlignment="Center" Height="19" Stroke="DarkGray" VerticalAlignment="Center" Width="19"/>
<Path x:Name="arrow" Data="M1,1.5L4.5,5 8,1.5" HorizontalAlignment="Center" SnapsToDevicePixels="False" Stroke="#FF666666" StrokeThickness="2" VerticalAlignment="Center"/>
</Grid>
<ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}" HorizontalAlignment="Center" Margin="0,4,0,0" Grid.Row="1" RecognizesAccessKey="True" SnapsToDevicePixels="True" VerticalAlignment="Top"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Data" TargetName="arrow" Value="M1,4.5L4.5,1 8,4.5"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Stroke" TargetName="circle" Value="#FF3C7FB1"/>
<Setter Property="Fill" TargetName="circle" Value="Transparent"/>
<Setter Property="Stroke" TargetName="arrow" Value="#FF222222"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Stroke" TargetName="circle" Value="#FF526C7B"/>
<Setter Property="StrokeThickness" TargetName="circle" Value="1.5"/>
<Setter Property="Fill" TargetName="circle" Value="Transparent"/>
<Setter Property="Stroke" TargetName="arrow" Value="#FF003366"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Stroke" TargetName="circle" Value="DarkGray"/>
<Setter Property="Fill" TargetName="circle" Value="Transparent"/>
<Setter Property="Stroke" TargetName="arrow" Value="#FF666666"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
You can use it by setting it in the group style :
<Expander IsExpanded="True" Template="{DynamicResource CustomExpanderControlTemplate}">
Problem
I am experiencing major performance issues with `ListView` whenever I implement grouping. I have found somewhat similar questions on StackOverflow, but none seem to help! Here is my current situation (I have simplified my project so there is less noise): I have a `ContentControl` with a `ListView` as the child. The `ListView` is bound to an `ObservableCollection`, which is initially empty. As time passes, objects are added to to the collection (in this example, 500 items are added every 10 seconds using a `DispatcherTimer`). The size of the `ObservableCollection` will vary, but it's possible the collection could end up being over 25,000 items. When the `ObservableCollection` has less than 2000 (not exact figure), column resizing looks like this: However, as more objects are added to the `ObservableCollection`, there is a noticeable drop in performance (you need to scroll down for this to occur). This will eventually lead to the `Application` locking up. I was thinking the problem could be resolved using `Virtualization`, so I tried using the following: ``` <ListView x:Name="ListView1" Style="{DynamicResource lvStyle}" VirtualizingPanel.IsVirtualizingWhenGrouping="True" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.IsContainerVirtualizable="True" ScrollViewer.IsDeferredScrollingEnabled="True"> ``` However, nothing seems to work! Not to mention, `VirtualizingPanel.IsVirtualizingWhenGrouping="True"` causes the `ListView` to lock up entirely. I have also looked into Paul McClean's excellent Data Virtualization, however, it does not handle grouping. Question: When grouping items in a `ListView`, is there a way to resize the columns without drastically affecting the Application's performance? Ideally, I would like to reduce memory overhead, so I am all for implementing some sort of asynchronous solution. CODE: XAML: ``` <ContentControl x:Class="ListViewDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:ListViewDemo" xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:dat="clr-namespace:System.Windows.Data;assembly=PresentationFramework" xmlns:Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Classic" mc:Ignorable="d" d:DesignHeight="400" d:DesignWidth="300"> <ContentControl.Resources> <Style x:Key="lvStyle" TargetType="{x:Type ListView}"> <Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="True"/> <Setter Property="VirtualizingStackPanel.VirtualizationMode" Value="Recycling"/> <Setter Property="ScrollViewer.IsDeferredScrollingEnabled" Value="True"/> <Setter Property="ListView.ItemsSource" Value="{Binding}"/> <Setter Property="ListView.View"> <Setter.Value> <GridView> <GridViewColumn Header="Name"> <GridViewColumn.CellTemplate> <DataTemplate> <TextBlock Text="{Binding Name}"/> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> <GridViewColumn Header="Date"> <GridViewColumn.CellTemplate> <DataTemplate> <TextBlock Text="{Binding Date}"/> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> <GridViewColumn Header="Desc"> <GridViewColumn.CellTemplate> <DataTemplate> <TextBlock Text="{Binding Desc}"/> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> </GridView> </Setter.Value> </Setter> </Style> </ContentControl.Resources> <Grid> <ListView x:Name="ListView1" Style="{DynamicResource lvStyle}" VirtualizingPanel.IsVirtualizingWhenGrouping="True" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.IsContainerVirtualizable="True" ScrollViewer.IsDeferredScrollingEnabled="True"> <ListView.GroupStyle> <GroupStyle> <GroupStyle.ContainerStyle> <Style TargetType="{x:Type GroupItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type GroupItem}"> <DockPanel> <Border DockPanel.Dock="Top"> <TextBlock x:Name="groupItem" Text="{Binding ItemCount, StringFormat={}({0} Results)}"></TextBlock> </Border> <ItemsPresenter DockPanel.Dock="Bottom"></ItemsPresenter> </DockPanel> </ControlTemplate> </Setter.Value> </Setter> </Style> </GroupStyle.ContainerStyle> </GroupStyle> </ListView.GroupStyle> </ListView> </Grid> </ContentControl> ``` CODE-BEHIND: ``` using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Windows.Threading; namespace ListViewDemo { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : ContentControl { private ObservableCollection<Event> eventCollection = new ObservableCollection<Event>(); public MainWindow() { InitializeComponent(); DispatcherTimer dispatcherTimer = new DispatcherTimer(); dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick); dispatcherTimer.Interval = new TimeSpan(0, 0, 10); dispatcherTimer.Start(); ListView1.ItemsSource = eventCollection; ListView1.Items.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Descending)); ListView1.Items.SortDescriptions.Add(new SortDescription("Date", ListSortDirection.Descending)); ListView1.Items.GroupDescriptions.Add(new PropertyGroupDescription("Seconds")); } private void dispatcherTimer_Tick(object sender, EventArgs e) { for(var i = 0; i < 500; i++){ eventCollection.Add(new Event { Name = string.Format("Name_{0}", eventCollection.Count), Date = DateTime.Now.ToString("MM.dd.yy HH:mm"), Seconds = Convert.ToInt32(DateTime.Now.ToString("ss")), Desc = "Description" }); } } public class Event { public string Name { get; set; } public string Date { get; set; } public int Seconds { get; set; } public string Desc { get; set; } } } } ```