Click here to Skip to main content
15,878,809 members
Articles / Operating Systems / Win10

Pull-to-refresh on a Windows 10 UWP

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
29 Oct 2015CPOL4 min read 20.3K   1   5
Pull-to-refresh on a Windows 10 UWP

If you’ve ever tried to pull this off, you’ve likely either:

  1. pulled your hair out then drank ruthlessly in celebration or 
  2. researched what it takes and said “yeah… looks like we’re going with a button”

I was the same way.

While perusing through NuGet yesterday, I found a (relatively) new project, PullToRefresh.UWP. “WHAT?!” I thought and immediately hit the project page link. Unfortunately, the entire site is in Chinese (though Internet Explorer does a decent job of translating w/ the Bing Bar) so I thought I’d write this to help out other devs that might be trying to use it, and show how I threw it in to my latest UWP.

Obviously, first things first:

nuget install PullToRefresh.UWP

Once you’ve done that, it’s time to integrate! As the project page shows with its XAML example, the most basic implementation looks like:

HTML
<pr:PullToRefreshBox x:Name="pr" 
RefreshInvoked="PullToRefreshBox_RefreshInvoked">
    <ListView x:Name="lv" 
    ItemTemplate="{StaticResource ColorfulRectangle}" />
</pr:PullToRefreshBox>

But let’s break this apart into the parts that matter. If you look at what PullToRefresh.UWP offers you, you have a few other controls, namely PullToRefreshScrollViewer. But alas, they’re deprecated and tell you to use the Box. I like this approach as it means you can literally shove *any* scrolling container into the Box and then the work is done for you.

When you use the base implementation, your refresh trigger height is 80px (you can find this by inspecting an instance of PullToRefreshBox and looking at RefreshThreshold). The nice thing is you can change this to be whatever you want.

The default implementation also includes a nice “progressively drawn” circle showing the progress toward the threshold, but alas the “Pull” and “Refresh” wording is in Chinese. To change this, you need to template the Top Indicator of the Box like so:

HTML
<ptr:PullToRefreshBox Grid.Row="1"
                        RefreshInvoked="PullToRefreshBox_RefreshInvoked">
    <ptr:PullToRefreshBox.TopIndicatorTemplate>
        <DataTemplate>
            <ptr:PullRefreshProgressControl Progress="{Binding}"
                                            PullToRefreshText="Pull"
                                            ReleaseToRefreshText="Release" />
        </DataTemplate>
    </ptr:PullToRefreshBox.TopIndicatorTemplate>

There are a couple of things at play here:

  1. The TopIndicatorTemplate is set to contain a default instance of PullRefreshProgressControl, with the Pull and Release text set to what we want
  2. The Progress property of the ProgressControl is bound to the datacontext of the IndicatorTemplate. This is by default set to the % complete the box has been “pulled” relative to its threshold.

When the Progress value is set, the Control is doing work internally to paint the circle that gradually completes, and then switch the Visual State Manager to a “ReleaseToRefresh” state value. This state is what changes the text from the PullToRefreshText value to the ReleaseToRefreshText value. On the project page, you can see a fully templated instance of the progress control which uses only text and reacts to this Visual State change.

But what if we want to go to the next level and make the style completely our own?

It’s as simple as our good friend UserControl. I created one like this:

HTML
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="using:MyApp.Controls"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:Foundation="using:Windows.Foundation"
             x:Name="userControl"
             x:Class="MyApp.Controls.PullToRefresh"
             mc:Ignorable="d">

    <Grid VerticalAlignment="Bottom">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="VisualStateGroup">
                <VisualStateGroup.Transitions>
                    <VisualTransition GeneratedDuration="0:0:0.3"
                                      To="ReleaseToRefresh">
                        <VisualTransition.GeneratedEasingFunction>
                            <QuarticEase EasingMode="EaseIn" />
                        </VisualTransition.GeneratedEasingFunction>
                        <Storyboard>
                            <ObjectAnimationUsingKeyFrames 
                            Storyboard.TargetProperty="(UIElement.Visibility)"
                                            Storyboard.TargetName="PullTextBlock">
                                <DiscreteObjectKeyFrame KeyTime="0">
                                    <DiscreteObjectKeyFrame.Value>
                                        <Visibility>Visible</Visibility>
                                    </DiscreteObjectKeyFrame.Value>
                                </DiscreteObjectKeyFrame>
                                <DiscreteObjectKeyFrame KeyTime="0:0:0.3">
                                    <DiscreteObjectKeyFrame.Value>
                                        <Visibility>Collapsed</Visibility>
                                    </DiscreteObjectKeyFrame.Value>
                                </DiscreteObjectKeyFrame>
                            </ObjectAnimationUsingKeyFrames>
                            <ObjectAnimationUsingKeyFrames 
                            Storyboard.TargetProperty="(UIElement.Visibility)"
                                             Storyboard.TargetName="ReleaseTextBlock">
                                <DiscreteObjectKeyFrame KeyTime="0">
                                    <DiscreteObjectKeyFrame.Value>
                                        <Visibility>Visible</Visibility>
                                    </DiscreteObjectKeyFrame.Value>
                                </DiscreteObjectKeyFrame>
                                <DiscreteObjectKeyFrame KeyTime="0:0:0.3">
                                    <DiscreteObjectKeyFrame.Value>
                                        <Visibility>Visible</Visibility>
                                    </DiscreteObjectKeyFrame.Value>
                                </DiscreteObjectKeyFrame>
                            </ObjectAnimationUsingKeyFrames>
                            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)"
                                             Storyboard.TargetName="PullTextBlock">
                                <EasingDoubleKeyFrame KeyTime="0"
                                                      Value="1">
                                    <EasingDoubleKeyFrame.EasingFunction>
                                        <QuarticEase EasingMode="EaseIn" />
                                    </EasingDoubleKeyFrame.EasingFunction>
                                </EasingDoubleKeyFrame>
                                <EasingDoubleKeyFrame KeyTime="0:0:0.3"
                                                      Value="0" />
                            </DoubleAnimationUsingKeyFrames>
                            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)"
                                             Storyboard.TargetName="ReleaseTextBlock">
                                <EasingDoubleKeyFrame KeyTime="0"
                                                      Value="0" />
                                <EasingDoubleKeyFrame KeyTime="0:0:0.3"
                                                      Value="1" />
                            </DoubleAnimationUsingKeyFrames>
                        </Storyboard>
                    </VisualTransition>
                </VisualStateGroup.Transitions>
                <VisualState x:Name="Normal" />
                <VisualState x:Name="ReleaseToRefresh">
                    <VisualState.Setters>
                        <Setter Target="PullTextBlock.(UIElement.Visibility)"
                                Value="Collapsed" />
                        <Setter Target="ReleaseTextBlock.(UIElement.Visibility)"
                                Value="Visible" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>

        <Grid.RowDefinitions>
            <RowDefinition Height="30" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <RelativePanel x:Name="IconPanel"
                       HorizontalAlignment="Center"
                       RenderTransformOrigin="0.5,0.5">
            <SymbolIcon x:Name="SyncSymbol"
                        Symbol="Sync"
                        RelativePanel.AlignVerticalCenterWithPanel="True"
                        RelativePanel.AlignHorizontalCenterWithPanel="True"
                        RenderTransformOrigin="0.5,0.5"
                        Style="{x:Bind SymbolStyle, Mode=OneWay}">
                <SymbolIcon.RenderTransform>
                    <CompositeTransform ScaleX="1.5"
                                        ScaleY="1.5" />
                </SymbolIcon.RenderTransform>
            </SymbolIcon>
            <SymbolIcon x:Name="UpArrow"
                        Symbol="Up"
                        RelativePanel.AlignHorizontalCenterWithPanel="True"
                        RelativePanel.AlignVerticalCenterWithPanel="True"
                        RenderTransformOrigin="0.5,0.5"
                        Style="{x:Bind SymbolStyle, Mode=OneWay}">
                <SymbolIcon.RenderTransform>
                    <CompositeTransform Rotation="180"
                                        ScaleX="0.75"
                                        ScaleY="0.75" />
                </SymbolIcon.RenderTransform>
            </SymbolIcon>
        </RelativePanel>

        <TextBlock x:Uid="PullToRefreshTextBox"
                   x:Name="PullTextBlock"
                   Grid.Row="1"
                   Text="Pull to Refresh_zz"
                   HorizontalAlignment="Center"
                   Style="{x:Bind TextStyle, Mode=OneWay}" />
        <TextBlock x:Name="ReleaseTextBlock"
                   x:Uid="ReleaseTextBox"
                   x:DeferLoadStrategy="Lazy"
                   Grid.Row="1"
                   HorizontalAlignment="Center"
                   Text="Release_zz"
                   Visibility="Collapsed"
                   Style="{x:Bind TextStyle, Mode=OneWay}" />
    </Grid>
</UserControl>
C#
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

// The User Control item template is documented at http://go.microsoft.com/fwlink/?LinkId=234236

namespace MyApp.Controls
{
    public sealed partial class PullToRefresh : UserControl
    {
        public PullToRefresh()
        {
            this.InitializeComponent();
        }

        public double PullProgress
        {
            get { return (double)GetValue(PullProgressProperty); }
            set { SetValue(PullProgressProperty, value); }
        }
        // Using a DependencyProperty as the backing store for PullProgress.  
        // This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PullProgressProperty =
            DependencyProperty.Register("PullProgress", 
            typeof(double), typeof(PullToRefresh), new PropertyMetadata(0, (o, p) =>
            {
                var ptr = o as PullToRefresh;
                if (ptr != null)
                {
                    var percentProgress = (double)p.NewValue;
                    var rotationAmount = Math.Min(percentProgress * 180, 180);
                    ptr.IconPanel.RenderTransform = new RotateTransform
                    {
                        Angle = rotationAmount
                    };

                    VisualStateManager.GoToState(ptr, (percentProgress >= 1) ? 
                    "ReleaseToRefresh" : "Normal", true);
                }
            }));

        public Style SymbolStyle
        {
            get { return (Style)GetValue(SymbolStyleProperty); }
            set { SetValue(SymbolStyleProperty, value); }
        }
        // Using a DependencyProperty as the backing store for SymbolStyle. 
        // This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SymbolStyleProperty =
            DependencyProperty.Register("SymbolStyle", 
            typeof(Style), typeof(PullToRefresh), new PropertyMetadata(null));

        public Style TextStyle
        {
            get { return (Style)GetValue(TextStyleProperty); }
            set { SetValue(TextStyleProperty, value); }
        }
        // Using a DependencyProperty as the backing store for TextStyle. 
        // This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TextStyleProperty =
            DependencyProperty.Register("TextStyle", 
            typeof(Style), typeof(PullToRefresh), new PropertyMetadata(null));
    }
}

Here are the notables:

  1. I give a DependencyProperty to bind to the Progress value, same as the out-of-the-box ProgressControl that was shown earlier.
  2. I provide Style DependencyProperties for the Iconography and the Text of the control and bind the Style property of the Icon and Text elements of the control to those.
  3. Using Blend, I set up the Visual State Manager with transitions that cross-fade the “Pull” and “Refresh” texts when the user hits the threshold.
  4. In the Handler for the Progress DependencyProperty, I rotate the RelativePanel in which I put the ‘Sync’ and ‘Up’ icons based on the progress value, with a max value of 180 (this “rotates” the iconography to where the ‘Up’ icon goes from pointing down (due to its initial state of being flipped) to pointing up when the user should release to perform the refresh.

The usage of my UserControl within the Box looks like:

HTML
<ptr:PullToRefreshBox.TopIndicatorTemplate>
    <DataTemplate>
        <myControls:PullToRefresh PullProgress="{Binding}"
                                    VerticalAlignment="Bottom">
            <myControls:PullToRefresh.SymbolStyle>
                <Style TargetType="SymbolIcon">
                    <Setter Property="Foreground"
                            Value="{StaticResource 
                            ApplicationSecondaryForegroundThemeBrush}" />
                </Style>
            </myControls:PullToRefresh.SymbolStyle>
            <myControls:PullToRefresh.TextStyle>
                <Style TargetType="TextBlock"
                        BasedOn="{StaticResource ArticleListItemSummary}">
                    <Setter Property="Foreground"
                            Value="{StaticResource 
                            ApplicationSecondaryForegroundThemeBrush}" />
                </Style>
            </myControls:PullToRefresh.TextStyle>
        </myControls:PullToRefresh>
    </DataTemplate>
</ptr:PullToRefreshBox.TopIndicatorTemplate>

Which simply puts my control in where the ProgressControl was for the OEM example. Then, I style it to set the foreground of the symbol and text to my app’s secondary foreground theme brush.

Finally, there was one nuance I fought with, which was the main thing that compelled me to write this post: the RefreshThreshold value. This value must be less than the total height of the contents of the IndicatorTemplate. Here’s what I mean:

If you have this:

XML
 1: <ptr:PullToRefreshBox Grid.Row="1"
 2:                         RefreshInvoked="PullToRefreshBox_RefreshInvoked"
 3:                         RefreshThreshold="160">
 4:     <ptr:PullToRefreshBox.TopIndicatorTemplate>
 5:         <DataTemplate>
 6:             <myControls:PullToRefresh PullProgress="{Binding}"
 7:                                         VerticalAlignment="Bottom">
 8:                 <myControls:PullToRefresh.SymbolStyle>
 9:                     <Style TargetType="SymbolIcon">
10:                         <Setter Property="Foreground"
11:                                 Value="{StaticResource ApplicationSecondaryForegroundThemeBrush}" />
12:                     </Style>
13:                 </myControls:PullToRefresh.SymbolStyle>
14:                 <myControls:PullToRefresh.TextStyle>
15:                     <Style TargetType="TextBlock"
16:                             BasedOn="{StaticResource ArticleListItemSummary}">
17:                         <Setter Property="Foreground"
18:                                 Value="{StaticResource ApplicationSecondaryForegroundThemeBrush}" />
19:                     </Style>
20:                 </myControls:PullToRefresh.TextStyle>
21:             </myControls:PullToRefresh>
22:         </DataTemplate>
23:     </ptr:PullToRefreshBox.TopIndicatorTemplate>

Notice line #3: it specifies that the user should pull down 160px before refreshing kicks in. The problem is the size of my PullToRefresh control is going to be ‘Auto’ and not set to be 160 high, so when it gets the “Visual Size” of the element, it stops at some value < 160, so Progress never hits 1.0 (complete).

Logical step? Set the height of my control:

XML
1: <ptr:PullToRefreshBox Grid.Row="1"
2:                         RefreshInvoked="PullToRefreshBox_RefreshInvoked"
3:                         RefreshThreshold="160">
4:     <ptr:PullToRefreshBox.TopIndicatorTemplate>
5:         <DataTemplate>
6:             <myControls:PullToRefresh PullProgress="{Binding}"
7:                                         Height="160"
8:                                         VerticalAlignment="Bottom">

Making lines 3 & 7 equal seems right, but doesn’t quite do it. Why? I have no idea, honestly. What I had to do to make this work was make the height of the Indicator Template at least +1 from the value of RefreshThreshold.

XML
<ptr:PullToRefreshBox Grid.Row="1"
                        RefreshInvoked="PullToRefreshBox_RefreshInvoked"
                        RefreshThreshold="160">
    <ptr:PullToRefreshBox.TopIndicatorTemplate>
        <DataTemplate>
            <myControls:PullToRefresh PullProgress="{Binding}"
                                        Height="161"
                                        VerticalAlignment="Bottom">

Once I did this, everything worked.

Behold, the magic!

Image 1

I hope this helps you to integrate this desperately-needed component in to your UWP! It’s worth noting this will work on any UWP when touch interaction is enabled. For instance, when using a mouse in an app, you can’t click & pull a scroll viewer, so that doesn’t work, but switching to touch interaction (e.g.: using your finger on a Surface Pro vs the trackpad), it works just like it does on Phone.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
I'm a Sr. Software Engineer in the Seattle area primarily focused on serverless technologies in the cloud. In my free time I enjoy hiking & other adventures with my family around the Puget Sound and the country! You can find out more about me at my homepage: http://bc3.tech/brandonh

Comments and Discussions

 
QuestionWhat is the licence of this nuget package? Pin
Member 1233577917-Mar-16 4:20
Member 1233577917-Mar-16 4:20 
AnswerRe: What is the licence of this nuget package? Pin
BC3Tech17-Mar-16 5:27
BC3Tech17-Mar-16 5:27 
GeneralMy vote of 5 Pin
jbienz16-Feb-16 9:52
jbienz16-Feb-16 9:52 
QuestionEither Scroll bar failure or failure of Virtualization Pin
CodingNinja18-Jan-16 22:34
CodingNinja18-Jan-16 22:34 
Hi, I am from China, if you didn't understand what I said, then must be the Bing translation did not understand what I say!

I saw the Chinese blog, also tried PullToRefresh.UWP, and I tried Microsoft/Windows-universal-samples/XamlPullToRefresh
in Github, They all have the same problem, let we see:

↓This is a simple sample, it's OK!
XML
<pr:PullToRefreshBox x:Name="pr" RefreshInvoked="PullToRefreshBox_RefreshInvoked">
    <ListView x:Name="lv" ItemTemplate="{StaticResource ColorfulRectangle}" />
</pr:PullToRefreshBox>


↓If my page is Image + ListView, I must use a StackPanel(or Grid etc.) to wrap they. Result: Scroll bar does not work↓
XML
<pr:PullToRefreshBox x:Name="pr" RefreshInvoked="PullToRefreshBox_RefreshInvoked">
    <StackPanel>
        <Image />
        <ListView x:Name="lv" ItemTemplate="{StaticResource ColorfulRectangle}" />
    </StackPanel>
</pr:PullToRefreshBox>


↓So I had to add a ScrollViewer. Result: Scroll bar is OK, but Virtualization does not work(Incremental loading failure, App run and ListView load all data)↓
XML
<pr:PullToRefreshBox x:Name="pr" RefreshInvoked="PullToRefreshBox_RefreshInvoked">
    <ScrollViewer>
      <StackPanel>
          <Image />
          <ListView x:Name="lv" ItemTemplate="{StaticResource ColorfulRectangle}" />
      </StackPanel>
    </ScrollViewer>
</pr:PullToRefreshBox>


Do you have a PERFECT solution?
QuestionObject reference not set to an instance of object exception Pin
priom2214-Dec-15 15:53
priom2214-Dec-15 15:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.