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


2015.03.24

Run Node.js Azure Mobile Services locally

Want to unit test or debug your Node.js-based Azure Mobile Service? Here's how.

View details »


2015.03.10

Compare Across Forks

My new favorite feature on GitHub

View details »


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 »


More Posts >