ProgressRing for Windows Phone 8

A Windows-8-style ProgressRing for Windows Phone 8

The ProgressRing control for Metro (Windows Store style) apps is a great way to show the familiar loading spinner while your app performs some long-running action. But what about Windows Phone 8? There is the ProgressBar control (which has been updated for WP8 to fix some performance issues), but I wanted the round spinny thing instead of the flat movey thing, so I created it.

The code below consists of a .cs file that contains the guts of the control, as well as the <Style> definition. A couple of fun things to note:

  • The <Style> is the exact style copied from the Windows 8 resources, so the control looks and behaves identically

  • Note the check for hasAppliedTemplate - this is required to avoid some wonky behavior that I blogged about recently where VisualStateManager doesnt like to be called before OnApplyTemplate() has been called.

Anyway, next time you need some spinning-dots goodness in your WP8 app, give it a try and let me know what you think.

ProgressRing.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace Monsters.WindowsPhone.Controls
{
    public class ProgressRing : Control
    {
        bool hasAppliedTemplate = false;

        public ProgressRing()
        {
            this.DefaultStyleKey = typeof(ProgressRing);
            TemplateSettings = new TemplateSettingValues(60);
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            hasAppliedTemplate = true;
            UpdateState(this.IsActive);
        }

        void UpdateState(bool isActive)
        {
            if (hasAppliedTemplate)
            {
                string state = isActive ? "Active" : "Inactive";
                System.Windows.VisualStateManager.GoToState(this, state, true);
            }
        }

        protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
        {
            var width = 100D;
            if(!System.ComponentModel.DesignerProperties.IsInDesignTool)
                width = this.Width != double.NaN ? this.Width : availableSize.Width;
            TemplateSettings = new TemplateSettingValues(width);
            return base.MeasureOverride(availableSize);
        }

        public bool IsActive
        {
            get { return (bool)GetValue(IsActiveProperty); }
            set { SetValue(IsActiveProperty, value); }
        }

        // Using a DependencyProperty as the backing store for IsActive.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsActiveProperty =
            DependencyProperty.Register("IsActive", typeof(bool), typeof(ProgressRing), new PropertyMetadata(false, new PropertyChangedCallback(IsActiveChanged)));

        private static void IsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
        {
            var pr = (ProgressRing) d;
            var isActive = (bool)args.NewValue;
            pr.UpdateState(isActive);
        }

        public TemplateSettingValues TemplateSettings
        {
            get { return (TemplateSettingValues)GetValue(TemplateSettingsProperty); }
            set { SetValue(TemplateSettingsProperty, value); }
        }

        // Using a DependencyProperty as the backing store for TemplateSettings.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TemplateSettingsProperty =
            DependencyProperty.Register("TemplateSettings", typeof(TemplateSettingValues), typeof(ProgressRing), new PropertyMetadata(new TemplateSettingValues(100)));


        public class TemplateSettingValues : System.Windows.DependencyObject
        {
            public TemplateSettingValues(double width)
            {
                MaxSideLength = 400;
                EllipseDiameter = width/10;
                EllipseOffset = new System.Windows.Thickness(EllipseDiameter);
            }

            public double MaxSideLength
            {
                get { return (double)GetValue(MaxSideLengthProperty); }
                set { SetValue(MaxSideLengthProperty, value); }
            }

            // Using a DependencyProperty as the backing store for MaxSideLength.  This enables animation, styling, binding, etc...
            public static readonly DependencyProperty MaxSideLengthProperty =
                DependencyProperty.Register("MaxSideLength", typeof(double), typeof(TemplateSettingValues), new PropertyMetadata(0D));

            public double EllipseDiameter
            {
                get { return (double)GetValue(EllipseDiameterProperty); }
                set { SetValue(EllipseDiameterProperty, value); }
            }

            // Using a DependencyProperty as the backing store for EllipseDiameter.  This enables animation, styling, binding, etc...
            public static readonly DependencyProperty EllipseDiameterProperty =
                DependencyProperty.Register("EllipseDiameter", typeof(double), typeof(TemplateSettingValues), new PropertyMetadata(0D));

            public Thickness EllipseOffset
            {
                get { return (Thickness)GetValue(EllipseOffsetProperty); }
                set { SetValue(EllipseOffsetProperty, value); }
            }

            // Using a DependencyProperty as the backing store for EllipseOffset.  This enables animation, styling, binding, etc...
            public static readonly DependencyProperty EllipseOffsetProperty =
                DependencyProperty.Register("EllipseOffset", typeof(Thickness), typeof(TemplateSettingValues), new PropertyMetadata(new Thickness()));
        }
    }
}

ProgressRing Style

<!-- Default style for Windows.UI.Xaml.Controls.ProgressRing -->
<Style TargetType="controls:ProgressRing">
    <Setter Property="Foreground" Value="{StaticResource AccentBrush}" />
    <Setter Property="IsHitTestVisible" Value="False" />
    <Setter Property="HorizontalAlignment" Value="Center" />
    <Setter Property="VerticalAlignment" Value="Center" />
    <Setter Property="MinHeight" Value="20" />
    <Setter Property="MinWidth" Value="20" />
    <Setter Property="IsTabStop" Value="False" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="controls:ProgressRing">
                <Border x:Name="ProgressRingRoot" Background="{TemplateBinding Background}"
                    BorderThickness="{TemplateBinding BorderThickness}"
                    BorderBrush="{TemplateBinding BorderBrush}">
                    <Border.Resources>
                        <Style x:Key="ProgressRingEllipseStyle" TargetType="Ellipse">
                            <Setter Property="Opacity" Value="0" />
                            <Setter Property="HorizontalAlignment" Value="Left" />
                            <Setter Property="VerticalAlignment" Value="Top" />
                        </Style>
                    </Border.Resources>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="SizeStates">
                            <VisualState x:Name="Large">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Duration="0"
                                                                Storyboard.TargetName="SixthCircle"
                                                                Storyboard.TargetProperty="Visibility">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Small" />
                        </VisualStateGroup>
                        <VisualStateGroup x:Name="ActiveStates">
                            <VisualState x:Name="Inactive" />
                            <VisualState x:Name="Active">
                                <Storyboard RepeatBehavior="Forever">
                                    <ObjectAnimationUsingKeyFrames Duration="0"
                                                                Storyboard.TargetName="Ring"
                                                                Storyboard.TargetProperty="Visibility">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames
                                    Storyboard.TargetName="E1"
                                    Storyboard.TargetProperty="Opacity"
                                    BeginTime="0">
                                        <DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames
                                    Storyboard.TargetName="E2"
                                    Storyboard.TargetProperty="Opacity"
                                    BeginTime="00:00:00.167">
                                        <DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames
                                    Storyboard.TargetName="E3"
                                    Storyboard.TargetProperty="Opacity"
                                    BeginTime="00:00:00.334">
                                        <DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames
                                    Storyboard.TargetName="E4"
                                    Storyboard.TargetProperty="Opacity"
                                    BeginTime="00:00:00.501">
                                        <DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames
                                    Storyboard.TargetName="E5"
                                    Storyboard.TargetProperty="Opacity"
                                    BeginTime="00:00:00.668">
                                        <DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames
                                    Storyboard.TargetName="E6"
                                    Storyboard.TargetProperty="Opacity"
                                    BeginTime="00:00:00.835">
                                        <DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
                                        <DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames
                                    Storyboard.TargetName="E1R"
                                    BeginTime="0"
                                    Storyboard.TargetProperty="Angle">
                                        <SplineDoubleKeyFrame KeyTime="0" Value="-110" KeySpline="0.13,0.21,0.1,0.7"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="10" KeySpline="0.02,0.33,0.38,0.77"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="93"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="205" KeySpline="0.57,0.17,0.95,0.75"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="357" KeySpline="0,0.19,0.07,0.72"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="439"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="585" KeySpline="0,0,0.95,0.37"/>
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames
                                    Storyboard.TargetName="E2R"
                                    BeginTime="00:00:00.167"
                                    Storyboard.TargetProperty="Angle">
                                        <SplineDoubleKeyFrame KeyTime="0" Value="-116" KeySpline="0.13,0.21,0.1,0.7"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="4" KeySpline="0.02,0.33,0.38,0.77"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="87"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="199" KeySpline="0.57,0.17,0.95,0.75"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="351" KeySpline="0,0.19,0.07,0.72"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="433"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="579" KeySpline="0,0,0.95,0.37"/>
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames
                                    Storyboard.TargetName="E3R"
                                    BeginTime="00:00:00.334"
                                    Storyboard.TargetProperty="Angle">
                                        <SplineDoubleKeyFrame KeyTime="0" Value="-122" KeySpline="0.13,0.21,0.1,0.7"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="-2" KeySpline="0.02,0.33,0.38,0.77"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="81"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="193" KeySpline="0.57,0.17,0.95,0.75"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="345" KeySpline="0,0.19,0.07,0.72"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="427"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="573" KeySpline="0,0,0.95,0.37"/>
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames
                                    Storyboard.TargetName="E4R"
                                    BeginTime="00:00:00.501"
                                    Storyboard.TargetProperty="Angle">
                                        <SplineDoubleKeyFrame KeyTime="0" Value="-128" KeySpline="0.13,0.21,0.1,0.7"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="-8" KeySpline="0.02,0.33,0.38,0.77"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="75"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="187" KeySpline="0.57,0.17,0.95,0.75"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="339" KeySpline="0,0.19,0.07,0.72"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="421"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="567" KeySpline="0,0,0.95,0.37"/>
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames
                                    Storyboard.TargetName="E5R"
                                    BeginTime="00:00:00.668"
                                    Storyboard.TargetProperty="Angle">
                                        <SplineDoubleKeyFrame KeyTime="0" Value="-134" KeySpline="0.13,0.21,0.1,0.7"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="-14" KeySpline="0.02,0.33,0.38,0.77"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="69"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="181" KeySpline="0.57,0.17,0.95,0.75"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="331" KeySpline="0,0.19,0.07,0.72"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="415"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="561" KeySpline="0,0,0.95,0.37"/>
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames
                                    Storyboard.TargetName="E6R"
                                    BeginTime="00:00:00.835"
                                    Storyboard.TargetProperty="Angle">
                                        <SplineDoubleKeyFrame KeyTime="0" Value="-140" KeySpline="0.13,0.21,0.1,0.7"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="-20" KeySpline="0.02,0.33,0.38,0.77"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="63"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="175" KeySpline="0.57,0.17,0.95,0.75"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="325" KeySpline="0,0.19,0.07,0.72"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="409"/>
                                        <SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="555" KeySpline="0,0,0.95,0.37"/>
                                    </DoubleAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Grid x:Name="Ring"
                        Margin="{TemplateBinding Padding}"
                        MaxWidth="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.MaxSideLength}"
                        MaxHeight="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.MaxSideLength}"
                        Visibility="Collapsed"
                        RenderTransformOrigin=".5,.5"
                        FlowDirection="LeftToRight">
                        <Canvas RenderTransformOrigin=".5,.5">
                            <Canvas.RenderTransform>
                                <RotateTransform x:Name="E1R" />
                            </Canvas.RenderTransform>
                            <Ellipse
                            x:Name="E1"
                            Style="{StaticResource ProgressRingEllipseStyle}"
                            Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
                            Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
                            Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseOffset}"
                            Fill="{TemplateBinding Foreground}"/>
                        </Canvas>
                        <Canvas RenderTransformOrigin=".5,.5">
                            <Canvas.RenderTransform>
                                <RotateTransform x:Name="E2R" />
                            </Canvas.RenderTransform>
                            <Ellipse
                            x:Name="E2"
                            Style="{StaticResource ProgressRingEllipseStyle}"
                            Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
                            Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
                            Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseOffset}"
                            Fill="{TemplateBinding Foreground}"/>
                        </Canvas>
                        <Canvas RenderTransformOrigin=".5,.5">
                            <Canvas.RenderTransform>
                                <RotateTransform x:Name="E3R" />
                            </Canvas.RenderTransform>
                            <Ellipse
                            x:Name="E3"
                            Style="{StaticResource ProgressRingEllipseStyle}"
                            Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
                            Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
                            Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseOffset}"
                            Fill="{TemplateBinding Foreground}"/>
                        </Canvas>
                        <Canvas RenderTransformOrigin=".5,.5">
                            <Canvas.RenderTransform>
                                <RotateTransform x:Name="E4R" />
                            </Canvas.RenderTransform>
                            <Ellipse
                            x:Name="E4"
                            Style="{StaticResource ProgressRingEllipseStyle}"
                            Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
                            Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
                            Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseOffset}"
                            Fill="{TemplateBinding Foreground}"/>
                        </Canvas>
                        <Canvas RenderTransformOrigin=".5,.5">
                            <Canvas.RenderTransform>
                                <RotateTransform x:Name="E5R" />
                            </Canvas.RenderTransform>
                            <Ellipse
                            x:Name="E5"
                            Style="{StaticResource ProgressRingEllipseStyle}"
                            Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
                            Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
                            Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseOffset}"
                            Fill="{TemplateBinding Foreground}"/>
                        </Canvas>
                        <Canvas RenderTransformOrigin=".5,.5"
                            Visibility="Collapsed"
                            x:Name="SixthCircle">
                            <Canvas.RenderTransform>
                                <RotateTransform x:Name="E6R" />
                            </Canvas.RenderTransform>
                            <Ellipse
                            x:Name="E6"
                            Style="{StaticResource ProgressRingEllipseStyle}"
                            Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
                            Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
                            Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseOffset}"
                            Fill="{TemplateBinding Foreground}"/>
                        </Canvas>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
 

@briandunnington


2014.04.24

Triggering Storyboards with data binding

A clean way for your viewmodel's properties to trigger storyboard animations

View details »


2014.04.04

Declare Bluetooth capability for Windows Store and Windows Phone 8.1 apps

View details »


2014.04.03

Run arbitrary code on the UI thread asynchronously

A gotcha and a solution for easily running async code on the UI thread from a background thread.

View details »


2014.01.06

Azure Mobile Services No 'id' member found on type

When using Azure Mobile Services, you can sometimes get an error with the message "No 'id' member found on type" even when your type has an 'id' member. Here is why and how to solve it.

View details »


2013.11.06

Sharing Html & Images via the Share Contract

The right way to share html that contains images (and other external resources) via the Share charm.

View details »


More Posts >