I have performance problem with my listview. There is a WPF ListView with groupping and virtualisation.
<ListView Name="ListOfEvents"
SelectionMode="Extended"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.IsVirtualizingWhenGrouping="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Expander IsExpanded="False">
<Expander.Header>
<StackPanel Orientation="Horizontal">
<TextBlock>
<TextBlock.Inlines>
<Run Text="{Binding Name, Mode=OneWay}" FontWeight="Bold" Foreground="Gray" />
<Run Text=":"/>
</TextBlock.Inlines>
</TextBlock>
<TextBlock Text="{Binding ItemCount}" Foreground="Black" FontWeight="Bold"/>
</StackPanel>
</Expander.Header>
<ItemsPresenter/>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsVisible}" Value="False" >
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ListView.ItemContainerStyle>
<ListView.View>
<GridView>
<GridViewColumn Width="100" DisplayMemberBinding="{Binding Name}" >
<GridViewColumnHeader Content="{x:Static const:Resources.IDS_NAME}" Tag ="name" />
</GridViewColumn>
<GridViewColumn Width="100" DisplayMemberBinding="{Binding Status}" >
<GridViewColumnHeader Content="{x:Static const:Resources.IDS_STATUS}" Tag="status" />
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
Sometimes I need to display only one item per group using <Style.Triggers>. In such cases items' property IsVisible is changed to false, but when it happens the application freezes due to layout rendering. In profiling I observe following situation:
1 - Ok. All items are visible. Virtualisation works properly:
2 - Slow. One item per group. VirtualizingStackPanel contains all ListViewItems from group:
Any suggestions why does it happens? How can I solve it?
Better don't manipulate the visual tree (the displayed item containers in particular) explicitly while virtualizing. The VirtualizingStackPanel
tracks and manages the item containers. Setting the Visibility
to Collapsed
will remove the container from the visual tree resulting in breaking virtualization.
The recommended solution is to filter the collection source instead.
It's almost always more efficient and more convenient to manipulate the data items instead of the item containers.
To accomplish this, first remove the Visibility
trigger from the container Style
.
Then in your code, where you toggle the model's IsVisible
property, you should apply the filter on the ICollectionView
:
public Dictionary<string, int> CollectionViewGroupCountTable { get; private set; }
/*** Section A: Initialize collection view with grouping ***/
ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.ItemsSourceCollection);
collectionView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(MyDataItem.SomeProperty)));
// Create table every time the group desciptors have changed
this.CollectionViewGroupCountTable = collectionView.Groups
.Cast<CollectionViewGroup>()
.ToDictionary(
group => group.Name.ToString(),
group => group.ItemCount);
/*** Section B: Dynamically hide group items ***/
// Hide all items of each group except the first
ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.ItemsSourceCollection);
// Hide all items of each group except the first
foreach (CollectionViewGroup itemGroup in collectionView.Groups)
{
IEnumerable<MyDataItem> groupItemsToHide = itemGroup.Items
.Cast<MyDataItem>()
.Skip(1);
foreach (var groupItem in groupItemsToHide)
{
groupItem.IsVisible = false;
}
}
// Apply a filter to actually hide the items
// without disabling virtualization
collectionView.Filter = item => (item as MyDataItem).IsVisible;
GroupCountConverter.cs
public class GroupCountConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
CollectionViewGroup? collectionViewGroup = values
.OfType<CollectionViewGroup>()
.FirstOrDefault();
string collectionViewGroupName = collectionViewGroup?.Name?.ToString();
if (collectionViewGroupName is null)
{
return Binding.DoNothing;
}
IDictionary<string, int> collectionGroupCountTable = values
.OfType<IDictionary<string, int>>()
.FirstOrDefault();
if (collectionGroupCountTable is null)
{
return Binding.DoNothing;
}
return collectionGroupCountTable.TryGetValue(collectionViewGroupName, out int groupCount)
? groupCount.ToString()
: Binding.DoNothing;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
<ListView>
<ListView.Resources>
<local:GroupCountConverter x:Key="GroupCountConverter" />
</ListView.Resources>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Expander IsExpanded="False">
<Expander.Header>
<StackPanel Orientation="Horizontal">
<TextBlock>
<TextBlock.Inlines>
<Run Text="{Binding Name, Mode=OneWay}"
FontWeight="Bold"
Foreground="Gray" />
<Run Text=":" />
</TextBlock.Inlines>
</TextBlock>
<TextBlock Foreground="Black"
FontWeight="Bold">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource GroupCountConverter}">
<Binding />
<Binding RelativeSource="{RelativeSource AncestorType=ListBox}"
Path="DataContext.CollectionViewGroupCountTable" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</Expander.Header>
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
</ListView>