为什么System.Timers.Timer不会更新非忙碌的UI?

4

我一直在寻找原因,但是stackoverflow上我能找到的每一个主题都提示忙碌的UI是原因。我有非常基础的代码,只是为了测试,它甚至无法更新UI,尽管我确定它不会忙于做其他事情:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    static double t = 0;
    static double dt = 0.1;

    private void button1_Click(object sender, RoutedEventArgs e)
    {
        Timer timer = new Timer(5000);
        timer.Enabled = true;
        timer.Elapsed += new ElapsedEventHandler(Update);
        timer.Start();
    }

    void Update(object sender, ElapsedEventArgs e)
    {
        t = t + dt;
        testBlock1.Text = (t).ToString();
    }
}

我进行了调试并在 textBlock1.Text 的更新处设置了断点,每5秒就会停下来,但是UI从未更新。如代码中所示,当我将鼠标移动到用于启动计时器的Button上时,该按钮会显示其典型的“鼠标悬停按钮”动画。为什么会发生这种情况,而文本没有更新呢?
如果有人能指导我在stackoverflow上找到一个好的主题,请这样做,我将删除我的问题,但我找不到任何主题解释为什么即使UI没有忙其他事情,这种方法也不会更新UI。

@Ian 它会运行,但在t = t + dt;之后添加(t + dt).ToString();这一行并没有太多意义... - Adam Houldsworth
文本框的初始文本为“TextBox”,并且每次更新时都使用“t = t + dt”进行更新,如我的问题所示。 - philkark
@AdamHouldsworth,那只是一个小错误,我已经纠正了,谢谢。 - philkark
Timer 是它自己的线程。你所读到的问题答案是错误的,问题在于你试图在一个单独的线程中更新 UI Thread。解决方案是在 WPF 窗口中使用 Dispatcher 或在 Win32 表单中使用 Invoke - Security Hound
1
我不太理解你所说的“问题的答案”,但是这个页面上的两个答案都解释了它是一个跨线程问题,因此两个答案都直接适用于我的问题。 - philkark
2个回答

12

System.Timers.Timer默认情况下不会自动切换回UI线程。在Windows Forms中,您可以轻松地实现此操作:

Timer timer = new Timer(5000);
timer.SynchronizingObject = this;

...但是WPF UI元素没有实现ISynchronizeInvoke,这会使得在WPF中无法工作。

你可以使用Dispatcher在处理程序内部切换到UI线程,或者你可以直接使用DispatcherTimer进行操作。


但有趣的是,这将导致跨线程UI访问错误,而OP没有说明。或者这只在WinForms中发生? - Adam Houldsworth
但是计时器在UI线程中引发了一个事件,那么不应该独立于计时器在不同的线程中运行这一事实来更新UI吗? - philkark
@phil13131:这实际上是Jon试图指出的问题。您正在使用的“计时器”不会隐式更改线程,因此方法处理程序会在不同的线程上引发。 - Mike Perrenoud
@BigM 很有趣的是,当涉及跨线程访问问题时,C#会非常快地抛出异常,但在这种情况下它却被简单地忽略了。这是C#的特性还是bug?! - philkark
1
@Ramhound:我看不出这里有死锁的迹象。你为什么认为会有呢? - Jon Skeet
显示剩余6条评论

8
对于WPF,您应该使用DispatcherTimer - 此外,由于elapsed事件在其他线程而不是UI线程上引发,因此使用您发布的上述代码会出现跨线程错误。
private void button1_Click(object sender, RoutedEventArgs e)
{
   DispatcherTimer timer = new DispatcherTimer();
   timer.Interval = new TimeSpan(0, 0, 5);
   timer.Tick += new EventHandler(timer_Tick);
   timer.Start();
}

void timer_Tick(object sender, EventArgs e)
{
   t = t + dt;
   txt.Text = (t + dt).ToString();
}

编辑

此外,您可以像以下代码一样在UI分派程序上进行编组 -

void Update(object sender, ElapsedEventArgs e)
{
    App.Current.Dispatcher.Invoke((Action)delegate()
    {
        t = t + dt;
        txt.Text = (t + dt).ToString();
    });
}

你可以考虑删除这个回答,只需点赞@Jon提供的答案,因为他在最后一句中指出OP应该使用DispatchTimer - Mike Perrenoud
2
我认为我想保留这个答案,因为它为我的问题提供了简单的代码。总的来说,感谢你的回答,它有效。 - philkark
2
我更新了我的被接受的答案为你的,因为提供的代码直接适用于我的问题,再次感谢。 - philkark

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接