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

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

/// <summary>
/// Execute a method on the control's 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's 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);

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

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

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

Продолжение комментариев:

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

if (uiElement.InvokeRequired)

должно быть:

if (uiElement.InvokeRequired && uiElement.IsHandleCreated)

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

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

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

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

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

3 ответа

Решение

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

Вот одна ссылка, говорящая об этом. Есть и другие.

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

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

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 (!@this.IsHandleCreated)
        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);

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

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

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

Для стандарта 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 не является обязательным.

Другие вопросы по тегам