I am trying to use the final revision of this control from Stackexchange here:
https://codereview.stackexchange.com/questions/197197/countdown-control-with-arc-animation
When I use the code it counts down just a single second and finishes. I am not sure what issue is.
Hopefully someone can help out - thanks.
The code I am using is this:
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);
}
}
CodePudding user response:
Your control is not properly initialized. You are currently not handling the property changes of the Duration
property.
The dependency property values are applied after the control is instantiated (the constructor has returned): the XAML engine creates the element instance and then assigns the resources (e.g. a Style) and local values.
Therefore, your control will currently configure the animation (in the constructor) using the property's default Duration
value (which is Duration.Automatic
).
Generally, you must always assume that control properties are changing, e.g., via data binding or animation. To handle this scenarios you must register a dependency property changed callback - at least for every public property that has a direct impact on the behavior of the control.
SecondsRemaining
should be a read-only dependency property.You should use a
TextBlock
instead of aLabel
to display text.
To fix your issue, you must register a property changed callback for the Duration
property to update the DoubleAnimation
that depends on the value. Then store the actual DoubleAnimation
in a private property, so that you can change its Duration
on property changes:
public partial class Countdown : UserControl
{
public Duration Duration
{
get => (Duration)GetValue(DurationProperty);
set => SetValue(DurationProperty, value);
}
// Register the property changed callback
public static readonly DependencyProperty DurationProperty = DependencyProperty.Register(
nameof(Duration),
typeof(Duration),
typeof(Countdown),
new PropertyMetadata(new Duration(), OnDurationChanged));
// Store the DoubleAnimation in order to modify the Duration on property changes
private Timeline Timeline { get; set; }
public Countdown()
{
InitializeComponent();
// Store the DoubleAnimation in order to modify the Duration on property changes
this.Timeline = new DoubleAnimation(-90, 270, Duration);
Storyboard.SetTarget(this.Timeline, this.Arc);
Storyboard.SetTargetProperty(this.Timeline, new PropertyPath(nameof(Arc.EndAngle)));
_storyboard.Children.Add(this.Timeline);
DataContext = this;
}
// Handle the Duration property changes
private static void OnDurationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var this_ = d as Countdown;
this_.Timeline.Duration = (Duration)e.NewValue;
}
}