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

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

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

有什么办法,我的图书馆可以触发/调用主UI线程的事件/委托?

尤其是…

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

您的库可以检查事件调用列表中每个委托的目标,如果目标是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的想法expression为扩展方法,这对我来说是伟大的:

 /// <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),所以我把它整合到了我的代码库中。 我担心他的DynamicInvoke调用会抛出一个运行时exception,如果它调用的委托不是一个EventHandler委托,由于参数不匹配。 而且,由于您处于后台线程中,因此我认为您可能需要asynchronous调用UI方法,并且不关心是否完成。

我的下面的版本只能用于EventHandler委托,并会忽略其调用列表中的其他委托。 由于EventHandler委托没有返回,我们不需要结果。 这允许我通过在BeginInvoke调用中传递EventHandler完成asynchronous过程后调用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); } } } } 

我喜欢这些答案和例子,但是由于标准本质上你写的图书馆都是错的。 为了别人的目的,把你的事件编组到别的线程是很重要的。 保持你的事件发生在他们所在的位置并处理。 当这个事件发生变化的时候,让最终的开发者在那个时候做这件事是很重要的。