Share via


Windows Phone Mango change, Listbox: How to detect compression(end of scroll) states ?

[Update: This is available after Beta2(Trial) build. Wait for RC/RTM builds]

If you’ve ever used a ListBox or ScrollViewer control and wanted to implement “infinite scrolling”, great news: in Mango you can easily detect both the end of scroll and compression states.

Infinite scrolling is a nice concept used when the data for a Listbox control is fetched from a server. Many web services let you fetch the data in chunks by specifying a starting location and number of records to retrieve in order to implement paging

A lot of web services are designed for a client to fetch ~40 items. Then, as the end user scrolls to the bottom of the list, client code detects that the end of the scrolling region has been reached (when the list compresses), and then fetches the next 40 odd items from the server.

Obviousy it isn’t a true infinite scrolling list – memory concerns can prop up, but it’ll feel infinite to most users if you implement a reasonable maximum.

How to detect the end of scrolling

On Windows Phone 7.0, the developer community came up with a bunch of hacks that worked in some situations, but we wanted to really offer this functionality in the platform for Mango and feel that it’s a much cleaner implementation as a result. Less hacking required!

So in Mango, throw away your old code – we’ve made this very easy and 100% reliable!

The attached sample shows all the various events that can be hooked to when scrolling on a WP7.1 application.

image

We addressed this problem by introducing two new VisualStateGroups: HorizontalCompression and VerticalCompression.

The default style for the ScrollViewer does not include these new visual states – if you’re using them in your app, make sure to add a style to the app’s resources to expose these new VisualStateGroups.

Here’s the style:

    <Style TargetType="ScrollViewer">
<Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
<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 Storyboard.TargetName="VerticalScrollBar"
Storyboard.TargetProperty="Opacity" To="1" Duration="0"/>
<DoubleAnimation Storyboard.TargetName="HorizontalScrollBar"
Storyboard.TargetProperty="Opacity" To="1" Duration="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="NotScrolling">
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="VerticalCompression">
<VisualState x:Name="NoVerticalCompression"/>
<VisualState x:Name="CompressionTop"/>
<VisualState x:Name="CompressionBottom"/>
</VisualStateGroup>
<VisualStateGroup x:Name="HorizontalCompression">
<VisualState x:Name="NoHorizontalCompression"/>
<VisualState x:Name="CompressionLeft"/>
<VisualState x:Name="CompressionRight"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid Margin="{TemplateBinding Padding}">
<ScrollContentPresenter x:Name="ScrollContentPresenter" Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"/>
<ScrollBar x:Name="VerticalScrollBar" IsHitTestVisible="False" Height="Auto" Width="5"
HorizontalAlignment="Right" VerticalAlignment="Stretch" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
IsTabStop="False" Maximum="{TemplateBinding ScrollableHeight}" Minimum="0" Value="{TemplateBinding VerticalOffset}"
Orientation="Vertical" ViewportSize="{TemplateBinding ViewportHeight}" />
<ScrollBar x:Name="HorizontalScrollBar" IsHitTestVisible="False" Width="Auto" Height="5"
HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
IsTabStop="False" Maximum="{TemplateBinding ScrollableWidth}" Minimum="0" Value="{TemplateBinding HorizontalOffset}"
Orientation="Horizontal" ViewportSize="{TemplateBinding ViewportWidth}" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

If you compare this with the default style for ScrollViewer, you’ll see it’s really just the original PLUS the two additional VisualStateGroups.

As a reminder, the default style for each control can be found within the System.Windows.xaml file in the following location on a 64-bit machine: C:\Program Files (x86)\Microsoft SDKs\Windows Phone\v7.1\Design\System.Windows.xaml

So the two additional VisualStateGroups are:

                                <VisualStateGroup x:Name="VerticalCompression">
<VisualState x:Name="NoVerticalCompression"/>
<VisualState x:Name="CompressionTop"/>
<VisualState x:Name="CompressionBottom"/>
</VisualStateGroup>
<VisualStateGroup x:Name="HorizontalCompression">
<VisualState x:Name="NoHorizontalCompression"/>
<VisualState x:Name="CompressionLeft"/>
<VisualState x:Name="CompressionRight"/>
</VisualStateGroup>

The names are self explanatory; “CompressionLeft” means that the compression animation is happening on the ScrollViewer content towards the left side of the viewport.

There are eight combinations coming out of this: top, bottom, left, right, top-left (45 degree angle compression), top-right, bottom-left and bottom-right.

After overriding the default style, you can listen to the CurrentStateChanging event on the corresponding VisualStateGroup element.

To get to the ScrollViewer’s visual state group you need following (building on top of the standard WP7 project template):

private void MainPage_Loaded(objectsender, RoutedEventArgs e)
     {
         if(!App.ViewModel.IsDataLoaded)
         {
             App.ViewModel.LoadData();
         }

         if(alreadyHookedScrollEvents)
             return;

         alreadyHookedScrollEvents = true;
         MainListBox.AddHandler(ListBox.ManipulationCompletedEvent, (EventHandler<ManipulationCompletedEventArgs>)LB_ManipulationCompleted, true);
         sb = (ScrollBar)FindElementRecursive(MainListBox, typeof(ScrollBar));
         sv = (ScrollViewer)FindElementRecursive(MainListBox, typeof(ScrollViewer));

         if(sv != null)
         {
             // Visual States are always on the first child of the control template
            FrameworkElement element = VisualTreeHelper.GetChild(sv, 0) asFrameworkElement;
             if(element != null)
             {
                 VisualStateGroup group = FindVisualState(element, "ScrollStates");
                 if(group != null)
                 {
                     group.CurrentStateChanging += newEventHandler<VisualStateChangedEventArgs>(group_CurrentStateChanging);
                 }
                 VisualStateGroup vgroup = FindVisualState(element, "VerticalCompression");
                 VisualStateGroup hgroup = FindVisualState(element, "HorizontalCompression");
                 if(vgroup != null)
                 {
                     vgroup.CurrentStateChanging += newEventHandler<VisualStateChangedEventArgs>(vgroup_CurrentStateChanging);
                 }
                 if(hgroup != null)
                 {
                     hgroup.CurrentStateChanging += newEventHandler<VisualStateChangedEventArgs>(hgroup_CurrentStateChanging);
                 }
             }
         }          

     }

  private UIElement FindElementRecursive(FrameworkElement parent, Type targetType)
       {
           int childCount = VisualTreeHelper.GetChildrenCount(parent);
           UIElement returnElement = null;
           if (childCount > 0)
           {
               for (int i = 0; i < childCount; i++)
               {
                   Object element = VisualTreeHelper.GetChild(parent, i);
                   if (element.GetType() == targetType)
                   {
                       return element as UIElement;
                   }
                   else
                   {
                       returnElement = FindElementRecursive(VisualTreeHelper.GetChild(parent, i) as FrameworkElement, targetType);
                   }
               }
           }
           return returnElement;
       }

       private VisualStateGroup FindVisualState(FrameworkElement element, string name)
       {
           if (element == null)
               return null;

           IList groups = VisualStateManager.GetVisualStateGroups(element);
           foreach (VisualStateGroup group in groups)
               if (group.Name == name)
                   return group;

           return null;
       }

This will be pretty easy to follow along with if you download and run the sample app. Let us know if this helps you complete your scrolling-related scenarios.

Other important points

  • VerticalCompression and HorizontalCompression VisualStateGroups are available ONLY for 7.1
  • Both are available ONLY for ManipulationMode = System,
  • These groups are not part of the default style, but are available to use.
  • You can read about Control Syles and Templates in detail here

Thanks !

SLMPerf

ListBoxVisualStatesDemo.zip

Comments

  • Anonymous
    July 02, 2011
    not working on wp 7.1 beta 2 (phone is runing the mango firmware)

  • Anonymous
    July 03, 2011
    Is VerticalCompression and HorizontalCompression Visual StateGroups going to be incorporated in 7.1Beta2 ? or will their be new templates available for scrolling-related scenarios?

  • Anonymous
    July 03, 2011
    @Anon: These new VisualStateGroups are not incorporated as part of WP7.1 beta2 emulator, but works on latest mango mainline device build. Try the attached sample as soon as you get your hands on the Mango device update. Thank you for raising the issue. @Dale: Change mentioned in this post is the only change to ScrollViewer template. As mentioned, the new visual state groups wont be part of the default template, you will need to add those.

  • Anonymous
    July 04, 2011
    I have 7661.WP7_5_Trial (the one that was sent out to developers last week), and it does not work. Do I need a newer one?

  • Anonymous
    July 05, 2011
    @Anon: AFAIK, Beta2 = trial builds = the latest public released build, doesnt not have this change. Updated the post: this will be available after Beta2, wait for RC/RTM builds.

  • Anonymous
    July 05, 2011
    This makes sense now, thanks.

  • Anonymous
    September 14, 2011
    If I have the 7.1 SDK and deploy the resulting app on 7.0, will this work?  If not, what do you recommend for backward compatibility?

  • Anonymous
    March 18, 2012
    Is that possible to change Compression's value? I mean the way i hit the top/end of my listBox (heavy/light).

  • Anonymous
    May 28, 2012
    I'm not using a ListBox, but just a ScrollViewer with a number of elements in it.  When I pass the ScrollViewer to this call: FrameworkElement element = VisualTreeHelper.GetChild(sv, 0) asFrameworkElement; I get an "ArgumentOutOfRangeException": {"Specified argument was out of the range of valid values.rnParameter name: childIndex"}

  • Anonymous
    July 18, 2012
    I've got the same problem as punit. I can't get it to work when using a ScrollViewer element, instead of a scrollable ListBox. Any idea on how to fix that?

  • Anonymous
    September 02, 2012
    Thanks for this excellent post. I converted the code to vb for use in my project, and posted the adapted code. ericsgems.wordpress.com/.../daily-gem-vb-silverlight-wp7-5-infinite-scrolling-lists

  • Anonymous
    August 19, 2013
    Hi Guys, I am finding this really hard with templated controls like for example I have Panorama which has templated items, and some templates have lists in them. Please let me know if I need to be clearer. Thanks Ronak