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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1698889/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 20:04:20  来源:igfitidea点击:

Raise Events in .NET on the main UI thread

c#multithreadingdesign-patterns

提问by Brandon

I'm developing a class libraryin .NET that other developers will consume eventually. This library makes use of a few worker threads, and those threads fire status events that will cause some UI controls to be updatedin the WinForms / WPF application.

我正在.NET 中开发一个其他开发人员最终会使用的类库。这个库使用了一些工作线程,这些线程会触发状态事件,这些事件将导致在 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. This can get old quickly, and something doesn't feel right about making the end developer do this, so...

通常,对于每次更新,您都需要检查 WinForms 上的 .InvokeRequired 属性或等效的 WPF 属性,并在主 UI 线程上调用它以进行更新。这可能很快就会过时,让最终开发人员这样做感觉有些不对劲,所以......

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

采纳答案by itowlson

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:

您的库可以检查事件调用列表中每个委托的目标,如果目标是 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. This gives users of your library a bit more visibility and control than the "secretly check the delegate target" approach.

另一种使线程契约更加明确的方法是要求您的库的客户端为他们希望您在其上引发事件的线程传入 ISynchronizeInvoke 或 SynchronizationContext。与“秘密检查委托目标”方法相比,这为您的库的用户提供了更多的可见性和控制权。

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 中。

回答by SLaks

You can use the SynchronizationContextclass to marshall calls to the UI thread in WinForms or WPF by using SynchronizationContext.Current.

您可以使用SynchronizationContext类通过使用SynchronizationContext.Current.

回答by Anton

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

回答by Mike Bouck

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

回答by hoodaticus

I liked Mike Bouk's answer (+1) so much, I incorporated it into my codebase. 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. 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.

我非常喜欢 Mike Bouk 的回答 (+1),我将它合并到我的代码库中。我担心如果它调用的委托不是 EventHandler 委托,由于参数不匹配,他的 DynamicInvoke 调用将抛出运行时异常。并且由于您在后台线程中,我假设您可能希望异步调用 UI 方法并且您不关心它是否会完成。

My version below can only be used with EventHandler delegates and will ignore other delegates in its invocation list. Since EventHandler delegates return nothing, we don't need the result. This allows me to call EndInvoke after the asynchronous process completes by passing the EventHandler in the BeginInvoke call. The call will return this EventHandler in IAsyncResult.AsyncState by way of the AsynchronousCallback, at which point EventHandler.EndInvoke is called.

我的以下版本只能与 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); 
}

And the usage:

以及用法:

MyEventHandlerEvent.Raise(this, MyEventArgs);

回答by theGecko

I found relying on the method being an EventHandler doesn't always work and ISynchronizeInvoke doesn't work for WPF. My attempt therefore looks like this, it may help someone:

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

回答by Michael

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.

我喜欢这些答案和示例,但从本质上讲,按照标准,您编写的库都是错​​误的。重要的是不要为了他人而将您的事件编组到其他线程。保持您的事件在它们所在的地方被触发并在它们所属的地方处理。当该事件更改线程时,让最终开发人员在那个时间点这样做很重要。

回答by Cory

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. It uses the EventHandler delegate template, and the C#7 pattern matching, and LINQ to filter and set type.

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