Очистка кода, замусоренного InvokeRequired

Я знаю, что при манипулировании элементами пользовательского интерфейса из любого потока, отличного от UI, вы должны маршировать свои вызовы в потоке пользовательского интерфейса, чтобы избежать проблем. Общий консенсус в том, что вы должны использовать test InvokeRequired, и если true, используйте .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, не имея необходимости копировать и вставлять этот шаблон повсюду?

Ответ 1

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

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 });
}

Ответ 2

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

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

Ответ 3

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

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

     // notice that we are updating the form 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] Секундомер [Direct on UI Thread]: 300 мс
  • TimedAction:: Go() + 0 - Отладка: [DEBUG] Секундомер [Invoke on UI Thread]: 299ms
  • TimedAction:: Go() + 0 - Отладка: [DEBUG] Секундомер [Отдельный вызов в потоке пользовательского интерфейса]: 649 мс

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

Ответ 4

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

Мина немного отличается тем, что она может немного более безопасно обрабатывать пустые элементы управления и может при необходимости возвращать результаты. Оба из них пригодились мне при попытке вызвать отображение 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 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 okay.
var dialogResult = this.Parent.MaybeInvoke(() => MessageBox.Show(this, "Yes or no?", "Choice", MessageBoxButtons.YesNo));

Ответ 5

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

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

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

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

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

Ответ 6

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

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

Ответ 7

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

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 });

Ответ 8

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