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

时间:2009-11-09 03:02:08

标签: c# multithreading design-patterns

我正在.NET中开发一个类库,其他开发人员最终将使用它。该库使用了一些工作线程,这些线程触发了状态事件,这些事件将导致WinForms / WPF应用程序中的某些UI控件更新

通常,对于每次更新,您需要检查WinForms上的.InvokeRequired属性或等效的WPF属性,并在主UI线程上调用此更新。这可能会很快变老,而且让最终开发人员这样做是不对的,所以......

我的库可以通过主UI线程触发/调用事件/代理吗?

特别是......

  1. 我应该自动“检测”要使用的“主”线程吗?
  2. 如果没有,我是否应该要求最终开发人员在应用程序启动时调用某些(伪)UseThisThreadForEvents()方法,以便从该调用中获取目标线程?

8 个答案:

答案 0 :(得分:37)

您的库可以在事件的调用列表中检查每个委托的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中。

答案 1 :(得分:25)

这是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);

答案 2 :(得分:12)

您可以使用SynchronizationContext类来使用SynchronizationContext.Current编组对WinForms或WPF中的UI线程的调用。

答案 3 :(得分:11)

我非常喜欢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);

答案 4 :(得分:5)

您可以在库中存储主线程的调度程序,使用它来检查您是否在UI线程上运行,并在必要时在UI线程上执行。

WPF threading documentation提供了很好的介绍,并提供了如何执行此操作的示例。

以下是它的要点:

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
    }
}

答案 5 :(得分:5)

我发现依赖于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);
            }
        }
    }
}

答案 6 :(得分:1)

我喜欢这些答案和示例,但从本质上来说,标准是你写错了。重要的是不要为了别人而将你的事件编组到其他线程。让您的活动在他们所在的地方被解雇并在他们所属的地方进当该事件发生变化时,重要的是让最终开发人员在那个时刻做到这一点。

答案 7 :(得分:1)

我知道这是一个旧线程,但看到它确实帮助我开始构建类似的东西,所以我想分享我的代码。使用新的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;
        }
}