簡體   English   中英

在主UI線程上提升.NET中的事件

[英]Raise Events in .NET on the main UI thread

我正在開發.NET中的類庫 ,其他開發人員最終將使用它。 該庫使用一些工作線程, 這些線程觸發狀態事件,這將導致在WinForms / WPF應用程序中更新一些UI控件

通常,對於每次更新,您都需要檢查WinForms上的.InvokeRequired屬性或等效的WPF屬性,並在主UI線程上調用此更新。 這可能會很快變老,並且讓最終開發人員做到這一點感覺不對,所以......

有沒有辦法我的庫可以從主UI線程觸發/調用事件/委托?

特別是...

  1. 我應該自動“檢測”要使用的“主”線程嗎?
  2. 如果沒有,我應該要求最終開發人員在應用程序啟動時調用某些(偽) UseThisThreadForEvents()方法,以便從該調用中獲取目標線程嗎?

您的庫可以在事件的調用列表中檢查每個委托的Target,如果該目標是ISynchronizeInvoke,則編組對目標線程的調用:

private void RaiseEventOnUIThread(Delegate theEvent, object[] args)
{
  foreach (Delegate d in theEvent.GetInvocationList())
  {
    ISynchronizeInvoke syncer = d.Target as ISynchronizeInvoke;
    if (syncer == null)
    {
      d.DynamicInvoke(args);
    }
    else
    {
      syncer.BeginInvoke(d, args);  // cleanup omitted
    }
  }
}

另一種使線程合約更明確的方法是要求庫的客戶端為他們希望您引發事件的線程傳入ISynchronizeInvoke或SynchronizationContext。 這使得庫的用戶比“秘密檢查委托目標”方法更具可見性和控制力。

關於你的第二個問題,我會將線程編組內容放在你的OnXxx或用戶代碼調用的任何可能導致事件被引發的API中。

這是itwolson的想法,表達為一種對我有用的擴展方法:

/// <summary>Extension methods for EventHandler-type delegates.</summary>
public static class EventExtensions
{
    /// <summary>Raises the event (on the UI thread if available).</summary>
    /// <param name="multicastDelegate">The event to raise.</param>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">An EventArgs that contains the event data.</param>
    /// <returns>The return value of the event invocation or null if none.</returns>
    public static object Raise(this MulticastDelegate multicastDelegate, object sender, EventArgs e)
    {
        object retVal = null;

        MulticastDelegate threadSafeMulticastDelegate = multicastDelegate;
        if (threadSafeMulticastDelegate != null)
        {
            foreach (Delegate d in threadSafeMulticastDelegate.GetInvocationList())
            {
                var synchronizeInvoke = d.Target as ISynchronizeInvoke;
                if ((synchronizeInvoke != null) && synchronizeInvoke.InvokeRequired)
                {
                    retVal = synchronizeInvoke.EndInvoke(synchronizeInvoke.BeginInvoke(d, new[] { sender, e }));
                }
                else
                {
                    retVal = d.DynamicInvoke(new[] { sender, e });
                }
            }
        }

        return retVal;
    }
}

然后你就這樣舉起你的活動:

MyEvent.Raise(this, EventArgs.Empty);

您可以使用SynchronizationContext類通過使用SynchronizationContext.Current來對WinForms或WPF中的UI線程進行編組調度。

我非常喜歡Mike Bouk的答案(+1),我把它整合到我的代碼庫中。 我擔心由於參數不匹配,如果它調用的Delegate不是EventHandler委托,他的DynamicInvoke調用將拋出運行時異常。 而且由於你在后台線程中,我假設你可能想要異步調用UI方法,並且你不關心它是否會完成。

我下面的版本只能與EventHandler委托一起使用,並且會忽略其調用列表中的其他委托。 由於EventHandler委托不返回任何內容,因此我們不需要結果。 這允許我在異步過程完成后通過在BeginInvoke調用中傳遞EventHandler來調用EndInvoke。 該調用將通過AsynchronousCallback在IAsyncResult.AsyncState中返回此EventHandler,此時將調用EventHandler.EndInvoke。

/// <summary>
/// Safely raises any EventHandler event asynchronously.
/// </summary>
/// <param name="sender">The object raising the event (usually this).</param>
/// <param name="e">The EventArgs for this event.</param>
public static void Raise(this MulticastDelegate thisEvent, object sender, 
    EventArgs e)
{
  EventHandler uiMethod; 
  ISynchronizeInvoke target; 
  AsyncCallback callback = new AsyncCallback(EndAsynchronousEvent);

  foreach (Delegate d in thisEvent.GetInvocationList())
  {
    uiMethod = d as EventHandler;
    if (uiMethod != null)
    {
      target = d.Target as ISynchronizeInvoke; 
      if (target != null) target.BeginInvoke(uiMethod, new[] { sender, e }); 
      else uiMethod.BeginInvoke(sender, e, callback, uiMethod);
    }
  }
}

private static void EndAsynchronousEvent(IAsyncResult result) 
{ 
  ((EventHandler)result.AsyncState).EndInvoke(result); 
}

用法:

MyEventHandlerEvent.Raise(this, MyEventArgs);

您可以在庫中存儲主線程的調度程序,使用它來檢查您是否在UI線程上運行,並在必要時通過它在UI線程上執行。

WPF線程文檔提供了一個很好的介紹和如何執行此操作的示例。

以下是它的要點:

private Dispatcher _uiDispatcher;

// Call from the main thread
public void UseThisThreadForEvents()
{
     _uiDispatcher = Dispatcher.CurrentDispatcher;
}

// Some method of library that may be called on worker thread
public void MyMethod()
{
    if (Dispatcher.CurrentDispatcher != _uiDispatcher)
    {
        _uiDispatcher.Invoke(delegate()
        {
            // UI thread code
        });
    }
    else
    {
         // UI thread code
    }
}

我發現依賴於方法是一個EventHandler並不總是有效,並且ISynchronizeInvoke不適用於WPF。 因此,我的嘗試看起來像是這樣,它可能會幫助某人:

public static class Extensions
{
    // Extension method which marshals events back onto the main thread
    public static void Raise(this MulticastDelegate multicast, object sender, EventArgs args)
    {
        foreach (Delegate del in multicast.GetInvocationList())
        {
            // Try for WPF first
            DispatcherObject dispatcherTarget = del.Target as DispatcherObject;
            if (dispatcherTarget != null && !dispatcherTarget.Dispatcher.CheckAccess())
            {
                // WPF target which requires marshaling
                dispatcherTarget.Dispatcher.BeginInvoke(del, sender, args);
            }
            else
            {
                // Maybe its WinForms?
                ISynchronizeInvoke syncTarget = del.Target as ISynchronizeInvoke;
                if (syncTarget != null && syncTarget.InvokeRequired)
                {
                    // WinForms target which requires marshaling
                    syncTarget.BeginInvoke(del, new object[] { sender, args });
                }
                else
                {
                    // Just do it.
                    del.DynamicInvoke(sender, args);
                }
            }
        }
    }
    // Extension method which marshals actions back onto the main thread
    public static void Raise<T>(this Action<T> action, T args)
    {
        // Try for WPF first
        DispatcherObject dispatcherTarget = action.Target as DispatcherObject;
        if (dispatcherTarget != null && !dispatcherTarget.Dispatcher.CheckAccess())
        {
            // WPF target which requires marshaling
            dispatcherTarget.Dispatcher.BeginInvoke(action, args);
        }
        else
        {
            // Maybe its WinForms?
            ISynchronizeInvoke syncTarget = action.Target as ISynchronizeInvoke;
            if (syncTarget != null && syncTarget.InvokeRequired)
            {
                // WinForms target which requires marshaling
                syncTarget.BeginInvoke(action, new object[] { args });
            }
            else
            {
                // Just do it.
                action.DynamicInvoke(args);
            }
        }
    }
}

我喜歡這些答案和例子,但從標准來看,你正在編寫圖書館。 重要的是不要為了別人而將你的事件編組到其他線程。 讓您的活動在他們所在的地方被解雇並在他們所屬的地方進 當該事件發生變化時,重要的是讓最終開發人員在那個時刻做到這一點。

我知道這是一個舊線程,但看到它確實幫助我開始構建類似的東西,所以我想分享我的代碼。 使用新的C#7功能,我能夠創建一個線程感知的Raise函數。 它使用EventHandler委托模板和C#7模式匹配,以及LINQ來過濾和設置類型。

public static void ThreadAwareRaise<TEventArgs>(this EventHandler<TEventArgs> customEvent,
    object sender, TEventArgs e) where TEventArgs : EventArgs
{
    foreach (var d in customEvent.GetInvocationList().OfType<EventHandler<TEventArgs>>())
        switch (d.Target)
        {
            case DispatcherObject dispatchTartget:
                dispatchTartget.Dispatcher.BeginInvoke(d, sender, e);
                break;
            case ISynchronizeInvoke syncTarget when syncTarget.InvokeRequired:
                syncTarget.BeginInvoke(d, new[] {sender, e});
                break;
            default:
                d.Invoke(sender, e);
                break;
        }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM