Margin on ItemsControl of virtualizing ListBox not

2019-04-21 16:25发布

I have a problem with a class which extends ListBox in Windows Phone 7 Silverlight. The idea is to have a full ScrollViewer (black, e.g. fills the whole phone screen) and that the ItemsPresenter (red) has a margin (green). This is used to have a margin around the whole list but the scroll bars begin in the top right edge and end in the bottom right edge of the dark rectangle:

enter image description here

The problem is, that the ScrollViewer can't scroll to the very end, it cuts 50 pixels off of the last element in the list. If I use StackPanel instead of VirtualizingStackPanel the margins are correct BUT the list is no longer virtualizing.

Thanks for any ideas, I've tried a lot but nothing is working. Is this a control bug?

SOLUTION: Use the InnerMargin property of the ExtendedListBox control from the MyToolkit library!

C#:

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

XAML (e.g. 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> 

Update 1

I created a simple sample app: The scrollbar can't scroll at the end... If you change the VirtualizingStackPanel to StackPanel in App.xaml and it works as expected but without virtualization

SampleApp.zip

Update 2 Added some sample pictures. Scrollbars are blue to show their position.

Expected results (use StackPanel instead of VirtualizingStackPanel):

Correct_01: Scrollbar at top

enter image description here

Correct_01: Scrollbar at middle

enter image description here

Correct_01: Scrollbar at bottom

enter image description here

Wrong examples:

Wrong_01: Margin always visible (example: scroll position middle)

enter image description here

Only solution is to add a dummy element at the end of the list to compensate the margin. I'll try to add this dummy element dynamically inside the control logic... Add some logic into the bound ObservableCollection or the view model is no option.

UPDATE: I added my final solution as a separate answer. Checkout the ExtendedListBox class.

4条回答
放我归山
2楼-- · 2019-04-21 16:39

My current solution: Always change the margin of the last element of the list...

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; 
        }
    }
}

Using this "hack" to change the margin of the last list item on the fly (no need to add something to the bound list) I developed this final control:

(The listbox has a new event for PrepareContainerForItem, a property and event for IsScrolling (there is also an extended LowProfileImageLoader with IsSuspended property, which can be set in the IsScrolling event to improve scrolling smoothness...) and the new property InnerMargin for the described problem...

Update: Checkout the ExtendedListBox class of my MyToolkit library which provides the solution described here...

查看更多
聊天终结者
3楼-- · 2019-04-21 16:46

I think what would be an easier way instead of messing with styles is -

First, you don't need top and bottom margins as you shouldn't have Horizontal scrollbars anyway. You can just add these two margins to your listbox directly.

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

Then, to have a little gap (i.e. your left and right margin) between the listbox items and the scrollbar, you just need to set a left and right margin of 50 in your ItemContainerStyle.

            <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>

UPDATE (Performance Impact!)

Okay, please keep all your existing code, and then add this line to the ScrollViewer inside your custom listbox style.

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

It seems setting ManipulationMode="Control" (default one is "System") has fixed your problem, however, doing this might cause a worse performance of the ScrollViewer, please take a look at this post. I think it is a bug.

Do you load a lot of data into this listbox? You really need to test the performance on an actual phone. If the scrolling is smooth I think it could be a way to go, if not let me know I will try to think of something else...

查看更多
Deceive 欺骗
4楼-- · 2019-04-21 16:53

Setting margins on ItemsPresenter (or any child of a ScrollViewer) breaks the internal logic of ScrollViewer. Try setting the same value as Padding on the ScrollViewer, i.e.:

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

Update: (after looking at the attached project)

In ScrollViewer's template. The binding for the Padding property was set on the main grid of the control and not on the ScrollContentPresenter as it's done in WPF\silverlight. This made the scroll bar's position to be affected by setting the padding property. In effect, on the ScrollViewer, setting Padding is equivalent to setting Margin. (Microsoft, why changing templates for the worst!? Was it intentional?).

Anyway, add this style before the style of the list box in 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>

And add some changes to the style of the list box:

  1. Add setter for Padding on the list box: <Setter Property="Padding" Value="30,50" />
  2. To the scroll viewer in the template, add Padding="{TemplateBinding Padding}" and Style="{StaticResource ScrollViewerStyle1}".
  3. Remove margin assignment on ItemsPresenter.

This introduces another buggy behavior: the scroll bar is not scrolling till the bottom of the screen. Not a major issue compared to clipping the last item, but annoying nonetheless.

查看更多
放荡不羁爱自由
5楼-- · 2019-04-21 16:55

What I usually do when I want to have padding in a ListBox, so for example I can make it occupy the entire screen even the part under a transparent ApplicationBar, but still be able to access the last item in the ListBox - I use a DataTemplateSelector (http://compositewpf.codeplex.com/SourceControl/changeset/view/52595#1024547) and define one (or more) templates for regular items and also a template for a PaddingViewModel that defines a certain height. Then - I make sure my ItemsSource is a collection that has that PaddingViewModel as the last item. Then my padding DataTemplate adds the padding at the end of the list and I can also have ListBox items with different templates.

<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>

One more thing you have to note - there are some bugs in the ListBox/VirtualizingStackPanel that when your items are not of consistent height - you might sometimes not see the bottom items in the ListBox and need to scroll up and down to fix it. (http://social.msdn.microsoft.com/Forums/ar/windowsphone7series/thread/58bead85-4324-411c-988f-fadb983b14a7)

查看更多
登录 后发表回答