jrandomuser
jrandomuser

Reputation: 1720

Implementing & Displaying UI Timer with WPF MVVM

I have an application that displays various alarms and statuses. Some alarms when tripped are supposed to display a timer counting up from the time in which it was activated. I have read through several implementations and even questions here on SO but nothing seems to work 100%.

The following solution comes the closest. The timer displays and it does update, but it only updates 1 second about every 30 to 45 seconds. The UI Property is bound to the TimeElapsed Property, setting the TimeStarted property is what starts everything.

 public class Alarm : INotifyPropertyChanged
 {
    DispatcherTimer timer = null;
    Stopwatch stopWatch = new Stopwatch();

    public Alarm()
    {
        Application.Current.Dispatcher.Invoke(() =>
        { 
            timer = new DispatcherTimer();
            timer.Tick += timer_Tick;
            timer.Interval = new TimeSpan(0, 0, 1);

        }, DispatcherPriority.Normal); 
    }

    private TimeSpan initialDifference;
    private DateTime? timeStarted;
    public DateTime? TimeStarted
    {
        get { return timeStarted; }
        set
        {
            // If the value is new
            if (timeStarted != value)
            {
                // If timeStarted was previously null then start the new timer
                if (!timeStarted.HasValue)
                {
                    timeStarted = value;

                    // Get the initial difference between Now and TimeStarted
                    initialDifference = DateTime.Now.Subtract(TimeStarted.Value);

                    //irolTimer = new System.Threading.Timer(TickTick, null, 1000, 1000);

                    Application.Current.Dispatcher.Invoke(() =>
                    {
                        stopWatch.Start();
                        timer.Start(); 
                    }, DispatcherPriority.Normal);

                }
                // If the timeStarted had a value but now its gone (stop the timer)
                else if (timeStarted.HasValue && value == null)
                {
                    if (stopWatch.IsRunning)
                        stopWatch.Stop();

                    timeStarted = value;
                }
                // If we already have a timer going but for some reason we just received a different start time
                else if (timeStarted.HasValue && value != null)
                {
                    timeStarted = value;

                    // Change the initial difference
                    initialDifference = DateTime.Now.Subtract(TimeStarted.Value);
                } 

                OnPropertyChanged("TimeStarted");
            }
        }
    }

    private string timeElapsed = string.Empty;
    public string TimeElapsed
    {
        get
        {
            return timeElapsed;
        }
        set
        {
            timeElapsed = value;
            OnPropertyChanged("TimeElapsed");
        }
    }

    void timer_Tick(object sender, EventArgs e)
    { 
        if (stopWatch.IsRunning)
        {
            TimeSpan elapsed = stopWatch.Elapsed;
            TimeSpan total = initialDifference + elapsed;

            TimeElapsed = String.Format("{0:00}:{1:00}:{2:00}", total.Hours, total.Minutes, total.Seconds / 10); 
        }
    }

 }

Adding Dispatcher.Invoke to the Tick event causes the timer not to show at all. I've tried a few different implementations including using a regular System.Threading.Timer with no luck. The StopWatch seemed like overkill and I thought I could accomplish the same thing with the following but it caused it to stop working:

  TimeElapsed = DateTime.Now.Subtract(TimeStarted.Value).ToString("hh:mm:ss");

Here is a screenshot, I had to edit it due to security concerns but I tried to add Text back for context. The item circled in Red is the text for the timer. The Xaml is straight forward as follows:

<StackPanel DockPanel.Dock="Left">
     <TextBlock FontSize="18" Text="{Binding Path=DisplayName, IsAsync=True}" Style="{StaticResource Alarm}" Foreground="White" FontWeight="Bold"  Padding="2,2,2,2" />
     <TextBlock Text="{Binding Path=TimeElapsed, IsAsync=True}" Foreground="White" FontSize="12" FontWeight="Bold" />   
 </StackPanel>

enter image description here

Upvotes: 3

Views: 2691

Answers (1)

Adam Rhodes
Adam Rhodes

Reputation: 300

Your code should work OK. To make sure I dropped it into an empty WPF project and created a simple list of alarms with a simple data template, and it worked as expected.

One thing I did notice though is that you performed an integer division on the number of elapsed seconds, as seen here:

TimeElapsed = String.Format("{0:00}:{1:00}:{2:00}", total.Hours, total.Minutes, total.Seconds / 10);

This causes the TimeElapsed property only update once every 10 seconds, as the string doesn't change until the total seconds reaches the next factor of 10.

Upvotes: 4

Related Questions