简体   繁体   English

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

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

I'm developing a class library in .NET that other developers will consume eventually. 我正在开发.NET中的类库 ,其他开发人员最终将使用它。 This library makes use of a few worker threads, and those threads fire status events that will cause some UI controls to be updated in the WinForms / WPF application. 该库使用一些工作线程, 这些线程触发状态事件,这将导致在WinForms / WPF应用程序中更新一些UI控件

Normally, for every update, you would need to check the .InvokeRequired property on WinForms or equivalent WPF property and invoke this on the main UI thread for updating. 通常,对于每次更新,您都需要检查WinForms上的.InvokeRequired属性或等效的WPF属性,并在主UI线程上调用此更新。 This can get old quickly, and something doesn't feel right about making the end developer do this, so... 这可能会很快变老,并且让最终开发人员做到这一点感觉不对,所以......

Is there any way that my library can fire/invoke the events/delegates from the main UI thread? 有没有办法我的库可以从主UI线程触发/调用事件/委托?

In particular... 特别是...

  1. Should I automatically "detect" the "main" thread to use? 我应该自动“检测”要使用的“主”线程吗?
  2. If not, should I require the end developer to call some (pseudo) UseThisThreadForEvents() method when the application starts so I can grab the target thread from that call? 如果没有,我应该要求最终开发人员在应用程序启动时调用某些(伪) UseThisThreadForEvents()方法,以便从该调用中获取目标线程吗?

Your library could check the Target of each delegate in the event's invocation list, and marshal the call to the target thread if that target is ISynchronizeInvoke: 您的库可以在事件的调用列表中检查每个委托的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
    }
  }
}

Another approach, which makes the threading contract more explicit, is to require clients of your library to pass in an ISynchronizeInvoke or SynchronizationContext for the thread on which they want you to raise events. 另一种使线程合约更明确的方法是要求库的客户端为他们希望您引发事件的线程传入ISynchronizeInvoke或SynchronizationContext。 This gives users of your library a bit more visibility and control than the "secretly check the delegate target" approach. 这使得库的用户比“秘密检查委托目标”方法更具可见性和控制力。

In regard to your second question, I would place the thread marshalling stuff within your OnXxx or whatever API the user code calls that could result in an event being raised. 关于你的第二个问题,我会将线程编组内容放在你的OnXxx或用户代码调用的任何可能导致事件被引发的API中。

Here's itwolson's idea expressed as an extension method which is working great for me: 这是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;
    }
}

You then just raise your event like so: 然后你就这样举起你的活动:

MyEvent.Raise(this, EventArgs.Empty);

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

I liked Mike Bouk's answer (+1) so much, I incorporated it into my codebase. 我非常喜欢Mike Bouk的答案(+1),我把它整合到我的代码库中。 I am concerned that his DynamicInvoke call will throw a runtime exception if the Delegate it invokes is not an EventHandler delegate, due to mismatched parameters. 我担心由于参数不匹配,如果它调用的Delegate不是EventHandler委托,他的DynamicInvoke调用将抛出运行时异常。 And since you're in a background thread, I assume you may want to call the UI method asynchronously and that you are not concerned with whether it ever finishes. 而且由于你在后台线程中,我假设你可能想要异步调用UI方法,并且你不关心它是否会完成。

My version below can only be used with EventHandler delegates and will ignore other delegates in its invocation list. 我下面的版本只能与EventHandler委托一起使用,并且会忽略其调用列表中的其他委托。 Since EventHandler delegates return nothing, we don't need the result. 由于EventHandler委托不返回任何内容,因此我们不需要结果。 This allows me to call EndInvoke after the asynchronous process completes by passing the EventHandler in the BeginInvoke call. 这允许我在异步过程完成后通过在BeginInvoke调用中传递EventHandler来调用EndInvoke。 The call will return this EventHandler in IAsyncResult.AsyncState by way of the AsynchronousCallback, at which point EventHandler.EndInvoke is called. 该调用将通过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); 
}

And the usage: 用法:

MyEventHandlerEvent.Raise(this, MyEventArgs);

You can store the dispatcher for the main thread in your library, use it to check if you are running on the UI thread, and execute on the UI thread through it if necessary. 您可以在库中存储主线程的调度程序,使用它来检查您是否在UI线程上运行,并在必要时通过它在UI线程上执行。

The WPF threading documentation provides a good introduction and samples on how to do this. WPF线程文档提供了一个很好的介绍和如何执行此操作的示例。

Here is the gist of it: 以下是它的要点:

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

I found relying on the method being an EventHandler doesn't always work and ISynchronizeInvoke doesn't work for WPF. 我发现依赖于方法是一个EventHandler并不总是有效,并且ISynchronizeInvoke不适用于WPF。 My attempt therefore looks like this, it may help someone: 因此,我的尝试看起来像是这样,它可能会帮助某人:

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

I like these answers and examples but inherently by standard you are writing the library all wrong. 我喜欢这些答案和例子,但从标准来看,你正在编写图书馆。 It's important not to marshal your events to other threads for the sake of others. 重要的是不要为了别人而将你的事件编组到其他线程。 Keep your events fired where they are and handled where they belong. 让您的活动在他们所在的地方被解雇并在他们所属的地方进 When the time comes for that event to change threads it's important to let the end developer do that at that point in time. 当该事件发生变化时,重要的是让最终开发人员在那个时刻做到这一点。

I know this is an old thread, but seeing as it really helped me get started on building something similar, so I want to share my code. 我知道这是一个旧线程,但看到它确实帮助我开始构建类似的东西,所以我想分享我的代码。 Using the new C#7 features, I was able to create a thread aware Raise function. 使用新的C#7功能,我能够创建一个线程感知的Raise函数。 It uses the EventHandler delegate template, and the C#7 pattern matching, and LINQ to filter and set type. 它使用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