问题
我们需要在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>
答案 0 :(得分:3)
我花了更多的时间尝试这个,而不是我应该拥有的,并且无法让它发挥作用。我理解这里发生了什么,但在纯XAML中,我无法弄清楚如何解决这个问题。我想我看到如何解决问题,但它涉及转换器。
警告:当我解释我的结论时,情况会变得复杂。
根本问题来自于控件的宽度拉伸到其容器的宽度。启用虚拟化后,宽度不会更改。在ScrollViewer
内的基础ListBox
内,ViewportWidth
属性对应于您看到的宽度。当另一个控件进一步伸展(您选择它)时,ViewportWidth
仍然相同,但ExtentWidth
显示全宽。将所有控件的宽度绑定到ExtentWidth
的宽度应该有效...
但事实并非如此。我将FontSize设置为100,以便在我的情况下更快地进行测试。选择项目后,ExtentWidth="4109.13
。沿着树走到ControlTemplate的Border
,我看到ActualWidth="4107.13"
。为什么2像素差异? ListBoxItem包含一个带有2像素填充的边框,导致ContentPresenter渲染得更小。
我在help from here添加了以下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像素填充,控件将在无限循环中调整大小,填充向ExtentWidth
添加2个像素,这会调整边框宽度,这会为{{增加2个像素1}}等等,直到你删除代码并刷新。
如果您添加了从ExtentWidth中减去2的转换器,我认为这可能会有效。但是,当滚动条不存在(您没有选择任何内容)时,ExtentWidth
。因此,绑定到ExtentWidth="0"
而不是MinWidth
可能会更好,因此当没有滚动条可见时,项目会正确显示:
Width
更好的解决方案如果您可以直接数据绑定MinWidth="{Binding ElementName=scrollViewer, Path=ExtentWidth, Converter={StaticResource PaddingSubtractor}}"
本身的MinWidth
。您可以直接绑定到ExtentWidth,并且不需要转换器。但是我不知道如何访问该项目。
编辑:为了组织起见,这是执行此操作所需的剪辑。使其他一切变得不必要:
ListBoxItem
答案 1 :(得分: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,这将使水平滚动条显示,如预期的那样。 新问题是,如果将Slider向左拖动以减少FontSize ,水平滚动条永远不会消失。
有什么想法吗?