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

标签 wpf listbox virtualization

问题

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

错误 : WPF ListBox 控件在使用虚拟化时无法正确显示项目。

如何重现

我们已将问题提炼为如下所示的独立 xaml。

将 xaml 复制并粘贴到 XAMLPad 中。

最初,ListBox 中没有选定的项目,因此正如预期的那样,所有项目都具有相同的大小并且它们完全填满了可用空间。

现在,单击第一项。
正如预期的那样,由于我们的 DataTemplate,所选项目将展开以显示其他信息。

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

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

错误:未选择的可见项目不再拉伸(stretch)以填充可用空间。所有可见项目应具有相同的宽度。

这是一个已知的错误?
有没有办法通过 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>

最佳答案

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

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

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

但事实并非如此。我将 FontSize 设置为 100 以便在我的情况下进行更快的测试。选择项目时,ExtentWidth="4109.13 .沿着树向下到您的 ControlTemplate 的 Border , 我看到 ActualWidth="4107.13" .为什么会有 2 个像素的差异? ListBoxItem 包含一个带有 2 像素填充的边框,导致 ContentPresenter 呈现稍小一些。

我添加了以下 Stylehelp from here允许我直接访问 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 添加了一个名字以此目的。

然后,我尝试将边框的宽度绑定(bind)到 ExtentWidth:
Width="{Binding ElementName=scrollViewer, Path=ExtentWidth}"

然而,由于 2 像素的填充,控件将在无限循环中调整大小,填充将 2 像素添加到 ExtentWidth。 ,它会调整边框宽度的大小,从而为 ExtentWidth 增加 2 个像素。等,直到您删除代码并刷新。

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

一个 更好的解决方案如果您可以直接数据绑定(bind) MinWidthListBoxItem本身。您可以直接绑定(bind)到 ExtentWidth,并且不需要转换器。但是我不知道如何访问该项目。

编辑:为了组织起见,这是执行此操作所需的剪辑。使其他一切都变得不必要:
<Style TargetType="{x:Type ListBoxItem}">
    <Setter Property="MinWidth" Value="{Binding Path=ExtentWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ScrollViewer}}}" />
</Style>

关于WPF ListBox 虚拟化搞砸了显示的项目,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1882321/

相关文章:

wpf - 为什么 Visual Studio 2010 在 VMware 中会出现渲染问题?

c# - 在 C++ 背景下迁移到 WPF 和 C# 的资源

c# - 如何单次同步两个列表?

.net - 如何在 ListBox 的第一项上捕获 keyup 事件?

docker - docker在Linux机器上运行时是否使用虚拟化解决方案?

c# - 编辑 DataGridTextColumn 时在当前行上执行方法

wpf - 如何在 UIElement 上重置 DesiredSize

cryptography - 虚拟化对加密强度高的随机数生成器有什么影响?

linux - 如何压缩或写入 zero's/dev/zero 到交换文件?

wpf - 带有命令绑定(bind)的 KeyBinding 不适用于 TextBox UpdateSourceTrigger LostFocus