他の開発者が最終的に使用するクラスライブラリを.NETで開発しています。このライブラリは、いくつかのワーカースレッドを使用し、WinForms/WPFアプリケーションでこれらのスレッドは、一部のUIコントロールを更新するステータスイベントを発生させますを使用します。
通常、更新ごとに、WinFormsの.InvokeRequiredプロパティまたは同等のWPFプロパティを確認し、メインUIスレッドでこれを呼び出して更新する必要があります。これはすぐに古くなる可能性があり、最終開発者にこれを行わせるのは適切ではないので...
ライブラリがメインのUIスレッドからイベント/デリゲートを起動/呼び出すことができる方法はありますか?
特に...
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を渡すことを要求することです。これにより、ライブラリのユーザーは、「デリゲートターゲットをひそかにチェックする」アプローチよりも可視性と制御性が向上します。
2番目の質問に関しては、スレッドマーシャリングをOnXxx内に配置するか、ユーザーコードが呼び出してイベントが発生する可能性のあるAPIを配置します。
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);
SynchronizationContext クラスを使用して、WinFormsまたはWPFのUIスレッドへの呼び出しをマーシャルする SynchronizationContext.Current
。
私はMike Boukの答え(+1)がとても好きだったので、コードベースに組み込んだ。パラメーターの不一致により、DynamicInvoke呼び出しが呼び出すデリゲートがEventHandlerデリゲートでない場合、DynamicInvoke呼び出しがランタイム例外をスローするのではないかと心配しています。また、バックグラウンドスレッドを使用しているので、UIメソッドを非同期的に呼び出したい場合や、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);
メインスレッドのディスパッチャーをライブラリに保存し、それを使用して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);
}
}
}
}
私はこれが古いスレッドであることを知っていますが、それが本当に似たようなものの構築を始めるのに本当に役立つので、コードを共有したいと思います。新しい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;
}
}
私はこれらの回答と例が好きですが、本質的には標準でライブラリを間違って作成しているのです。他の人のために、イベントを他のスレッドにマーシャリングしないことが重要です。イベントが発生した場所で発生させ、それらが属している場所で処理します。そのイベントでスレッドを変更するときが来たら、その時点でエンド開発者にそれを行わせることが重要です。