Подтвердить что ты не робот

Поднять события в .NET на основной поток пользовательского интерфейса

Я разрабатываю библиотеку классов в .NET, которую в конечном итоге будут использовать другие разработчики. В этой библиотеке используются несколько рабочих потоков, а эти потоки сообщают о событиях, которые заставят некоторые элементы управления пользовательским интерфейсом обновляться в приложении WinForms/WPF.

Обычно для каждого обновления вам нужно проверить свойство .InvokeRequired на WinForms или эквивалентное свойство WPF и вызвать это в основном потоке пользовательского интерфейса для обновления. Это может стать старым быстро, и что-то не очень хорошо говорит о том, чтобы сделать конечный разработчик таким, чтобы...

Есть ли способ, которым моя библиотека может запускать/вызывать события/делегаты из основного потока пользовательского интерфейса?

В частности...

  • Должен ли я автоматически "обнаруживать" "основной" поток для использования?
  • Если нет, должен ли я потребовать, чтобы конечный разработчик вызывал некоторый (псевдо) UseThisThreadForEvents() метод при запуске приложения, чтобы я мог захватить целевой поток из этого вызова?
4b9b3361

Ответ 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

Мне нравятся эти ответы и примеры, но по стандарту вы пишете библиотеку, все неправильно. Важно не направлять ваши события на другие темы ради других. Храните ваши события в том месте, где они есть, и обрабатывайте их там, где они есть. Когда придет время для этого события для изменения потоков, важно, чтобы конечный разработчик сделал это в этот момент времени.