Como atualizo a GUI de outro thread?

1393

Qual é a maneira mais simples de atualizar um Labelde outro Thread?

  • Estou Formem execução thread1, e a partir disso estou iniciando outro thread ( thread2).

  • Enquanto thread2está a processar alguns arquivos eu gostaria de atualizar um Labelno Formcom o estado actual da thread2obra de.

Como eu pude fazer isso?

CruelIO
fonte
25
O .net 2.0+ não tem a classe BackgroundWorker apenas para isso. É segmento da interface do usuário ciente. 1. Criar um BackgroundWorker 2. Adicionar dois delegados (um para processamento, e um para a conclusão)
Preet Sangha
13
talvez um pouco tarde: codeproject.com/KB/cs/Threadsafe_formupdating.aspx
MichaelD
4
Veja a resposta para o .NET 4.5 e C # 5.0: stackoverflow.com/a/18033198/2042090
Ryszard Degan
5
Esta pergunta não se aplica à Gtk # GUI. Para Gtk #, veja esta e esta resposta.
precisa saber é o seguinte
Cuidado: as respostas a essa pergunta agora são uma bagunça desordenada do OT ("aqui está o que eu fiz para o meu aplicativo WPF") e artefatos históricos do .NET 2.0.
Marc L.

Respostas:

768

Para o .NET 2.0, aqui está um bom código que escrevi que faz exatamente o que você deseja e funciona para qualquer propriedade em Control:

private delegate void SetControlPropertyThreadSafeDelegate(
    Control control, 
    string propertyName, 
    object propertyValue);

public static void SetControlPropertyThreadSafe(
    Control control, 
    string propertyName, 
    object propertyValue)
{
  if (control.InvokeRequired)
  {
    control.Invoke(new SetControlPropertyThreadSafeDelegate               
    (SetControlPropertyThreadSafe), 
    new object[] { control, propertyName, propertyValue });
  }
  else
  {
    control.GetType().InvokeMember(
        propertyName, 
        BindingFlags.SetProperty, 
        null, 
        control, 
        new object[] { propertyValue });
  }
}

Chame assim:

// thread-safe equivalent of
// myLabel.Text = status;
SetControlPropertyThreadSafe(myLabel, "Text", status);

Se você estiver usando o .NET 3.0 ou superior, poderá reescrever o método acima como um método de extensão da Controlclasse, o que simplificaria a chamada para:

myLabel.SetPropertyThreadSafe("Text", status);

ATUALIZAÇÃO 10/05/2010:

Para o .NET 3.0, você deve usar este código:

private delegate void SetPropertyThreadSafeDelegate<TResult>(
    Control @this, 
    Expression<Func<TResult>> property, 
    TResult value);

public static void SetPropertyThreadSafe<TResult>(
    this Control @this, 
    Expression<Func<TResult>> property, 
    TResult value)
{
  var propertyInfo = (property.Body as MemberExpression).Member 
      as PropertyInfo;

  if (propertyInfo == null ||
      !@this.GetType().IsSubclassOf(propertyInfo.ReflectedType) ||
      @this.GetType().GetProperty(
          propertyInfo.Name, 
          propertyInfo.PropertyType) == null)
  {
    throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
  }

  if (@this.InvokeRequired)
  {
      @this.Invoke(new SetPropertyThreadSafeDelegate<TResult> 
      (SetPropertyThreadSafe), 
      new object[] { @this, property, value });
  }
  else
  {
      @this.GetType().InvokeMember(
          propertyInfo.Name, 
          BindingFlags.SetProperty, 
          null, 
          @this, 
          new object[] { value });
  }
}

que usa expressões LINQ e lambda para permitir uma sintaxe muito mais limpa, mais simples e segura:

myLabel.SetPropertyThreadSafe(() => myLabel.Text, status); // status has to be a string or this will fail to compile

Além de o nome da propriedade agora ser verificado no momento da compilação, o tipo da propriedade também é, portanto, é impossível (por exemplo) atribuir um valor de string a uma propriedade booleana e, portanto, causar uma exceção de tempo de execução.

Infelizmente, isso não impede ninguém de fazer coisas estúpidas, como passar Controla propriedade e o valor de outra pessoa , de modo que o seguinte será felizmente compilado:

myLabel.SetPropertyThreadSafe(() => aForm.ShowIcon, false);

Portanto, adicionei as verificações de tempo de execução para garantir que a propriedade transferida realmente pertença à Controlqual o método está sendo chamado. Não é perfeito, mas ainda é muito melhor que a versão .NET 2.0.

Se alguém tiver mais sugestões sobre como melhorar esse código para segurança em tempo de compilação, comente!

Ian Kemp
fonte
3
Há casos em que this.GetType () é avaliado da mesma forma que propertyInfo.ReflectedType (por exemplo, LinkLabel no WinForms). Eu não tenho uma grande experiência em C #, mas acho que a condição de exceção deve ser: if (propertyInfo == null || ([email protected] (). IsSubclassOf (propertyInfo.ReflectedType) && @ this.GetType ( )! = propertyInfo.ReflectedType) || @ this.GetType (). GetProperty (propertyInfo.Name, propertyInfo.PropertyType) == null)
Corvin
9
@lan isso pode SetControlPropertyThreadSafe(myLabel, "Text", status)ser chamado de outro módulo ou classe ou forma
Smith
71
A solução fornecida é desnecessariamente complexa. Veja a solução de Marc Gravell, ou a solução de Zaid Masud, se você valoriza a simplicidade.
31414 Frank Hileman #
8
Essa solução desperdiça toneladas de recursos se você atualizar várias propriedades, pois cada Invocação custa muitos recursos. Eu não acho que seja assim que o recurso do Thread Safety foi planejado. Do Encapsulte suas ações de atualização de interface do usuário e invocá-lo uma vez (e não por propriedade)
Console
4
Por que diabos você usaria esse código no componente BackgroundWorker?
Andy
1080

A maneira mais simples é um método anônimo passado para Label.Invoke:

// Running on the worker thread
string newText = "abc";
form.Label.Invoke((MethodInvoker)delegate {
    // Running on the UI thread
    form.Label.Text = newText;
});
// Back on the worker thread

Observe que Invokebloqueia a execução até que ela seja concluída - este é um código síncrono. A pergunta não pergunta sobre código assíncrono, mas há muito conteúdo no Stack Overflow sobre a gravação de código assíncrono quando você deseja aprender sobre ele.

Marc Gravell
fonte
8
Vendo como o OP não mencionou qualquer classe / instância , exceto a forma, que não é uma má padrão ...
Marc Gravell
39
Não esqueça que a palavra-chave "this" está referenciando uma classe "Control".
AZ.
8
@codecompleting é seguro de qualquer maneira, e já sabemos que trabalhamos, então por que verificar algo que sabemos?
Marc Gravell
4
@ Dragouf não é realmente - um dos pontos de usar esse método é que você já sabe quais partes são executadas no trabalhador e quais são executadas no thread da interface do usuário. Não há necessidade de verificar.
Marc Gravell
3
@ Joan.bdm não há contexto suficientemente próximo para eu comentar sobre isso
Marc Gravell
401

Manuseando trabalhos longos

Desde o .NET 4.5 e C # 5.0, você deve usar o padrão assíncrono baseado em tarefas (TAP) junto com o async - aguarde palavras-chave em todas as áreas (incluindo a GUI):

TAP é o padrão de design assíncrono recomendado para novos desenvolvimentos

em vez do modelo de programação assíncrona (APM) e do padrão assíncrono baseado em eventos (EAP) (o último inclui a classe BackgroundWorker ).

Em seguida, a solução recomendada para o novo desenvolvimento é:

  1. Implementação assíncrona de um manipulador de eventos (Sim, isso é tudo):

    private async void Button_Clicked(object sender, EventArgs e)
    {
        var progress = new Progress<string>(s => label.Text = s);
        await Task.Factory.StartNew(() => SecondThreadConcern.LongWork(progress),
                                    TaskCreationOptions.LongRunning);
        label.Text = "completed";
    }
  2. Implementação do segundo thread que notifica o thread da interface do usuário:

    class SecondThreadConcern
    {
        public static void LongWork(IProgress<string> progress)
        {
            // Perform a long running work...
            for (var i = 0; i < 10; i++)
            {
                Task.Delay(500).Wait();
                progress.Report(i.ToString());
            }
        }
    }

Observe o seguinte:

  1. Código curto e limpo, escrito de maneira seqüencial, sem retornos de chamada e threads explícitos.
  2. Tarefa em vez de Thread .
  3. palavra - chave async , que permite usar wait, o que impede o manipulador de eventos de atingir o estado de conclusão até que a tarefa seja concluída e, enquanto isso, não bloqueia o thread da interface do usuário.
  4. Classe Progress (consulte IProgress Interface ), que suporta o princípio de design SoC (Separation of Concerns) e não requer expedição e chamada explícitas. Ele usa o atual SynchronizationContext de seu local de criação (aqui o thread da interface do usuário).
  5. TaskCreationOptions.LongRunning que sugere que não enfileirem a tarefa no ThreadPool .

Para exemplos mais detalhados, consulte: O futuro do C #: coisas boas chegam para aqueles que 'esperam' por Joseph Albahari .

Veja também sobre o conceito de UI Threading Model .

Manipulando exceções

O snippet abaixo é um exemplo de como lidar com exceções e com a Enabledpropriedade do botão de alternância para impedir vários cliques durante a execução em segundo plano.

private async void Button_Click(object sender, EventArgs e)
{
    button.Enabled = false;

    try
    {
        var progress = new Progress<string>(s => button.Text = s);
        await Task.Run(() => SecondThreadConcern.FailingWork(progress));
        button.Text = "Completed";
    }
    catch(Exception exception)
    {
        button.Text = "Failed: " + exception.Message;
    }

    button.Enabled = true;
}

class SecondThreadConcern
{
    public static void FailingWork(IProgress<string> progress)
    {
        progress.Report("I will fail in...");
        Task.Delay(500).Wait();

        for (var i = 0; i < 3; i++)
        {
            progress.Report((3 - i).ToString());
            Task.Delay(500).Wait();
        }

        throw new Exception("Oops...");
    }
}
Ryszard Dżegan
fonte
2
Se SecondThreadConcern.LongWork()lança uma exceção, ela pode ser capturada pelo thread da interface do usuário? Este é um excelente post, aliás.
precisa saber é o seguinte
2
Adicionei uma seção adicional à resposta para atender aos seus requisitos. Saudações.
Ryszard Dżegan
3
A classe ExceptionDispatchInfo é responsável por esse milagre de repetir novamente a exceção de segundo plano no thread da interface do usuário no padrão de espera assíncrona.
Ryszard Dżegan
1
Sou eu que penso que essa maneira de fazer isso é muito mais detalhada do que apenas invocar Invoke / Begin ?!
MeTitus 28/09
2
Task.Delay(500).Wait()? Qual é o sentido de criar uma tarefa para bloquear apenas o segmento atual? Você nunca deve bloquear um thread do conjunto de threads!
Yarik
236

Variação da solução mais simples de Marc Gravell para o .NET 4:

control.Invoke((MethodInvoker) (() => control.Text = "new text"));

Ou use o delegado de ação:

control.Invoke(new Action(() => control.Text = "new text"));

Veja aqui uma comparação dos dois: MethodInvoker vs Action for Control.BeginInvoke

Zaid Masud
fonte
1
o que é 'controle' neste exemplo? Meu controle de interface do usuário? Tentando implementar isso no WPF em um controle de rótulo e Invoke não é um membro do meu rótulo.
Dbloom
O que há no método de extensão como @styxriver stackoverflow.com/a/3588137/206730 ?
Kanjet
declare 'Ação y;' dentro da classe ou método alterando a propriedade text e atualize o texto com este trecho de código 'yourcontrol.Invoke (y = () => yourcontrol.Text = "new text");'
Antonio Leite
4
@Dbloom não é um membro, porque é apenas para WinForms. Para WPF você usa Dispatcher.Invoke
SLW
1
Eu estava seguindo esta solução, mas às vezes minha interface do usuário não estava sendo atualizada. Descobri que eu preciso this.refresh()de força invalidar e repintar o GUI .. se é útil ..
Rakibul Haq
137

Dispare e esqueça o método de extensão para o .NET 3.5+

using System;
using System.Windows.Forms;

public static class ControlExtensions
{
    /// <summary>
    /// Executes the Action asynchronously on the UI thread, does not block execution on the calling thread.
    /// </summary>
    /// <param name="control"></param>
    /// <param name="code"></param>
    public static void UIThread(this Control @this, Action code)
    {
        if (@this.InvokeRequired)
        {
            @this.BeginInvoke(code);
        }
        else
        {
            code.Invoke();
        }
    }
}

Isso pode ser chamado usando a seguinte linha de código:

this.UIThread(() => this.myLabel.Text = "Text Goes Here");
StyxRiver
fonte
5
Qual é o sentido do uso deste @? "Controle" não seria equivalente? Existem benefícios para @this?
argyle
14
@jeromeyers - O @thisé simplesmente o nome da variável; nesse caso, a referência ao controle atual que chama o ramal. Você pode renomeá-lo para a fonte ou o que quer que seja o seu barco. Eu uso @this, porque está se referindo a 'este controle' que está chamando a extensão e é consistente (pelo menos na minha cabeça) com o uso da palavra-chave 'this' no código normal (sem extensão).
StyxRiver
1
Isso é ótimo, fácil e para mim a melhor solução. Você pode incluir todo o trabalho que você precisa fazer no thread da interface do usuário. Exemplo: this.UIThread (() => {txtMessage.Text = message; listBox1.Items.Add (message);});
Auto
1
Eu realmente gosto desta solução. Nit menor: eu chamaria esse método em OnUIThreadvez de UIThread.
Home
2
Por isso nomeei essa extensão RunOnUiThread. Mas isso é apenas gosto pessoal.
Grisgram 30/01/19
66

Esta é a maneira clássica de fazer isso:

using System;
using System.Windows.Forms;
using System.Threading;

namespace Test
{
    public partial class UIThread : Form
    {
        Worker worker;

        Thread workerThread;

        public UIThread()
        {
            InitializeComponent();

            worker = new Worker();
            worker.ProgressChanged += new EventHandler<ProgressChangedArgs>(OnWorkerProgressChanged);
            workerThread = new Thread(new ThreadStart(worker.StartWork));
            workerThread.Start();
        }

        private void OnWorkerProgressChanged(object sender, ProgressChangedArgs e)
        {
            // Cross thread - so you don't get the cross-threading exception
            if (this.InvokeRequired)
            {
                this.BeginInvoke((MethodInvoker)delegate
                {
                    OnWorkerProgressChanged(sender, e);
                });
                return;
            }

            // Change control
            this.label1.Text = e.Progress;
        }
    }

    public class Worker
    {
        public event EventHandler<ProgressChangedArgs> ProgressChanged;

        protected void OnProgressChanged(ProgressChangedArgs e)
        {
            if(ProgressChanged!=null)
            {
                ProgressChanged(this,e);
            }
        }

        public void StartWork()
        {
            Thread.Sleep(100);
            OnProgressChanged(new ProgressChangedArgs("Progress Changed"));
            Thread.Sleep(100);
        }
    }


    public class ProgressChangedArgs : EventArgs
    {
        public string Progress {get;private set;}
        public ProgressChangedArgs(string progress)
        {
            Progress = progress;
        }
    }
}

Seu segmento de trabalho tem um evento. Seu encadeamento da interface do usuário inicia outro encadeamento para executar o trabalho e conecta esse evento de trabalho para que você possa exibir o estado do encadeamento de trabalho.

Na interface do usuário, você precisará cruzar os threads para alterar o controle real ... como um rótulo ou uma barra de progresso.

Tem
fonte
62

A solução simples é usar Control.Invoke.

void DoSomething()
{
    if (InvokeRequired) {
        Invoke(new MethodInvoker(updateGUI));
    } else {
        // Do Something
        updateGUI();
    }
}

void updateGUI() {
    // update gui here
}
OregonGhost
fonte
bem feito pela simplicidade! não apenas simples, mas também funciona bem! Eu realmente não entendi por que a Microsoft não poderia torná-lo mais simples, como deveria ser! Para chamar uma linha no segmento principal, devemos escrever algumas funções!
MBH
1
@MBH Concordo. BTW, você percebeu a resposta stackoverflow.com/a/3588137/199364 acima, que define um método de extensão? Faça isso uma vez em uma classe utilitários personalizados, então não tem que se importar mais que a Microsoft não fez isso para nós :)
ToolmakerSteve
@ToolmakerSteve Isso é exatamente o que significava ser! você está certo, podemos encontrar uma maneira, mas quero dizer, do ponto de vista DRY (não se repita), o problema que tem uma solução comum, pode ser resolvido por eles com o mínimo esforço da Microsoft, o que economizará muito tempo para você. programadores :)
MBH
47

O código de encadeamento geralmente é com erros e sempre difícil de testar. Você não precisa escrever um código de encadeamento para atualizar a interface do usuário de uma tarefa em segundo plano. Basta usar a classe BackgroundWorker para executar a tarefa e seu método ReportProgress para atualizar a interface do usuário. Geralmente, você apenas reporta uma porcentagem concluída, mas há outra sobrecarga que inclui um objeto de estado. Aqui está um exemplo que apenas relata um objeto string:

    private void button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.WorkerReportsProgress = true;
        backgroundWorker1.RunWorkerAsync();
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "A");
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "B");
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "C");
    }

    private void backgroundWorker1_ProgressChanged(
        object sender, 
        ProgressChangedEventArgs e)
    {
        label1.Text = e.UserState.ToString();
    }

Tudo bem se você sempre deseja atualizar o mesmo campo. Se você tiver atualizações mais complicadas, poderá definir uma classe para representar o estado da interface do usuário e passá-lo para o método ReportProgress.

Uma coisa final, não se esqueça de definir o WorkerReportsProgresssinalizador, ou o ReportProgressmétodo será completamente ignorado.

Don Kirkby
fonte
2
No final do processamento, também é possível atualizar a interface do usuário via backgroundWorker1_RunWorkerCompleted.
DavidRR
41

A grande maioria das respostas usa Control.Invokeuma condição de corrida esperando para acontecer . Por exemplo, considere a resposta aceita:

string newText = "abc"; // running on worker thread
this.Invoke((MethodInvoker)delegate { 
    someLabel.Text = newText; // runs on UI thread
});

Se o usuário fechar o formulário antes de this.Invokeser chamado (lembre-se, thisé o Formobjeto), ObjectDisposedExceptionprovavelmente será acionado.

A solução é usar SynchronizationContext, especificamente SynchronizationContext.Currentcomo sugere hamilton.danielb (outras respostas dependem de SynchronizationContextimplementações específicas que são completamente desnecessárias). Eu modificaria levemente o código dele para usar em SynchronizationContext.Postvez de SynchronizationContext.Sendfazê-lo (como normalmente não há necessidade de o thread de trabalho aguardar):

public partial class MyForm : Form
{
    private readonly SynchronizationContext _context;
    public MyForm()
    {
        _context = SynchronizationContext.Current
        ...
    }

    private MethodOnOtherThread()
    {
         ...
         _context.Post(status => someLabel.Text = newText,null);
    }
}

Observe que, no .NET 4.0 e superior, você realmente deve usar tarefas para operações assíncronas. Veja a resposta da n-san para a abordagem equivalente baseada em tarefas (usando TaskScheduler.FromCurrentSynchronizationContext).

Por fim, no .NET 4.5 e superior, você também pode usar Progress<T>(que basicamente é capturado SynchronizationContext.Currentapós a criação), como demonstrado por Ryszard Degan, nos casos em que a operação de longa duração precisa executar o código da interface do usuário enquanto ainda está trabalhando.

Ohad Schneider
fonte
37

Você precisará garantir que a atualização ocorra no thread correto; o thread da interface do usuário.

Para fazer isso, você precisará chamar o manipulador de eventos em vez de chamá-lo diretamente.

Você pode fazer isso aumentando seu evento assim:

(O código é digitado aqui da minha cabeça, por isso não verifiquei a sintaxe correta, etc., mas deve ajudá-lo.)

if( MyEvent != null )
{
   Delegate[] eventHandlers = MyEvent.GetInvocationList();

   foreach( Delegate d in eventHandlers )
   {
      // Check whether the target of the delegate implements 
      // ISynchronizeInvoke (Winforms controls do), and see
      // if a context-switch is required.
      ISynchronizeInvoke target = d.Target as ISynchronizeInvoke;

      if( target != null && target.InvokeRequired )
      {
         target.Invoke (d, ... );
      }
      else
      {
          d.DynamicInvoke ( ... );
      }
   }
}

Observe que o código acima não funcionará em projetos WPF, pois os controles WPF não implementam a ISynchronizeInvokeinterface.

A fim de se certificar de que o código acima funciona com Windows Forms e WPF, e todas as outras plataformas, você pode ter um olhar para os AsyncOperation, AsyncOperationManagere SynchronizationContextclasses.

Para gerar eventos facilmente dessa maneira, criei um método de extensão, que me permite simplificar o aumento de um evento apenas chamando:

MyEvent.Raise(this, EventArgs.Empty);

Obviamente, você também pode fazer uso da classe BackGroundWorker, que abstrairá esse assunto para você.

Frederik Gheysels
fonte
Na verdade, mas eu não gosto de 'bagunçar' meu código GUI com esse assunto. Minha GUI não deve se importar se precisa chamar ou não. Em outras palavras: eu não acho que seja da responsabilidade da GUI executar o contexto-swithc.
Frederik Gheysels 19/03/09
1
Dividir o delegado etc parece um exagero - por que não apenas: SynchronizationContext.Current.Send (delegate {MyEvent (...);}, null);
Marc Gravell
Você sempre tem acesso ao SynchronizationContext? Mesmo se sua classe estiver em uma biblioteca de classes?
Frederik Gheysels 19/03/09
29

Você precisará chamar o método no thread da GUI. Você pode fazer isso chamando Control.Invoke.

Por exemplo:

delegate void UpdateLabelDelegate (string message);

void UpdateLabel (string message)
{
    if (InvokeRequired)
    {
         Invoke (new UpdateLabelDelegate (UpdateLabel), message);
         return;
    }

    MyLabelControl.Text = message;
}
Kieron
fonte
1
A linha de chamada me dá um erro do compilador. A melhor correspondência método sobrecarregado para 'System.Windows.Forms.Control.Invoke (System.Delegate, objeto [])' tem alguns argumentos inválidos
CruelIO
28

Por causa da trivialidade do cenário, eu realmente teria a pesquisa de threads da interface do usuário para o status. Eu acho que você vai achar que pode ser bem elegante.

public class MyForm : Form
{
  private volatile string m_Text = "";
  private System.Timers.Timer m_Timer;

  private MyForm()
  {
    m_Timer = new System.Timers.Timer();
    m_Timer.SynchronizingObject = this;
    m_Timer.Interval = 1000;
    m_Timer.Elapsed += (s, a) => { MyProgressLabel.Text = m_Text; };
    m_Timer.Start();
    var thread = new Thread(WorkerThread);
    thread.Start();
  }

  private void WorkerThread()
  {
    while (...)
    {
      // Periodically publish progress information.
      m_Text = "Still working...";
    }
  }
}

A abordagem evita a operação de empacotamento necessária ao usar os métodos ISynchronizeInvoke.Invokee ISynchronizeInvoke.BeginInvoke. Não há nada errado em usar a técnica de empacotamento, mas há algumas ressalvas que você precisa estar ciente.

  • Certifique-se de não ligar com BeginInvokemuita frequência ou isso pode sobrecarregar a bomba de mensagens.
  • A chamada Invokeno segmento de trabalho é uma chamada de bloqueio. Ele interromperá temporariamente o trabalho que está sendo feito nesse segmento.

A estratégia que proponho nesta resposta inverte os papéis de comunicação dos threads. Em vez de o thread do trabalhador enviar os dados, o thread da interface do usuário faz pesquisas. Esse é um padrão comum usado em muitos cenários. Como tudo que você deseja fazer é exibir informações de progresso do thread de trabalho, acho que você descobrirá que essa solução é uma ótima alternativa à solução de empacotamento. Tem as seguintes vantagens.

  • A interface do usuário e os threads de trabalho permanecem fracamente acoplados em oposição à abordagem Control.Invokeou Control.BeginInvokeque os acopla firmemente.
  • O thread da interface do usuário não impedirá o progresso do thread do trabalhador.
  • O thread de trabalho não pode dominar o tempo que o thread da interface do usuário gasta a atualização.
  • Os intervalos nos quais os threads da interface do usuário e do trabalhador executam operações podem permanecer independentes.
  • O encadeamento do trabalhador não pode exceder a bomba de mensagens do encadeamento da interface do usuário.
  • O segmento da interface do usuário determina quando e com que frequência a interface do usuário é atualizada.
Brian Gideon
fonte
3
Boa ideia. A única coisa que você não mencionou é como você descarta adequadamente o timer depois que o WorkerThread é concluído. Observe que isso pode causar problemas quando o aplicativo termina (ou seja, o usuário fecha o aplicativo). Você tem uma idéia de como resolver isso?
Matt
@ Matt Em vez de usar um manipulador de anonimato por Elapsedevento, você usar um método de membro para que você possa remover o temporizador quando o formulário é descartado ...
Phil1970
@ Phil1970 - Bom ponto. Você quis dizer gostar System.Timers.ElapsedEventHandler handler = (s, a) => { MyProgressLabel.Text = m_Text; };e atribuí-lo via m_Timer.Elapsed += handler;, mais tarde, no contexto de descarte, m_Timer.Elapsed -= handler;estou certo? E para o descarte / fechamento seguindo os conselhos discutidos aqui .
Matt
27

Nenhum dos itens de Invoke nas respostas anteriores é necessário.

Você precisa olhar para WindowsFormsSynchronizationContext:

// In the main thread
WindowsFormsSynchronizationContext mUiContext = new WindowsFormsSynchronizationContext();

...

// In some non-UI Thread

// Causes an update in the GUI thread.
mUiContext.Post(UpdateGUI, userData);

...

void UpdateGUI(object userData)
{
    // Update your GUI controls here
}
Jon H
fonte
4
o que você acha que o método Post usa sob o capô? :)
increddibelly 5/05
23

Essa é semelhante à solução acima, usando o .NET Framework 3.0, mas resolveu o problema do suporte à segurança em tempo de compilação .

public  static class ControlExtension
{
    delegate void SetPropertyValueHandler<TResult>(Control souce, Expression<Func<Control, TResult>> selector, TResult value);

    public static void SetPropertyValue<TResult>(this Control source, Expression<Func<Control, TResult>> selector, TResult value)
    {
        if (source.InvokeRequired)
        {
            var del = new SetPropertyValueHandler<TResult>(SetPropertyValue);
            source.Invoke(del, new object[]{ source, selector, value});
        }
        else
        {
            var propInfo = ((MemberExpression)selector.Body).Member as PropertyInfo;
            propInfo.SetValue(source, value, null);
        }
    }
}

Usar:

this.lblTimeDisplay.SetPropertyValue(a => a.Text, "some string");
this.lblTimeDisplay.SetPropertyValue(a => a.Visible, false);

O compilador falhará se o usuário transmitir o tipo de dados errado.

this.lblTimeDisplay.SetPropertyValue(a => a.Visible, "sometext");
Francis
fonte
23

Salvete! Depois de pesquisar essa pergunta, achei as respostas de FrankG e Oregon Ghost as mais fáceis e úteis para mim. Agora, eu codifico no Visual Basic e executei esse trecho através de um conversor; então não tenho certeza de como isso acontece.

Eu tenho um formulário de diálogo chamado form_Diagnostics,que possui uma caixa de richtext, chamadaupdateDiagWindow, que eu estou usando como uma espécie de exibição de log. Eu precisava ser capaz de atualizar o texto de todos os tópicos. As linhas extras permitem que a janela role automaticamente para as linhas mais recentes.

E assim, agora posso atualizar a exibição com uma linha, de qualquer lugar do programa inteiro da maneira que você acha que funcionaria sem nenhum encadeamento:

  form_Diagnostics.updateDiagWindow(whatmessage);

Código principal (coloque isso dentro do código de classe do seu formulário):

#region "---------Update Diag Window Text------------------------------------"
// This sub allows the diag window to be updated by all threads
public void updateDiagWindow(string whatmessage)
{
    var _with1 = diagwindow;
    if (_with1.InvokeRequired) {
        _with1.Invoke(new UpdateDiagDelegate(UpdateDiag), whatmessage);
    } else {
        UpdateDiag(whatmessage);
    }
}
// This next line makes the private UpdateDiagWindow available to all threads
private delegate void UpdateDiagDelegate(string whatmessage);
private void UpdateDiag(string whatmessage)
{
    var _with2 = diagwindow;
    _with2.appendtext(whatmessage);
    _with2.SelectionStart = _with2.Text.Length;
    _with2.ScrollToCaret();
}
#endregion
bgmCoder
fonte
21

Para muitos propósitos, é tão simples quanto isto:

public delegate void serviceGUIDelegate();
private void updateGUI()
{
  this.Invoke(new serviceGUIDelegate(serviceGUI));
}

"serviceGUI ()" é um método no nível da GUI no formulário (this) que pode alterar quantos controles você desejar. Chame "updateGUI ()" do outro segmento. Os parâmetros podem ser adicionados para transmitir valores ou (provavelmente mais rápido) usar variáveis ​​de escopo de classe com bloqueios, conforme necessário, se houver a possibilidade de um conflito entre os segmentos acessando-os, o que poderia causar instabilidade. Use BeginInvoke em vez de Invoke se o encadeamento que não seja da GUI for crítico em termos de tempo (lembrando o aviso de Brian Gideon).

Frankg
fonte
21

Isso na variação do C # 3.0 da solução de Ian Kemp:

public static void SetPropertyInGuiThread<C,V>(this C control, Expression<Func<C, V>> property, V value) where C : Control
{
    var memberExpression = property.Body as MemberExpression;
    if (memberExpression == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    var propertyInfo = memberExpression.Member as PropertyInfo;
    if (propertyInfo == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    if (control.InvokeRequired)
        control.Invoke(
            (Action<C, Expression<Func<C, V>>, V>)SetPropertyInGuiThread,
            new object[] { control, property, value }
        );
    else
        propertyInfo.SetValue(control, value, null);
}

Você chama assim:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
  1. Ele adiciona verificação nula ao resultado da "como MemberExpression".
  2. Melhora a segurança de tipo estática.

Caso contrário, o original é uma solução muito agradável.

Rotaerk
fonte
21
Label lblText; //initialized elsewhere

void AssignLabel(string text)
{
   if (InvokeRequired)
   {
      BeginInvoke((Action<string>)AssignLabel, text);
      return;
   }

   lblText.Text = text;           
}

Observe que BeginInvoke()é preferível ao Invoke()contrário, porque é menos provável que cause conflitos (no entanto, este não é um problema aqui ao atribuir texto a um rótulo):

Ao usar, Invoke()você está aguardando o método retornar. Agora, pode ser que você faça algo no código chamado que precisará aguardar o encadeamento, o que pode não ser imediatamente óbvio se estiver oculto em algumas funções que você está chamando, o que pode acontecer indiretamente por meio de manipuladores de eventos. Então você estaria esperando o encadeamento, o encadeamento estaria esperando por você e você está em um impasse.

Isso realmente causou a interrupção de alguns dos nossos softwares lançados. Foi fácil de corrigir, substituindo Invoke()por BeginInvoke(). A menos que você precise de operação síncrona, que pode ser o caso se você precisar de um valor de retorno, use BeginInvoke().

ILoveFortran
fonte
20

Quando encontrei o mesmo problema, procurei ajuda do Google, mas, em vez de me dar uma solução simples, ela me confundiu mais, dando exemplos de MethodInvoker e blá blá blá. Então eu decidi resolver isso sozinho. Aqui está a minha solução:

Faça um delegado como este:

Public delegate void LabelDelegate(string s);

void Updatelabel(string text)
{
   if (label.InvokeRequired)
   {
       LabelDelegate LDEL = new LabelDelegate(Updatelabel);
       label.Invoke(LDEL, text);
   }
   else
       label.Text = text
}

Você pode chamar essa função em um novo thread como este

Thread th = new Thread(() => Updatelabel("Hello World"));
th.start();

Não se confunda Thread(() => .....). Uso uma função anônima ou expressão lambda quando trabalho em um thread. Para reduzir as linhas de código, você também pode usar o ThreadStart(..)método que não devo explicar aqui.

ahmar
fonte
17

Basta usar algo como isto:

 this.Invoke((MethodInvoker)delegate
            {
                progressBar1.Value = e.ProgressPercentage; // runs on UI thread
            });
Hassan Shouman
fonte
Se você possui e.ProgressPercentage, você já não está no thread da interface do usuário do método que está chamando isso?
LarsTech
O evento ProgressChanged é executado no thread da interface do usuário. Essa é uma das conveniências de usar o BackgroundWorker. O evento Concluído também é executado na GUI. A única coisa em execução no thread que não é da interface do usuário é o método DoWork.
LarsTech
15

Você pode usar o delegado já existente Action:

private void UpdateMethod()
{
    if (InvokeRequired)
    {
        Invoke(new Action(UpdateMethod));
    }
}
Embedd_Khurja
fonte
14

Minha versão é inserir uma linha de "mantra" recursivo:

Para sem argumentos:

    void Aaaaaaa()
    {
        if (InvokeRequired) { Invoke(new Action(Aaaaaaa)); return; } //1 line of mantra

        // Your code!
    }

Para uma função que possui argumentos:

    void Bbb(int x, string text)
    {
        if (InvokeRequired) { Invoke(new Action<int, string>(Bbb), new[] { x, text }); return; }
        // Your code!
    }

É isso .


Alguma argumentação : geralmente é ruim para a legibilidade do código colocar {} após umif () instrução em uma linha. Mas, neste caso, é o mesmo "mantra" rotineiro. Não quebra a legibilidade do código se esse método for consistente com o projeto. E evita que seu código seja jogado fora (uma linha de código em vez de cinco).

Como você vê, if(InvokeRequired) {something long}você apenas sabe "esta função é segura para chamar de outro thread".

MajesticRa
fonte
13

Tente atualizar o rótulo usando este

public static class ExtensionMethods
{
    private static Action EmptyDelegate = delegate() { };

    public static void Refresh(this UIElement uiElement)
    {
        uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
    }
}
Ivaylo Slavov
fonte
É para Windows Forms ?
Kanjet
13

Crie uma variável de classe:

SynchronizationContext _context;

Defina-o no construtor que cria sua interface do usuário:

var _context = SynchronizationContext.Current;

Quando você deseja atualizar o rótulo:

_context.Send(status =>{
    // UPDATE LABEL
}, null);
mente negra
fonte
12

Você deve usar invocar e delegar

private delegate void MyLabelDelegate();
label1.Invoke( new MyLabelDelegate(){ label1.Text += 1; });
A. Zalonis
fonte
12

A maioria das outras respostas é um pouco complexa para mim nesta questão (eu sou novo em C #), então estou escrevendo o meu:

Eu tenho um aplicativo WPF e defini um trabalhador como abaixo:

Questão:

BackgroundWorker workerAllocator;
workerAllocator.DoWork += delegate (object sender1, DoWorkEventArgs e1) {
    // This is my DoWork function.
    // It is given as an anonymous function, instead of a separate DoWork function

    // I need to update a message to textbox (txtLog) from this thread function

    // Want to write below line, to update UI
    txt.Text = "my message"

    // But it fails with:
    //  'System.InvalidOperationException':
    //  "The calling thread cannot access this object because a different thread owns it"
}

Solução:

workerAllocator.DoWork += delegate (object sender1, DoWorkEventArgs e1)
{
    // The below single line works
    txtLog.Dispatcher.BeginInvoke((Action)(() => txtLog.Text = "my message"));
}

Ainda estou para descobrir o que a linha acima significa, mas funciona.

Para WinForms :

Solução:

txtLog.Invoke((MethodInvoker)delegate
{
    txtLog.Text = "my message";
});
Manohar Reddy Poreddy
fonte
A pergunta era sobre Winforms, não WPF.
108918 Marc
Obrigado. Solução WinForms adicionada acima.
Manohar Reddy Poreddy
... que é apenas uma cópia de muitas outras respostas sobre essa mesma pergunta, mas tudo bem. Por que não fazer parte da solução e apenas excluir sua resposta?
Marc L.
hmm, correto, você está, se apenas, você leu minha resposta com atenção, a parte inicial (a razão pela qual escrevi a resposta) e, esperançosamente, com um pouco mais de atenção, você vê que há alguém que teve exatamente o mesmo problema e votou hoje hoje para minha resposta simples e, com ainda mais atenção, se você poderia prever a verdadeira história de por que tudo isso aconteceu, o google me envia aqui mesmo quando procuro o wpf. Claro, desde que você perdeu essas três razões mais ou menos óbvias, entendo por que você não remove o voto negativo. Em vez de limpar a opção correta, crie algo novo, que é muito mais difícil.
Manohar Reddy Poreddy 11/07
8

A maneira mais fácil que eu penso:

   void Update()
   {
       BeginInvoke((Action)delegate()
       {
           //do your update
       });
   }
Vasily Semenov
fonte
8

Por exemplo, acesse um controle que não seja o thread atual:

Speed_Threshold = 30;
textOutput.Invoke(new EventHandler(delegate
{
    lblThreshold.Text = Speed_Threshold.ToString();
}));

Existe o lblThresholdLabel e Speed_Thresholdé uma variável global.

Da Xiong
fonte
8

Quando você está no encadeamento da interface do usuário, pode solicitar seu agendador de tarefas de contexto de sincronização. Daria a você um TaskScheduler que agenda tudo no thread da interface do usuário.

Em seguida, você pode encadear suas tarefas para que, quando o resultado estiver pronto, outra tarefa (agendada no encadeamento da interface do usuário) as escolha e as atribua a um rótulo.

public partial class MyForm : Form
{
  private readonly TaskScheduler _uiTaskScheduler;
  public MyForm()
  {
    InitializeComponent();
    _uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
  }

  private void buttonRunAsyncOperation_Click(object sender, EventArgs e)
  {
    RunAsyncOperation();
  }

  private void RunAsyncOperation()
  {
    var task = new Task<string>(LengthyComputation);
    task.ContinueWith(antecedent =>
                         UpdateResultLabel(antecedent.Result), _uiTaskScheduler);
    task.Start();
  }

  private string LengthyComputation()
  {
    Thread.Sleep(3000);
    return "47";
  }

  private void UpdateResultLabel(string text)
  {
    labelResult.Text = text;
  }
}

Isso funciona para tarefas (não threads) que são a maneira preferida de escrever código simultâneo agora .

nosalan
fonte
1
Chamando Task.Startnormalmente não é uma boa prática blogs.msdn.com/b/pfxteam/archive/2012/01/14/10256832.aspx
Ohad Schneider
8

Acabei de ler as respostas e isso parece ser um tópico muito quente. Atualmente, estou usando o .NET 3.5 SP1 e o Windows Forms.

A fórmula conhecida amplamente descrita nas respostas anteriores que utiliza o InvokeRequired propriedade cobre a maioria dos casos, mas não o pool inteiro.

E se o Handle ainda não tiver sido criado?

A propriedade InvokeRequired , conforme descrito aqui (referência de Propriedade Control.InvokeRequired para MSDN) retorna true se a chamada foi feita a partir de um thread que não seja o thread da GUI, false se a chamada foi feita a partir do thread da GUI ou se o Handle foi ainda não criado.

Você pode encontrar uma exceção se desejar que um formulário modal seja mostrado e atualizado por outro encadeamento. Como você deseja que esse formulário seja mostrado modalmente, você pode fazer o seguinte:

private MyForm _gui;

public void StartToDoThings()
{
    _gui = new MyForm();
    Thread thread = new Thread(SomeDelegate);
    thread.Start();
    _gui.ShowDialog();
}

E o delegado pode atualizar um Label na GUI:

private void SomeDelegate()
{
    // Operations that can take a variable amount of time, even no time
    //... then you update the GUI
    if(_gui.InvokeRequired)
        _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
    else
        _gui.Label1.Text = "Done!";
}

Isso pode causar um InvalidOperationException se as operações antes da atualização do rótulo de "levar menos tempo" (lê-lo e interpretá-lo como uma simplificação) do que o tempo que leva para o segmento GUI para criar o formulário 's punho . Isso acontece dentro do ShowDialog () método .

Você também deve verificar o identificador assim:

private void SomeDelegate()
{
    // Operations that can take a variable amount of time, even no time
    //... then you update the GUI
    if(_gui.IsHandleCreated)  //  <---- ADDED
        if(_gui.InvokeRequired)
            _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
        else
            _gui.Label1.Text = "Done!";
}

Você pode manipular a operação para executar se o identificador ainda não tiver sido criado: Você pode simplesmente ignorar a atualização da GUI (como mostrado no código acima) ou pode esperar (mais arriscado). Isso deve responder à pergunta.

Material opcional: Pessoalmente, vim codificando o seguinte:

public class ThreadSafeGuiCommand
{
  private const int SLEEPING_STEP = 100;
  private readonly int _totalTimeout;
  private int _timeout;

  public ThreadSafeGuiCommand(int totalTimeout)
  {
    _totalTimeout = totalTimeout;
  }

  public void Execute(Form form, Action guiCommand)
  {
    _timeout = _totalTimeout;
    while (!form.IsHandleCreated)
    {
      if (_timeout <= 0) return;

      Thread.Sleep(SLEEPING_STEP);
      _timeout -= SLEEPING_STEP;
    }

    if (form.InvokeRequired)
      form.Invoke(guiCommand);
    else
      guiCommand();
  }
}

Alimento meus formulários que são atualizados por outro thread com uma instância deste ThreadSafeGuiCommand e defino métodos que atualizam a GUI (no meu formulário) da seguinte maneira:

public void SetLabeTextTo(string value)
{
  _threadSafeGuiCommand.Execute(this, delegate { Label1.Text = value; });
}

Dessa maneira, tenho certeza de que minha GUI será atualizada, independentemente do encadeamento que fará a chamada, opcionalmente, aguardando um período de tempo bem definido (o tempo limite).

Sume
fonte
1
Vim aqui para encontrar isso, como também verifico IsHandleCreated. Uma outra propriedade para verificar é IsDisposed. Se seu formulário estiver descartado, você não poderá chamar Invoke (). Se o usuário fechou o formulário antes que o encadeamento em segundo plano pudesse ser concluído, você não deseja que ele retorne à interface do usuário quando o formulário for descartado.
11116 Jon
Eu diria que é uma má idéia começar ... Normalmente, você mostraria o formulário filho imediatamente e teria uma barra de progresso ou algum outro feedback ao fazer o processamento em segundo plano. Ou você faria todo o processamento primeiro e depois passaria o resultado para o novo formulário na criação. Fazer as duas coisas ao mesmo tempo geralmente teria benefícios marginais, mas um código muito menos sustentável.
Phil1970
O cenário descrito leva em consideração um formulário modal usado como uma exibição de progresso de um trabalho de encadeamento em segundo plano. Como deve ser modal, deve ser mostrado chamando o método Form.ShowDialog () . Ao fazer isso, você evita que seu código que segue a chamada seja executado até que o formulário seja fechado. Portanto, a menos que você possa iniciar o thread de segundo plano de maneira diferente do exemplo fornecido (e, é claro, você pode), este formulário deve ser mostrado modalmente após o início do thread de segundo plano. Nesse caso, você precisa verificar a criação do identificador. Se você não precisa de um formulário modal, é outra história.
Sumé