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

В ходе обслуживания старого приложения, которое сильно нарушало правила межпотокового обновления в 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 . Я только недавно обнаружил, что они происходят от Component вместо Control . Вместо этого следует использовать содержащий ToolStrip invoke.

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

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

if (uiElement.InvokeRequired)

должно быть:

if (uiElement.InvokeRequired && uiElement.IsHandleCreated)

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

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

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

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

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

Ответы [ 3 ]

11 голосов
/ 03 апреля 2009

Вы должны также создать методы расширения 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);

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

this.InvokeEx(f => f.toolStripItem1.Text = "Hello World");
5 голосов
/ 03 апреля 2009

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

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

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

0 голосов
/ 30 января 2014

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

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

...