Search code examples
wpfvb.netxaml.net-core

What makes my VirtualizingStackPanel not virtualize when Orientation=Horizontal?


I have a ListView with which I try te emulate the Windows Explorer "List" view. To show the items, I use the VirtualizingWrapPanel by S. Baeumlisberger and it scrolls horizontally and virtualizes its items just fine.

However, at a certain point users are going to want to group items (by file type for example). This is where things get tricky. When grouping, the ListView uses the <GroupStyle.Panel> to display several VirtualizingWrapPanel's, one for each group of items.

For this <GroupStyle.Panel>, I use a VirtualizingStackPanel with it's Orientation set to Horizontal, because I want the groups to display horizontally, just like in Windows Explorer. This works, but it won't virtualize it's items (whereas it does when I set it to Vertical).

This is my XAML:

<ListView x:Name="PART_ListView" SelectionMode="Extended" 
            FocusVisualStyle="{x:Null}" ScrollViewer.IsDeferredScrollingEnabled="False" 
            ScrollViewer.CanContentScroll="True" BorderThickness="0" 
            Grid.IsSharedSizeScope="True" VirtualizingPanel.ScrollUnit="Pixel" 
            VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.IsVirtualizingWhenGrouping="True"
            VirtualizingPanel.VirtualizationMode="Recycling" VirtualizingPanel.CacheLength="2" 
            VirtualizingPanel.CacheLengthUnit="Page">
    <ListView.Resources>
        <GroupStyle x:Key="groupStyle">
            <GroupStyle.Panel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel Orientation="Horizontal" />
                </ItemsPanelTemplate>
            </GroupStyle.Panel>
            <GroupStyle.ContainerStyle>
                <Style TargetType="{x:Type GroupItem}">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate>
                                <Expander Margin="0,0,20,0" IsExpanded="True" Style="{DynamicResource lailaShell_GroupByExpanderStyle}">
                                    <Expander.Resources>
                                        <converters:ListViewGroupingHeightConverter x:Key="listViewGroupingHeightConverter" />
                                    </Expander.Resources>
                                    <Expander.Header>
                                        <StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch" x:Name="expanderHeader">
                                            <TextBlock Text="{Binding Name}" Foreground="DarkBlue" VerticalAlignment="Center" />
                                        </StackPanel>
                                    </Expander.Header>
                                    <ItemsPresenter Margin="5,0,0,0">
                                        <ItemsPresenter.Height>
                                            <MultiBinding Converter="{StaticResource listViewGroupingHeightConverter}">
                                                <Binding Path="." ElementName="expanderHeader" />
                                                <Binding Path="ActualHeight" ElementName="PART_ListView" />
                                            </MultiBinding>
                                        </ItemsPresenter.Height>
                                    </ItemsPresenter>
                                </Expander>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </GroupStyle.ContainerStyle>
        </GroupStyle>
    </ListView.Resources>
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <vwp:VirtualizingWrapPanel SpacingMode="None">
                <vwp:VirtualizingWrapPanel.Style>
                    <Style>
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding Path=Items.GroupDescriptions.Count, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListView}}}" Value="0">
                                <Setter Property="vwp:VirtualizingWrapPanel.Margin" Value="20,0,0,0" />
                            </DataTrigger>
                            <DataTrigger Binding="{Binding Path=Items.GroupDescriptions.Count, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListView}}}" Value="1">
                                <Setter Property="vwp:VirtualizingWrapPanel.Margin" Value="0,0,0,0" />
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </vwp:VirtualizingWrapPanel.Style>
            </vwp:VirtualizingWrapPanel>
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
    <i:Interaction.Behaviors>
        <behaviors:SelectionBehavior />
    </i:Interaction.Behaviors>
    <ListView.ItemTemplate>
        <DataTemplate>
            <Border>
                <Grid>
                    <Grid.ColumnDefinitions >
                        <ColumnDefinition Width="16" />
                        <ColumnDefinition Width="4" />
                        <ColumnDefinition Width="16" />
                        <ColumnDefinition Width="4" />
                        <ColumnDefinition Width="Auto" SharedSizeGroup="A" />
                    </Grid.ColumnDefinitions>
                    <Image Source="{Binding PropertiesByKeyAsText[e77e90df-6271-4f5b-834f-2dd1f245dda4:2].FirstIcon16Async, IsAsync=True}"
                            Width="14" Height="16">
                        <Image.Style>
                            <Style>
                                <Setter Property="Image.Visibility" Value="Collapsed" />
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding PropertiesByKeyAsText[e77e90df-6271-4f5b-834f-2dd1f245dda4:2].HasIcon}" Value="True">
                                        <Setter Property="Image.Visibility" Value="Visible" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Image.Style>
                    </Image>
                    <Image Grid.Column="2" Source="{Binding IconAsync[16], IsAsync=True}" Width="16" Height="16" 
                            UseLayoutRounding="True" SnapsToDevicePixels="True">
                        <Image.Style>
                            <Style>
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding IsHidden}" Value="True">
                                        <Setter Property="Image.Opacity" Value="0.5" />
                                    </DataTrigger>
                                    <DataTrigger Binding="{Binding IsCut}" Value="True">
                                        <Setter Property="Image.Opacity" Value="0.5" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Image.Style>
                    </Image>
                    <Image Grid.Column="2" Source="{Binding OverlayImageAsync, IsAsync=True}" Width="16" Height="16" 
                            UseLayoutRounding="True" SnapsToDevicePixels="True"
                            VerticalAlignment="Bottom" HorizontalAlignment="Left">
                        <Image.Style>
                            <Style>
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding IsHidden}" Value="True">
                                        <Setter Property="Image.Opacity" Value="0.5" />
                                    </DataTrigger>
                                    <DataTrigger Binding="{Binding IsCut}" Value="True">
                                        <Setter Property="Image.Opacity" Value="0.5" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Image.Style>
                    </Image>
                    <TextBlock Grid.Column="4" MaxWidth="200"
                                Text="{Binding DisplayName}" TextTrimming="CharacterEllipsis">
                        <TextBlock.Style>
                            <Style>
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding IsCompressed}" Value="True">
                                        <Setter Property="TextBlock.Foreground" Value="Blue" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </TextBlock.Style>
                    </TextBlock>
                </Grid>
            </Border>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

You can find the full source code at https://sourceforge.net/p/laila-shell/code/ci/master/tree/ if you're interested.


Solution

  • I don't known why, but I started getting in trouble with the VirtualizingWrapPanel constantly flickering up to the point it became unusable and when I breaked the program I saw in the stack trace that it was constantly measuring and arranging. So I reckoned it had trouble sticking to a constant item size so I set the ItemSize property of the VirtualizingWrapPanel to something constant. And now all of a sudden all of my problems are gone and everything is working:

    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <vwp:VirtualizingWrapPanel 
                SpacingMode="None" 
                Orientation="Vertical" 
                ItemSize="240,20">
            </vwp:VirtualizingWrapPanel>
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>