在.NET中在主UI线程上引发事件

51

我正在使用.NET开发一个类库,最终会被其他开发人员使用。这个类库利用了几个工作线程,这些线程会触发状态事件,从而更新WinForms / WPF应用程序中的一些UI控件。

通常,每次更新都需要在WinForms上检查.InvokeRequired属性或等价的WPF属性,并在主UI线程上调用该属性以进行更新。这很快就变得老套,让最终的开发人员这样做并不合适,因此...

我的类库是否有办法从主UI线程触发/调用事件/委托?

特别地...

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

我假设你已经排除了使用BackGroundworker的可能性?参考:https://dev59.com/CErSa4cB1Zd3GeqPUTR0 - BillW
@BillW,是的,有一些复杂的要求排除了BackgroundWorker作为运行后台线程的手段。 - Brandon
9个回答

39

如果事件的调用列表中的每个委托的目标是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或任何可能导致触发事件的用户代码调用中。


这个很好地解决了问题;我对答案进行了一些语法上的小修改,但还是非常感谢! - Brandon
我显然做错了什么,当尝试实现这个函数时,我会收到“参数计数不匹配”的错误。@itowlson或@Brandon能否编辑此答案,以展示如何从新创建的类中调用此函数的示例?我尝试了类似于RaiseEventOnUIThread(MyCustomEvent, new object[] { myCustomEventArgs });的东西。注意:在我的情况下,myCustomEventArgs对象的类型是NewCustomEventArgs。 - Arvo Bowen
@arvo - 你的MyCustomEvent委托签名是什么?它有多少个参数? - Brandon
我想现在我明白了委托是如何工作的,基于@Mike Bouck提供的答案。我的签名是一个EventArgs签名。所以对于我的情况,它需要看起来像RaiseEventOnUIThread(Delegate theEvent, object sender, EventArgs e)。实际上,我更喜欢扩展方法,所以我选择了它。但还是感谢你的回复! - Arvo Bowen
1
可能适用于WinForms但不适用于WPF。请使用下面的theGecko答案。 - joedotnot

27

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

我想知道为什么你创建了一个multicastDelegate的副本。我在这里找到了答案:https://dev59.com/8HRA5IYBdhLWcg3w6SNH - Trevor
或者,如果您正在使用自定义事件参数- public static object Raise<TEventArgs>(this MulticastDelegate multicastDelegate, object sender, TEventArgs e) where TEventArgs : EventArgs - stuartd
当你使用this时,我认为复制根本没有任何意义。它永远不可能为空。 - Tomáš Zato

14

6
如果你喜欢这篇文章的话,很可能会得到更多的点赞——http://www.codeproject.com/KB/cpp/SyncContextTutorial.aspx - RichardOD
1
http://www.codeproject.com/Articles/31971/Understanding-SynchronizationContext-Part-I http://www.codeproject.com/Articles/32113/Understanding-SynchronizationContext-Part-II - Daniel Dušek
3
这句话实际上并没有提供任何有用的信息。如果能展示如何在代码示例中实际使用该类,我会给它点赞。 - Tim Long

10

我非常喜欢Mike Bouk的答案(+1),所以将其合并到了我的代码库中。但我担心他的DynamicInvoke调用会抛出运行时异常,如果它调用的Delegate不是一个EventHandler代理,则参数不匹配。由于你在后台线程中,我假设你可能想异步调用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);

好的!不错的改进——我现在会借鉴你的了! :-) - Mike Bouck
1
看起来在通用 Windows 平台上没有 ISynchronizeInvoke 可用。真是个麻烦!有没有其他方法可以实现相同的结果? - Tim Long
有人能提供一个VB.Net版本吗?我会非常感激的 :) - 41686d6564 stands w. Palestine

6

我发现仅依靠 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);
            }
        }
    }
}

有人可以提供一个VB.Net版本吗?我会非常感激! - 41686d6564 stands w. Palestine

5
你可以在库中存储主线程的分发器,使用它来检查是否在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
    }
}

当我尝试这样做时,我一直收到“匿名方法无法分配给System.Delegate”的错误提示。.Invoke不会接受这样的委托。 - mpen

2

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

0

重新启动一个旧的线程。在开始新线程之前,您可以像这样保存对现有线程的引用:

private SynchronizationContext synchronizationContext;
synchronizationContext = SynchronizationContext.Current;

然后在新线程中,您可以引用原始线程并在那里执行代码:

synchronizationContext.Post(new SendOrPostCallback((state) => {
                DoSomeStuff();
            }), null);

DoSomeStuff() 中的所有内容都将在原始线程中执行,如果这意味着事件触发并在 Winform 应用程序中处理 - 那就这样吧。


0
我喜欢这些答案和例子,但从标准上来说,你编写的库是错误的。重要的是不为了他人而将事件转移到其他线程。让事件在原地触发并处理。当事件需要在不同线程中执行时,重要的是在那个时间点让最终开发人员自行处理。

所有这些代码示例都是在动态调用委托之前测试是否需要动态调用委托到 UI 线程。通过将此纳入库中,您可以为使用该库的任何人节省发现库内部工作原理并利用除主 UI 线程以外的线程的时间。 - Mick

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接