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