繁体   English   中英

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

[英]How to raise an event from an object that uses a timer back to the UI thread

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

有没有办法从我的对象的用户那里隐藏这些额外的努力?

为了讨论的目的,我将包括一个简单的例子:

想象一下,我有一个带有1个richtextbox的表单:

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

    }
}

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

“跨线程操作无效:控制'richTextBox1'从其创建的线程以外的线程访问”

这对我来说很有意义,我可以不同地包装事件处理程序方法:

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

但是现在我的对象的用户必须为我控件中的计时器事件引发的任何事件执行此操作。 那么,有什么东西可以添加到我的PollingListener类中,它不会改变它的方法的签名来传递额外的引用,这些引用会允许我的对象的用户忘记UI线程后台的封送事件吗?

感谢您提供的任何输入。

评论后添加:

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

我想到的一件事是在构建时创建自己的Forms / WPF计时器,然后使用它和一些同步来隐藏跨线程的协调细节。 我们可以从您的示例中推断出轮询器的构造应始终在您的消费者线程的上下文中进行。

这是一种相当黑客的方式来实现你想要的东西,但它可以完成契约,因为你的poll-listener的构造发生在消费者的线程(它有一个Windows消息泵来为Forms / WPF定时器的调度提供动力)并且该类的其余操作可以从任何线程发生,因为表单Timer的tick会从原始线程发出心跳。 正如其他评论和答案所指出的那样,最好重新评估和修复您的轮询操作与消费者之间的操作关系

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

ManualResetEvent @ 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使用Timer,在WPF中你使用DispatchTimer然后你在与UI相同的线程上执行测试,并且没有跨线程问题。

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

我认为这段摘录很有启发性:“与System.Windows.Forms.Timer不同,System.Timers.Timer类默认情况下会调用从公共语言运行时(CLR)线程池获取的工作线程上的timer事件处理程序。[...] System.Timers.Timer类提供了一种处理这种困境的简单方法 - 它公开了一个公共SynchronizingObject属性。将此属性设置为Windows窗体的实例(或Windows窗体上的控件)将确保Elapsed事件处理程序中的代码在实例化SynchronizingObject的同一线程上运行。“

System.Times.Timer doc说的是SynchronizingObject:

获取或设置用于封送在间隔过去时发出的事件处理程序调用的对象。

这两个都暗示如果您将在UI线程上创建的控件作为同步对象传递,那么计时器将有效地封送到UI线程的计时器事件调用。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM