Очистка кода, заваленного InvokeRequired - PullRequest
45 голосов
/ 06 октября 2010

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

Это приводит к большому количеству кода, который выглядит так:

private void UpdateSummary(string text)
{
    if (this.InvokeRequired)
    {
        this.Invoke(new Action(() => UpdateSummary(text)));
    }
    else
    {
        summary.Text = text;
    }
}

Мой вопрос таков: могу ли я пропустить тест InvokeRequired и просто вызвать Invoke, вот так:

private void UpdateSummary(string text)
{
    this.Invoke(new Action(() => summary.Text = text));
}

Есть ли проблема с этим? Если да, то есть ли лучший способ сохранить тест InvokeRequired, не копируя и не вставляя этот шаблон повсюду?

Ответы [ 8 ]

64 голосов
/ 06 октября 2010

Ну как на счет этого:

public static class ControlHelpers
{
    public static void InvokeIfRequired<T>(this T control, Action<T> action) where T : ISynchronizeInvoke
    {
        if (control.InvokeRequired)
        {
            control.Invoke(new Action(() => action(control)), null);
        }
        else
        {
            action(control);
        }
    }
}

Используйте это так:

private void UpdateSummary(string text)
{
    summary.InvokeIfRequired(s => { s.Text = text });
}
8 голосов
/ 06 октября 2010

Вызов Invoke из потока пользовательского интерфейса несколько неэффективен.

Вместо этого вы можете создать метод расширения InvokeIfNeeded, который принимает параметр Action. (это также позволит вам удалить new Action(...) с места вызова)

7 голосов
/ 06 октября 2010

Я читал аргументы взад и вперед о добавлении логической проверки, чтобы выяснить, должен ли вызываться IFF, если он не в потоке пользовательского интерфейса и не в самом потоке пользовательского интерфейса. Я написал класс, который проверяет время выполнения (через Секундомер ) различных методов, чтобы получить приблизительную оценку эффективности одного метода над другим.

Некоторые из вас могут удивить результатами (эти тесты были запущены с помощью события Form.Shown ):

     // notice that we are updating the form's title bar 10,000 times
     // directly on the UI thread
     TimedAction.Go
     (
        "Direct on UI Thread",
        () =>
        {
           for (int i = 0; i < 10000; i++)
           {
              this.Text = "1234567890";
           }
        }
     );

     // notice that we are invoking the update of the title bar
     // (UI thread -> [invoke] -> UI thread)
     TimedAction.Go
     (
        "Invoke on UI Thread",
        () =>
        {
           this.Invoke
           (
              new Action
              (
                 () =>
                 {
                    for (int i = 0; i < 10000; i++)
                    {
                       this.Text = "1234567890";
                    }
                 }
              )
           );
        }
     );

     // the following is invoking each UPDATE on the UI thread from the UI thread
     // (10,000 invokes)
     TimedAction.Go
     (
        "Separate Invoke on UI Thread",
        () =>
        {
           for (int i = 0; i < 10000; i++)
           {
              this.Invoke
              (
                 new Action
                 (
                    () =>
                    {
                       this.Text = "1234567890";
                    }
                 )
              );
           }
        }
     );

Результаты выглядят следующим образом:

  • TimedAction :: Go () + 0 - Отладка: [DEBUG] Секундомер [Прямой в потоке пользовательского интерфейса]: 300 мс
  • TimedAction :: Go () + 0 - Отладка: [DEBUG] Секундомер [Invoke on UI Thread]: 299ms
  • TimedAction :: Go () + 0 - Отладка: [DEBUG] Секундомер [Отдельный вызов в потоке пользовательского интерфейса]: 649ms

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

5 голосов
/ 06 октября 2010

Я понимаю, что уже есть один ответ, который довольно часто встречается на , но я также хотел опубликовать свое мнение (которое я также разместил здесь ).

Мой метод немного отличается тем, что он может немного более безопасно обрабатывать нулевые элементы управления и может возвращать результаты при необходимости. И то, и другое пригодилось для меня, когда я пытаюсь вызвать Invoke, показывая MessageBox в родительской форме, которая может быть нулевой, и возвращает DialogResult, показывающий этот MessageBox.


using System;
using System.Windows.Forms;

/// <summary>
/// Extension methods acting on Control objects.
/// </summary>
internal static class ControlExtensionMethods
{
    /// <summary>
    /// Invokes the given action on the given control's UI thread, if invocation is needed.
    /// </summary>
    /// <param name="control">Control on whose UI thread to possibly invoke.</param>
    /// <param name="action">Action to be invoked on the given control.</param>
    public static void MaybeInvoke(this Control control, Action action)
    {
        if (control != null && control.InvokeRequired)
        {
            control.Invoke(action);
        }
        else
        {
            action();
        }
    }

    /// <summary>
    /// Maybe Invoke a Func that returns a value.
    /// </summary>
    /// <typeparam name="T">Return type of func.</typeparam>
    /// <param name="control">Control on which to maybe invoke.</param>
    /// <param name="func">Function returning a value, to invoke.</param>
    /// <returns>The result of the call to func.</returns>
    public static T MaybeInvoke<T>(this Control control, Func<T> func)
    {
        if (control != null && control.InvokeRequired)
        {
            return (T)(control.Invoke(func));
        }
        else
        {
            return func();
        }
    }
}

Использование:

myForm.MaybeInvoke(() => this.Text = "Hello world");

// Sometimes the control might be null, but that's okay.
var dialogResult = this.Parent.MaybeInvoke(() => MessageBox.Show(this, "Yes or no?", "Choice", MessageBoxButtons.YesNo));
3 голосов
/ 06 октября 2010

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

Причины, по которым подход опроса должен рассматриваться в этом случае, заключаются в следующем:

  • Это нарушает тесную связь между пользовательским интерфейсом и рабочими потоками, которые навязывает Control.Invoke.
  • Это возлагает ответственность за обновление потока пользовательского интерфейса на поток пользовательского интерфейса, где он должен принадлежать в любом случае.
  • Поток пользовательского интерфейса получает указание, когда и как часто должно происходить обновление.
  • Нет риска переполнения насоса сообщений пользовательского интерфейса, как в случае с методами маршалинга, инициированными рабочим потоком.
  • Рабочему потоку не нужно ждать подтверждения того, что обновление было выполнено, прежде чем переходить к следующим шагам (т. Е. Вы получаете большую пропускную способность как для пользовательского интерфейса, так и для рабочих потоков).

Поэтому подумайте о создании System.Windows.Forms.Timer, который периодически проверяет текст, отображаемый на Control, а не инициируеттолчок из рабочего потока.Опять же, не зная ваших точных требований, я не хочу определенно указывать направление, в котором вы должны идти, но в большинстве во многих случаях это на лучше, чем опция Control.Invoke.

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

2 голосов
/ 06 октября 2010

Мой предпочтительный подход к элементам управления только для просмотра заключается в том, чтобы все состояния элемента управления были инкапсулированы в классе, который можно обновлять, даже не проходя через несогласованные состояния (простой способ сделать это - поместить все, что нужно, обновляется вместе в неизменный класс и создает новый экземпляр класса всякий раз, когда требуется обновление). Затем создайте метод Interlocked.Exchange флага updateNeeded и, если обновление не ожидается, но IsHandleCreated имеет значение true, тогда BeginInvoke запустите процедуру обновления. Процедура обновления должна очистить флаг updateNeeded как первое, что она делает, прежде чем делать какие-либо обновления (если кто-то попытается обновить элемент управления в этот момент, другой запрос будет BeginInvoked). Обратите внимание, что вы должны быть готовы перехватить и проглотить исключение (я думаю, что IllegalOperation), если элемент управления удаляется так же, как вы готовитесь к его обновлению.

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

1 голос
/ 26 августа 2015

Я пока не могу комментировать, надеюсь, кто-то увидит это и добавит его к принятому ответу, который в противном случае является точным.

control.Invoke(new Action(() => action(control))); должно читаться как
control.Invoke(new Action(() => action(control)), null);

Как написано, принятый ответ не будет компилироваться, потому что ISynchronizeInvoke.Invoke() не имеет перегрузки только с 1 аргументом, как Control.Invoke().

Еще одна вещь заключается в том, что использование может быть более понятным, как
summary.InvokeIfRequired(c => { summary.Text = text; }); а не как написано summary.InvokeIfRequired(c => { textBox.Text = text });

0 голосов
/ 06 октября 2010

Проще использовать BackgroudWorker, если это возможно, для обеспечения адаптивности пользовательского интерфейса и использовать ReportProgress для обновления пользовательского интерфейса, поскольку он работает в том же потоке, что и пользовательский интерфейс, поэтому вам не понадобится InvokeRequired.

...