WPF 应用程序,“时钟文本框”在几个小时后冻结 1.任务版本2. DispatcherTimer 版本3. System.Timers.Timer 版本

问题描述

我制作了一个简单的 WPF 全屏并始终位于顶部窗口,其中有一个带有 dddd dd MMMM yyyy HH:mm:ss 时间戳的时钟。该应用程序在始终开启的触摸屏上 24/24 全天候开放,问题是一段时间后我看到时钟在某个时间冻结。应用程序本身不会被冻结,因为我放了一些按钮,它完全可以工作。

我如何更新时钟 TextBox

在所有版本的时钟中,我都有同样的问题!

1.任务版本

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);
}

Page_Unloaded 事件中:

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

2. dispatcherTimer 版本

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);
}

Page_Unloaded 事件中:

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

3. System.Timers.Timer 版本

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);
    });
}

Page_Unloaded 事件中:

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

到目前为止我尝试过的:

  • 如您从我的代码中看到的那样更改计时器实现
  • 在定时器 tick 回调中添加日志以查看它是否由于 UI 调度问题或定时器问题而停止。有趣的是,我看到计时器在一定时间后停止计时
  • 我认为这可能是 Windows 的问题,这可能会停止额外的线程,但我没有找到任何关于此的线索!

您有什么建议吗?

解决方法

问题是您有一个 try/catch,它在 while 循环之外,而您的代码只是在吞咽异常 - 因此,当抛出异常时,它将停止循环无法恢复。

您至少需要对第一个代码示例进行 2 处更改以使其可靠运行:

  • 您需要捕获异常并妥善处理它们。永远不要默默地吞下异常。
  • try/catch 移至 while 循环的内部

此外,正如我在评论中指出的,您发布的代码过于复杂:

  • 您不需要使用 Task.RunDispatcher.InvokeAsync,因为 WPF 设置了自己的 SynchronizationContext,以确保 await 将在默认情况下在 UI 线程中恢复(除非你使用 ConfigureAwait(false),当然)
    • 所以永远不要在 UI 代码中使用 ConfigureAwait(false)!)
  • 对于以秒为单位显示时间的时钟,您的 Task.Delay 超时 800ms 将导致卡顿和尴尬的更新(因为在 800 毫秒时,更新将在 800、1600、2400、3200、等不与挂钟秒对齐)。
    • 每当您制作连续值的离散样本(在这种情况下:时间秒)时,您应该使用信号源的奈奎斯特频率(它是预期频率的两倍:因此要获得 1 秒的样本,您应该每 500 毫秒采集一次样本 - 虽然对于时间显示的情况,我更喜欢 100 毫秒的间隔来考虑 UI 调度程序的抖动。如果你做得更高(例如 60 fps 为 16 毫秒),你应该只在秒值发生变化时更新 UI,否则你就是在浪费 CPU 和 GPU 周期)。

你只需要这个:

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 );
}