Уместно ли расширять 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);
И что касается Component
s, просто вызовите форму или сам контейнер.
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
не является обязательным.