Pull-down-to-refresh a WP7 ListBox or ScrollViewer


Update 12/19/2011: Updated the attached demo project to target the WP 7.1 (Mango) SDK. It still requires setting ManipulationMode=Control on the ScrollViewer.

I really like the pull-down-and-release-to-refresh gesture seen in various smartphone apps. In my opinion it doesn’t conflict with the Metro UI guidelines, so I don’t see any reason why it shouldn’t be used in a WP7 app. Here is an explanation of how it can be done in WP7 without much difficulty. If you just want the complete source code, jump to the end to download the source along with a demo project.

The WP7 ScrollViewer control has a built-in stretchy feel to it if you try to pull it down from the top or up from the bottom, so it is already well-suited to this kind of interaction. What we need to do is detect when and how far the ScrollViewer is pulled down from the top, when it is released from that point, and also reveal an indicator control “behind” the ScrollViewer to provide visual feedback as it is pulled down. And since a ListBox uses a ScrollViewer internally to scroll its items, the same solution will work just as well with a ListBox.

Detecting a pull-down motion

The ScrollViewer class does not directly publish any events about its scrolling motion. So, how can we know when the ScrollViewer is being “pulled down”? With a little experimentation, I found that the ScrollViewer applies a CompositeTransform to its Content for vertical translation (and subtle compression) when the contents are dragged down from the top or up from the bottom. But for normal scrolling through the length of the list, the CompositeTransform is just equivalent to an identity transform (0 translation, 1.0 scale). This makes it very easy to know when and how far the ScrollViewer is pulled down from the top: just look for a positive Y translation on the RenderTransform of the content.

Caution: This technique relies on undocumented internal behavior of the ScrollViewer. It’s possible that a future revision of the ScrollViewer could change its internal scroll rendering in a way that breaks this kind of pull-to-refresh code. I even hesitate to blog this for that reason, however in my estimation the risk is low, and can be mitigated in a couple ways: Any code depending on the current scroll rendering behavior should degrade gracefully (don’t crash when something expected is not found), and the app should provide some alternate means of refreshing the scrolled contents such as a context menu etc. I have no official knowledge on this subject, and I can’t even guarantee that an app that implements this gesture will pass the marketplace review process. So let me know how it works out!

Moving beyond the disclaimer, here’s a snippet of code showing how to handle a ScrollViewer’s mouse-move event to detect a pull-down gesture.

void targetScrollViewer_MouseMove(object sender, MouseEventArgs e)
{
UIElement scrollContent = (UIElement)this.targetScrollViewer.Content;
CompositeTransform ct = scrollContent.RenderTransform as CompositeTransform;
if (ct != null)
{
if (ct.TranslateY > this.PullThreshold)
{
// Show the ready-to-release indicator
}
else if (ct.TranslateY > 0)
{
// Show the pull-down indicator
}
else
{
// Hide the indicators
}
}
}

Detecting a release is very similar: handle a mouse-up event and check whether the Y translation at the time of release is greater than the minimum distance.

Revealing a pull-down indicator

There are a few ways to reveal another control as the ScrollViewer is pulled down. The easiest method is to layout the control directly behind the ScrollViewer, and make the ScrollViewer contents opaque so that the control is normally covered. But I wanted to allow for a transparent background on scrolled items, as is very typical. So, the solution I settled on has a control that is initially 0-height and has its height dynamically adjusted as the ScrollViewer is pulled down. To implement this, I created a PullDistance DependencyProperty that gets dynamically updated by the mouse-move event handler, and bound the Height property of the pulling-down indicator to that. For additional visual effect, I created a similar PullFraction property and bound it to the indicator’s Opacity, so that the indicator fades in proportional to the pull-down.

The full XAML is too verbose to post inline, but here’s a small snippet from the control template that shows the bindings mentioned above.

<StackPanel x:Name="PullingDownPanel"
Height="{TemplateBinding PullDistance}" Opacity="{TemplateBinding PullFraction}"
Margin="{Binding PullDistance, RelativeSource={RelativeSource TemplatedParent},
Converter={StaticResource NegativeValueConverter}, ConverterParameter=Bottom}"
>
<ContentPresenter ContentTemplate="{TemplateBinding PullingDownTemplate}" />
</StackPanel>

Note the Margin binding, which is a bit tricky. The purpose of that is to apply a negative bottom margin opposite to the height, in order to keep the layout height of the control at 0. The custom value-converter class converts a double value into a Thickness object with negative margin of the same magnitude. Without it, the entire ScrollViewer would get “pushed” down as the control before it was enlarged.

Putting it all together

With the two tricky parts done, the rest is pretty typical for building custom controls. I’m not going to walk though the details here, as the concepts are covered well in other places. But here’s a brief list of what else went into my PullDownToRefreshPanel custom control:

  • A RefreshRequested event to signal the app when the user has completed the refresh gesture
  • DataTemplate properties for each of the three indicators (pulling down, ready to release, refreshing) to allow customizing those visuals without needing to re-template the whole control
  • Visual-state definitions in the control template XAML to control the visibility of each of the templates according to the current visual state
  • An IsRefreshing property to allow the app to easily enter and exit the refreshing visual state (which shows an indeterminate progress bar by default)
  • Automatic detection of a nearby ScrollViewer or ListBox — just put the PullDownToRefreshPanel in the same parent container and it will hook itself up

Demo

For demonstration purposes, I’ve added the PullDownToRefreshPanel code to my ReorderListBox demo app. It should work just as well with any other ListBox or ScrollPanel. Sorry, it won’t work with a LongListSelector — that control does custom scrolling without using a ScrollViewer. I’ve attached to this post a ZIP containing source code for both controls and the demo project.

ReorderListBox-WP71.zip

Comments (9)

  1. Kornelis says:

    Hi Jason,

    this is really great Work :))

    This is exactly what i was lookin for.

    But I found a small "issues".

    On "targetScrollViewer_MouseLeftButtonUp" you set the IsRefreshing property after the event was fired.

    This causes the effect, that if you set IsRefreshing directly to false within the EventHandler it will never finish refreshing.

    Btw. is there somehing like a naming convention for the Themes/Generic.xaml?

    I tried to move the code to my own styles.xaml where i store all my styles and templates but the the PDTR-Control didn't work anymore.

    Cheers

    Kornelis

  2. I think you're right about the IsRefreshing state. Actually my intent had been that the event-handler should set IsRefreshing = true if desired (only if refreshing is kicked off as a background process), so line 309 should be deleted from targetScrollViewer_MouseLeftButtonUp.

    And yes, the WP7 control template mechanism requires the templates to be in Themes/Generic.xaml. (It is a little odd, since "themes" are not really supported.) Often this is hidden from the developer since custom controls are typically in a separate assembly, but I did not do that in this case.

  3. Nick says:

    Jason, have you tried this control with the Mango SDK – it appears that there is an implementation difference with the ListBox and the ScrollViewer that cause this control to not work. I've tried drilling into the code to try to work out why but get stuck somewhere in the MouseMove event handler – it appears that the RenderTransform is never a CompositeTransform. Any assistance would be appreciated in getting this to work.

  4. @Nick, Thnks for the issue report! See my update at the end of the post for the Mango fix.

    @Kornelis, I also fixed that IsRefreshing bug in the updated code.

  5. Jack says:

    @Jason, having an issue with list not scrolling while reordering items.  I realize this is a Mango only issue and I tried your suggestion ScrollViewer.ManipulationMode="Control" – but the list still will not scroll while reordering items.  Using final Mango SDK release.

  6. Jack says:

    I reworte some of the code within the routines that handle the shifting of the list at the top and bottom edges.  

    This has an added advantage of using the build in mango scrollviewer pulldown features (thanks for the MS link) and I don't need to set ManipulationMode = "Control" which means I'm also getting all the performance enhancements built into the Mango Listbox.

    I can't thank you enough for posting your orignal code, it has really helped me understand controls and animation effects within silverlight – something I was really struggling with.

  7. Chandra says:

    In my case, I am using stackpanel in scrollerviewer. I have applied same logic of "PullDownToRefreshPanel". But is not refreshing the stackpanel on devices, but its working fine on emulators.

    Please suggest me the solution.

  8. @Jason I'm using your pull-down-to-refresh for my application. It works great. However, when I use it for sometimes, it work incorrectly. When I pull down, it call refresh callback, but when the list is up, the text "Holding to refresh" of your pull-down-to-refresh doesnot hide, it overlaps with my listbox. Please help

  9. Alfa says:

    Hi, I have been trying to implement swipe left and swipe right on the reorder list. But have failed miserably. I'm looking for a list box which could drag and drop, and on which i can swipe left and swipe right. I have implemented the swipe left and swipe left on another project by attaching manipulation events to the listboxitem datatemplate in xaml code. Somehow even though I attach the manipulation events on the xaml it does not even get called..

    Thanks