簡體   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