【问题标题】:Margin on ItemsControl of virtualizing ListBox not working properly虚拟化 ListBox 的 ItemsControl 的边距无法正常工作
【发布时间】:2023-03-16 09:01:01
【问题描述】:

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

问题是,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):

Correct_01:顶部的滚动条

Correct_01:中间的滚动条

Correct_01:底部滚动条

错误示例:

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

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

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

【问题讨论】:

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


    【解决方案1】:

    我目前的解决方案:总是改变列表最后一个元素的边距...

    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的属性和事件(还有一个扩展的LowProfileImageLoaderIsSuspended属性,可以在IsScrolling事件中设置改进滚动平滑度...)和用于描述问题的新属性InnerMargin...

    更新:查看我的 MyToolkit 库的 ExtendedListBox 类,它提供了此处描述的解决方案...

    【讨论】:

    • @OffBySome:为了改进代码进行了一些更改。在答案中查看更新的代码或转到 mytoolkit 链接...
    【解决方案2】:

    我认为比乱码样式更简单的方法是 -

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

    <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 的性能变差,请查看this post。我认为这是一个错误。

    您是否将大量数据加载到此列表框中?您确实需要在实际手机上测试性能。如果滚动流畅,我认为这可能是一种方法,如果不让我知道,我会尝试考虑其他事情......

    【讨论】:

    • 这不起作用,因为顶部和底部的绿色边距始终可见...参见图片 Wrong_01
    • 是的,这似乎可行。我必须测试这是否会影响性能...
    • 是的,请告诉我进展如何。
    • 性能很差。这对我来说不是一个解决方案,也许对于简单的列表,但对于一个通用的列表框控件它不适用......我的列表里面有非常复杂的项目,滚动不流畅,甚至跳回(非常奇怪的行为......) .但是感谢您的努力...
    • 我刚刚添加了另一种可能的解决方案,请检查一下,看看它是否适合您。
    【解决方案3】:

    当我想在 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)

    【讨论】:

    • 感谢您的回答。我的问题的想法是找到避免这种“黑客攻击”的解决方案。我不想强迫我的控件的客户端在视图模型中添加一些东西。目前我正在使用与您相同的技巧:我正在添加一个缺少边距的边框控件以避免问题...
    • 我同意这不太理想。也许有更好的 ListBox 实现没有这个限制。在更简单的情况下,我要么使用它,要么使用常规的 StackPanel。
    【解决方案4】:

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

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

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

    在 ScrollViewer 的模板中。 Padding 属性的绑定是在控件的主网格上设置的,而不是在 ScrollContentPresenter 上设置的,因为它在 WPF\silverlight 中完成。这使得滚动条的位置受到设置填充属性的影响。实际上,在 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. 为列表框添加填充设置器:&lt;Setter Property="Padding" Value="30,50" /&gt;
    2. 在模板中的滚动查看器中,添加Padding="{TemplateBinding Padding}"Style="{StaticResource ScrollViewerStyle1}"
    3. 删除ItemsPresenter 上的边距分配。

    这引入了另一个错误行为:滚动条不会滚动到屏幕底部。与剪裁最后一项相比,这不是一个大问题,但还是很烦人。

    【讨论】:

    • 没有。这不行……滚动条在里面……和&lt;ScrollViewer Margin="30,50"&gt;一样……我已经试过了……
    • 你能附上这个问题的屏幕截图吗?
    • 没有。这行不通。看起来像 Wrong_01。边距必须围绕所有项目。在 ListBox 或 ScrollViewer 上设置边距或填充将不起作用,因为这将引入始终可见的边距。我只需要在所有项目的顶部元素和最后一个元素处留有边距...
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-12
    • 2016-02-23
    • 1970-01-01
    相关资源
    最近更新 更多