Countdown control with arc animation

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;







up vote
11
down vote

favorite
1












I've implemented countdown timer control with arc animation that looks like this



countdown control




Implementation notes:



  • For arc visualization I've created class Arc derived from Shape (code is based on this post).



  • I've created Countdown control (derived from UserControl). For setting timeout I've added Seconds dependency property. I'm using binding Content="Binding Seconds" to display seconds. Animation duration is set in code behind



    Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));


    because I'm not sure if it's possible to do it in XAML without writing custom converter. I think that creating custom converter is not justified here.



  • For control's scaling content is wrapped in Viewbox сontrol.


  • For seconds animation I'm using DispatcherTimer, nothing special. Is it the best to go here?



Code



Arc.cs



public class Arc : Shape

public Point Center

get => (Point)GetValue(CenterProperty);
set => SetValue(CenterProperty, value);


// Using a DependencyProperty as the backing store for Center. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CenterProperty =
DependencyProperty.Register("Center", typeof(Point), typeof(Arc),
new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));

// Start angle in degrees
public double StartAngle

get => (double)GetValue(StartAngleProperty);
set => SetValue(StartAngleProperty, value);


// Using a DependencyProperty as the backing store for StartAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));

// End angle in degrees
public double EndAngle

get => (double)GetValue(EndAngleProperty);
set => SetValue(EndAngleProperty, value);


// Using a DependencyProperty as the backing store for EndAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty EndAngleProperty =
DependencyProperty.Register("EndAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));

public double Radius

get => (double)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);


// Using a DependencyProperty as the backing store for Radius. This enables animation, styling, binding, etc...
public static readonly DependencyProperty RadiusProperty =
DependencyProperty.Register("Radius", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));

public bool SmallAngle

get => (bool)GetValue(SmallAngleProperty);
set => SetValue(SmallAngleProperty, value);


// Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SmallAngleProperty =
DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));

protected override Geometry DefiningGeometry

get

double startAngleRadians = StartAngle * Math.PI / 180;
double endAngleRadians = EndAngle * Math.PI / 180;

double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

if (a1 < a0)
a1 += Math.PI * 2;

SweepDirection d = SweepDirection.Counterclockwise;
bool large;

if (SmallAngle)

large = false;
double t = a1;
d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;

else
large = (Math.Abs(a1 - a0) < Math.PI);

Point p0 = Center + new Vector(Math.Cos(a0), Math.Sin(a0)) * Radius;
Point p1 = Center + new Vector(Math.Cos(a1), Math.Sin(a1)) * Radius;

List<PathSegment> segments = new List<PathSegment>

new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true)
;

List<PathFigure> figures = new List<PathFigure>

new PathFigure(p0, segments, true)

IsClosed = false

;

return new PathGeometry(figures, FillRule.EvenOdd, null);





Countdown.xaml



<UserControl x:Class="WpfApp3.Countdown"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="450" Loaded="UserControl_Loaded">
<UserControl.Triggers>
<EventTrigger RoutedEvent="UserControl.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Name="Animation"
Storyboard.TargetName="Arc"
Storyboard.TargetProperty="EndAngle"
From="-90"
To="270" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</UserControl.Triggers>
<Viewbox>
<Grid Width="100" Height="100">
<Border Background="#222" Margin="5" CornerRadius="50">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
</StackPanel>
</Border>

<local:Arc
x:Name="Arc"
Center="50, 50"
StartAngle="-90"
EndAngle="-90"
Stroke="#45d3be"
StrokeThickness="5"
Radius="45" />
</Grid>
</Viewbox>
</UserControl>


Countdown.xaml.cs



public partial class Countdown : UserControl

public int Seconds

get => (int)GetValue(SecondsProperty);
set => SetValue(SecondsProperty, value);


public static readonly DependencyProperty SecondsProperty =
DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));

private readonly DispatcherTimer _timer = new DispatcherTimer Interval = TimeSpan.FromSeconds(1) ;

public Countdown()

InitializeComponent();
DataContext = this;


private void UserControl_Loaded(object sender, EventArgs e)

Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
if (Seconds > 0)

_timer.Start();
_timer.Tick += Timer_Tick;



private void Timer_Tick(object sender, EventArgs e)

Seconds--;
if (Seconds == 0) _timer.Stop();




Control is placed on Window like this



<local:Countdown Width="300" Height="300" Seconds="25" />






share|improve this question



























    up vote
    11
    down vote

    favorite
    1












    I've implemented countdown timer control with arc animation that looks like this



    countdown control




    Implementation notes:



    • For arc visualization I've created class Arc derived from Shape (code is based on this post).



    • I've created Countdown control (derived from UserControl). For setting timeout I've added Seconds dependency property. I'm using binding Content="Binding Seconds" to display seconds. Animation duration is set in code behind



      Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));


      because I'm not sure if it's possible to do it in XAML without writing custom converter. I think that creating custom converter is not justified here.



    • For control's scaling content is wrapped in Viewbox сontrol.


    • For seconds animation I'm using DispatcherTimer, nothing special. Is it the best to go here?



    Code



    Arc.cs



    public class Arc : Shape

    public Point Center

    get => (Point)GetValue(CenterProperty);
    set => SetValue(CenterProperty, value);


    // Using a DependencyProperty as the backing store for Center. This enables animation, styling, binding, etc...
    public static readonly DependencyProperty CenterProperty =
    DependencyProperty.Register("Center", typeof(Point), typeof(Arc),
    new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));

    // Start angle in degrees
    public double StartAngle

    get => (double)GetValue(StartAngleProperty);
    set => SetValue(StartAngleProperty, value);


    // Using a DependencyProperty as the backing store for StartAngle. This enables animation, styling, binding, etc...
    public static readonly DependencyProperty StartAngleProperty =
    DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
    new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));

    // End angle in degrees
    public double EndAngle

    get => (double)GetValue(EndAngleProperty);
    set => SetValue(EndAngleProperty, value);


    // Using a DependencyProperty as the backing store for EndAngle. This enables animation, styling, binding, etc...
    public static readonly DependencyProperty EndAngleProperty =
    DependencyProperty.Register("EndAngle", typeof(double), typeof(Arc),
    new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));

    public double Radius

    get => (double)GetValue(RadiusProperty);
    set => SetValue(RadiusProperty, value);


    // Using a DependencyProperty as the backing store for Radius. This enables animation, styling, binding, etc...
    public static readonly DependencyProperty RadiusProperty =
    DependencyProperty.Register("Radius", typeof(double), typeof(Arc),
    new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));

    public bool SmallAngle

    get => (bool)GetValue(SmallAngleProperty);
    set => SetValue(SmallAngleProperty, value);


    // Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SmallAngleProperty =
    DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
    new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

    static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));

    protected override Geometry DefiningGeometry

    get

    double startAngleRadians = StartAngle * Math.PI / 180;
    double endAngleRadians = EndAngle * Math.PI / 180;

    double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
    double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

    if (a1 < a0)
    a1 += Math.PI * 2;

    SweepDirection d = SweepDirection.Counterclockwise;
    bool large;

    if (SmallAngle)

    large = false;
    double t = a1;
    d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;

    else
    large = (Math.Abs(a1 - a0) < Math.PI);

    Point p0 = Center + new Vector(Math.Cos(a0), Math.Sin(a0)) * Radius;
    Point p1 = Center + new Vector(Math.Cos(a1), Math.Sin(a1)) * Radius;

    List<PathSegment> segments = new List<PathSegment>

    new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true)
    ;

    List<PathFigure> figures = new List<PathFigure>

    new PathFigure(p0, segments, true)

    IsClosed = false

    ;

    return new PathGeometry(figures, FillRule.EvenOdd, null);





    Countdown.xaml



    <UserControl x:Class="WpfApp3.Countdown"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:WpfApp"
    mc:Ignorable="d"
    d:DesignHeight="450" d:DesignWidth="450" Loaded="UserControl_Loaded">
    <UserControl.Triggers>
    <EventTrigger RoutedEvent="UserControl.Loaded">
    <BeginStoryboard>
    <Storyboard>
    <DoubleAnimation Name="Animation"
    Storyboard.TargetName="Arc"
    Storyboard.TargetProperty="EndAngle"
    From="-90"
    To="270" />
    </Storyboard>
    </BeginStoryboard>
    </EventTrigger>
    </UserControl.Triggers>
    <Viewbox>
    <Grid Width="100" Height="100">
    <Border Background="#222" Margin="5" CornerRadius="50">
    <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
    <Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
    <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
    </StackPanel>
    </Border>

    <local:Arc
    x:Name="Arc"
    Center="50, 50"
    StartAngle="-90"
    EndAngle="-90"
    Stroke="#45d3be"
    StrokeThickness="5"
    Radius="45" />
    </Grid>
    </Viewbox>
    </UserControl>


    Countdown.xaml.cs



    public partial class Countdown : UserControl

    public int Seconds

    get => (int)GetValue(SecondsProperty);
    set => SetValue(SecondsProperty, value);


    public static readonly DependencyProperty SecondsProperty =
    DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));

    private readonly DispatcherTimer _timer = new DispatcherTimer Interval = TimeSpan.FromSeconds(1) ;

    public Countdown()

    InitializeComponent();
    DataContext = this;


    private void UserControl_Loaded(object sender, EventArgs e)

    Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
    if (Seconds > 0)

    _timer.Start();
    _timer.Tick += Timer_Tick;



    private void Timer_Tick(object sender, EventArgs e)

    Seconds--;
    if (Seconds == 0) _timer.Stop();




    Control is placed on Window like this



    <local:Countdown Width="300" Height="300" Seconds="25" />






    share|improve this question























      up vote
      11
      down vote

      favorite
      1









      up vote
      11
      down vote

      favorite
      1






      1





      I've implemented countdown timer control with arc animation that looks like this



      countdown control




      Implementation notes:



      • For arc visualization I've created class Arc derived from Shape (code is based on this post).



      • I've created Countdown control (derived from UserControl). For setting timeout I've added Seconds dependency property. I'm using binding Content="Binding Seconds" to display seconds. Animation duration is set in code behind



        Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));


        because I'm not sure if it's possible to do it in XAML without writing custom converter. I think that creating custom converter is not justified here.



      • For control's scaling content is wrapped in Viewbox сontrol.


      • For seconds animation I'm using DispatcherTimer, nothing special. Is it the best to go here?



      Code



      Arc.cs



      public class Arc : Shape

      public Point Center

      get => (Point)GetValue(CenterProperty);
      set => SetValue(CenterProperty, value);


      // Using a DependencyProperty as the backing store for Center. This enables animation, styling, binding, etc...
      public static readonly DependencyProperty CenterProperty =
      DependencyProperty.Register("Center", typeof(Point), typeof(Arc),
      new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));

      // Start angle in degrees
      public double StartAngle

      get => (double)GetValue(StartAngleProperty);
      set => SetValue(StartAngleProperty, value);


      // Using a DependencyProperty as the backing store for StartAngle. This enables animation, styling, binding, etc...
      public static readonly DependencyProperty StartAngleProperty =
      DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
      new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));

      // End angle in degrees
      public double EndAngle

      get => (double)GetValue(EndAngleProperty);
      set => SetValue(EndAngleProperty, value);


      // Using a DependencyProperty as the backing store for EndAngle. This enables animation, styling, binding, etc...
      public static readonly DependencyProperty EndAngleProperty =
      DependencyProperty.Register("EndAngle", typeof(double), typeof(Arc),
      new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));

      public double Radius

      get => (double)GetValue(RadiusProperty);
      set => SetValue(RadiusProperty, value);


      // Using a DependencyProperty as the backing store for Radius. This enables animation, styling, binding, etc...
      public static readonly DependencyProperty RadiusProperty =
      DependencyProperty.Register("Radius", typeof(double), typeof(Arc),
      new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));

      public bool SmallAngle

      get => (bool)GetValue(SmallAngleProperty);
      set => SetValue(SmallAngleProperty, value);


      // Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
      public static readonly DependencyProperty SmallAngleProperty =
      DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
      new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

      static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));

      protected override Geometry DefiningGeometry

      get

      double startAngleRadians = StartAngle * Math.PI / 180;
      double endAngleRadians = EndAngle * Math.PI / 180;

      double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
      double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

      if (a1 < a0)
      a1 += Math.PI * 2;

      SweepDirection d = SweepDirection.Counterclockwise;
      bool large;

      if (SmallAngle)

      large = false;
      double t = a1;
      d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;

      else
      large = (Math.Abs(a1 - a0) < Math.PI);

      Point p0 = Center + new Vector(Math.Cos(a0), Math.Sin(a0)) * Radius;
      Point p1 = Center + new Vector(Math.Cos(a1), Math.Sin(a1)) * Radius;

      List<PathSegment> segments = new List<PathSegment>

      new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true)
      ;

      List<PathFigure> figures = new List<PathFigure>

      new PathFigure(p0, segments, true)

      IsClosed = false

      ;

      return new PathGeometry(figures, FillRule.EvenOdd, null);





      Countdown.xaml



      <UserControl x:Class="WpfApp3.Countdown"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:local="clr-namespace:WpfApp"
      mc:Ignorable="d"
      d:DesignHeight="450" d:DesignWidth="450" Loaded="UserControl_Loaded">
      <UserControl.Triggers>
      <EventTrigger RoutedEvent="UserControl.Loaded">
      <BeginStoryboard>
      <Storyboard>
      <DoubleAnimation Name="Animation"
      Storyboard.TargetName="Arc"
      Storyboard.TargetProperty="EndAngle"
      From="-90"
      To="270" />
      </Storyboard>
      </BeginStoryboard>
      </EventTrigger>
      </UserControl.Triggers>
      <Viewbox>
      <Grid Width="100" Height="100">
      <Border Background="#222" Margin="5" CornerRadius="50">
      <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
      <Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
      <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
      </StackPanel>
      </Border>

      <local:Arc
      x:Name="Arc"
      Center="50, 50"
      StartAngle="-90"
      EndAngle="-90"
      Stroke="#45d3be"
      StrokeThickness="5"
      Radius="45" />
      </Grid>
      </Viewbox>
      </UserControl>


      Countdown.xaml.cs



      public partial class Countdown : UserControl

      public int Seconds

      get => (int)GetValue(SecondsProperty);
      set => SetValue(SecondsProperty, value);


      public static readonly DependencyProperty SecondsProperty =
      DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));

      private readonly DispatcherTimer _timer = new DispatcherTimer Interval = TimeSpan.FromSeconds(1) ;

      public Countdown()

      InitializeComponent();
      DataContext = this;


      private void UserControl_Loaded(object sender, EventArgs e)

      Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
      if (Seconds > 0)

      _timer.Start();
      _timer.Tick += Timer_Tick;



      private void Timer_Tick(object sender, EventArgs e)

      Seconds--;
      if (Seconds == 0) _timer.Stop();




      Control is placed on Window like this



      <local:Countdown Width="300" Height="300" Seconds="25" />






      share|improve this question













      I've implemented countdown timer control with arc animation that looks like this



      countdown control




      Implementation notes:



      • For arc visualization I've created class Arc derived from Shape (code is based on this post).



      • I've created Countdown control (derived from UserControl). For setting timeout I've added Seconds dependency property. I'm using binding Content="Binding Seconds" to display seconds. Animation duration is set in code behind



        Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));


        because I'm not sure if it's possible to do it in XAML without writing custom converter. I think that creating custom converter is not justified here.



      • For control's scaling content is wrapped in Viewbox сontrol.


      • For seconds animation I'm using DispatcherTimer, nothing special. Is it the best to go here?



      Code



      Arc.cs



      public class Arc : Shape

      public Point Center

      get => (Point)GetValue(CenterProperty);
      set => SetValue(CenterProperty, value);


      // Using a DependencyProperty as the backing store for Center. This enables animation, styling, binding, etc...
      public static readonly DependencyProperty CenterProperty =
      DependencyProperty.Register("Center", typeof(Point), typeof(Arc),
      new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));

      // Start angle in degrees
      public double StartAngle

      get => (double)GetValue(StartAngleProperty);
      set => SetValue(StartAngleProperty, value);


      // Using a DependencyProperty as the backing store for StartAngle. This enables animation, styling, binding, etc...
      public static readonly DependencyProperty StartAngleProperty =
      DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
      new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));

      // End angle in degrees
      public double EndAngle

      get => (double)GetValue(EndAngleProperty);
      set => SetValue(EndAngleProperty, value);


      // Using a DependencyProperty as the backing store for EndAngle. This enables animation, styling, binding, etc...
      public static readonly DependencyProperty EndAngleProperty =
      DependencyProperty.Register("EndAngle", typeof(double), typeof(Arc),
      new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));

      public double Radius

      get => (double)GetValue(RadiusProperty);
      set => SetValue(RadiusProperty, value);


      // Using a DependencyProperty as the backing store for Radius. This enables animation, styling, binding, etc...
      public static readonly DependencyProperty RadiusProperty =
      DependencyProperty.Register("Radius", typeof(double), typeof(Arc),
      new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));

      public bool SmallAngle

      get => (bool)GetValue(SmallAngleProperty);
      set => SetValue(SmallAngleProperty, value);


      // Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
      public static readonly DependencyProperty SmallAngleProperty =
      DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
      new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

      static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));

      protected override Geometry DefiningGeometry

      get

      double startAngleRadians = StartAngle * Math.PI / 180;
      double endAngleRadians = EndAngle * Math.PI / 180;

      double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
      double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

      if (a1 < a0)
      a1 += Math.PI * 2;

      SweepDirection d = SweepDirection.Counterclockwise;
      bool large;

      if (SmallAngle)

      large = false;
      double t = a1;
      d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;

      else
      large = (Math.Abs(a1 - a0) < Math.PI);

      Point p0 = Center + new Vector(Math.Cos(a0), Math.Sin(a0)) * Radius;
      Point p1 = Center + new Vector(Math.Cos(a1), Math.Sin(a1)) * Radius;

      List<PathSegment> segments = new List<PathSegment>

      new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true)
      ;

      List<PathFigure> figures = new List<PathFigure>

      new PathFigure(p0, segments, true)

      IsClosed = false

      ;

      return new PathGeometry(figures, FillRule.EvenOdd, null);





      Countdown.xaml



      <UserControl x:Class="WpfApp3.Countdown"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:local="clr-namespace:WpfApp"
      mc:Ignorable="d"
      d:DesignHeight="450" d:DesignWidth="450" Loaded="UserControl_Loaded">
      <UserControl.Triggers>
      <EventTrigger RoutedEvent="UserControl.Loaded">
      <BeginStoryboard>
      <Storyboard>
      <DoubleAnimation Name="Animation"
      Storyboard.TargetName="Arc"
      Storyboard.TargetProperty="EndAngle"
      From="-90"
      To="270" />
      </Storyboard>
      </BeginStoryboard>
      </EventTrigger>
      </UserControl.Triggers>
      <Viewbox>
      <Grid Width="100" Height="100">
      <Border Background="#222" Margin="5" CornerRadius="50">
      <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
      <Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
      <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
      </StackPanel>
      </Border>

      <local:Arc
      x:Name="Arc"
      Center="50, 50"
      StartAngle="-90"
      EndAngle="-90"
      Stroke="#45d3be"
      StrokeThickness="5"
      Radius="45" />
      </Grid>
      </Viewbox>
      </UserControl>


      Countdown.xaml.cs



      public partial class Countdown : UserControl

      public int Seconds

      get => (int)GetValue(SecondsProperty);
      set => SetValue(SecondsProperty, value);


      public static readonly DependencyProperty SecondsProperty =
      DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));

      private readonly DispatcherTimer _timer = new DispatcherTimer Interval = TimeSpan.FromSeconds(1) ;

      public Countdown()

      InitializeComponent();
      DataContext = this;


      private void UserControl_Loaded(object sender, EventArgs e)

      Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
      if (Seconds > 0)

      _timer.Start();
      _timer.Tick += Timer_Tick;



      private void Timer_Tick(object sender, EventArgs e)

      Seconds--;
      if (Seconds == 0) _timer.Stop();




      Control is placed on Window like this



      <local:Countdown Width="300" Height="300" Seconds="25" />








      share|improve this question












      share|improve this question




      share|improve this question








      edited Jun 26 at 3:59









      200_success

      123k14143399




      123k14143399









      asked Jun 25 at 8:48









      Vadim Ovchinnikov

      1,0101417




      1,0101417




















          4 Answers
          4






          active

          oldest

          votes

















          up vote
          3
          down vote



          accepted










          I don't like that you are running two timers/timelines in parallel. Instead you could run the animation from code behind. It also gives you the opportunity to trigger it from other places than load:



          private void StartAnimation()

          double from = -90;
          double to = 270;
          int seconds = Seconds;
          TimeSpan duration = TimeSpan.FromSeconds(Seconds);

          DoubleAnimation animation = new DoubleAnimation(from, to, new Duration(duration));

          Storyboard.SetTarget(animation, Arc);
          Storyboard.SetTargetProperty(animation, new PropertyPath("EndAngle"));

          Storyboard storyboard = new Storyboard();
          storyboard.CurrentTimeInvalidated += (s, e) =>

          int diff = (int)((s as ClockGroup).CurrentTime.Value.TotalSeconds);
          Seconds = seconds - diff;
          ;

          storyboard.Children.Add(animation);
          storyboard.Begin();


          private void UserControl_Loaded(object sender, RoutedEventArgs e)

          StartAnimation();
          //Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
          //if (Seconds > 0)
          //
          // _timer.Tick += Timer_Tick;
          // _timer.Start();
          //




          <UserControl x:Class="WpfApp3.Countdown"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
          xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
          xmlns:local="clr-namespace:WpfApp"
          mc:Ignorable="d"
          d:DesignHeight="450" d:DesignWidth="800"
          Loaded="UserControl_Loaded"
          >
          <Viewbox>
          <Grid Width="100" Height="100">
          <Border Background="#222" Margin="5" CornerRadius="50">
          <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
          <Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
          <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
          </StackPanel>
          </Border>

          <local:Arc
          x:Name="Arc"
          Center="50, 50"
          StartAngle="-90"
          EndAngle="-90"
          Stroke="#45d3be"
          StrokeThickness="5"
          Radius="45" />
          </Grid>
          </Viewbox>
          </UserControl>





          share|improve this answer






























            up vote
            10
            down vote













            There are a couple of things which could be more consistent.




             public static readonly DependencyProperty StartAngleProperty =
            DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
            new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));






             public static readonly DependencyProperty SecondsProperty =
            DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));



            The dependency property in Countdown.xaml.cs uses nameof, but the ones in Arc.cs don't. IMO they should.





             // Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
            public static readonly DependencyProperty SmallAngleProperty =
            DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
            new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

            static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));



            The dependency properties in Arc.cs all have a comment, but the constructor doesn't. IMO the constructor is the harder of the two to understand: if someone knows enough WPF to understand that without a comment, they don't need the comments on the dependency properties.





             double startAngleRadians = StartAngle * Math.PI / 180;
            double endAngleRadians = EndAngle * Math.PI / 180;

            double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
            double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

            if (a1 < a0)
            a1 += Math.PI * 2;

            SweepDirection d = SweepDirection.Counterclockwise;
            bool large;

            if (SmallAngle)

            large = false;
            double t = a1;
            d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;

            else
            large = (Math.Abs(a1 - a0) < Math.PI);



            I could definitely use some comments to explain what's going on here. The last line in particular looks very counterintuitive.



            Also there's a dead line: t isn't used anywhere.





             <Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
            <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />



            Not very localisable...





             _timer.Start();
            _timer.Tick += Timer_Tick;



            It's unlikely to occur in practice, but technically you've got a race condition there. I see only an advantage in switching the order of the two lines.





            ... For setting timeout I've added Seconds dependency property. I'm using binding Content="Binding Seconds" to display seconds. Animation duration is
            set in code behind



            Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));


            because I'm not sure if it's possible to do it in XAML without writing
            custom converter. I think that creating custom converter is not
            justified here.




            I think you can do it without a custom converter by making the binding property a TimeSpan (changing the name from Seconds to something like TimeRemaining) and using StringFormat in the binding for the label content. I haven't tested this.



            If you do this then you might want to handle non-integer TimeSpan.TotalSeconds values by rounding to the nearest second in the setter.






            share|improve this answer





















            • Regarding "Not very localisable…" if I would need localization I'll just add something like Content="x:Static p:Resources.Sec". I'm using Resx-files based localization, don't know if it's the best option, but didn't find good alternatives, what do you think? But I assume adding this piece of code to the question would be redundant.
              – Vadim Ovchinnikov
              Jun 25 at 19:54










            • I haven't found any good standard l10n approach for WPF. I ended up doing a DIY approach with a custom converter and various ugly hacks. If you've thought about the issue and made a decision that YAGNI then that's fair enough.
              – Peter Taylor
              Jun 25 at 21:11

















            up vote
            7
            down vote













            1) DispatcherTimer is good for periodic UI updates, but it should not be used to measure time, because it is just not accurate enough. For precise time measurement you have to use Stopwatch class inside the timer callback.



            2) Seconds property apparently does two things: it starts as "countdown duration" but after control is loaded it acts as "remaining time". I would use two properties here, so that they can be databound separately when necessary.



            3) DataContext = this; - don't set DataContext on public re-usable types. Someone (yourself included) can eventually decide to use this class in MVVM environment, but changing DataContext of your class will break it. Internally, you should use RelativeSource or ElementName properties of Binding class or DependencyProperty callbacks to do your bidding, while leaving DataContext empty.



            Here is an example of using ElementName:



            <UserControl x:Class="WpfApp3.Countdown"
            ...
            x:Name="this">

            ...
            <!-- Content binds directly to Countdown.Seconds dependency property, DataContext is ignored -->
            <Label Content="Binding Seconds, ElementName=this" />
            ...
            </UserControl>





            share|improve this answer























            • Regarding last point: suppose I want to move my properties from control class to some view model class. When I have DataContext assigned I just need to change DataContext stored reference, but when I don't have one I need to rewrite all my bindings. So I'm not sure if using DataContext is bad idea here. Am I missing something?
              – Vadim Ovchinnikov
              Jun 27 at 6:52










            • @VadimOvchinnikov, I'm not sure I follow. Why do you want to move the properties? Just leave them where they are. For MVVM to work, you need to have a property on each side: one property on your view, and another property on your viewmodel. Then you can bind them by setting viewmodel as datacontext: <Countdown Duration=Binding DurationPropertyOfViewModel DataContext=Binding ViewModel/>
              – Nikita B
              Jun 27 at 8:35

















            up vote
            3
            down vote













            Incorporated nearly all recommendations to my solution, but the most radical was accepted answer. Any feedback very welcome!



            Also added public Start and Stop methods and Elapsed event.



            Results:



            Arc.cs



            public class Arc : Shape

            public Point Center

            get => (Point)GetValue(CenterProperty);
            set => SetValue(CenterProperty, value);


            public static readonly DependencyProperty CenterProperty =
            DependencyProperty.Register(nameof(Center), typeof(Point), typeof(Arc),
            new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));

            public double StartAngle

            get => (double)GetValue(StartAngleProperty);
            set => SetValue(StartAngleProperty, value);


            public static readonly DependencyProperty StartAngleProperty =
            DependencyProperty.Register(nameof(StartAngle), typeof(double), typeof(Arc),
            new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));

            public double EndAngle

            get => (double)GetValue(EndAngleProperty);
            set => SetValue(EndAngleProperty, value);


            public static readonly DependencyProperty EndAngleProperty =
            DependencyProperty.Register(nameof(EndAngle), typeof(double), typeof(Arc),
            new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));

            public double Radius

            get => (double)GetValue(RadiusProperty);
            set => SetValue(RadiusProperty, value);


            public static readonly DependencyProperty RadiusProperty =
            DependencyProperty.Register(nameof(Radius), typeof(double), typeof(Arc),
            new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));

            public bool SmallAngle

            get => (bool)GetValue(SmallAngleProperty);
            set => SetValue(SmallAngleProperty, value);


            public static readonly DependencyProperty SmallAngleProperty =
            DependencyProperty.Register(nameof(SmallAngle), typeof(bool), typeof(Arc),
            new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

            static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));

            protected override Geometry DefiningGeometry

            get

            double startAngleRadians = StartAngle * Math.PI / 180;
            double endAngleRadians = EndAngle * Math.PI / 180;

            double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
            double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

            if (a1 < a0)
            a1 += Math.PI * 2;

            SweepDirection d = SweepDirection.Counterclockwise;
            bool large;

            if (SmallAngle)

            large = false;
            d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;

            else
            large = (Math.Abs(a1 - a0) < Math.PI);

            Point p0 = Center + new Vector(Math.Cos(a0), Math.Sin(a0)) * Radius;
            Point p1 = Center + new Vector(Math.Cos(a1), Math.Sin(a1)) * Radius;

            List<PathSegment> segments = new List<PathSegment>

            new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true)
            ;

            List<PathFigure> figures = new List<PathFigure>

            new PathFigure(p0, segments, true)

            IsClosed = false

            ;

            return new PathGeometry(figures, FillRule.EvenOdd, null);





            Countdown.xaml



            <UserControl x:Class="WpfApp.Countdown"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:local="clr-namespace:WpfApp"
            mc:Ignorable="d"
            d:DesignHeight="450" d:DesignWidth="450" Loaded="Countdown_Loaded">
            <Viewbox>
            <Grid Width="100" Height="100">
            <Border Background="#222" Margin="5" CornerRadius="50">
            <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
            <Label Foreground="#fff" Content="Binding SecondsRemaining" FontSize="50" Margin="0, -10, 0, 0" />
            <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -15, 0, 0" />
            </StackPanel>
            </Border>

            <uc:Arc
            x:Name="Arc"
            Center="50, 50"
            StartAngle="-90"
            EndAngle="-90"
            Stroke="#45d3be"
            StrokeThickness="5"
            Radius="45" />
            </Grid>
            </Viewbox>
            </UserControl>


            Countdown.xaml.cs



            public partial class Countdown : UserControl

            public Duration Duration

            get => (Duration)GetValue(DurationProperty);
            set => SetValue(DurationProperty, value);


            public static readonly DependencyProperty DurationProperty =
            DependencyProperty.Register(nameof(Duration), typeof(Duration), typeof(Countdown), new PropertyMetadata(new Duration()));

            public int SecondsRemaining

            get => (int)GetValue(SecondsRemainingProperty);
            set => SetValue(SecondsRemainingProperty, value);


            public static readonly DependencyProperty SecondsRemainingProperty =
            DependencyProperty.Register(nameof(SecondsRemaining), typeof(int), typeof(Countdown), new PropertyMetadata(0));

            public event EventHandler Elapsed;

            private readonly Storyboard _storyboard = new Storyboard();

            public Countdown()

            InitializeComponent();

            DoubleAnimation animation = new DoubleAnimation(-90, 270, Duration);
            Storyboard.SetTarget(animation, Arc);
            Storyboard.SetTargetProperty(animation, new PropertyPath(nameof(Arc.EndAngle)));
            _storyboard.Children.Add(animation);

            DataContext = this;


            private void Countdown_Loaded(object sender, EventArgs e)

            if (IsVisible)
            Start();


            public void Start()

            Stop();

            _storyboard.CurrentTimeInvalidated += Storyboard_CurrentTimeInvalidated;
            _storyboard.Completed += Storyboard_Completed;

            _storyboard.Begin();


            public void Stop()

            _storyboard.CurrentTimeInvalidated -= Storyboard_CurrentTimeInvalidated;
            _storyboard.Completed -= Storyboard_Completed;

            _storyboard.Stop();


            private void Storyboard_CurrentTimeInvalidated(object sender, EventArgs e)

            ClockGroup cg = (ClockGroup)sender;
            if (cg.CurrentTime == null) return;
            TimeSpan elapsedTime = cg.CurrentTime.Value;
            SecondsRemaining = (int)Math.Ceiling((Duration.TimeSpan - elapsedTime).TotalSeconds);


            private void Storyboard_Completed(object sender, EventArgs e)

            if (IsVisible)
            Elapsed?.Invoke(this, EventArgs.Empty);




            Example of usage:



            <local:Countdown Width="300" Height="300" Duration="0:0:15" />





            share|improve this answer























              Your Answer




              StackExchange.ifUsing("editor", function ()
              return StackExchange.using("mathjaxEditing", function ()
              StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix)
              StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
              );
              );
              , "mathjax-editing");

              StackExchange.ifUsing("editor", function ()
              StackExchange.using("externalEditor", function ()
              StackExchange.using("snippets", function ()
              StackExchange.snippets.init();
              );
              );
              , "code-snippets");

              StackExchange.ready(function()
              var channelOptions =
              tags: "".split(" "),
              id: "196"
              ;
              initTagRenderer("".split(" "), "".split(" "), channelOptions);

              StackExchange.using("externalEditor", function()
              // Have to fire editor after snippets, if snippets enabled
              if (StackExchange.settings.snippets.snippetsEnabled)
              StackExchange.using("snippets", function()
              createEditor();
              );

              else
              createEditor();

              );

              function createEditor()
              StackExchange.prepareEditor(
              heartbeatType: 'answer',
              convertImagesToLinks: false,
              noModals: false,
              showLowRepImageUploadWarning: true,
              reputationToPostImages: null,
              bindNavPrevention: true,
              postfix: "",
              onDemand: true,
              discardSelector: ".discard-answer"
              ,immediatelyShowMarkdownHelp:true
              );



              );








               

              draft saved


              draft discarded


















              StackExchange.ready(
              function ()
              StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f197197%2fcountdown-control-with-arc-animation%23new-answer', 'question_page');

              );

              Post as a guest






























              4 Answers
              4






              active

              oldest

              votes








              4 Answers
              4






              active

              oldest

              votes









              active

              oldest

              votes






              active

              oldest

              votes








              up vote
              3
              down vote



              accepted










              I don't like that you are running two timers/timelines in parallel. Instead you could run the animation from code behind. It also gives you the opportunity to trigger it from other places than load:



              private void StartAnimation()

              double from = -90;
              double to = 270;
              int seconds = Seconds;
              TimeSpan duration = TimeSpan.FromSeconds(Seconds);

              DoubleAnimation animation = new DoubleAnimation(from, to, new Duration(duration));

              Storyboard.SetTarget(animation, Arc);
              Storyboard.SetTargetProperty(animation, new PropertyPath("EndAngle"));

              Storyboard storyboard = new Storyboard();
              storyboard.CurrentTimeInvalidated += (s, e) =>

              int diff = (int)((s as ClockGroup).CurrentTime.Value.TotalSeconds);
              Seconds = seconds - diff;
              ;

              storyboard.Children.Add(animation);
              storyboard.Begin();


              private void UserControl_Loaded(object sender, RoutedEventArgs e)

              StartAnimation();
              //Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
              //if (Seconds > 0)
              //
              // _timer.Tick += Timer_Tick;
              // _timer.Start();
              //




              <UserControl x:Class="WpfApp3.Countdown"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:local="clr-namespace:WpfApp"
              mc:Ignorable="d"
              d:DesignHeight="450" d:DesignWidth="800"
              Loaded="UserControl_Loaded"
              >
              <Viewbox>
              <Grid Width="100" Height="100">
              <Border Background="#222" Margin="5" CornerRadius="50">
              <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
              <Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
              <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
              </StackPanel>
              </Border>

              <local:Arc
              x:Name="Arc"
              Center="50, 50"
              StartAngle="-90"
              EndAngle="-90"
              Stroke="#45d3be"
              StrokeThickness="5"
              Radius="45" />
              </Grid>
              </Viewbox>
              </UserControl>





              share|improve this answer



























                up vote
                3
                down vote



                accepted










                I don't like that you are running two timers/timelines in parallel. Instead you could run the animation from code behind. It also gives you the opportunity to trigger it from other places than load:



                private void StartAnimation()

                double from = -90;
                double to = 270;
                int seconds = Seconds;
                TimeSpan duration = TimeSpan.FromSeconds(Seconds);

                DoubleAnimation animation = new DoubleAnimation(from, to, new Duration(duration));

                Storyboard.SetTarget(animation, Arc);
                Storyboard.SetTargetProperty(animation, new PropertyPath("EndAngle"));

                Storyboard storyboard = new Storyboard();
                storyboard.CurrentTimeInvalidated += (s, e) =>

                int diff = (int)((s as ClockGroup).CurrentTime.Value.TotalSeconds);
                Seconds = seconds - diff;
                ;

                storyboard.Children.Add(animation);
                storyboard.Begin();


                private void UserControl_Loaded(object sender, RoutedEventArgs e)

                StartAnimation();
                //Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
                //if (Seconds > 0)
                //
                // _timer.Tick += Timer_Tick;
                // _timer.Start();
                //




                <UserControl x:Class="WpfApp3.Countdown"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                xmlns:local="clr-namespace:WpfApp"
                mc:Ignorable="d"
                d:DesignHeight="450" d:DesignWidth="800"
                Loaded="UserControl_Loaded"
                >
                <Viewbox>
                <Grid Width="100" Height="100">
                <Border Background="#222" Margin="5" CornerRadius="50">
                <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
                <Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
                <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
                </StackPanel>
                </Border>

                <local:Arc
                x:Name="Arc"
                Center="50, 50"
                StartAngle="-90"
                EndAngle="-90"
                Stroke="#45d3be"
                StrokeThickness="5"
                Radius="45" />
                </Grid>
                </Viewbox>
                </UserControl>





                share|improve this answer

























                  up vote
                  3
                  down vote



                  accepted







                  up vote
                  3
                  down vote



                  accepted






                  I don't like that you are running two timers/timelines in parallel. Instead you could run the animation from code behind. It also gives you the opportunity to trigger it from other places than load:



                  private void StartAnimation()

                  double from = -90;
                  double to = 270;
                  int seconds = Seconds;
                  TimeSpan duration = TimeSpan.FromSeconds(Seconds);

                  DoubleAnimation animation = new DoubleAnimation(from, to, new Duration(duration));

                  Storyboard.SetTarget(animation, Arc);
                  Storyboard.SetTargetProperty(animation, new PropertyPath("EndAngle"));

                  Storyboard storyboard = new Storyboard();
                  storyboard.CurrentTimeInvalidated += (s, e) =>

                  int diff = (int)((s as ClockGroup).CurrentTime.Value.TotalSeconds);
                  Seconds = seconds - diff;
                  ;

                  storyboard.Children.Add(animation);
                  storyboard.Begin();


                  private void UserControl_Loaded(object sender, RoutedEventArgs e)

                  StartAnimation();
                  //Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
                  //if (Seconds > 0)
                  //
                  // _timer.Tick += Timer_Tick;
                  // _timer.Start();
                  //




                  <UserControl x:Class="WpfApp3.Countdown"
                  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                  xmlns:local="clr-namespace:WpfApp"
                  mc:Ignorable="d"
                  d:DesignHeight="450" d:DesignWidth="800"
                  Loaded="UserControl_Loaded"
                  >
                  <Viewbox>
                  <Grid Width="100" Height="100">
                  <Border Background="#222" Margin="5" CornerRadius="50">
                  <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
                  <Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
                  <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
                  </StackPanel>
                  </Border>

                  <local:Arc
                  x:Name="Arc"
                  Center="50, 50"
                  StartAngle="-90"
                  EndAngle="-90"
                  Stroke="#45d3be"
                  StrokeThickness="5"
                  Radius="45" />
                  </Grid>
                  </Viewbox>
                  </UserControl>





                  share|improve this answer















                  I don't like that you are running two timers/timelines in parallel. Instead you could run the animation from code behind. It also gives you the opportunity to trigger it from other places than load:



                  private void StartAnimation()

                  double from = -90;
                  double to = 270;
                  int seconds = Seconds;
                  TimeSpan duration = TimeSpan.FromSeconds(Seconds);

                  DoubleAnimation animation = new DoubleAnimation(from, to, new Duration(duration));

                  Storyboard.SetTarget(animation, Arc);
                  Storyboard.SetTargetProperty(animation, new PropertyPath("EndAngle"));

                  Storyboard storyboard = new Storyboard();
                  storyboard.CurrentTimeInvalidated += (s, e) =>

                  int diff = (int)((s as ClockGroup).CurrentTime.Value.TotalSeconds);
                  Seconds = seconds - diff;
                  ;

                  storyboard.Children.Add(animation);
                  storyboard.Begin();


                  private void UserControl_Loaded(object sender, RoutedEventArgs e)

                  StartAnimation();
                  //Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
                  //if (Seconds > 0)
                  //
                  // _timer.Tick += Timer_Tick;
                  // _timer.Start();
                  //




                  <UserControl x:Class="WpfApp3.Countdown"
                  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                  xmlns:local="clr-namespace:WpfApp"
                  mc:Ignorable="d"
                  d:DesignHeight="450" d:DesignWidth="800"
                  Loaded="UserControl_Loaded"
                  >
                  <Viewbox>
                  <Grid Width="100" Height="100">
                  <Border Background="#222" Margin="5" CornerRadius="50">
                  <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
                  <Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
                  <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
                  </StackPanel>
                  </Border>

                  <local:Arc
                  x:Name="Arc"
                  Center="50, 50"
                  StartAngle="-90"
                  EndAngle="-90"
                  Stroke="#45d3be"
                  StrokeThickness="5"
                  Radius="45" />
                  </Grid>
                  </Viewbox>
                  </UserControl>






                  share|improve this answer















                  share|improve this answer



                  share|improve this answer








                  edited Jun 26 at 8:06


























                  answered Jun 25 at 14:11









                  Henrik Hansen

                  3,7781417




                  3,7781417






















                      up vote
                      10
                      down vote













                      There are a couple of things which could be more consistent.




                       public static readonly DependencyProperty StartAngleProperty =
                      DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
                      new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));






                       public static readonly DependencyProperty SecondsProperty =
                      DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));



                      The dependency property in Countdown.xaml.cs uses nameof, but the ones in Arc.cs don't. IMO they should.





                       // Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
                      public static readonly DependencyProperty SmallAngleProperty =
                      DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
                      new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

                      static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));



                      The dependency properties in Arc.cs all have a comment, but the constructor doesn't. IMO the constructor is the harder of the two to understand: if someone knows enough WPF to understand that without a comment, they don't need the comments on the dependency properties.





                       double startAngleRadians = StartAngle * Math.PI / 180;
                      double endAngleRadians = EndAngle * Math.PI / 180;

                      double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
                      double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

                      if (a1 < a0)
                      a1 += Math.PI * 2;

                      SweepDirection d = SweepDirection.Counterclockwise;
                      bool large;

                      if (SmallAngle)

                      large = false;
                      double t = a1;
                      d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;

                      else
                      large = (Math.Abs(a1 - a0) < Math.PI);



                      I could definitely use some comments to explain what's going on here. The last line in particular looks very counterintuitive.



                      Also there's a dead line: t isn't used anywhere.





                       <Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
                      <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />



                      Not very localisable...





                       _timer.Start();
                      _timer.Tick += Timer_Tick;



                      It's unlikely to occur in practice, but technically you've got a race condition there. I see only an advantage in switching the order of the two lines.





                      ... For setting timeout I've added Seconds dependency property. I'm using binding Content="Binding Seconds" to display seconds. Animation duration is
                      set in code behind



                      Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));


                      because I'm not sure if it's possible to do it in XAML without writing
                      custom converter. I think that creating custom converter is not
                      justified here.




                      I think you can do it without a custom converter by making the binding property a TimeSpan (changing the name from Seconds to something like TimeRemaining) and using StringFormat in the binding for the label content. I haven't tested this.



                      If you do this then you might want to handle non-integer TimeSpan.TotalSeconds values by rounding to the nearest second in the setter.






                      share|improve this answer





















                      • Regarding "Not very localisable…" if I would need localization I'll just add something like Content="x:Static p:Resources.Sec". I'm using Resx-files based localization, don't know if it's the best option, but didn't find good alternatives, what do you think? But I assume adding this piece of code to the question would be redundant.
                        – Vadim Ovchinnikov
                        Jun 25 at 19:54










                      • I haven't found any good standard l10n approach for WPF. I ended up doing a DIY approach with a custom converter and various ugly hacks. If you've thought about the issue and made a decision that YAGNI then that's fair enough.
                        – Peter Taylor
                        Jun 25 at 21:11














                      up vote
                      10
                      down vote













                      There are a couple of things which could be more consistent.




                       public static readonly DependencyProperty StartAngleProperty =
                      DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
                      new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));






                       public static readonly DependencyProperty SecondsProperty =
                      DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));



                      The dependency property in Countdown.xaml.cs uses nameof, but the ones in Arc.cs don't. IMO they should.





                       // Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
                      public static readonly DependencyProperty SmallAngleProperty =
                      DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
                      new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

                      static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));



                      The dependency properties in Arc.cs all have a comment, but the constructor doesn't. IMO the constructor is the harder of the two to understand: if someone knows enough WPF to understand that without a comment, they don't need the comments on the dependency properties.





                       double startAngleRadians = StartAngle * Math.PI / 180;
                      double endAngleRadians = EndAngle * Math.PI / 180;

                      double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
                      double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

                      if (a1 < a0)
                      a1 += Math.PI * 2;

                      SweepDirection d = SweepDirection.Counterclockwise;
                      bool large;

                      if (SmallAngle)

                      large = false;
                      double t = a1;
                      d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;

                      else
                      large = (Math.Abs(a1 - a0) < Math.PI);



                      I could definitely use some comments to explain what's going on here. The last line in particular looks very counterintuitive.



                      Also there's a dead line: t isn't used anywhere.





                       <Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
                      <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />



                      Not very localisable...





                       _timer.Start();
                      _timer.Tick += Timer_Tick;



                      It's unlikely to occur in practice, but technically you've got a race condition there. I see only an advantage in switching the order of the two lines.





                      ... For setting timeout I've added Seconds dependency property. I'm using binding Content="Binding Seconds" to display seconds. Animation duration is
                      set in code behind



                      Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));


                      because I'm not sure if it's possible to do it in XAML without writing
                      custom converter. I think that creating custom converter is not
                      justified here.




                      I think you can do it without a custom converter by making the binding property a TimeSpan (changing the name from Seconds to something like TimeRemaining) and using StringFormat in the binding for the label content. I haven't tested this.



                      If you do this then you might want to handle non-integer TimeSpan.TotalSeconds values by rounding to the nearest second in the setter.






                      share|improve this answer





















                      • Regarding "Not very localisable…" if I would need localization I'll just add something like Content="x:Static p:Resources.Sec". I'm using Resx-files based localization, don't know if it's the best option, but didn't find good alternatives, what do you think? But I assume adding this piece of code to the question would be redundant.
                        – Vadim Ovchinnikov
                        Jun 25 at 19:54










                      • I haven't found any good standard l10n approach for WPF. I ended up doing a DIY approach with a custom converter and various ugly hacks. If you've thought about the issue and made a decision that YAGNI then that's fair enough.
                        – Peter Taylor
                        Jun 25 at 21:11












                      up vote
                      10
                      down vote










                      up vote
                      10
                      down vote









                      There are a couple of things which could be more consistent.




                       public static readonly DependencyProperty StartAngleProperty =
                      DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
                      new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));






                       public static readonly DependencyProperty SecondsProperty =
                      DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));



                      The dependency property in Countdown.xaml.cs uses nameof, but the ones in Arc.cs don't. IMO they should.





                       // Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
                      public static readonly DependencyProperty SmallAngleProperty =
                      DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
                      new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

                      static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));



                      The dependency properties in Arc.cs all have a comment, but the constructor doesn't. IMO the constructor is the harder of the two to understand: if someone knows enough WPF to understand that without a comment, they don't need the comments on the dependency properties.





                       double startAngleRadians = StartAngle * Math.PI / 180;
                      double endAngleRadians = EndAngle * Math.PI / 180;

                      double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
                      double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

                      if (a1 < a0)
                      a1 += Math.PI * 2;

                      SweepDirection d = SweepDirection.Counterclockwise;
                      bool large;

                      if (SmallAngle)

                      large = false;
                      double t = a1;
                      d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;

                      else
                      large = (Math.Abs(a1 - a0) < Math.PI);



                      I could definitely use some comments to explain what's going on here. The last line in particular looks very counterintuitive.



                      Also there's a dead line: t isn't used anywhere.





                       <Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
                      <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />



                      Not very localisable...





                       _timer.Start();
                      _timer.Tick += Timer_Tick;



                      It's unlikely to occur in practice, but technically you've got a race condition there. I see only an advantage in switching the order of the two lines.





                      ... For setting timeout I've added Seconds dependency property. I'm using binding Content="Binding Seconds" to display seconds. Animation duration is
                      set in code behind



                      Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));


                      because I'm not sure if it's possible to do it in XAML without writing
                      custom converter. I think that creating custom converter is not
                      justified here.




                      I think you can do it without a custom converter by making the binding property a TimeSpan (changing the name from Seconds to something like TimeRemaining) and using StringFormat in the binding for the label content. I haven't tested this.



                      If you do this then you might want to handle non-integer TimeSpan.TotalSeconds values by rounding to the nearest second in the setter.






                      share|improve this answer













                      There are a couple of things which could be more consistent.




                       public static readonly DependencyProperty StartAngleProperty =
                      DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
                      new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));






                       public static readonly DependencyProperty SecondsProperty =
                      DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));



                      The dependency property in Countdown.xaml.cs uses nameof, but the ones in Arc.cs don't. IMO they should.





                       // Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
                      public static readonly DependencyProperty SmallAngleProperty =
                      DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
                      new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

                      static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));



                      The dependency properties in Arc.cs all have a comment, but the constructor doesn't. IMO the constructor is the harder of the two to understand: if someone knows enough WPF to understand that without a comment, they don't need the comments on the dependency properties.





                       double startAngleRadians = StartAngle * Math.PI / 180;
                      double endAngleRadians = EndAngle * Math.PI / 180;

                      double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
                      double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

                      if (a1 < a0)
                      a1 += Math.PI * 2;

                      SweepDirection d = SweepDirection.Counterclockwise;
                      bool large;

                      if (SmallAngle)

                      large = false;
                      double t = a1;
                      d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;

                      else
                      large = (Math.Abs(a1 - a0) < Math.PI);



                      I could definitely use some comments to explain what's going on here. The last line in particular looks very counterintuitive.



                      Also there's a dead line: t isn't used anywhere.





                       <Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
                      <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />



                      Not very localisable...





                       _timer.Start();
                      _timer.Tick += Timer_Tick;



                      It's unlikely to occur in practice, but technically you've got a race condition there. I see only an advantage in switching the order of the two lines.





                      ... For setting timeout I've added Seconds dependency property. I'm using binding Content="Binding Seconds" to display seconds. Animation duration is
                      set in code behind



                      Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));


                      because I'm not sure if it's possible to do it in XAML without writing
                      custom converter. I think that creating custom converter is not
                      justified here.




                      I think you can do it without a custom converter by making the binding property a TimeSpan (changing the name from Seconds to something like TimeRemaining) and using StringFormat in the binding for the label content. I haven't tested this.



                      If you do this then you might want to handle non-integer TimeSpan.TotalSeconds values by rounding to the nearest second in the setter.







                      share|improve this answer













                      share|improve this answer



                      share|improve this answer











                      answered Jun 25 at 10:22









                      Peter Taylor

                      14k2454




                      14k2454











                      • Regarding "Not very localisable…" if I would need localization I'll just add something like Content="x:Static p:Resources.Sec". I'm using Resx-files based localization, don't know if it's the best option, but didn't find good alternatives, what do you think? But I assume adding this piece of code to the question would be redundant.
                        – Vadim Ovchinnikov
                        Jun 25 at 19:54










                      • I haven't found any good standard l10n approach for WPF. I ended up doing a DIY approach with a custom converter and various ugly hacks. If you've thought about the issue and made a decision that YAGNI then that's fair enough.
                        – Peter Taylor
                        Jun 25 at 21:11
















                      • Regarding "Not very localisable…" if I would need localization I'll just add something like Content="x:Static p:Resources.Sec". I'm using Resx-files based localization, don't know if it's the best option, but didn't find good alternatives, what do you think? But I assume adding this piece of code to the question would be redundant.
                        – Vadim Ovchinnikov
                        Jun 25 at 19:54










                      • I haven't found any good standard l10n approach for WPF. I ended up doing a DIY approach with a custom converter and various ugly hacks. If you've thought about the issue and made a decision that YAGNI then that's fair enough.
                        – Peter Taylor
                        Jun 25 at 21:11















                      Regarding "Not very localisable…" if I would need localization I'll just add something like Content="x:Static p:Resources.Sec". I'm using Resx-files based localization, don't know if it's the best option, but didn't find good alternatives, what do you think? But I assume adding this piece of code to the question would be redundant.
                      – Vadim Ovchinnikov
                      Jun 25 at 19:54




                      Regarding "Not very localisable…" if I would need localization I'll just add something like Content="x:Static p:Resources.Sec". I'm using Resx-files based localization, don't know if it's the best option, but didn't find good alternatives, what do you think? But I assume adding this piece of code to the question would be redundant.
                      – Vadim Ovchinnikov
                      Jun 25 at 19:54












                      I haven't found any good standard l10n approach for WPF. I ended up doing a DIY approach with a custom converter and various ugly hacks. If you've thought about the issue and made a decision that YAGNI then that's fair enough.
                      – Peter Taylor
                      Jun 25 at 21:11




                      I haven't found any good standard l10n approach for WPF. I ended up doing a DIY approach with a custom converter and various ugly hacks. If you've thought about the issue and made a decision that YAGNI then that's fair enough.
                      – Peter Taylor
                      Jun 25 at 21:11










                      up vote
                      7
                      down vote













                      1) DispatcherTimer is good for periodic UI updates, but it should not be used to measure time, because it is just not accurate enough. For precise time measurement you have to use Stopwatch class inside the timer callback.



                      2) Seconds property apparently does two things: it starts as "countdown duration" but after control is loaded it acts as "remaining time". I would use two properties here, so that they can be databound separately when necessary.



                      3) DataContext = this; - don't set DataContext on public re-usable types. Someone (yourself included) can eventually decide to use this class in MVVM environment, but changing DataContext of your class will break it. Internally, you should use RelativeSource or ElementName properties of Binding class or DependencyProperty callbacks to do your bidding, while leaving DataContext empty.



                      Here is an example of using ElementName:



                      <UserControl x:Class="WpfApp3.Countdown"
                      ...
                      x:Name="this">

                      ...
                      <!-- Content binds directly to Countdown.Seconds dependency property, DataContext is ignored -->
                      <Label Content="Binding Seconds, ElementName=this" />
                      ...
                      </UserControl>





                      share|improve this answer























                      • Regarding last point: suppose I want to move my properties from control class to some view model class. When I have DataContext assigned I just need to change DataContext stored reference, but when I don't have one I need to rewrite all my bindings. So I'm not sure if using DataContext is bad idea here. Am I missing something?
                        – Vadim Ovchinnikov
                        Jun 27 at 6:52










                      • @VadimOvchinnikov, I'm not sure I follow. Why do you want to move the properties? Just leave them where they are. For MVVM to work, you need to have a property on each side: one property on your view, and another property on your viewmodel. Then you can bind them by setting viewmodel as datacontext: <Countdown Duration=Binding DurationPropertyOfViewModel DataContext=Binding ViewModel/>
                        – Nikita B
                        Jun 27 at 8:35














                      up vote
                      7
                      down vote













                      1) DispatcherTimer is good for periodic UI updates, but it should not be used to measure time, because it is just not accurate enough. For precise time measurement you have to use Stopwatch class inside the timer callback.



                      2) Seconds property apparently does two things: it starts as "countdown duration" but after control is loaded it acts as "remaining time". I would use two properties here, so that they can be databound separately when necessary.



                      3) DataContext = this; - don't set DataContext on public re-usable types. Someone (yourself included) can eventually decide to use this class in MVVM environment, but changing DataContext of your class will break it. Internally, you should use RelativeSource or ElementName properties of Binding class or DependencyProperty callbacks to do your bidding, while leaving DataContext empty.



                      Here is an example of using ElementName:



                      <UserControl x:Class="WpfApp3.Countdown"
                      ...
                      x:Name="this">

                      ...
                      <!-- Content binds directly to Countdown.Seconds dependency property, DataContext is ignored -->
                      <Label Content="Binding Seconds, ElementName=this" />
                      ...
                      </UserControl>





                      share|improve this answer























                      • Regarding last point: suppose I want to move my properties from control class to some view model class. When I have DataContext assigned I just need to change DataContext stored reference, but when I don't have one I need to rewrite all my bindings. So I'm not sure if using DataContext is bad idea here. Am I missing something?
                        – Vadim Ovchinnikov
                        Jun 27 at 6:52










                      • @VadimOvchinnikov, I'm not sure I follow. Why do you want to move the properties? Just leave them where they are. For MVVM to work, you need to have a property on each side: one property on your view, and another property on your viewmodel. Then you can bind them by setting viewmodel as datacontext: <Countdown Duration=Binding DurationPropertyOfViewModel DataContext=Binding ViewModel/>
                        – Nikita B
                        Jun 27 at 8:35












                      up vote
                      7
                      down vote










                      up vote
                      7
                      down vote









                      1) DispatcherTimer is good for periodic UI updates, but it should not be used to measure time, because it is just not accurate enough. For precise time measurement you have to use Stopwatch class inside the timer callback.



                      2) Seconds property apparently does two things: it starts as "countdown duration" but after control is loaded it acts as "remaining time". I would use two properties here, so that they can be databound separately when necessary.



                      3) DataContext = this; - don't set DataContext on public re-usable types. Someone (yourself included) can eventually decide to use this class in MVVM environment, but changing DataContext of your class will break it. Internally, you should use RelativeSource or ElementName properties of Binding class or DependencyProperty callbacks to do your bidding, while leaving DataContext empty.



                      Here is an example of using ElementName:



                      <UserControl x:Class="WpfApp3.Countdown"
                      ...
                      x:Name="this">

                      ...
                      <!-- Content binds directly to Countdown.Seconds dependency property, DataContext is ignored -->
                      <Label Content="Binding Seconds, ElementName=this" />
                      ...
                      </UserControl>





                      share|improve this answer















                      1) DispatcherTimer is good for periodic UI updates, but it should not be used to measure time, because it is just not accurate enough. For precise time measurement you have to use Stopwatch class inside the timer callback.



                      2) Seconds property apparently does two things: it starts as "countdown duration" but after control is loaded it acts as "remaining time". I would use two properties here, so that they can be databound separately when necessary.



                      3) DataContext = this; - don't set DataContext on public re-usable types. Someone (yourself included) can eventually decide to use this class in MVVM environment, but changing DataContext of your class will break it. Internally, you should use RelativeSource or ElementName properties of Binding class or DependencyProperty callbacks to do your bidding, while leaving DataContext empty.



                      Here is an example of using ElementName:



                      <UserControl x:Class="WpfApp3.Countdown"
                      ...
                      x:Name="this">

                      ...
                      <!-- Content binds directly to Countdown.Seconds dependency property, DataContext is ignored -->
                      <Label Content="Binding Seconds, ElementName=this" />
                      ...
                      </UserControl>






                      share|improve this answer















                      share|improve this answer



                      share|improve this answer








                      edited Jun 25 at 12:55


























                      answered Jun 25 at 11:28









                      Nikita B

                      12.3k11551




                      12.3k11551











                      • Regarding last point: suppose I want to move my properties from control class to some view model class. When I have DataContext assigned I just need to change DataContext stored reference, but when I don't have one I need to rewrite all my bindings. So I'm not sure if using DataContext is bad idea here. Am I missing something?
                        – Vadim Ovchinnikov
                        Jun 27 at 6:52










                      • @VadimOvchinnikov, I'm not sure I follow. Why do you want to move the properties? Just leave them where they are. For MVVM to work, you need to have a property on each side: one property on your view, and another property on your viewmodel. Then you can bind them by setting viewmodel as datacontext: <Countdown Duration=Binding DurationPropertyOfViewModel DataContext=Binding ViewModel/>
                        – Nikita B
                        Jun 27 at 8:35
















                      • Regarding last point: suppose I want to move my properties from control class to some view model class. When I have DataContext assigned I just need to change DataContext stored reference, but when I don't have one I need to rewrite all my bindings. So I'm not sure if using DataContext is bad idea here. Am I missing something?
                        – Vadim Ovchinnikov
                        Jun 27 at 6:52










                      • @VadimOvchinnikov, I'm not sure I follow. Why do you want to move the properties? Just leave them where they are. For MVVM to work, you need to have a property on each side: one property on your view, and another property on your viewmodel. Then you can bind them by setting viewmodel as datacontext: <Countdown Duration=Binding DurationPropertyOfViewModel DataContext=Binding ViewModel/>
                        – Nikita B
                        Jun 27 at 8:35















                      Regarding last point: suppose I want to move my properties from control class to some view model class. When I have DataContext assigned I just need to change DataContext stored reference, but when I don't have one I need to rewrite all my bindings. So I'm not sure if using DataContext is bad idea here. Am I missing something?
                      – Vadim Ovchinnikov
                      Jun 27 at 6:52




                      Regarding last point: suppose I want to move my properties from control class to some view model class. When I have DataContext assigned I just need to change DataContext stored reference, but when I don't have one I need to rewrite all my bindings. So I'm not sure if using DataContext is bad idea here. Am I missing something?
                      – Vadim Ovchinnikov
                      Jun 27 at 6:52












                      @VadimOvchinnikov, I'm not sure I follow. Why do you want to move the properties? Just leave them where they are. For MVVM to work, you need to have a property on each side: one property on your view, and another property on your viewmodel. Then you can bind them by setting viewmodel as datacontext: <Countdown Duration=Binding DurationPropertyOfViewModel DataContext=Binding ViewModel/>
                      – Nikita B
                      Jun 27 at 8:35




                      @VadimOvchinnikov, I'm not sure I follow. Why do you want to move the properties? Just leave them where they are. For MVVM to work, you need to have a property on each side: one property on your view, and another property on your viewmodel. Then you can bind them by setting viewmodel as datacontext: <Countdown Duration=Binding DurationPropertyOfViewModel DataContext=Binding ViewModel/>
                      – Nikita B
                      Jun 27 at 8:35










                      up vote
                      3
                      down vote













                      Incorporated nearly all recommendations to my solution, but the most radical was accepted answer. Any feedback very welcome!



                      Also added public Start and Stop methods and Elapsed event.



                      Results:



                      Arc.cs



                      public class Arc : Shape

                      public Point Center

                      get => (Point)GetValue(CenterProperty);
                      set => SetValue(CenterProperty, value);


                      public static readonly DependencyProperty CenterProperty =
                      DependencyProperty.Register(nameof(Center), typeof(Point), typeof(Arc),
                      new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));

                      public double StartAngle

                      get => (double)GetValue(StartAngleProperty);
                      set => SetValue(StartAngleProperty, value);


                      public static readonly DependencyProperty StartAngleProperty =
                      DependencyProperty.Register(nameof(StartAngle), typeof(double), typeof(Arc),
                      new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));

                      public double EndAngle

                      get => (double)GetValue(EndAngleProperty);
                      set => SetValue(EndAngleProperty, value);


                      public static readonly DependencyProperty EndAngleProperty =
                      DependencyProperty.Register(nameof(EndAngle), typeof(double), typeof(Arc),
                      new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));

                      public double Radius

                      get => (double)GetValue(RadiusProperty);
                      set => SetValue(RadiusProperty, value);


                      public static readonly DependencyProperty RadiusProperty =
                      DependencyProperty.Register(nameof(Radius), typeof(double), typeof(Arc),
                      new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));

                      public bool SmallAngle

                      get => (bool)GetValue(SmallAngleProperty);
                      set => SetValue(SmallAngleProperty, value);


                      public static readonly DependencyProperty SmallAngleProperty =
                      DependencyProperty.Register(nameof(SmallAngle), typeof(bool), typeof(Arc),
                      new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

                      static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));

                      protected override Geometry DefiningGeometry

                      get

                      double startAngleRadians = StartAngle * Math.PI / 180;
                      double endAngleRadians = EndAngle * Math.PI / 180;

                      double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
                      double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

                      if (a1 < a0)
                      a1 += Math.PI * 2;

                      SweepDirection d = SweepDirection.Counterclockwise;
                      bool large;

                      if (SmallAngle)

                      large = false;
                      d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;

                      else
                      large = (Math.Abs(a1 - a0) < Math.PI);

                      Point p0 = Center + new Vector(Math.Cos(a0), Math.Sin(a0)) * Radius;
                      Point p1 = Center + new Vector(Math.Cos(a1), Math.Sin(a1)) * Radius;

                      List<PathSegment> segments = new List<PathSegment>

                      new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true)
                      ;

                      List<PathFigure> figures = new List<PathFigure>

                      new PathFigure(p0, segments, true)

                      IsClosed = false

                      ;

                      return new PathGeometry(figures, FillRule.EvenOdd, null);





                      Countdown.xaml



                      <UserControl x:Class="WpfApp.Countdown"
                      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                      xmlns:local="clr-namespace:WpfApp"
                      mc:Ignorable="d"
                      d:DesignHeight="450" d:DesignWidth="450" Loaded="Countdown_Loaded">
                      <Viewbox>
                      <Grid Width="100" Height="100">
                      <Border Background="#222" Margin="5" CornerRadius="50">
                      <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
                      <Label Foreground="#fff" Content="Binding SecondsRemaining" FontSize="50" Margin="0, -10, 0, 0" />
                      <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -15, 0, 0" />
                      </StackPanel>
                      </Border>

                      <uc:Arc
                      x:Name="Arc"
                      Center="50, 50"
                      StartAngle="-90"
                      EndAngle="-90"
                      Stroke="#45d3be"
                      StrokeThickness="5"
                      Radius="45" />
                      </Grid>
                      </Viewbox>
                      </UserControl>


                      Countdown.xaml.cs



                      public partial class Countdown : UserControl

                      public Duration Duration

                      get => (Duration)GetValue(DurationProperty);
                      set => SetValue(DurationProperty, value);


                      public static readonly DependencyProperty DurationProperty =
                      DependencyProperty.Register(nameof(Duration), typeof(Duration), typeof(Countdown), new PropertyMetadata(new Duration()));

                      public int SecondsRemaining

                      get => (int)GetValue(SecondsRemainingProperty);
                      set => SetValue(SecondsRemainingProperty, value);


                      public static readonly DependencyProperty SecondsRemainingProperty =
                      DependencyProperty.Register(nameof(SecondsRemaining), typeof(int), typeof(Countdown), new PropertyMetadata(0));

                      public event EventHandler Elapsed;

                      private readonly Storyboard _storyboard = new Storyboard();

                      public Countdown()

                      InitializeComponent();

                      DoubleAnimation animation = new DoubleAnimation(-90, 270, Duration);
                      Storyboard.SetTarget(animation, Arc);
                      Storyboard.SetTargetProperty(animation, new PropertyPath(nameof(Arc.EndAngle)));
                      _storyboard.Children.Add(animation);

                      DataContext = this;


                      private void Countdown_Loaded(object sender, EventArgs e)

                      if (IsVisible)
                      Start();


                      public void Start()

                      Stop();

                      _storyboard.CurrentTimeInvalidated += Storyboard_CurrentTimeInvalidated;
                      _storyboard.Completed += Storyboard_Completed;

                      _storyboard.Begin();


                      public void Stop()

                      _storyboard.CurrentTimeInvalidated -= Storyboard_CurrentTimeInvalidated;
                      _storyboard.Completed -= Storyboard_Completed;

                      _storyboard.Stop();


                      private void Storyboard_CurrentTimeInvalidated(object sender, EventArgs e)

                      ClockGroup cg = (ClockGroup)sender;
                      if (cg.CurrentTime == null) return;
                      TimeSpan elapsedTime = cg.CurrentTime.Value;
                      SecondsRemaining = (int)Math.Ceiling((Duration.TimeSpan - elapsedTime).TotalSeconds);


                      private void Storyboard_Completed(object sender, EventArgs e)

                      if (IsVisible)
                      Elapsed?.Invoke(this, EventArgs.Empty);




                      Example of usage:



                      <local:Countdown Width="300" Height="300" Duration="0:0:15" />





                      share|improve this answer



























                        up vote
                        3
                        down vote













                        Incorporated nearly all recommendations to my solution, but the most radical was accepted answer. Any feedback very welcome!



                        Also added public Start and Stop methods and Elapsed event.



                        Results:



                        Arc.cs



                        public class Arc : Shape

                        public Point Center

                        get => (Point)GetValue(CenterProperty);
                        set => SetValue(CenterProperty, value);


                        public static readonly DependencyProperty CenterProperty =
                        DependencyProperty.Register(nameof(Center), typeof(Point), typeof(Arc),
                        new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));

                        public double StartAngle

                        get => (double)GetValue(StartAngleProperty);
                        set => SetValue(StartAngleProperty, value);


                        public static readonly DependencyProperty StartAngleProperty =
                        DependencyProperty.Register(nameof(StartAngle), typeof(double), typeof(Arc),
                        new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));

                        public double EndAngle

                        get => (double)GetValue(EndAngleProperty);
                        set => SetValue(EndAngleProperty, value);


                        public static readonly DependencyProperty EndAngleProperty =
                        DependencyProperty.Register(nameof(EndAngle), typeof(double), typeof(Arc),
                        new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));

                        public double Radius

                        get => (double)GetValue(RadiusProperty);
                        set => SetValue(RadiusProperty, value);


                        public static readonly DependencyProperty RadiusProperty =
                        DependencyProperty.Register(nameof(Radius), typeof(double), typeof(Arc),
                        new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));

                        public bool SmallAngle

                        get => (bool)GetValue(SmallAngleProperty);
                        set => SetValue(SmallAngleProperty, value);


                        public static readonly DependencyProperty SmallAngleProperty =
                        DependencyProperty.Register(nameof(SmallAngle), typeof(bool), typeof(Arc),
                        new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

                        static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));

                        protected override Geometry DefiningGeometry

                        get

                        double startAngleRadians = StartAngle * Math.PI / 180;
                        double endAngleRadians = EndAngle * Math.PI / 180;

                        double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
                        double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

                        if (a1 < a0)
                        a1 += Math.PI * 2;

                        SweepDirection d = SweepDirection.Counterclockwise;
                        bool large;

                        if (SmallAngle)

                        large = false;
                        d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;

                        else
                        large = (Math.Abs(a1 - a0) < Math.PI);

                        Point p0 = Center + new Vector(Math.Cos(a0), Math.Sin(a0)) * Radius;
                        Point p1 = Center + new Vector(Math.Cos(a1), Math.Sin(a1)) * Radius;

                        List<PathSegment> segments = new List<PathSegment>

                        new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true)
                        ;

                        List<PathFigure> figures = new List<PathFigure>

                        new PathFigure(p0, segments, true)

                        IsClosed = false

                        ;

                        return new PathGeometry(figures, FillRule.EvenOdd, null);





                        Countdown.xaml



                        <UserControl x:Class="WpfApp.Countdown"
                        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                        xmlns:local="clr-namespace:WpfApp"
                        mc:Ignorable="d"
                        d:DesignHeight="450" d:DesignWidth="450" Loaded="Countdown_Loaded">
                        <Viewbox>
                        <Grid Width="100" Height="100">
                        <Border Background="#222" Margin="5" CornerRadius="50">
                        <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
                        <Label Foreground="#fff" Content="Binding SecondsRemaining" FontSize="50" Margin="0, -10, 0, 0" />
                        <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -15, 0, 0" />
                        </StackPanel>
                        </Border>

                        <uc:Arc
                        x:Name="Arc"
                        Center="50, 50"
                        StartAngle="-90"
                        EndAngle="-90"
                        Stroke="#45d3be"
                        StrokeThickness="5"
                        Radius="45" />
                        </Grid>
                        </Viewbox>
                        </UserControl>


                        Countdown.xaml.cs



                        public partial class Countdown : UserControl

                        public Duration Duration

                        get => (Duration)GetValue(DurationProperty);
                        set => SetValue(DurationProperty, value);


                        public static readonly DependencyProperty DurationProperty =
                        DependencyProperty.Register(nameof(Duration), typeof(Duration), typeof(Countdown), new PropertyMetadata(new Duration()));

                        public int SecondsRemaining

                        get => (int)GetValue(SecondsRemainingProperty);
                        set => SetValue(SecondsRemainingProperty, value);


                        public static readonly DependencyProperty SecondsRemainingProperty =
                        DependencyProperty.Register(nameof(SecondsRemaining), typeof(int), typeof(Countdown), new PropertyMetadata(0));

                        public event EventHandler Elapsed;

                        private readonly Storyboard _storyboard = new Storyboard();

                        public Countdown()

                        InitializeComponent();

                        DoubleAnimation animation = new DoubleAnimation(-90, 270, Duration);
                        Storyboard.SetTarget(animation, Arc);
                        Storyboard.SetTargetProperty(animation, new PropertyPath(nameof(Arc.EndAngle)));
                        _storyboard.Children.Add(animation);

                        DataContext = this;


                        private void Countdown_Loaded(object sender, EventArgs e)

                        if (IsVisible)
                        Start();


                        public void Start()

                        Stop();

                        _storyboard.CurrentTimeInvalidated += Storyboard_CurrentTimeInvalidated;
                        _storyboard.Completed += Storyboard_Completed;

                        _storyboard.Begin();


                        public void Stop()

                        _storyboard.CurrentTimeInvalidated -= Storyboard_CurrentTimeInvalidated;
                        _storyboard.Completed -= Storyboard_Completed;

                        _storyboard.Stop();


                        private void Storyboard_CurrentTimeInvalidated(object sender, EventArgs e)

                        ClockGroup cg = (ClockGroup)sender;
                        if (cg.CurrentTime == null) return;
                        TimeSpan elapsedTime = cg.CurrentTime.Value;
                        SecondsRemaining = (int)Math.Ceiling((Duration.TimeSpan - elapsedTime).TotalSeconds);


                        private void Storyboard_Completed(object sender, EventArgs e)

                        if (IsVisible)
                        Elapsed?.Invoke(this, EventArgs.Empty);




                        Example of usage:



                        <local:Countdown Width="300" Height="300" Duration="0:0:15" />





                        share|improve this answer

























                          up vote
                          3
                          down vote










                          up vote
                          3
                          down vote









                          Incorporated nearly all recommendations to my solution, but the most radical was accepted answer. Any feedback very welcome!



                          Also added public Start and Stop methods and Elapsed event.



                          Results:



                          Arc.cs



                          public class Arc : Shape

                          public Point Center

                          get => (Point)GetValue(CenterProperty);
                          set => SetValue(CenterProperty, value);


                          public static readonly DependencyProperty CenterProperty =
                          DependencyProperty.Register(nameof(Center), typeof(Point), typeof(Arc),
                          new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));

                          public double StartAngle

                          get => (double)GetValue(StartAngleProperty);
                          set => SetValue(StartAngleProperty, value);


                          public static readonly DependencyProperty StartAngleProperty =
                          DependencyProperty.Register(nameof(StartAngle), typeof(double), typeof(Arc),
                          new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));

                          public double EndAngle

                          get => (double)GetValue(EndAngleProperty);
                          set => SetValue(EndAngleProperty, value);


                          public static readonly DependencyProperty EndAngleProperty =
                          DependencyProperty.Register(nameof(EndAngle), typeof(double), typeof(Arc),
                          new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));

                          public double Radius

                          get => (double)GetValue(RadiusProperty);
                          set => SetValue(RadiusProperty, value);


                          public static readonly DependencyProperty RadiusProperty =
                          DependencyProperty.Register(nameof(Radius), typeof(double), typeof(Arc),
                          new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));

                          public bool SmallAngle

                          get => (bool)GetValue(SmallAngleProperty);
                          set => SetValue(SmallAngleProperty, value);


                          public static readonly DependencyProperty SmallAngleProperty =
                          DependencyProperty.Register(nameof(SmallAngle), typeof(bool), typeof(Arc),
                          new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

                          static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));

                          protected override Geometry DefiningGeometry

                          get

                          double startAngleRadians = StartAngle * Math.PI / 180;
                          double endAngleRadians = EndAngle * Math.PI / 180;

                          double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
                          double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

                          if (a1 < a0)
                          a1 += Math.PI * 2;

                          SweepDirection d = SweepDirection.Counterclockwise;
                          bool large;

                          if (SmallAngle)

                          large = false;
                          d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;

                          else
                          large = (Math.Abs(a1 - a0) < Math.PI);

                          Point p0 = Center + new Vector(Math.Cos(a0), Math.Sin(a0)) * Radius;
                          Point p1 = Center + new Vector(Math.Cos(a1), Math.Sin(a1)) * Radius;

                          List<PathSegment> segments = new List<PathSegment>

                          new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true)
                          ;

                          List<PathFigure> figures = new List<PathFigure>

                          new PathFigure(p0, segments, true)

                          IsClosed = false

                          ;

                          return new PathGeometry(figures, FillRule.EvenOdd, null);





                          Countdown.xaml



                          <UserControl x:Class="WpfApp.Countdown"
                          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                          xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                          xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                          xmlns:local="clr-namespace:WpfApp"
                          mc:Ignorable="d"
                          d:DesignHeight="450" d:DesignWidth="450" Loaded="Countdown_Loaded">
                          <Viewbox>
                          <Grid Width="100" Height="100">
                          <Border Background="#222" Margin="5" CornerRadius="50">
                          <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
                          <Label Foreground="#fff" Content="Binding SecondsRemaining" FontSize="50" Margin="0, -10, 0, 0" />
                          <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -15, 0, 0" />
                          </StackPanel>
                          </Border>

                          <uc:Arc
                          x:Name="Arc"
                          Center="50, 50"
                          StartAngle="-90"
                          EndAngle="-90"
                          Stroke="#45d3be"
                          StrokeThickness="5"
                          Radius="45" />
                          </Grid>
                          </Viewbox>
                          </UserControl>


                          Countdown.xaml.cs



                          public partial class Countdown : UserControl

                          public Duration Duration

                          get => (Duration)GetValue(DurationProperty);
                          set => SetValue(DurationProperty, value);


                          public static readonly DependencyProperty DurationProperty =
                          DependencyProperty.Register(nameof(Duration), typeof(Duration), typeof(Countdown), new PropertyMetadata(new Duration()));

                          public int SecondsRemaining

                          get => (int)GetValue(SecondsRemainingProperty);
                          set => SetValue(SecondsRemainingProperty, value);


                          public static readonly DependencyProperty SecondsRemainingProperty =
                          DependencyProperty.Register(nameof(SecondsRemaining), typeof(int), typeof(Countdown), new PropertyMetadata(0));

                          public event EventHandler Elapsed;

                          private readonly Storyboard _storyboard = new Storyboard();

                          public Countdown()

                          InitializeComponent();

                          DoubleAnimation animation = new DoubleAnimation(-90, 270, Duration);
                          Storyboard.SetTarget(animation, Arc);
                          Storyboard.SetTargetProperty(animation, new PropertyPath(nameof(Arc.EndAngle)));
                          _storyboard.Children.Add(animation);

                          DataContext = this;


                          private void Countdown_Loaded(object sender, EventArgs e)

                          if (IsVisible)
                          Start();


                          public void Start()

                          Stop();

                          _storyboard.CurrentTimeInvalidated += Storyboard_CurrentTimeInvalidated;
                          _storyboard.Completed += Storyboard_Completed;

                          _storyboard.Begin();


                          public void Stop()

                          _storyboard.CurrentTimeInvalidated -= Storyboard_CurrentTimeInvalidated;
                          _storyboard.Completed -= Storyboard_Completed;

                          _storyboard.Stop();


                          private void Storyboard_CurrentTimeInvalidated(object sender, EventArgs e)

                          ClockGroup cg = (ClockGroup)sender;
                          if (cg.CurrentTime == null) return;
                          TimeSpan elapsedTime = cg.CurrentTime.Value;
                          SecondsRemaining = (int)Math.Ceiling((Duration.TimeSpan - elapsedTime).TotalSeconds);


                          private void Storyboard_Completed(object sender, EventArgs e)

                          if (IsVisible)
                          Elapsed?.Invoke(this, EventArgs.Empty);




                          Example of usage:



                          <local:Countdown Width="300" Height="300" Duration="0:0:15" />





                          share|improve this answer















                          Incorporated nearly all recommendations to my solution, but the most radical was accepted answer. Any feedback very welcome!



                          Also added public Start and Stop methods and Elapsed event.



                          Results:



                          Arc.cs



                          public class Arc : Shape

                          public Point Center

                          get => (Point)GetValue(CenterProperty);
                          set => SetValue(CenterProperty, value);


                          public static readonly DependencyProperty CenterProperty =
                          DependencyProperty.Register(nameof(Center), typeof(Point), typeof(Arc),
                          new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));

                          public double StartAngle

                          get => (double)GetValue(StartAngleProperty);
                          set => SetValue(StartAngleProperty, value);


                          public static readonly DependencyProperty StartAngleProperty =
                          DependencyProperty.Register(nameof(StartAngle), typeof(double), typeof(Arc),
                          new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));

                          public double EndAngle

                          get => (double)GetValue(EndAngleProperty);
                          set => SetValue(EndAngleProperty, value);


                          public static readonly DependencyProperty EndAngleProperty =
                          DependencyProperty.Register(nameof(EndAngle), typeof(double), typeof(Arc),
                          new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));

                          public double Radius

                          get => (double)GetValue(RadiusProperty);
                          set => SetValue(RadiusProperty, value);


                          public static readonly DependencyProperty RadiusProperty =
                          DependencyProperty.Register(nameof(Radius), typeof(double), typeof(Arc),
                          new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));

                          public bool SmallAngle

                          get => (bool)GetValue(SmallAngleProperty);
                          set => SetValue(SmallAngleProperty, value);


                          public static readonly DependencyProperty SmallAngleProperty =
                          DependencyProperty.Register(nameof(SmallAngle), typeof(bool), typeof(Arc),
                          new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

                          static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));

                          protected override Geometry DefiningGeometry

                          get

                          double startAngleRadians = StartAngle * Math.PI / 180;
                          double endAngleRadians = EndAngle * Math.PI / 180;

                          double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
                          double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

                          if (a1 < a0)
                          a1 += Math.PI * 2;

                          SweepDirection d = SweepDirection.Counterclockwise;
                          bool large;

                          if (SmallAngle)

                          large = false;
                          d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;

                          else
                          large = (Math.Abs(a1 - a0) < Math.PI);

                          Point p0 = Center + new Vector(Math.Cos(a0), Math.Sin(a0)) * Radius;
                          Point p1 = Center + new Vector(Math.Cos(a1), Math.Sin(a1)) * Radius;

                          List<PathSegment> segments = new List<PathSegment>

                          new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true)
                          ;

                          List<PathFigure> figures = new List<PathFigure>

                          new PathFigure(p0, segments, true)

                          IsClosed = false

                          ;

                          return new PathGeometry(figures, FillRule.EvenOdd, null);





                          Countdown.xaml



                          <UserControl x:Class="WpfApp.Countdown"
                          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                          xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                          xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                          xmlns:local="clr-namespace:WpfApp"
                          mc:Ignorable="d"
                          d:DesignHeight="450" d:DesignWidth="450" Loaded="Countdown_Loaded">
                          <Viewbox>
                          <Grid Width="100" Height="100">
                          <Border Background="#222" Margin="5" CornerRadius="50">
                          <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
                          <Label Foreground="#fff" Content="Binding SecondsRemaining" FontSize="50" Margin="0, -10, 0, 0" />
                          <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -15, 0, 0" />
                          </StackPanel>
                          </Border>

                          <uc:Arc
                          x:Name="Arc"
                          Center="50, 50"
                          StartAngle="-90"
                          EndAngle="-90"
                          Stroke="#45d3be"
                          StrokeThickness="5"
                          Radius="45" />
                          </Grid>
                          </Viewbox>
                          </UserControl>


                          Countdown.xaml.cs



                          public partial class Countdown : UserControl

                          public Duration Duration

                          get => (Duration)GetValue(DurationProperty);
                          set => SetValue(DurationProperty, value);


                          public static readonly DependencyProperty DurationProperty =
                          DependencyProperty.Register(nameof(Duration), typeof(Duration), typeof(Countdown), new PropertyMetadata(new Duration()));

                          public int SecondsRemaining

                          get => (int)GetValue(SecondsRemainingProperty);
                          set => SetValue(SecondsRemainingProperty, value);


                          public static readonly DependencyProperty SecondsRemainingProperty =
                          DependencyProperty.Register(nameof(SecondsRemaining), typeof(int), typeof(Countdown), new PropertyMetadata(0));

                          public event EventHandler Elapsed;

                          private readonly Storyboard _storyboard = new Storyboard();

                          public Countdown()

                          InitializeComponent();

                          DoubleAnimation animation = new DoubleAnimation(-90, 270, Duration);
                          Storyboard.SetTarget(animation, Arc);
                          Storyboard.SetTargetProperty(animation, new PropertyPath(nameof(Arc.EndAngle)));
                          _storyboard.Children.Add(animation);

                          DataContext = this;


                          private void Countdown_Loaded(object sender, EventArgs e)

                          if (IsVisible)
                          Start();


                          public void Start()

                          Stop();

                          _storyboard.CurrentTimeInvalidated += Storyboard_CurrentTimeInvalidated;
                          _storyboard.Completed += Storyboard_Completed;

                          _storyboard.Begin();


                          public void Stop()

                          _storyboard.CurrentTimeInvalidated -= Storyboard_CurrentTimeInvalidated;
                          _storyboard.Completed -= Storyboard_Completed;

                          _storyboard.Stop();


                          private void Storyboard_CurrentTimeInvalidated(object sender, EventArgs e)

                          ClockGroup cg = (ClockGroup)sender;
                          if (cg.CurrentTime == null) return;
                          TimeSpan elapsedTime = cg.CurrentTime.Value;
                          SecondsRemaining = (int)Math.Ceiling((Duration.TimeSpan - elapsedTime).TotalSeconds);


                          private void Storyboard_Completed(object sender, EventArgs e)

                          if (IsVisible)
                          Elapsed?.Invoke(this, EventArgs.Empty);




                          Example of usage:



                          <local:Countdown Width="300" Height="300" Duration="0:0:15" />






                          share|improve this answer















                          share|improve this answer



                          share|improve this answer








                          edited Jul 16 at 17:14


























                          answered Jun 25 at 21:25









                          Vadim Ovchinnikov

                          1,0101417




                          1,0101417






















                               

                              draft saved


                              draft discarded


























                               


                              draft saved


                              draft discarded














                              StackExchange.ready(
                              function ()
                              StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f197197%2fcountdown-control-with-arc-animation%23new-answer', 'question_page');

                              );

                              Post as a guest













































































                              Popular posts from this blog

                              Greedy Best First Search implementation in Rust

                              Function to Return a JSON Like Objects Using VBA Collections and Arrays

                              C++11 CLH Lock Implementation