关于虚拟化ListBox的ItemsControl的保证金不能正常工作

时间:2011-11-04 12:04:35

标签: silverlight windows-phone-7 xaml windows-phone-7.1

我在Windows Phone 7 Silverlight中扩展ListBox的类有问题。我的想法是拥有一个完整的ScrollViewer(黑色,例如填充整个手机屏幕),ItemsPresenter(红色)有一个边距(绿色)。这用于在整个列表周围有一个边距,但滚动条从右上边缘开始,到黑暗矩形的右下边缘结束:

enter image description here

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

感谢任何想法,我已经尝试了很多,但没有任何工作。这是一个控制错误吗?

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

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并且它按预期工作但没有虚拟化< / p>

SampleApp.zip

更新2 添加了一些示例图片。滚动条呈蓝色显示其位置。

预期结果(使用StackPanel代替VirtualizingStackPanel):

Correct_01:顶部的滚动条

enter image description here

Correct_01:中间的滚动条

enter image description here

Correct_01:底部的滚动条

enter image description here

错误的例子:

Wrong_01:保证金始终可见(例如:滚动位置中间)

enter image description here

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

更新:我将最终解决方案添加为单独的答案。 查看ExtendedListBox课程。

4 个答案:

答案 0 :(得分: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类,该库提供此处描述的解决方案...

答案 1 :(得分: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"(默认设置为“系统”)修复了您的问题,但这样做可能会导致ScrollViewer性能下降,请查看{{3} }。我认为这是一个错误。

您是否将大量数据加载到此列表框中?你真的需要在实际手机上测试性能。如果滚动顺畅,我认为这可能是一种方法,如果不是让我知道我会尝试去想别的......

答案 2 :(得分: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中存在一些错误,当您的项目高度不一致时 - 有时您可能看不到ListBox中的底部项目,需要向上和向下滚动以修复它。 (http://social.msdn.microsoft.com/Forums/ar/windowsphone7series/thread/58bead85-4324-411c-988f-fadb983b14a7

答案 3 :(得分:-2)

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

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

更新:(查看附件后)

在ScrollViewer的模板中。 Padding属性的绑定是在控件的主网格上设置的,而不是在ScrollContentPresenter上设置,因为它在WPF \ silverlight中完成。这使得滚动条的位置受到设置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:<Setter Property="Padding" Value="30,50" />
  2. 要模板中的滚动查看器,请添加Padding="{TemplateBinding Padding}"Style="{StaticResource ScrollViewerStyle1}"
  3. 删除ItemsPresenter上的保证金分配。
  4. 这引入了另一个错误行为:滚动条不滚动直到屏幕底部。与削减最后一项相比,这不是一个主要问题,但仍然很烦人。