9

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.

Rico Suter
  • 11,548
  • 6
  • 67
  • 93

4 Answers4

2

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

Rico Suter
  • 11,548
  • 6
  • 67
  • 93
  • @OffBySome: There were some changes to improve the code. See updated code in answer or go to mytoolkit links... – Rico Suter Mar 17 '12 at 16:54
1

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

Justin XL
  • 38,763
  • 7
  • 88
  • 133
  • This does not work, because the green margin on top and bottom is always visible... See picture Wrong_01 – Rico Suter Nov 10 '11 at 13:26
  • Yes, it seems that this is working. I have to test if this impacts performance... – Rico Suter Nov 10 '11 at 14:22
  • Yeah definitely, let me know how it goes. – Justin XL Nov 10 '11 at 14:26
  • Performance is very worse. This is not a solution for me, maybe for simple lists, but for a generic listbox control it's not applicable... My list has very complex items inside and the scrolling is not smooth and even jumps back (very strange behaviour...). But thanks for your effort... – Rico Suter Nov 10 '11 at 14:39
  • I have just added another possible solution, please check it out and see if it works for you. – Justin XL Nov 10 '11 at 23:00
  • Is it virtualizing (haven't tried it)? I think that if `ItemsPresenter` is not directly inside the `ScrollViewer`, it is not virtualizing any more... I tried to put the `ItemsPresenter` in a `StackPanel` and set the margin on the `StackPanel`: It looked correct but was not virtualizing any more... (I have now an acceptable solution, see question end) – Rico Suter Nov 10 '11 at 23:05
  • ahh, damn! i shoulda known. btw, have you tried the LongListSelector from the toolkit? – Justin XL Nov 10 '11 at 23:17
  • LongListSelector is not an option for my app.. But thanks for your help. Feel free to use the final classes :-) – Rico Suter Nov 10 '11 at 23:19
  • Hehe couldn't really help and hopefully this issue can be addressed in the future. :) – Justin XL Nov 11 '11 at 00:30
0

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)

Filip Skakun
  • 31,624
  • 6
  • 74
  • 100
  • Thanks for your answer. The idea of my question is to find a solution to avoid this "hack". I don't want to force the client of my control to add something into the view model. At the moment I'm using the same hack as you: I'm adding a border control with the missing margin to avoid the problem... – Rico Suter Nov 10 '11 at 12:55
  • I agree this is less than ideal. Maybe there are better ListBox implementations out there that do not have this limitations. I either use this or use a regular StackPanel in simpler cases. – Filip Skakun Nov 10 '11 at 17:56
-2

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.

Rico Suter
  • 11,548
  • 6
  • 67
  • 93
XAMeLi
  • 6,189
  • 2
  • 22
  • 29
  • No. This doesn't work... The scrollbars are inside... It's the very same as ``... I already tried this... – Rico Suter Nov 07 '11 at 17:18
  • Can you attach a screen shot with the problem? – XAMeLi Nov 09 '11 at 19:44
  • No. This doesn't work. Looks like Wrong_01. The margin must surround around all items. Setting a margin or padding on the ListBox or ScrollViewer will not work, because this will introduce an always visible margin. I need a margin only at the top element and last element of all items... – Rico Suter Nov 10 '11 at 13:53