Home > Blockchain >  WPF Countdown user control issue
WPF Countdown user control issue

Time:03-22

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 a Label 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;
  }
}
  • Related