É apropriado estender o Controle para fornecer uma funcionalidade Invoke / BeginInvoke consistente e segura?

No decorrer da minha manutenção de um aplicativo mais antigo que violou as regras de atualização em vários segmentos do WinForms, criei o seguinte método de extensão como uma forma de corrigir rapidamente chamadas inválidas quando as descobri:

///  /// Execute a method on the control's owning thread. ///  /// The control that is being updated. /// The method that updates uiElement. /// 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. 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(); } } 

Uso da amostra:

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

Eu gosto de como posso aproveitar closures para ler, também, embora forceSynchronous precise ser verdadeiro nesse caso:

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

Eu não questiono a utilidade desse método para corrigir chamadas ilegais no código legado, mas e o novo código?

É um bom design usar esse método para atualizar a interface do usuário em um novo software quando você não souber qual segmento está tentando atualizar a interface do usuário ou se o novo código do WinForms geralmente contiver um método específico e dedicado com o Invoke() apropriado. encanamento relacionado para todas essas atualizações de interface do usuário? (Vou tentar usar as outras técnicas de processamento de fundo apropriadas primeiro, por exemplo, BackgroundWorker.)

Curiosamente, isso não funcionará para ToolStripItems . Eu descobri recentemente que eles derivam diretamente do Component em vez de Control . Em vez disso, a invocação do ToolStrip contendo deve ser usada.

Acompanhamento aos comentários:

Alguns comentários sugerem que:

 if (uiElement.InvokeRequired) 

deveria estar:

 if (uiElement.InvokeRequired && uiElement.IsHandleCreated) 

Considere a seguinte documentação do msdn :

Isso significa que InvokeRequired pode retornar false se Invoke não for necessário (a chamada ocorre no mesmo thread) ou se o controle tiver sido criado em um thread diferente, mas o handle do controle ainda não tiver sido criado.

No caso em que o identificador do controle ainda não foi criado, você não deve simplesmente chamar propriedades, methods ou events no controle. Isso pode fazer com que o identificador do controle seja criado no thread de segundo plano, isolando o controle em um thread sem uma bomba de mensagens e tornando o aplicativo instável.

Você pode proteger contra esse caso, verificando também o valor de IsHandleCreated quando InvokeRequired retorna false em um thread de segundo plano.

Se o controle foi criado em um thread diferente, mas o identificador do controle ainda não foi criado, InvokeRequired retorna false. Isso significa que se InvokeRequired retornar true , IsHandleCreated sempre será verdadeiro. O teste novamente é redundante e incorreto.

Eu gosto da ideia geral, mas vejo um problema. É importante processar EndInvokes ou você pode ter vazamentos de resources. Eu sei que muitas pessoas não acreditam nisso, mas é verdade.

Aqui está um link falando sobre isso . Há outros também.

Mas a principal resposta que tenho é: Sim, acho que você tem uma boa ideia aqui.

Você deve criar methods de extensão Begin e End também. E se você usar genéricos, você pode fazer a chamada parecer um pouco melhor.

 public static class ControlExtensions { public static void InvokeEx(this T @this, Action 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(this T @this, Action action) where T : Control { return @this.BeginInvoke((Action)delegate { @this.InvokeEx(action); }); } public static void EndInvokeEx(this T @this, IAsyncResult result) where T : Control { @this.EndInvoke(result); } } 

Agora suas chamadas ficam um pouco mais curtas e limpas:

 this.lblTimeDisplay.InvokeEx(l => l.Text = this.task.Duration.ToString()); var result = this.BeginInvokeEx(f => f.Text = "Different Title"); // ... wait this.EndInvokeEx(result); 

E no que diz respeito aos Component , basta invocar o formulário ou recipiente em si.

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

Esta não é realmente uma resposta, mas responde alguns comentários para a resposta aceita.

Para padrões IAsyncResult padrão, o método BeginXXX contém o parâmetro AsyncCallback , portanto, se você quiser dizer “Não me importo com isso – basta chamar EndInvoke quando terminar e ignorar o resultado”, você pode fazer algo assim (isso é para Action mas deve poder ser ajustado para outros tipos de delegates):

  ... 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(); 

(Infelizmente eu não tenho uma solução para não ter uma function auxiliar sem declarar uma variável toda vez que usar esse padrão).

Mas para o Control.BeginInvoke não temos o AsyncCallBack , então não há maneira fácil de expressar isso com o Control.EndInvoke garantido para ser chamado. A maneira como ele foi projetado indica o fato de que Control.EndInvoke é opcional.