É apropriado para estender o controle para fornecer funcionalidade Invoke / BeginInvoke consistentemente seguro?
-
23-08-2019 - |
Pergunta
No decorrer da minha manutenção para um aplicativo mais antigo que mal violado as regras de atualização cross-fio em winforms, eu criei o seguinte método de extensão como uma maneira de corrigir rapidamente as chamadas ilegais quando eu descobri-los:
/// <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();
}
}
Exemplo de uso:
this.lblTimeDisplay.SafeInvoke(() => this.lblTimeDisplay.Text = this.task.Duration.ToString(), false);
Eu gosto de como eu posso alavancar fechamentos de ler, também, embora forceSynchronous precisa ser verdadeira nesse caso:
string taskName = string.Empty;
this.txtTaskName.SafeInvoke(() => taskName = this.txtTaskName.Text, true);
Eu não questionam a utilidade deste método para a fixação de chamadas ilegais no código legado, mas que sobre novo código?
É bom projeto para usar esse método para atualização UI em um pedaço de um novo software quando você pode não saber o segmento está tentando atualizar a interface do usuário, ou deveria novo código Winforms geralmente contêm um específico, método dedicado com o Invoke()
apropriada encanamento -relacionados para todas essas atualizações de interface do usuário? (Vou tentar usar as outras técnicas de processamento de fundo apropriadas em primeiro lugar, é claro, por exemplo BackgroundWorker.)
Curiosamente isso não vai funcionar para ToolStripItems . Eu só descobri recentemente que decorrem directamente da Component vez da partir de Controlo . Em vez disso, deve ser usado invocação do ToolStrip
contendo.
Acompanhamento aos comentários:
Alguns comentários sugerem que:
if (uiElement.InvokeRequired)
deve ser:
if (uiElement.InvokeRequired && uiElement.IsHandleCreated)
Considere o seguinte href="http://msdn.microsoft.com/en-us/library/system.windows.forms.control.invokerequired.aspx" rel="noreferrer"> documentação :
Isto significa que InvokeRequired lata false retorno se Invoke não é necessária (A chamada ocorre no mesmo segmento), ou se o controle foi criado em um thread diferente, mas do controle alça ainda não foi criado.
No caso em que o identificador do controle ainda não foi criado, você deve não simplesmente chamar propriedades, métodos, ou eventos de controle. este poder causa o identificador do controle a ser criado na discussão de fundo, isolando o controlo de um segmento sem uma bomba de mensagem e fazer o aplicação instável.
Você pode proteger contra este caso a também verificar o valor de IsHandleCreated quando InvokeRequired retorna falso em uma discussão de fundo.
Se o controle foi criado em um segmento diferente, mas o identificador do controle ainda não foi criado, InvokeRequired
retorna false. Isto significa que se InvokeRequired
retornos true
, IsHandleCreated
será sempre verdade. Testá-lo novamente é redundante e incorreta.
Solução
Eu gosto da idéia geral, mas eu vejo um problema. É importante processar EndInvokes, ou você pode ter vazamentos de recursos. Eu conheço um monte de gente não acreditar, mas é realmente verdade.
Aqui está uma ligação falando sobre isso . Há outros também.
Mas a principal resposta que eu tenho é:. Sim, eu acho que você tem uma boa idéia aqui
Outras dicas
Você deve criar Begin e métodos de extensão End também. E se você usar os genéricos, você pode fazer a chamada olhar um pouco mais agradável.
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);
}
}
Agora suas chamadas obter um aspirador de pouco mais curto e:
this.lblTimeDisplay.InvokeEx(l => l.Text = this.task.Duration.ToString());
var result = this.BeginInvokeEx(f => f.Text = "Different Title");
// ... wait
this.EndInvokeEx(result);
E com relação a Component
s, apenas invoco sobre a forma ou o próprio recipiente.
this.InvokeEx(f => f.toolStripItem1.Text = "Hello World");
Esta não é realmente uma resposta, mas responde a alguns comentários para a resposta aceita.
Para padrões IAsyncResult
padrão, o método BeginXXX
contém parâmetro AsyncCallback
, por isso, se você quer dizer "Eu não me importo com isso - apenas chamar EndInvoke quando ele é feito e ignorar o resultado", você pode fazer algo como isto ( isto é para Action
mas deve ser capaz de ser ajustado para outros tipos de delegado):
...
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 função auxiliar sem declarar uma variável de cada vez que o uso desse padrão).
Mas para Control.BeginInvoke
não temos AsyncCallBack
, então não há nenhuma maneira fácil de expressar isso com Control.EndInvoke
garantido para ser chamado. A forma como ele foi concebido prompts o fato de que Control.EndInvoke
é opcional.