虚拟化ListBox的ItemsControl边距无法正常工作

9
我在Windows Phone 7 Silverlight中扩展了一个ListBox类,但遇到了问题。想法是有一个完整的ScrollViewer(黑色,填满整个手机屏幕),并且ItemsPresenter(红色)有一个margin(绿色)。这用于在整个列表周围设置边距,但是滚动条始于黑色矩形的右上角,并在其右下角结束:

enter image description here

问题是,ScrollViewer无法滚动到最后,它会截断列表中最后一个元素的50个像素。如果我使用StackPanel代替VirtualizingStackPanel,则边距是正确的,但是列表不再虚拟化。

感谢任何想法,我尝试了很多但没有效果。这是控件的错误吗?

解决方案:使用MyToolkit库中的ExtendedListBox控件的InnerMargin属性!

C#:

public class MyListBox : ListBox
{
    public MyListBox()
    {
        DefaultStyleKey = typeof(MyListBox);
    }
}

XAML(例如App.xaml):

<Application 
    x:Class="MyApp.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"       
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone">

    <Application.Resources>
        <ResourceDictionary>
            <Style TargetType="local:MyListBox">
                <Setter Property="ItemsPanel">
                    <Setter.Value>
                        <ItemsPanelTemplate>
                            <VirtualizingStackPanel Orientation="Vertical" />
                        </ItemsPanelTemplate>
                    </Setter.Value>
                </Setter>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate>
                            <ScrollViewer>
                                <ItemsPresenter Margin="30,50,30,50" x:Name="itemsPresenter" />
                            </ScrollViewer>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
                <Setter Property="ItemContainerStyle">
                    <Setter.Value>
                        <Style TargetType="ListBoxItem">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate>
                                        <ContentPresenter HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </Setter.Value>
                </Setter>
            </Style>
        </ResourceDictionary>
    </Application.Resources>

    ...
</Application> 

更新1:

我创建了一个简单的示例应用程序:滚动条无法在末尾滚动... 如果你在App.xaml中将VirtualizingStackPanel更改为StackPanel,它将按预期工作,但没有虚拟化。

SampleApp.zip

更新2:

添加了一些示例图片。滚动条是蓝色的,以显示它们的位置。

预期结果(使用StackPanel而不是VirtualizingStackPanel):

正确_01:滚动条在顶部

enter image description here

正确_02:滚动条在中间

enter image description here

正确_03:滚动条在底部

enter image description here

错误示例:

错误_01:边距始终可见(例如:滚动位置中间)

enter image description here

唯一的解决方案是在列表末尾添加一个虚拟元素来补偿边距。我将尝试在控件逻辑中动态添加这个虚拟元素... 在绑定的ObservableCollection或视图模型中添加一些逻辑不是选项。

更新: 我将我的最终解决方案作为单独的答案添加了进去。 请查看ExtendedListBox类。

4个回答

2

我的当前解决方案:始终更改列表中最后一个元素的边距...

public Thickness InnerMargin
{
    get { return (Thickness)GetValue(InnerMarginProperty); }
    set { SetValue(InnerMarginProperty, value); }
}

public static readonly DependencyProperty InnerMarginProperty =
    DependencyProperty.Register("InnerMargin", typeof(Thickness),
    typeof(ExtendedListBox), new PropertyMetadata(new Thickness(), InnerMarginChanged));

private static void InnerMarginChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var box = (ExtendedListBox)d;
    if (box.lastElement != null)
        box.UpdateLastItemMargin();
    box.UpdateInnerMargin();
}

private void UpdateInnerMargin()
{
    if (scrollViewer != null)
    {
        var itemsPresenter = (ItemsPresenter)scrollViewer.Content;
        if (itemsPresenter != null)
            itemsPresenter.Margin = InnerMargin;
    }
}

private void UpdateLastItemMargin()
{
    lastElement.Margin = new Thickness(lastElementMargin.Left, lastElementMargin.Top, lastElementMargin.Right,
        lastElementMargin.Bottom + InnerMargin.Top + InnerMargin.Bottom);
}

private FrameworkElement lastElement = null;
private Thickness lastElementMargin;
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
    base.PrepareContainerForItemOverride(element, item);
    OnPrepareContainerForItem(new PrepareContainerForItemEventArgs(element, item));

    if ((InnerMargin.Top > 0.0 || InnerMargin.Bottom > 0.0))
    {
        if (Items.IndexOf(item) == Items.Count - 1) // is last element of list
        {
            if (lastElement != element) // margin not already set
            {
                if (lastElement != null)
                    lastElement.Margin = lastElementMargin;
                lastElement = (FrameworkElement)element;
                lastElementMargin = lastElement.Margin;
                UpdateLastItemMargin();
            }
        }
        else if (lastElement == element) // if last element is recycled it appears inside the list => reset margin
        {
            lastElement.Margin = lastElementMargin;
            lastElement = null; 
        }
    }
}

使用这个“hack”来实时改变最后一个列表项的边距(无需添加到绑定列表中)我开发了这个最终控件: (该列表框具有新的PrepareContainerForItem事件,IsScrolling属性和事件(还有一个扩展的LowProfileImageLoader,具有IsSuspended属性,可以在IsScrolling事件中设置以提高滚动平滑度...),以及用于描述问题的新属性InnerMargin... 更新:请查看我的MyToolkit库中的ExtendedListBox类,其中提供了本文所述的解决方案...

@OffBySome:有一些改动来改善代码。请查看答案中的更新代码或转到我的工具包链接... - Rico Suter

1

我认为与其纠结于样式,一个更简单的方法是 -

首先,你不需要顶部和底部边距,因为你不应该有水平滚动条。你可以直接将这两个边距添加到你的列表框中。

<local:MyListBox x:Name="MainListBox" ItemsSource="{Binding Items}" Margin="0,30">

接着,为了在列表框条目和滚动条之间留有一定的空隙(即左右边距),只需要在ItemContainerStyle中设置左右边距为50。

            <Setter Property="ItemContainerStyle">
                <Setter.Value>
                    <Style TargetType="ListBoxItem">
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate>
                                    <ContentPresenter HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="50,0"/>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </Setter.Value>
            </Setter>

更新(性能影响!)

好的,请保留您现有的所有代码,然后将此行添加到自定义列表框样式中的ScrollViewer

                        <ScrollViewer ScrollViewer.ManipulationMode="Control">
                            <ItemsPresenter Margin="30,50" x:Name="itemsPresenter" />
                        </ScrollViewer>

看起来设置ManipulationMode="Control"(默认为"System")已经解决了你的问题,但是这样做可能会导致ScrollViewer的性能更差,请参考this post。我认为这是一个bug。

你在这个列表框中加载了很多数据吗?你真的需要在实际手机上测试性能。如果滚动是流畅的,我认为这可能是一种方法,如果不是,请告诉我,我会尝试想出其他办法...


这不起作用,因为绿色边距在顶部和底部始终可见...请参见图片 Wrong_01。 - Rico Suter
我刚刚添加了另一个可能的解决方案,请查看并看看它是否适用于您。 - Justin XL
它是否虚拟化(尚未尝试)?我认为如果ItemsPresenter不直接位于ScrollViewer内,它就不再是虚拟化的了...我尝试将ItemsPresenter放在一个StackPanel中,并在StackPanel上设置边距:它看起来正确,但不再是虚拟化的了...(现在我有了一个可接受的解决方案,请参见问题结尾) - Rico Suter
啊,该死!我应该知道的。顺便问一下,你尝试过工具包中的LongListSelector吗? - Justin XL
LongListSelector不适用于我的应用程序。但感谢您的帮助。请随意使用最终类 :-) - Rico Suter
显示剩余4条评论

0
通常,当我想在 ListBox 中使用填充时,例如使其占据整个屏幕甚至是透明的 ApplicationBar 下方的部分,但仍然能够访问 ListBox 中的最后一项时,我会使用 DataTemplateSelector(http://compositewpf.codeplex.com/SourceControl/changeset/view/52595#1024547)并为常规项定义一个或多个模板以及一个定义特定高度的 PaddingViewModel 模板。然后,我确保我的 ItemsSource 是一个集合,其中具有 PaddingViewModel 作为最后一项。然后我的填充 DataTemplate 在列表末尾添加填充,我也可以拥有不同模板的 ListBox 项。
<ListBox.ItemTemplate>
    <DataTemplate>
        <local:DataTemplateSelector
            Content="{Binding}"
            HorizontalAlignment="Stretch"
            HorizontalContentAlignment="Stretch">
            <local:DataTemplateSelector.Resources>
                <DataTemplate
                    x:Key="ItemViewModel">
                    <!-- Your item template here -->
                </DataTemplate>
                <DataTemplate
                    x:Key="PaddingViewModel">
                    <Grid
                        Height="{Binding Height}" />
                </DataTemplate>
            </local:DataTemplateSelector.Resources>
        </local:DataTemplateSelector>
    </DataTemplate>
</ListBox.ItemTemplate>

还有一件事情需要注意 - 在 ListBox/VirtualizingStackPanel 中存在一些 bug,当你的项高度不一致时,你可能有时候看不到 ListBox 中的底部项,需要上下滚动来解决它。(http://social.msdn.microsoft.com/Forums/ar/windowsphone7series/thread/58bead85-4324-411c-988f-fadb983b14a7)


谢谢你的回答。我的问题的想法是找到一个解决方案来避免这种“hack”。我不想强迫我的控件客户端添加任何东西到视图模型中。目前,我正在使用与你相同的hack:我添加了一个带有缺失边距的边框控件来避免这个问题... - Rico Suter
我同意这不太理想。也许有更好的ListBox实现,没有这些限制。在简单情况下,我要么使用它,要么使用常规StackPanel。 - Filip Skakun

-2

ItemsPresenter(或任何ScrollViewer的子级)上设置边距会破坏ScrollViewer的内部逻辑。尝试在ScrollViewer上设置与填充相同的值,即:

<ScrollViewer Padding="30,50">
    ...
</ScrollViewer>

更新:(查看附加的项目后)

在 ScrollViewer 的模板中,Padding 属性的绑定设置在控件的主网格上,而不是像 WPF\silverlight 中那样设置在 ScrollContentPresenter 上。这使得滚动条的位置受到 Padding 属性的影响。实际上,在 ScrollViewer 上设置 Padding 相当于设置 Margin。(微软,为什么要把模板改得更糟糕?这是故意的吗?)

无论如何,在 App.xaml 中列表框的样式之前添加此样式:

<Style x:Key="ScrollViewerStyle1"
        TargetType="ScrollViewer">
    <Setter Property="VerticalScrollBarVisibility"
            Value="Auto" />
    <Setter Property="HorizontalScrollBarVisibility"
            Value="Disabled" />
    <Setter Property="Background"
            Value="Transparent" />
    <Setter Property="Padding"
            Value="0" />
    <Setter Property="BorderThickness"
            Value="0" />
    <Setter Property="BorderBrush"
            Value="Transparent" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ScrollViewer">
                <Border BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        Background="{TemplateBinding Background}">
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="ScrollStates">
                            <VisualStateGroup.Transitions>
                                <VisualTransition GeneratedDuration="00:00:00.5" />
                            </VisualStateGroup.Transitions>
                            <VisualState x:Name="Scrolling">
                                <Storyboard>
                                    <DoubleAnimation Duration="0"
                                                        To="1"
                                                        Storyboard.TargetProperty="Opacity"
                                                        Storyboard.TargetName="VerticalScrollBar" />
                                    <DoubleAnimation Duration="0"
                                                        To="1"
                                                        Storyboard.TargetProperty="Opacity"
                                                        Storyboard.TargetName="HorizontalScrollBar" />
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="NotScrolling" />
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Grid>
                        <ScrollContentPresenter x:Name="ScrollContentPresenter"
                                                Margin="{TemplateBinding Padding}"
                                                ContentTemplate="{TemplateBinding ContentTemplate}"
                                                Content="{TemplateBinding Content}" />
                        <ScrollBar x:Name="VerticalScrollBar"
                                    HorizontalAlignment="Right"
                                    Height="Auto"
                                    IsHitTestVisible="False"
                                    IsTabStop="False"
                                    Maximum="{TemplateBinding ScrollableHeight}"
                                    Minimum="0"
                                    Opacity="0"
                                    Orientation="Vertical"
                                    Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
                                    Value="{TemplateBinding VerticalOffset}"
                                    ViewportSize="{TemplateBinding ViewportHeight}"
                                    VerticalAlignment="Stretch"
                                    Width="5" />
                        <ScrollBar x:Name="HorizontalScrollBar"
                                    HorizontalAlignment="Stretch"
                                    Height="5"
                                    IsHitTestVisible="False"
                                    IsTabStop="False"
                                    Maximum="{TemplateBinding ScrollableWidth}"
                                    Minimum="0"
                                    Opacity="0"
                                    Orientation="Horizontal"
                                    Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
                                    Value="{TemplateBinding HorizontalOffset}"
                                    ViewportSize="{TemplateBinding ViewportWidth}"
                                    VerticalAlignment="Bottom"
                                    Width="Auto" />
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

并对列表框的样式进行一些更改:

  1. 为列表框添加填充设置器:<Setter Property="Padding" Value="30,50" />
  2. 在模板中的滚动查看器中添加 Padding="{TemplateBinding Padding}"Style="{StaticResource ScrollViewerStyle1}"
  3. 删除 ItemsPresenter 上的边距分配。

这会引入另一个错误行为:滚动条无法滚动到屏幕底部。与裁剪最后一个项目相比不是一个主要问题,但仍然很烦人。


不行,滚动条在里面...这和<ScrollViewer Margin="30,50">是一样的...我已经试过了... - Rico Suter
你能附上一个问题的屏幕截图吗? - XAMeLi
不行,这个不起作用。看起来像是Wrong_01。边距必须围绕所有项。在ListBox或ScrollViewer上设置边距或填充不起作用,因为这会引入一个始终可见的边距。我只需要在所有项的顶部元素和最后一个元素处设置边距... - Rico Suter

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