WPF ListBox虚拟化会搞砸所显示的项目

Ade*_*zah 9 wpf virtualization listbox

问题

我们需要在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>
Run Code Online (Sandbox Code Playgroud)

Wil*_*ins 3

我花了比我应该花的时间更多的时间来尝试这个,但无法让它发挥作用。我了解这里发生的情况,但在纯 XAML 中,我无法弄清楚如何解决该问题。我想我知道如何解决这个问题,但它涉及一个转换器。

警告:当我解释我的结论时,事情会变得复杂。

根本问题来自于控件的宽度拉伸到其容器的宽度这一事实。启用虚拟化后,宽度不会改变。在 的底层ScrollViewer内部ListBox,该ViewportWidth属性对应于您看到的宽度。当另一个控件进一步延伸(您选择它)时,ViewportWidth仍然相同,但ExtentWidth显示完整宽度。将所有控件的宽度绑定到ExtentWidth应该有效的宽度......

但事实并非如此。我将 FontSize 设置为 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>
Run Code Online (Sandbox Code Playgroud)

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

然后,我尝试将边框的宽度绑定到 ExtentWidth:

Width="{Binding ElementName=scrollViewer, Path=ExtentWidth}"
Run Code Online (Sandbox Code Playgroud)

但是,由于 2 像素填充,控件将在无限循环中调整大小,填充将 2 个像素添加到ExtentWidth,这将调整边框宽度的大小,这将再添加 2 个像素ExtentWidth,等等,直到您删除代码并刷新。

如果您添加了一个从 ExtentWidth 中减去 2 的转换器,我认为这可能会起作用。但是,当滚动条不存在时(您没有选择任何内容)ExtentWidth="0",. 因此,绑定到MinWidth而不是Width可能会更好地工作,以便当没有滚动条可见时项目可以正确显示:

MinWidth="{Binding ElementName=scrollViewer, Path=ExtentWidth, Converter={StaticResource PaddingSubtractor}}"
Run Code Online (Sandbox Code Playgroud)

更好的解决方案是如果您可以直接MinWidth对其ListBoxItem本身进行数据绑定。您可以直接绑定到 ExtentWidth,并且不需要转换器。但是我不知道如何访问该项目。

编辑:为了组织起见,这是执行此操作所需的剪辑。使其他一切变得不必要:

<Style TargetType="{x:Type ListBoxItem}">
    <Setter Property="MinWidth" Value="{Binding Path=ExtentWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ScrollViewer}}}" />
</Style>
Run Code Online (Sandbox Code Playgroud)