Search code examples
c#wpfdispatchertimersystem.timers.timer

WPF application, "clock textbox" freezes after some hours


I made a simple WPF full screen and always on top window, which has a clock with dddd dd MMMM yyyy HH:mm:ss timestamp. The application is open 24/24 on a always on touch-screen, the problem is that after some time I see the clock freezed at a certain time. The application isn't freezed by itself, because I put some buttons and it is totally working.

How I update the clock TextBox

In all versions of the clock I have this same problem!

1. Task version

private Task _clock;

private void LoadClock()
{
    _clockCancellationTokenSource = new CancellationTokenSource();
    
    _clock = Task.Run(async () =>
    {
        try
        {
            while (!_clockCancellationTokenSource.IsCancellationRequested)
            {
                DateTime now = DateTime.Now;

                _ = Dispatcher.InvokeAsync(() =>
                {
                    txtHour.Text = DateTime.Now.ToString("HH:mm:ss", CultureInfo.CurrentCulture);
                    txtDate.Text = now.ToString("dddd dd MMMM yyyy", CultureInfo.CurrentCulture);
                },
                DispatcherPriority.Normal);

                await Task.Delay(800, _clockCancellationTokenSource.Token);
            }
        }
        catch
        {
             // I don't do anything with the errors here, If any
        }

    }, _clockCancellationTokenSource.Token);
}

And in Page_Unloaded event:

if (_clock != null && !_clock.IsCompleted)
{
    _clockCancellationTokenSource?.Cancel();
    _clock.Wait();
    _clock.Dispose();
    _clockCancellationTokenSource?.Dispose();
}

2. DispatcherTimer version

private DispatcherTimer _clock;

private void LoadClock(bool show)
{
    _clock = new DispatcherTimer
    {
        Interval = TimeSpan.FromSeconds(1)
    };
    _clockTimer_Tick(null, null);
    _clock.Tick += _clockTimer_Tick;
    _clock.Start();
}

private void _clockTimer_Tick(object sender, object e)
{
    var now = DateTime.Now;
    txtHour.Text = DateTime.Now.ToString("HH:mm:ss", CultureInfo.CurrentCulture);
    txtDate.Text = now.ToString("dddd dd MMMM yyyy", CultureInfo.CurrentCulture);
}

And in Page_Unloaded event:

if (_clock != null)
{
    _clock.Stop();
    _clock.Tick -= _clockTimer_Tick;
}

3. System.Timers.Timer version

private Timer _clock;

private void LoadClock(bool show)
{
    // Timer per gestire l'orario
    _clock = new Timer(800);
    _clock.Elapsed += _clock_Elapsed;
    _clock.Start();
    _clock_Elapsed(null, null);
}

private void _clock_Elapsed(object sender, ElapsedEventArgs e)
{
    DateTime now = DateTime.Now;
    Dispatcher.Invoke(() =>
    {
        txtHour.Text = DateTime.Now.ToString("HH:mm:ss", CultureInfo.CurrentCulture);
        txtDate.Text = now.ToString("dddd dd MMMM yyyy", CultureInfo.CurrentCulture);
    });
}

And in Page_Unloaded event:

_clock.Stop();
_clock.Elapsed -= _clock_Elapsed;
_clock.Dispose();

What I have tried so far:

  • Changing the timers implementation as you seen from my code
  • Adding a log in the timer tick callback to see if It stops due to a UI dispatch problem or a timer problem. It's interesting that I see that the timer after a certain amount of time stops ticking.
  • I thought It could be something with Windows, that maybe stops the additional threads, but I didn't find any clue about this!

Do you have any suggestions?


Solution

  • The problem is you have a try/catch that's outside the while loop and your code is just swallowing exceptions - so when an exception is thrown it will stop the loop with no way of resuming it.

    At a minimum, you need to make 2 changes to your first code example to make it work reliably:

    • You need to catch exceptions and gracefully handle them. Never silently swallow exceptions.
    • Move the try/catch to inside the while loop.

    Additionally, and as I remarked in the comments, your posted code was far too complicated than it needed to be:

    • You don't need to use Task.Run nor Dispatcher.InvokeAsync because WPF sets-up its own SynchronizationContext which ensures that await will resume in the UI thread by default (unless you use ConfigureAwait(false), of course)
      • So never use ConfigureAwait(false) in UI code!)
    • Your Task.Delay timeout of 800ms for a clock that displays the time in seconds will result in janky and awkward updates (because at 800ms, the updates will be at 800, 1600, 2400, 3200, etc which don't align with wall-clock seconds).
      • Whenever you're making discrete samples of a continuous value (in this case: seconds in time) then you should use the Nyquist Frequency of the signal source (which is double the expected frequency: so to get samples of 1s you should take samples every 500ms - though for the case of time-display I prefer 100ms intervals to account for jitter with the UI scheduler. If you do go higher (e.g. 16ms for 60fps) you should only update the UI if the seconds value has changed, otherwise you're wasting CPU and GPU cycles).

    All you need is this:

    private bool clockEnabled = false;
    private readonly Task clockTask;
    
    public MyPage()
    {
        this.clockTask = this.RunClockAsync( default );
    }
    
    private override void OnLoad( ... )
    {
        this.clockEnabled = true;
    }
    
    private async Task RunClockAsync( CancellationToken cancellationToken = default )
    {
        while( !cancellationToken.IsCancellationRequested )
        {
            try
            {
                if( this.clockEnabled )
                {
                    // WPF will always run this code in the UI thread:
                    this.UpdateClockDisplay();
                }
    
                await Task.Delay( 100 ); // No need to pass `cancellationToken` to `Task.Delay` as we check `IsCancellationRequested ` ourselves.
            }
            catch( OperationCanceledException )
            {
                // This catch block only needs to exist if the cancellation token is passed-around inside the try block.
                return;
            }
            catch( Exception ex )
            {
                this.log.LogError( ex );
    
                MessageBox.Show( "Error: " + ex.ToString(), etc... );
            }
        }
    }
    
    private void UpdateClockDisplay()
    {
        DateTime now = DateTime.Now;
    
        // Don't cache CurrentCulture, because the user can change their system preferences at any time.
        CultureInfo ci = CultureInfo.CurrentCulture;
    
        
        this.txtHour.Text = now.ToString( "HH:mm:ss", ci  );
        this.txtDate.Text = now.ToString( "dddd dd MMMM yyyy", ci );
    }