如何将事件从使用计时器的对象引发回 UI 线程

本文关键字:对象 线程 UI 计时器 事件 | 更新日期: 2023-09-27 18:26:16

我有一个对象,它使用计时器偶尔轮询资源,然后在轮询发现需要注意的内容时引发事件。我已经查看了其他几个示例,但似乎找不到一种方法将事件编组回 UI 线程,而无需在 UI 线程上的事件处理程序上使用额外的代码。所以我的问题是:

有没有办法向我的对象的用户隐藏这种额外的努力?

为了讨论的目的,我将包括一个微不足道的例子:

想象一下,我有一个包含 1 个富文本框的表单:

private void Form1_Load(object sender, EventArgs e)
{
    var listener = new PollingListener();
    listener.Polled += new EventHandler<EventArgs>(listener_Polled);
}
void listener_Polled(object sender, EventArgs e)
{
    richTextBox1.Text += "Polled " + DateTime.Now.Second.ToString();
}

我也有这个对象:

public class PollingListener
{
    System.Timers.Timer timer = new System.Timers.Timer(1000);
    public event EventHandler<EventArgs> Polled;
    public PollingListener()
    {
        timer.Elapsed +=new System.Timers.ElapsedEventHandler(PollNow);
        timer.Start();
    }
    void PollNow(object sender, EventArgs e)
    {
        var temp = Polled;
        if (temp != null) Polled(this, new EventArgs());
    }
}

如果我运行它,正如预期的那样,它会产生异常

"跨线程操作无效:控制访问的'富文本框 1' 从创建它的线程以外的线程">

这对我来说很有意义,我可以以不同的方式包装事件处理程序方法,如下所示:

void listener_Polled(object sender, EventArgs e)
{
    this.BeginInvoke(new Action(() => { UpdateText() }));
}
void UpdateText()
{
    richTextBox1.Text += "Polled " + DateTime.Now.Second.ToString();
}

但是现在,我的对象的用户必须对从我控件中的计时器事件引发的任何事件执行此操作。那么,我是否可以向我的 PollingListener 类添加任何内容,而不会更改其方法的签名以传入额外的引用,从而允许我的对象的用户忽略后台对 UI 线程的封送事件?

感谢您的任何意见。

如何将事件从使用计时器的对象引发回 UI 线程

在注释后添加:

您需要获取一些可以利用的潜在细节来实现该目标。

我想到的一件事是在构造时创建自己的 Forms/WPF 计时器,然后使用此计时器和一些同步来隐藏跨线程协调的详细信息。我们可以从您的样本中推断,您的轮询器的构建应始终发生在您的消费者线程的上下文中。

这是一种相当黑客的方式来完成你想要的,但它可以完成契约,因为你的轮询侦听器的构造是从使用者的线程(它有一个 Windows 消息泵来推动窗体/WPF 计时器的调度(发生的,并且类的其余操作可以从任何线程发生,因为表单计时器的滴答声将从原始线程检测信号。 正如其他评论和答案所指出的那样,最好重新评估和修复您的轮询操作与消费者之间的操作关系

下面是该类 PollingListener2 的更新版本,它使用ManualResetEvent和隐藏System.Windows.Forms.Timer跨线程传输轮询通知。为简洁起见,省略了清理代码。建议在此类的生产版本中要求使用 IDisposable 进行显式清理。

手动重置事件 @ MSDN

public class PollingListener2
{
    System.Timers.Timer timer = new System.Timers.Timer(1000);
    public event EventHandler<EventArgs> Polled;
    System.Windows.Forms.Timer formsTimer;
    public System.Threading.ManualResetEvent pollNotice;
    public PollingListener2()
    {
        pollNotice = new System.Threading.ManualResetEvent(false);
        formsTimer = new System.Windows.Forms.Timer();
        formsTimer.Interval = 100;
        formsTimer.Tick += new EventHandler(formsTimer_Tick);
        formsTimer.Start();
        timer.Elapsed += new System.Timers.ElapsedEventHandler(PollNow);
        timer.Start();
    }
    void formsTimer_Tick(object sender, EventArgs e)
    {
        if (pollNotice.WaitOne(0))
        {
            pollNotice.Reset();
            var temp = Polled;
            if (temp != null)
            {
                Polled(this, new EventArgs());
            }
        }
    }
    void PollNow(object sender, EventArgs e)
    {
        pollNotice.Set();
    }
}

这在遥远的 Win32 过去有一些先例,有些人会使用隐藏窗口等来维护另一只脚,而无需使用者对其代码进行任何重大更改(有时不需要更改(。


源语言:

可以在 ControlForm 类型的帮助程序类上添加一个成员变量,并将其用作事件调度中BeginInvoke()/Invoke()调用的范围。

下面是示例类的副本,经过修改后以这种方式运行。

public class PollingListener
{
    System.Timers.Timer timer = new System.Timers.Timer(1000);
    public event EventHandler<EventArgs> Polled;
    public PollingListener(System.Windows.Forms.Control consumer)
    {
        timer.Elapsed += new System.Timers.ElapsedEventHandler(PollNow);
        timer.Start();
        consumerContext = consumer;
    }
    System.Windows.Forms.Control consumerContext;
    void PollNow(object sender, EventArgs e)
    {
        var temp = Polled;
        if ((temp != null) && (null != consumerContext))
        {
            consumerContext.BeginInvoke(new Action(() =>
                {
                    Polled(this, new EventArgs());
                }));
        }
    }
}

下面是一个示例,演示了此操作。 在调试模式下运行它并查看输出以验证它是否按预期工作。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        listener = new PollingListener(this);
    }
    PollingListener listener;
    private void Form1_Load(object sender, EventArgs e)
    {
        listener.Polled += new EventHandler<EventArgs>(listener_Poll);
    }
    void listener_Poll(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("ding.");
    }
}

如果 PollNow 中的处理工作相当小,那么您不需要在单独的线程上执行它。如果 WinForms 使用计时器,则在 WPF 中使用调度计时器,然后在与 UI 相同的线程上执行测试,并且不存在跨线程问题。

这个SO问题提示了以下评论:

我认为这段摘录很有启发性:"不像 System.Windows.Forms.Timer,System.Timers.Timer 类将,通过 默认值,在获取的工作线程上调用计时器事件处理程序 从公共语言运行时 (CLR( 线程池。[...]这 System.Timers.Timer 类提供了一种简单的方法来处理这个问题 困境 - 它公开了一个公共同步对象属性。设置此项 属性,以访问 Windows 窗体(或 Windows 上的控件(的实例 窗体(将确保已用事件处理程序中的代码在 实例化同步对象的同一线程。

System.Times.Timer 文档对 SynchronizingObject 说:

获取或设置用于封送事件处理程序调用的对象,这些调用是 间隔过后发出。

这两者都意味着,如果在 UI 线程上创建的控件作为同步对象传递,则计时器将有效地封送对 UI 线程的计时器事件调用。