Избегайте беды Invoke/BeginInvoke в обработке событий в WinForm для кросс-потоков?

Я по-прежнему сталкивается с фоновым потоком в пользовательском интерфейсе WinForm. Зачем? Вот некоторые из проблем:

  • Очевидно, самая важная проблема, я не могу изменить элемент управления, если я не выполняю тот же поток, который его создал.
  • Как вы знаете, Invoke, BeginInvoke и т.д. недоступны до тех пор, пока не будет создан элемент управления.
  • Даже после того, как RequiresInvoke возвращает true, BeginInvoke все равно может отбрасывать ObjectDisposed и даже если он не бросает, он никогда не сможет выполнить код, если элемент управления будет уничтожен.
  • Даже после того, как RequiresInvoke возвращает true, Invoke может бесконечно зависать в ожидании выполнения с помощью элемента управления, который был удален одновременно с вызовом Invoke.

Я ищу элегантное решение этой проблемы, но прежде чем я узнаю, что я ищу, я подумал, что я проясню проблему. Это должно принять общую проблему и поставить для нее более конкретный пример. В этом примере допустим, что мы передаем большие объемы данных через Интернет. Пользовательский интерфейс должен иметь возможность отображать диалог прогресса для уже запущенной передачи. Диалог прогресса должен обновляться постоянно и быстро (обновляется от 5 до 20 раз в секунду). Пользователь может в любое время отклонить диалог прогресса и снова вызвать его, если это необходимо. И далее, давайте притворяемся аргументами, что если диалог виден, он должен обработать каждое событие прогресса. Пользователь может нажать "Отмена" в диалоговом окне выполнения и с помощью изменения аргументов событий, отмените операцию.

Теперь мне нужно решение, которое будет соответствовать следующему полю ограничений:

  • Разрешить рабочему потоку вызывать метод в элементе управления/форме и блокировать/дождаться завершения выполнения.
  • Разрешить самому диалогу вызывать этот же метод при инициализации или тому подобное (и, следовательно, не использовать invoke).
  • Не устанавливайте нагрузку на реализацию метода обработки или вызывающего события, решение должно только изменить сам подписку на события.
  • Соответственно обрабатывать блокирующие вызовы в диалог, который может находиться в процессе утилизации. К сожалению, это не так просто, как проверка IsDisposed.
  • Должно быть возможно использовать любой тип события (предположим делегат типа EventHandler)
  • Не следует переводить исключения в TargetInvocationException.
  • Решение должно работать с .Net 2.0 и выше

Итак, можно ли это решить с учетом ограничений выше? Я искал и вырыл бесчисленные блоги и дискуссии, и, увы, я все еще с пустыми руками.

Обновление: я понимаю, что этот вопрос не имеет простого ответа. Я был на этом сайте всего пару дней, и я видел некоторых людей с большим опытом, отвечающим на вопросы. Я надеюсь, что один из этих людей решил это достаточно достаточно для того, чтобы я не потратил неделю или около того, чтобы принять разумное решение.

Обновление № 2: Хорошо, я попытаюсь описать проблему немного подробнее и посмотреть, что (если что-либо) вытряхивается. Следующие свойства, которые позволяют нам определить его состояние, имеют пару вещей, вызывающих беспокойство...

  • Control.InvokeRequired = Документировано для возврата false, если выполняется в текущем потоке, или если IsHandleCreated возвращает false для всех родителей. Я смущен реализацией InvokeRequired, имеющей возможность либо бросить ObjectDisposedException, либо потенциально даже воссоздать дескриптор объекта. И поскольку InvokeRequired может возвращать true, когда мы не можем вызвать (Dispose in progress), и он может возвращать false, даже если нам может понадобиться использовать invoke (Create in progress), это просто не может быть доверено во всех случаях. Единственный случай, когда я могу увидеть, где мы можем доверять InvokeRequired return false, - это когда IsHandleCreated возвращает true как до, так и после вызова (BTW, документы MSDN для InvokeRequired указывают на проверку для IsHandleCreated).

  • Control.IsHandleCreated = Возвращает true, если дескриптор был назначен элементу управления; в противном случае - false. Хотя IsHandleCreated - безопасный вызов, он может разбиться, если элемент управления находится в процессе воссоздания его дескриптора. Эта потенциальная проблема, по-видимому, может быть решена путем выполнения блокировки (контроля) при доступе к IsHandleCreated и InvokeRequired.

  • Control.Disposing = Возвращает true, если элемент управления находится в процессе утилизации.

  • Control.IsDisposed = Возвращает true, если элемент управления удален. Я рассматриваю возможность подписки на событие Disposed и проверку свойства IsDisposed, чтобы определить, будет ли BeginInvoke когда-либо завершаться. Большая проблема здесь заключается в отсутствии блокировки синхронизации, которая препятствует переносу Disposing → Disposed. Возможно, если вы подписаны на событие Disposed и после этого убедитесь, что Disposing == false && IsDisposed == false вы все еще можете никогда не видеть огонь Disposed event. Это связано с тем, что реализация Dispose sets Disposing = false, а затем устанавливает Disposed = true. Это дает вам возможность (хотя и небольшую), чтобы читать как Disposing, так и IsDisposed как ложные на удаленном элементе управления.

... у меня болит голова:( Надеюсь, что вышеприведенная информация проливает немного больше света на проблемы для тех, у кого есть эти проблемы. Я ценю ваши запасные мыслительные циклы на этом.

Закрытие проблемы... Ниже приведена половина метода Control.DestroyHandle():

if (!this.RecreatingHandle && (this.threadCallbackList != null))
{
    lock (this.threadCallbackList)
    {
        Exception exception = new ObjectDisposedException(base.GetType().Name);
        while (this.threadCallbackList.Count > 0)
        {
            ThreadMethodEntry entry = (ThreadMethodEntry) this.threadCallbackList.Dequeue();
            entry.exception = exception;
            entry.Complete();
        }
    }
}
if ((0x40 & ((int) ((long) UnsafeNativeMethods.GetWindowLong(new HandleRef(this.window, this.InternalHandle), -20)))) != 0)
{
    UnsafeNativeMethods.DefMDIChildProc(this.InternalHandle, 0x10, IntPtr.Zero, IntPtr.Zero);
}
else
{
    this.window.DestroyHandle();
}

Вы заметите, что ObjectDisposedException отправляется во все вызовы с перекрестными потоками. Вскоре после этого вызывается вызов this.window.DestroyHandle(), который, в свою очередь, уничтожает окно и устанавливает его дескриптор ссылки на IntPtr.Zero, тем самым предотвращая дальнейшие вызовы в методе BeginInvoke (точнее MarshaledInvoke, которые обрабатывают как BeginInvoke, так и Invoke). Проблема здесь в том, что после того, как блокировка освободится от threadCallbackList, новая запись может быть вставлена ​​до того, как поток управления закроет дескриптор окна. Похоже, что я вижу, хотя и нечасто, достаточно часто, чтобы остановить выпуск.

Обновление # 4:

Извините, что продолжайте перетаскивать это; однако, я думал, что стоит документировать здесь. Мне удалось решить большинство проблем выше, и я сужусь над решением, которое работает. Я ударил еще одну проблему, о которой я беспокоился, но до сих пор не видел "in-the-wild".

Эта проблема связана с гением, который написал свойство Control.Handle:

    public IntPtr get_Handle()
    {
        if ((checkForIllegalCrossThreadCalls && !inCrossThreadSafeCall) && this.InvokeRequired)
        {
            throw new InvalidOperationException(SR.GetString("IllegalCrossThreadCall", new object[] { this.Name }));
        }
        if (!this.IsHandleCreated)
        {
            this.CreateHandle();
        }
        return this.HandleInternal;
    }

Это само по себе не так плохо (независимо от моего мнения о получении {} модификаций); однако в сочетании с свойством InvokeRequired или методом Invoke/BeginInvoke это плохо. Вот основной поток Invoke:

if( !this.IsHandleCreated )
    throw;
... do more stuff
PostMessage( this.Handle, ... );

Проблема здесь в том, что из другого потока я могу успешно передать первый оператор if, после которого дескриптор уничтожается управляющим потоком, что приводит к тому, что get из свойства Handle заново создает дескриптор окна в моем потоке, Это может привести к возникновению исключения в исходном потоке управления. На самом деле, я действительно смущен, потому что нет никакого способа защититься от этого. Если бы они использовали свойство InternalHandle и тестировались на результат IntPtr.Zero, это не было проблемой.

Ответ 1

Ваш сценарий, как описано, аккуратно подходит BackgroundWorker - почему бы просто не использовать это? Ваши требования к решению слишком универсальны и, скорее, необоснованны - я сомневаюсь, что есть решение, которое удовлетворит их всех.

Ответ 2

Я столкнулся с этой проблемой некоторое время назад и придумал решение, включающее контексты синхронизации. Решение заключается в добавлении метода расширения в SynchronizationContext, который связывает конкретный делегат с потоком, к которому привязан SynchronizationContext. Он сгенерирует новый делегат, который при вызове будет маршировать вызов к соответствующему потоку, а затем вызвать первоначальный делегат. Это делает почти невозможным для потребителей делегата назвать это в неправильном контексте.

Сообщение в блоге по теме:

Ответ 3

Хорошо, через несколько дней я закончил создание решения. Он решает все перечисленные ограничения и цели в первоначальном сообщении. Использование прост и прямолинейно:

myWorker.SomeEvent += new EventHandlerForControl<EventArgs>(this, myWorker_SomeEvent).EventHandler;

Когда рабочий поток вызывает это событие, он обрабатывает требуемый вызов в поток управления. Он гарантирует, что он не будет вешать бесконечно и будет последовательно бросать ObjectDisposedException, если он не может выполнить в потоке управления. Я создал другие определения класса, один для игнорирования ошибки, а другой - для прямого вызова делегата, если элемент управления недоступен. Появляется, чтобы хорошо работать и полностью проходит несколько тестов, которые воспроизводят вышеперечисленные проблемы. Существует только одна проблема с решением, которое я не могу предотвратить, не нарушая ограничение № 3 выше. Эта проблема является последней (Обновление № 4) в описании проблемы, проблемы с потоками в Get Handle. Это может привести к неожиданному поведению в исходном потоке управления, и я регулярно видел InvalidOperationException(), вызывая при вызове Dispose(), поскольку дескриптор в процессе создания в моем потоке. Чтобы разрешить это, я гарантирую блокировку доступа к функциям, которые будут использовать свойство Control.Handle. Это позволяет форме перегрузить метод DestroyHandle и блокировать до вызова базовой реализации. Если это будет сделано, этот класс должен быть полностью потокобезопасным (насколько мне известно).

public class Form : System.Windows.Forms.Form
{
    protected override void DestroyHandle()
    {
        lock (this) base.DestroyHandle();
    }
}

Вы можете заметить, что основной аспект решения мертвого замка стал циклом опроса. Первоначально я успешно решил тестовые примеры, обработав контрольное событие для Disposed и HandleDestroyed и используя несколько команд ожидания. После более тщательного обзора я обнаружил, что подписка/отказ от подписки на эти события не являются потокобезопасными. Таким образом, я решил опросить IsHandleCreated вместо этого, чтобы не создавать ненужные противоречия в событиях потока и тем самым избежать возможности создания состояния мертвой блокировки.

В любом случае, вот решение, которое я придумал:

/// <summary>
/// Provies a wrapper type around event handlers for a control that are safe to be
/// used from events on another thread.  If the control is not valid at the time the
/// delegate is called an exception of type ObjectDisposedExcpetion will be raised.
/// </summary>
[System.Diagnostics.DebuggerNonUserCode]
public class EventHandlerForControl<TEventArgs> where TEventArgs : EventArgs
{
    /// <summary> The control who thread we will use for the invoke </summary>
    protected readonly Control _control;
    /// <summary> The delegate to invoke on the control </summary>
    protected readonly EventHandler<TEventArgs> _delegate;

    /// <summary>
    /// Constructs an EventHandler for the specified method on the given control instance.
    /// </summary>
    public EventHandlerForControl(Control control, EventHandler<TEventArgs> handler)
    {
        if (control == null) throw new ArgumentNullException("control");
        _control = control.TopLevelControl;
        if (handler == null) throw new ArgumentNullException("handler");
        _delegate = handler;
    }

    /// <summary>
    /// Constructs an EventHandler for the specified delegate converting it to the expected
    /// EventHandler&lt;TEventArgs> delegate type.
    /// </summary>
    public EventHandlerForControl(Control control, Delegate handler)
    {
        if (control == null) throw new ArgumentNullException("control");
        _control = control.TopLevelControl;
        if (handler == null) throw new ArgumentNullException("handler");

        //_delegate = handler.Convert<EventHandler<TEventArgs>>();
        _delegate = handler as EventHandler<TEventArgs>;
        if (_delegate == null)
        {
            foreach (Delegate d in handler.GetInvocationList())
            {
                _delegate = (EventHandler<TEventArgs>) Delegate.Combine(_delegate,
                    Delegate.CreateDelegate(typeof(EventHandler<TEventArgs>), d.Target, d.Method, true)
                );
            }
        }
        if (_delegate == null) throw new ArgumentNullException("_delegate");
    }


    /// <summary>
    /// Used to handle the condition that a control handle is not currently available.  This
    /// can either be before construction or after being disposed.
    /// </summary>
    protected virtual void OnControlDisposed(object sender, TEventArgs args)
    {
        throw new ObjectDisposedException(_control.GetType().Name);
    }

    /// <summary>
    /// This object will allow an implicit cast to the EventHandler&lt;T> type for easier use.
    /// </summary>
    public static implicit operator EventHandler<TEventArgs>(EventHandlerForControl<TEventArgs> instance)
    { return instance.EventHandler; }

    /// <summary>
    /// Handles the 'magic' of safely invoking the delegate on the control without producing
    /// a dead-lock.
    /// </summary>
    public void EventHandler(object sender, TEventArgs args)
    {
        bool requiresInvoke = false, hasHandle = false;
        try
        {
            lock (_control) // locked to avoid conflicts with RecreateHandle and DestroyHandle
            {
                if (true == (hasHandle = _control.IsHandleCreated))
                {
                    requiresInvoke = _control.InvokeRequired;
                    // must remain true for InvokeRequired to be dependable
                    hasHandle &= _control.IsHandleCreated;
                }
            }
        }
        catch (ObjectDisposedException)
        {
            requiresInvoke = hasHandle = false;
        }

        if (!requiresInvoke && hasHandle) // control is from the current thread
        {
            _delegate(sender, args);
            return;
        }
        else if (hasHandle) // control invoke *might* work
        {
            MethodInvokerImpl invocation = new MethodInvokerImpl(_delegate, sender, args);
            IAsyncResult result = null;
            try
            {
                lock (_control)// locked to avoid conflicts with RecreateHandle and DestroyHandle
                    result = _control.BeginInvoke(invocation.Invoker);
            }
            catch (InvalidOperationException)
            { }

            try
            {
                if (result != null)
                {
                    WaitHandle handle = result.AsyncWaitHandle;
                    TimeSpan interval = TimeSpan.FromSeconds(1);
                    bool complete = false;

                    while (!complete && (invocation.MethodRunning || _control.IsHandleCreated))
                    {
                        if (invocation.MethodRunning)
                            complete = handle.WaitOne();//no need to continue polling once running
                        else
                            complete = handle.WaitOne(interval);
                    }

                    if (complete)
                    {
                        _control.EndInvoke(result);
                        return;
                    }
                }
            }
            catch (ObjectDisposedException ode)
            {
                if (ode.ObjectName != _control.GetType().Name)
                    throw;// *likely* from some other source...
            }
        }

        OnControlDisposed(sender, args);
    }

    /// <summary>
    /// The class is used to take advantage of a special-case in the Control.InvokeMarshaledCallbackDo()
    /// implementation that allows us to preserve the exception types that are thrown rather than doing
    /// a delegate.DynamicInvoke();
    /// </summary>
    [System.Diagnostics.DebuggerNonUserCode]
    private class MethodInvokerImpl
    {
        readonly EventHandler<TEventArgs> _handler;
        readonly object _sender;
        readonly TEventArgs _args;
        private bool _received;

        public MethodInvokerImpl(EventHandler<TEventArgs> handler, object sender, TEventArgs args)
        {
            _received = false;
            _handler = handler;
            _sender = sender;
            _args = args;
        }

        public MethodInvoker Invoker { get { return this.Invoke; } }
        private void Invoke() { _received = true; _handler(_sender, _args); }

        public bool MethodRunning { get { return _received; } }
    }
}

Если вы видите что-то не так, пожалуйста, дайте мне знать.

Ответ 4

Я не собираюсь писать исчерпывающее решение для вас, которое отвечает всем вашим требованиям, но я предлагаю перспективу. В целом, однако, я думаю, что вы стреляете в Луну с этими требованиями.

Архитектура Invoke/BeginInvoke просто выполняет предоставленный делегат в потоке пользовательского интерфейса управления, отправляя ему сообщение Windows, и сам цикл сообщений выполняет делегат. Конкретные действия этого не имеют значения, но дело в том, что нет особой причины, по которой вам нужно использовать эту архитектуру для синхронизации потоков с потоком пользовательского интерфейса. Все, что вам нужно, это какой-то другой цикл, например, в Forms.Timer или что-то в этом роде, который контролирует выполнение Queue для делегатов и делает это. Было бы довольно просто реализовать свои собственные, хотя я не знаю, что конкретно вам может получить, что Invoke и BeginInvoke не предоставляют.

Ответ 5

Это не ответ на вторую часть вопроса, но я буду включать его только для справки:

private delegate object SafeInvokeCallback(Control control, Delegate method, params object[] parameters);
public static object SafeInvoke(this Control control, Delegate method, params object[] parameters)
{
    if (control == null)
        throw new ArgumentNullException("control");
    if (control.InvokeRequired)
    {
        IAsyncResult result = null;
        try { result = control.BeginInvoke(new SafeInvokeCallback(SafeInvoke), control, method, parameters); }
        catch (InvalidOperationException) { /* This control has not been created or was already (more likely) closed. */ }
        if (result != null)
            return control.EndInvoke(result);
    }
    else
    {
        if (!control.IsDisposed)
            return method.DynamicInvoke(parameters);
    }
    return null;
}

Этот код должен избегать наиболее распространенных ошибок с Invoke/BeginInvoke и его легко использовать. Просто поверните

if (control.InvokeRequired)
    control.Invoke(...)
else
    ...

в

control.SafeInvoke(...)

Подобная конструкция возможна для BeginInvoke.

Ответ 6

Ничего себе, длинный вопрос. Я попытаюсь организовать свой ответ, чтобы вы могли исправить меня, если я понял что-то не так, хорошо?

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

protected override void OnLoad()
{
    //...
    component.Event += new EventHandler(myHandler);
}

protected override void OnClosing()
{
    //...
    component.Event -= new EventHandler(myHandler);
}

myHandler будет запущен каждый раз, когда компонент в другом потоке должен выполнить что-то в пользовательском интерфейсе, например. Кроме того, настройка обработчика событий в OnLoad и отмена подписки в OnClosing гарантирует, что события будут получать/обрабатывать только пользовательский интерфейс, пока его дескриптор создан и готов к обработке событий. Вы даже не сможете запускать события в этом диалоговом окне, если оно находится в процессе утилизации, потому что вы больше не будете подписываться на это событие. Если другое событие запущено, пока все еще обрабатывается, оно будет поставлено в очередь.

Вы можете передать всю необходимую информацию в аргументах события: обновляете ли вы прогресс, закрываете окно и т.д.

2) Вам не требуется InvokeRequired, если вы используете модель, предложенную выше. В этом примере вы знаете, что единственная вещь, которая запускает myHandler, будет вашим компонентом, который, например, живет в другом потоке.

private void myHandler(object sender, EventArgs args)
{
    BeginInvoke(Action(myMethod));
}

Таким образом, вы всегда можете использовать invoke, чтобы убедиться, что вы находитесь в правильном потоке.

3) Остерегайтесь синхронных вызовов. Если вы хотите, вы можете заменить использование Invoke вместо BeginInvoke. Это заблокирует ваш компонент до тех пор, пока событие не будет обработано. Однако, если в пользовательском интерфейсе вам нужно общаться с чем-то, что является исключительным для потока, в котором живет ваш компонент, вы можете иметь проблемы с блокировкой. (Я не знаю, ясно ли я, пожалуйста, дайте мне знать). У меня были проблемы с исключениями при использовании отражения (TargetInvocationException) и BeginInvoke (поскольку они запускают другой поток, вы теряете часть трассировки стека), но я не помню, чтобы было много проблем с вызовами Invoke, поэтому вы должны быть безопасным, когда дело доходит до исключений.

Эй, длинный ответ. Если бы я случайно пропустил какое-либо из ваших требований или неправильно понял то, что вы сказали (английский не мой родной язык, поэтому мы никогда не уверены), пожалуйста, дайте мне знать.

Ответ 7

Я пытаюсь организовать все такие вызовы в GUI как огонь и забыть (обработка исключения, которое GUI может выкинуть из-за состояния гонки при утилизации формы).

Таким образом, если он никогда не выполняется, вред не делается.

Если GUI должен отвечать на рабочий поток, у него есть способ эффективно отменить уведомление. Для простых задач BackgroundWorker уже справляется с этим.

Ответ 8

Это довольно сложный вопрос. Как я упоминаю в комментарии, я не думаю, что он разрешился с учетом документированных ограничений. Вы можете взломать его с учетом конкретной реализации инфраструктуры .net: знание реализации различных функций-членов может помочь вам обмануть, захватив блокировки здесь и там, и зная, что "на самом деле OKAY, вызывать другие функции-члены в другом потоке."

Итак, мой основной ответ на данный момент: "Нет". Я ненавижу говорить, что это невозможно, потому что я очень верю в инфраструктуру .Net. Кроме того, я сравнительно новичок, не изучая рамки вообще или CS, но интернет открыт (даже для невежественных людей, таких как я)!

В другой теме аргумент может быть поставлен и хорошо поддерживается: "Вам никогда не понадобится Invoke, используйте только BeginInvoke, стреляйте и забывайте". Я не буду пытаться его поддержать или даже сказать, что это правильное утверждение, но я скажу, что общая реализация неверна и представляет собой рабочий (надеюсь) один.

Здесь общая реализация (взята из другого ответа здесь):

protected override void OnLoad()
{
    //...
    component.Event += new EventHandler(myHandler);
}

protected override void OnClosing()
{
    //...
    component.Event -= new EventHandler(myHandler);
}

Это не потокобезопасно. Компонент мог легко начать вызов списка вызовов непосредственно перед отменой подписки, и только после того, как мы закончим удаление, обработчик будет вызван. Реальная точка заключается в том, что она не задокументировала, как каждый компонент должен использовать механизм событий в .Net, и, честно говоря, он не должен отписывать вас вообще: как только вы выдали свой номер телефона, никто не должен был его стереть!

Лучше:

protected override void OnLoad(System.EventArgs e)
{
    component.Event += new System.EventHandler(myHandler);
}    
protected override void OnFormClosing(FormClosedEventArgs e)
{
    component.Event -= new System.EventHandler(myHandler);
    lock (lockobj)
    {
        closing = true;
    }
}
private void Handler(object a, System.EventArgs e)
{
    lock (lockobj)
    {
        if (closing)
            return;
        this.BeginInvoke(new System.Action(HandlerImpl));
    }
}
/*Must be called only on GUI thread*/
private void HandlerImpl()
{
    this.Hide();
}
private readonly object lockobj = new object();
private volatile bool closing = false;

Пожалуйста, дайте мне знать, если я что-то пропустил.

Ответ 10

Если я это понимаю, зачем вам когда-либо нужно удалять диалог прогресса во время работы приложения? Почему бы просто не показывать и не скрывать его по запросу пользователя? Похоже, что это сделает вашу проблему хотя бы немного проще.

Ответ 11

Почему бы не просто скрыть диалог, когда пользователь его уволит? Это должно работать нормально, если вы не показываете этот диалог модально. (используйте show вместо showdialog). Я считаю, что вы можете сохранить свой диалог прогресса поверх своего собственного окна (если вам нужно), передав хост в диалог при вызове show.

Ответ 12

Использование System.ComponentModel.ISynchronizeInvoke приятно при создании System.ComponentModel.Component, например BackgroundWorker. Следующий фрагмент кода - это то, как FileSystemWater обрабатывает события.

    ''' <summary>
    ''' Gets or sets the object used to marshal the event handler calls issued as a result of finding a file in a search.
    ''' </summary>
    <IODescription(SR.FSS_SynchronizingObject), DefaultValue(CType(Nothing, String))> _
    Public Property SynchronizingObject() As System.ComponentModel.ISynchronizeInvoke
        Get
            If (_synchronizingObject Is Nothing) AndAlso (MyBase.DesignMode) Then
                Dim oHost As IDesignerHost = DirectCast(MyBase.GetService(GetType(IDesignerHost)), IDesignerHost)
                If (Not (oHost Is Nothing)) Then
                    Dim oRootComponent As Object = oHost.RootComponent
                    If (Not (oRootComponent Is Nothing)) AndAlso (TypeOf oRootComponent Is ISynchronizeInvoke) Then
                        _synchronizingObject = DirectCast(oRootComponent, ISynchronizeInvoke)
                    End If
                End If
            End If
            Return _synchronizingObject
        End Get
        Set(ByVal Value As System.ComponentModel.ISynchronizeInvoke)
            _synchronizingObject = Value
        End Set
    End Property

    Private _onStartupHandler As EventHandler

    Protected Sub OnStartup(ByVal e As EventArgs)
        If ((Not Me.SynchronizingObject Is Nothing) AndAlso Me.SynchronizingObject.InvokeRequired) Then
            Me.SynchronizingObject.BeginInvoke(_onStartupHandler, New Object() {Me, e})
        Else
            _onStartupHandler.Invoke(Me, e)
        End If
    End Sub

Ответ 13

Вот что я сейчас использую. Он основан на использовании SynchronizationContext и был вдохновлен статьей блога JaredPar - см. Его ответ выше. Это может быть не идеально, но это позволяет избежать некоторых проблем с OP, которые я также испытывал.

   // Homemade Action-style delegates to provide .Net 2.0 compatibility, since .Net 2.0 does not 
   //  include a non-generic Action delegate nor Action delegates with more than one generic type 
   //  parameter. (The DMethodWithOneParameter<T> definition is not needed, could be Action<T> 
   //  instead, but is defined for consistency.) Some interesting observations can be found here:
   //  http://geekswithblogs.net/BlackRabbitCoder/archive/2011/11/03/c.net-little-wonders-the-generic-action-delegates.aspx
   public delegate void DMethodWithNoParameters();
   public delegate void DMethodWithOneParameter<T>(T parameter1);
   public delegate void DMethodWithTwoParameters<T1, T2>(T1 parameter1, T2 parameter2);
   public delegate void DMethodWithThreeParameters<T1, T2, T3>(T1 parameter1, T2 parameter2, T3 parameter3);


   /// <summary>
   /// Class containing support code to use the SynchronizationContext mechanism to dispatch the 
   /// execution of a method to the WinForms UI thread, from another thread. This can be used as an 
   /// alternative to the Control.BeginInvoke() mechanism which can be problematic under certain 
   /// conditions. See for example the discussion here:
   /// http://stackoverflow.com/info/1364116/avoiding-the-woes-of-invoke-begininvoke-in-cross-thread-winform-event-handling
   ///
   /// As currently coded this works with methods that take zero, one, two or three arguments, but 
   /// it is a trivial job to extend the code for methods taking more arguments.
   /// </summary>
   public class WinFormsHelper
   {
      // An arbitrary WinForms control associated with thread 1, used to check that thread-switching 
      //  with the SynchronizationContext mechanism should be OK
      private readonly Control _thread1Control = null;

      // SynchronizationContext for the WinForms environment UI thread
      private readonly WindowsFormsSynchronizationContext _synchronizationContext;


      /// <summary>
      /// Constructor. This must be called on the WinForms UI thread, typically thread 1. (Unless 
      /// running under the Visual Studio debugger, then the thread number is arbitrary.)
      ///
      /// The provided "thread 1 control" must be some WinForms control that will remain in 
      /// existence for as long as this object is going to be used, for example the main Form 
      /// control for the application.
      /// </summary>
      /// <param name="thread1Control">see above</param>
      public WinFormsHelper(Control thread1Control)
      {
         _thread1Control = thread1Control;
         if (thread1Control.InvokeRequired)
            throw new Exception("Not called on thread associated with WinForms controls.");

         _synchronizationContext =
                            SynchronizationContext.Current as WindowsFormsSynchronizationContext;
         if (_synchronizationContext == null) // Should not be possible?
            throw new Exception("SynchronizationContext.Current = null or wrong type.");
      }


      // The following BeginInvoke() methods follow a boilerplate pattern for how these methods 
      // should be implemented - they differ only in the number of arguments that the caller wants 
      // to provide.

      public void BeginInvoke(DMethodWithNoParameters methodWithNoParameters)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithNoParameters();
         }, null);
      }


      public void BeginInvoke<T>(DMethodWithOneParameter<T> methodWithOneParameter, T parameter1)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithOneParameter(parameter1);
         }, null);
      }


      public void BeginInvoke<T1, T2>(DMethodWithTwoParameters<T1, T2> methodWithTwoParameters,
                                      T1 parameter1, T2 parameter2)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithTwoParameters(parameter1, parameter2);
         }, null);
      }


      public void BeginInvoke<T1, T2, T3>(DMethodWithThreeParameters<T1, T2, T3> methodWithThreeParameters,
                                          T1 parameter1, T2 parameter2, T3 parameter3)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithThreeParameters(parameter1, parameter2, parameter3);
         }, null);
      }
   }