Уместно ли распространять Control для обеспечения неизменно безопасной функции Invoke/BeginInvoke?

В ходе моего обслуживания старого приложения, которое плохо нарушало правила обновления кросс-потоков в winforms, я создал следующий метод расширения как способ быстро исправить незаконные вызовы, когда я их обнаружил:

/// <summary>
/// Execute a method on the control owning thread.
/// </summary>
/// <param name="uiElement">The control that is being updated.</param>
/// <param name="updater">The method that updates uiElement.</param>
/// <param name="forceSynchronous">True to force synchronous execution of 
/// updater.  False to allow asynchronous execution if the call is marshalled
/// from a non-GUI thread.  If the method is called on the GUI thread,
/// execution is always synchronous.</param>
public static void SafeInvoke(this Control uiElement, Action updater, bool forceSynchronous)
{
    if (uiElement == null)
    {
        throw new ArgumentNullException("uiElement");
    }

    if (uiElement.InvokeRequired)
    {
        if (forceSynchronous)
        {
            uiElement.Invoke((Action)delegate { SafeInvoke(uiElement, updater, forceSynchronous); });
        }
        else
        {
            uiElement.BeginInvoke((Action)delegate { SafeInvoke(uiElement, updater, forceSynchronous); });
        }
    }
    else
    {
        if (!uiElement.IsHandleCreated)
        {
            // Do nothing if the handle isn't created already.  The user responsible
            // for ensuring that the handle they give us exists.
            return;
        }

        if (uiElement.IsDisposed)
        {
            throw new ObjectDisposedException("Control is already disposed.");
        }

        updater();
    }
}

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

this.lblTimeDisplay.SafeInvoke(() => this.lblTimeDisplay.Text = this.task.Duration.ToString(), false);

Мне нравится, как я могу использовать закрытие для чтения, также, хотя forceSynchronous должен быть истинным в этом случае:

string taskName = string.Empty;
this.txtTaskName.SafeInvoke(() => taskName = this.txtTaskName.Text, true);

Я не сомневаюсь в полезности этого метода для исправления незаконных вызовов в устаревшем коде, но как насчет нового кода?

Хорошо ли использовать этот метод для обновления пользовательского интерфейса в части нового программного обеспечения, если вы не знаете, какой поток пытается обновить ui, или если новый код Winforms обычно содержит специальный выделенный метод с соответствующим Invoke() связанная сантехника для всех таких обновлений пользовательского интерфейса? (Сначала я попытаюсь использовать другие соответствующие методы обработки фона, например, BackgroundWorker.)

Интересно, что это не сработает для ToolStripItems. Я только недавно обнаружил, что они происходят непосредственно из Component вместо Control. Вместо этого следует использовать содержащий ToolStrip invoke.

Последующие комментарии:

Некоторые комментарии предполагают, что:

if (uiElement.InvokeRequired)

должен быть:

if (uiElement.InvokeRequired && uiElement.IsHandleCreated)

Рассмотрим следующую msdn documentation:

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

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

Вы можете защитить от этого случая также проверяя значение IsHandleCreated, когда InvokeRequired возвращает false в фоновом потоке.

Если элемент управления был создан в другом потоке, но дескриптор элемента управления еще не создан, InvokeRequired возвращает значение false. Это означает, что если InvokeRequired возвращает true, IsHandleCreated всегда будет true. Повторное тестирование снова является избыточным и неправильным.

Ответ 1

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

Здесь одна ссылка об этом. Есть и другие.

Но главный ответ у меня есть: Да, я думаю, у вас здесь хорошая идея.

Ответ 2

Вам также следует создать методы продолжения и завершения расширения. И если вы используете дженерики, вы можете сделать вызов немного приятнее.

public static class ControlExtensions
{
  public static void InvokeEx<T>(this T @this, Action<T> action)
    where T : Control
  {
    if (@this.InvokeRequired)
    {
      @this.Invoke(action, new object[] { @this });
    }
    else
    {
      if ([email protected])
        return;
      if (@this.IsDisposed)
        throw new ObjectDisposedException("@this is disposed.");

      action(@this);
    }
  }

  public static IAsyncResult BeginInvokeEx<T>(this T @this, Action<T> action)
    where T : Control
  {
    return @this.BeginInvoke((Action)delegate { @this.InvokeEx(action); });
  }

  public static void EndInvokeEx<T>(this T @this, IAsyncResult result)
    where T : Control
  {
    @this.EndInvoke(result);
  }
}

Теперь ваши звонки становятся немного короче и чище:

this.lblTimeDisplay.InvokeEx(l => l.Text = this.task.Duration.ToString());

var result = this.BeginInvokeEx(f => f.Text = "Different Title");
// ... wait
this.EndInvokeEx(result);

И что касается Component s, просто вызовите его в самой форме или контейнере.

this.InvokeEx(f => f.toolStripItem1.Text = "Hello World");

Ответ 3

На самом деле это не ответ, но ответы на некоторые ответы принимаются.

Для стандартных шаблонов IAsyncResult метод BeginXXX содержит параметр AsyncCallback, поэтому, если вы хотите сказать "мне все равно", просто вызовите EndInvoke, когда это будет сделано, и проигнорируйте результат ", вы может сделать что-то вроде этого (это для Action, но должно быть настроено для других типов делегатов):

    ...
    public static void BeginInvokeEx(this Action a){
        a.BeginInvoke(a.EndInvoke, a);
    }
    ...
    // Don't worry about EndInvoke
    // it will be called when finish
    new Action(() => {}).BeginInvokeEx(); 

(К сожалению, у меня нет решения не иметь вспомогательную функцию без объявления переменной каждый раз при использовании этого шаблона).

Но для Control.BeginInvoke мы не имеем AsyncCallback, поэтому нет простого способа выразить это с помощью Control.EndInvoke, который должен быть вызван. Способ, которым он был разработан, подсказывает, что Control.EndInvoke является необязательным.