WPF ListBox虚拟化导致显示的项目出现问题。

9

问题

我们需要在 WPF ListBox 控件中高效地显示大量(>1000)的对象。 我们依靠 WPF ListBox 的虚拟化(通过 VirtualizingStackPanel)来高效地显示这些项。

错误:使用虚拟化时,WPF ListBox 控件不能正确显示项目。

如何重现

我们已将问题简化为下面独立的 xaml。

将 xaml 复制并粘贴到 XAMLPad 中。

最初,ListBox 中没有选定的项,因此所有项都是相同大小且完全填充可用空间,这是预期行为。

现在,点击第一项。 由于我们的 DataTemplate,所选项将展开以显示其他信息,这是预期行为。

正如预期的那样,这会导致水平滚动条出现,因为所选项现在比可用空间更宽。

现在使用鼠标单击并拖动水平滚动条向右。

错误:非选定的可见项不再拉伸以填充可用空间。所有可见项应具有相同的宽度。

这是一个已知的错误吗? 是否有任何途径可以通过 XAML 或编程方式修复此错误?


<Page 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:sys="clr-namespace:System;assembly=mscorlib" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
    <Page.Resources>

        <DataTemplate x:Key="MyGroupItemTemplate">
            <Border Background="White"
                    TextElement.Foreground="Black"
                    BorderThickness="1"
                    BorderBrush="Black"
                    CornerRadius="10,10,10,10"
                    Cursor="Hand"
                    Padding="5,5,5,5"
                    Margin="2"
                    >
                <StackPanel>
                    <TextBlock Text="{Binding Path=Text, FallbackValue=[Content]}" />
                    <TextBlock x:Name="_details" Visibility="Collapsed" Margin="0,10,0,10" Text="[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx]" />
                </StackPanel>
            </Border>
            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type ListBoxItem}},Path=IsSelected}"
                             Value="True">
                    <Setter Property="TextElement.FontWeight"
                            TargetName="_details"
                            Value="Bold"/>
                    <Setter Property="Visibility"
                            TargetName="_details"
                            Value="Visible"/>
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>

    </Page.Resources>

    <DockPanel x:Name="LayoutRoot">

        <Slider x:Name="_slider"
                DockPanel.Dock="Bottom" 
                Value="{Binding FontSize, ElementName=_list, Mode=TwoWay}" 
                Maximum="100"
                ToolTip="Font Size"
                AutoToolTipPlacement="BottomRight"/>

        <!--
          I want the items in this ListBox to completly fill the available space.
          Therefore, I set HorizontalContentAlignment="Stretch".

          By default, the WPF ListBox control uses a VirtualizingStackPanel.
          This makes it possible to view large numbers of items efficiently.
          You can turn on/off this feature by setting the ScrollViewer.CanContentScroll to "True"/"False".

          Bug: when virtualization is enabled (ScrollViewer.CanContentScroll="True"), the unselected
               ListBox items will no longer stretch to fill the available horizontal space.
               The only workaround is to disable virtualization (ScrollViewer.CanContentScroll="False").
        -->

        <ListBox x:Name="_list"
                 ScrollViewer.CanContentScroll="True"
                 Background="Gray" 
                 Foreground="White"
                 IsSynchronizedWithCurrentItem="True" 
                 TextElement.FontSize="28"
                 HorizontalContentAlignment="Stretch"
                 ItemTemplate="{DynamicResource MyGroupItemTemplate}">
            <TextBlock Text="[1] This is item 1." />
            <TextBlock Text="[2] This is item 2." />
            <TextBlock Text="[3] This is item 3." />
            <TextBlock Text="[4] This is item 4." />
            <TextBlock Text="[5] This is item 5." />
            <TextBlock Text="[6] This is item 6." />
            <TextBlock Text="[7] This is item 7." />
            <TextBlock Text="[8] This is item 8." />
            <TextBlock Text="[9] This is item 9." />
            <TextBlock Text="[10] This is item 10." />
        </ListBox>

    </DockPanel>
</Page>

谢谢威尔!请参见下面的“答案”以获取更多详细信息。 - Adel Hazzah
2个回答

3

我花了比应该更多的时间尝试这个问题,但是我无法解决它。我理解这里发生了什么,但是在纯XAML中,我很难弄清楚如何解决问题。我认为我知道如何解决这个问题,但需要使用一个转换器。

警告:随着我对结论的解释,事情将变得复杂。

根本的问题来自于控件的宽度会拉伸到容器的宽度上。当启用虚拟化时,宽度不会改变。在ListBox内部的基础ScrollViewer中,ViewportWidth属性对应您看到的宽度。当另一个控件延伸出去(您选择它)时,ViewportWidth仍然是相同的,但是ExtentWidth显示完整的宽度。将所有控件的宽度绑定到ExtentWidth应该可行......

但实际上并不行。在我的情况下,我将字体大小设置为100以进行快速测试。当选择一个项目时,ExtentWidth="4109.13"。沿着树向下到您的ControlTemplate的Border,可以看到ActualWidth="4107.13"。为什么会有2个像素的差异?ListBoxItem包含一个带有2个像素填充的边框,导致ContentPresenter呈现略小。

我添加了以下Style代码,并从这里得到了帮助,以便使我能够直接访问ExtentWidth:

<Style x:Key="{x:Type ListBox}" TargetType="ListBox">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="ListBox">
        <Border 
          Name="Border" 
          Background="White"
          BorderBrush="Black"
          BorderThickness="1"
          CornerRadius="2">
          <ScrollViewer 
            Name="scrollViewer"
            Margin="0"
            Focusable="false">
            <StackPanel IsItemsHost="True" />
          </ScrollViewer>
        </Border>
        <ControlTemplate.Triggers>
          <Trigger Property="IsEnabled" Value="false">
            <Setter TargetName="Border" Property="Background"
                    Value="White" />
            <Setter TargetName="Border" Property="BorderBrush"
                    Value="Black" />
          </Trigger>
          <Trigger Property="IsGrouping" Value="true">
            <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
          </Trigger>
        </ControlTemplate.Triggers>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

注意我为此添加了一个名称ScrollViewer

接下来,我尝试将您的边框的宽度绑定到ExtentWidth

Width="{Binding ElementName=scrollViewer, Path=ExtentWidth}"

然而,由于存在2像素的填充,控件会在无限循环中调整大小,填充会将2像素添加到ExtentWidth中,这会调整边框宽度,又会将2个像素添加到ExtentWidth中,如此反复,直到删除代码并刷新为止。
如果您添加了一个减去ExtentWidth 2的转换器,我认为这可能会起作用。然而,当滚动条不存在时(您没有选择任何内容),ExtentWidth="0"。因此,绑定到MinWidth而不是Width可能更好,以便在没有滚动条可见时正确显示项:
MinWidth="{Binding ElementName=scrollViewer, Path=ExtentWidth, Converter={StaticResource PaddingSubtractor}}"

更好的解决方案是直接将ListBoxItemMinWidthExtentWidth进行数据绑定,这样就不需要使用转换器。但是我不知道如何访问该项。

编辑:为了组织起见,这里是实现上述方法所需的代码片段。这将使其他所有内容都不必要:

<Style TargetType="{x:Type ListBoxItem}">
    <Setter Property="MinWidth" Value="{Binding Path=ExtentWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ScrollViewer}}}" />
</Style>

对于“这是一个 bug 吗?”的问题,我不知道,但考虑到虚拟化的内置支持,我希望它是,特别是如果没有比我想出来的更简单的解决方案。 - Will Eddins

2

感谢Will的深入分析!

基于Will的建议:“更好的解决方案是直接绑定ListBoxItem本身的MinWidth...但我不知道如何访问该项”,我能够使用纯xaml实现,如下:

<ListBox x:Name="_list" 
         Background="Gray" 
         Foreground="White"
         IsSynchronizedWithCurrentItem="True" 
         TextElement.FontSize="28"
         HorizontalContentAlignment="Stretch"
         ItemTemplate="{DynamicResource MyGroupItemTemplate}">

    <!-- Here is Will's suggestion, implemented in pure xaml. Seems to work.
         Next problem is if you drag the Slider to the right to increase the FontSize.
         This will make the horizontal scroll bar appear, as expected.
         Problem: the horizontal scroll bar never goes away if you drag the Slider to the left to reduce the FontSize.
    -->
    <ListBox.Resources>
        <Style TargetType="{x:Type ListBoxItem}">
            <Setter Property="MinWidth" Value="{Binding Path=ExtentWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ScrollViewer}}}" />
        </Style>
    </ListBox.Resources>

    <TextBlock Text="[1] This is item 1." />
    <TextBlock Text="[2] This is item 2." />
    <TextBlock Text="[3] This is item 3." />
    <TextBlock Text="[4] This is item 4." />
    <TextBlock Text="[5] This is item 5." />
    <TextBlock Text="[6] This is item 6." />
    <TextBlock Text="[7] This is item 7." />
    <TextBlock Text="[8] This is item 8." />
    <TextBlock Text="[9] This is item 9." />
    <TextBlock Text="[10] This is item 10." />
</ListBox>

我从亚当·内森(Adam Nathan)的著作《Windows Presentation Foundation Unleashed》中得到了灵感。

所以,这似乎解决了最初的问题。

新问题

你会注意到XAML中有一个Slider控件,可以增加/减小ListBox的字体。这里的想法是允许用户缩放ListBox内容,以便更容易查看。

如果你首先将滑块向右拖动以增加FontSize,则水平滚动条将出现,如预期一样。 新问题是,如果你将滑块向左拖动以减小FontSize,则水平滚动条永远不会消失

有什么想法吗?


你最好提出一个新问题,因为现在你不会得到很多关注。我可能稍后会看一下,因为我觉得原始问题很有趣。如果我的回答有帮助的话,也可以点个赞或接受答案,谢谢 :) - Will Eddins
抱歉...我对这个论坛的概念还很陌生,所以刚刚才弄清楚如何注册等。 - Adel Hazzah
显然,这是一个 WPF 的 bug,在 .NET 4.0 中已经修复。 - Adel Hazzah

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接