Поднять события в .NET на основной поток пользовательского интерфейса
Я разрабатываю библиотеку классов в .NET, которую в конечном итоге будут использовать другие разработчики. В этой библиотеке используются несколько рабочих потоков, а эти потоки сообщают о событиях, которые заставят некоторые элементы управления пользовательским интерфейсом обновляться в приложении WinForms/WPF.
Обычно для каждого обновления вам нужно проверить свойство .InvokeRequired на WinForms или эквивалентное свойство WPF и вызвать это в основном потоке пользовательского интерфейса для обновления. Это может стать старым быстро, и что-то не очень хорошо говорит о том, чтобы сделать конечный разработчик таким, чтобы...
Есть ли способ, которым моя библиотека может запускать/вызывать события/делегаты из основного потока пользовательского интерфейса?
В частности...
- Должен ли я автоматически "обнаруживать" "основной" поток для использования?
- Если нет, должен ли я потребовать, чтобы конечный разработчик вызывал некоторый (псевдо)
UseThisThreadForEvents()
метод при запуске приложения, чтобы я мог захватить целевой поток из этого вызова?
Ответы
Ответ 1
В вашей библиотеке можно проверить цель каждого делегата в списке вызовов событий и маршализовать вызов целевому потоку, если эта цель 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 для потока, по которому они хотят, чтобы вы увеличивали события. Это дает пользователям вашей библиотеки немного больше видимости и контроля, чем "тайком проверку целевого назначения делегата".
Что касается вашего второго вопроса, я бы поставил поток marshalling в вашем OnXxx или любом API-интерфейсе, который вызывает пользовательский код, который может привести к возникновению события.
Ответ 2
Здесь идея 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);
Ответ 3
Вы можете использовать класс SynchronizationContext для маршрутизации вызовов в поток пользовательского интерфейса в WinForms или WPF с помощью SynchronizationContext.Current
.
Ответ 4
Мне очень понравился Майк Бук (+1), я включил его в свою кодовую базу. Я обеспокоен тем, что его вызов DynamicInvoke вызовет исключение во время выполнения, если вызывающий его делегат не является делегатом EventHandler из-за несоответствующих параметров. И поскольку вы находитесь в фоновом потоке, я предполагаю, что вы можете асинхронно вызывать UI-метод и что вас не интересует, заканчивается ли он.
Моя версия ниже может использоваться только с делегатами EventHandler и будет игнорировать другие делегаты в своем списке вызовов. Поскольку делегаты EventHandler ничего не возвращают, нам не нужен результат. Это позволяет мне вызвать EndInvoke после завершения асинхронного процесса, передав EventHandler в вызове BeginInvoke. Вызов возвращает этот EventHandler в IAsyncResult.AsyncState посредством AsynchronousCallback, после чего вызывается 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);
Ответ 5
Вы можете сохранить диспетчер для основного потока в своей библиотеке, использовать его, чтобы проверить, запущен ли он в потоке пользовательского интерфейса, и при необходимости выполнять через поток пользовательского интерфейса через него.
документация 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
}
}
Ответ 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);
}
}
}
}
Ответ 7
Мне нравятся эти ответы и примеры, но по стандарту вы пишете библиотеку, все неправильно. Важно не направлять ваши события на другие темы ради других. Храните ваши события в том месте, где они есть, и обрабатывайте их там, где они есть. Когда придет время для этого события для изменения потоков, важно, чтобы конечный разработчик сделал это в этот момент времени.