Como atualizar a interface do usuário de outro thread em execução em outra class

Atualmente, estou escrevendo meu primeiro programa em C # e sou extremamente novo no idioma (usado apenas para trabalhar com C até agora). Eu fiz muita pesquisa, mas todas as respostas eram gerais demais e eu simplesmente não conseguia fazer isso funcionar.

Então, aqui meu problema (muito comum): Eu tenho um aplicativo WPF que recebe inputs de algumas checkboxs de texto preenchidas pelo usuário e, em seguida, usa isso para fazer muitos cálculos com elas. Eles devem demorar cerca de 2 a 3 minutos, por isso gostaria de atualizar uma barra de progresso e um bloco de texto informando qual é o status atual. Também preciso armazenar as inputs da interface do usuário do usuário e fornecê-las ao thread, portanto, tenho uma terceira class, que uso para criar um object e gostaria de passar esse object para o thread de segundo plano. Obviamente eu iria executar os cálculos em outro thread, para que a interface do usuário não congele, mas eu não sei como atualizar a interface do usuário, uma vez que todos os methods de cálculo são parte de outra class. Depois de muita pesquisa eu acho que o melhor método seria usar despachantes e TPL e não um backgroundworker, mas honestamente eu não tenho certeza de como eles funcionam e depois de cerca de 20 horas de tentativa e erro com outras respostas, eu decidi perguntar uma pergunta eu mesmo.

Aqui uma estrutura muito simples do meu programa:

public partial class MainWindow : Window { public MainWindow() { Initialize Component(); } private void startCalc(object sender, RoutedEventArgs e) { inputValues input = new inputValues(); calcClass calculations = new calcClass(); try { input.pota = Convert.ToDouble(aVar.Text); input.potb = Convert.ToDouble(bVar.Text); input.potc = Convert.ToDouble(cVar.Text); input.potd = Convert.ToDouble(dVar.Text); input.potf = Convert.ToDouble(fVar.Text); input.potA = Convert.ToDouble(AVar.Text); input.potB = Convert.ToDouble(BVar.Text); input.initStart = Convert.ToDouble(initStart.Text); input.initEnd = Convert.ToDouble(initEnd.Text); input.inita = Convert.ToDouble(inita.Text); input.initb = Convert.ToDouble(initb.Text); input.initc = Convert.ToDouble(initb.Text); } catch { MessageBox.Show("Some input values are not of the expected Type.", "Wrong Input", MessageBoxButton.OK, MessageBoxImage.Error); } Thread calcthread = new Thread(new ParameterizedThreadStart(calculations.testMethod); calcthread.Start(input); } public class inputValues { public double pota, potb, potc, potd, potf, potA, potB; public double initStart, initEnd, inita, initb, initc; } public class calcClass { public void testmethod(inputValues input) { Thread.CurrentThread.Priority = ThreadPriority.Lowest; int i; //the input object will be used somehow, but that doesn't matter for my problem for (i = 0; i < 1000; i++) { Thread.Sleep(10); } } } 

Eu ficaria muito grato se alguém tivesse uma explicação simples de como atualizar a interface do usuário de dentro do método de teste. Desde que eu sou novo em C # e programação orientada a object, respostas muito complicadas eu provavelmente não vai entender, eu vou fazer o meu melhor embora.

Além disso, se alguém tiver uma idéia melhor em geral (talvez usando backgroundworker ou qualquer outra coisa), estou aberto para vê-la.

Primeiro você precisa usar Dispatcher.Invoke para alterar a interface do usuário de outro segmento e fazer isso de outra class, você pode usar events.
Em seguida, você pode se registrar nesse evento na class principal e despachar as alterações para a interface do usuário e, na class de cálculo, você lança o evento quando deseja notificar a interface do usuário:

 class MainWindow : Window { private void startCalc() { //your code CalcClass calc = new CalcClass(); calc.ProgressUpdate += (s, e) => { Dispatcher.Invoke((Action)delegate() { /* update UI */ }); }; Thread calcthread = new Thread(new ParameterizedThreadStart(calc.testMethod)); calcthread.Start(input); } } class CalcClass { public event EventHandler ProgressUpdate; public void testMethod(object input) { //part 1 if(ProgressUpdate != null) ProgressUpdate(this, new YourEventArgs(status)); //part 2 } } 

ATUALIZAR:
Como parece, essa ainda é uma pergunta e uma resposta frequentemente visitadas. Quero atualizar essa resposta com a forma como eu faria agora (com o .NET 4.5) – isso é um pouco mais, pois mostrarei algumas possibilidades diferentes:

 class MainWindow : Window { Task calcTask = null; void buttonStartCalc_Clicked(object sender, EventArgs e) { StartCalc(); } // #1 async void buttonDoCalc_Clicked(object sender, EventArgs e) // #2 { await CalcAsync(); // #2 } void StartCalc() { var calc = PrepareCalc(); calcTask = Task.Run(() => calc.TestMethod(input)); // #3 } Task CalcAsync() { var calc = PrepareCalc(); return Task.Run(() => calc.TestMethod(input)); // #4 } CalcClass PrepareCalc() { //your code var calc = new CalcClass(); calc.ProgressUpdate += (s, e) => Dispatcher.Invoke((Action)delegate() { // update UI }); return calc; } } class CalcClass { public event EventHandler> ProgressUpdate; // #5 public TestMethod(InputValues input) { //part 1 ProgressUpdate.Raise(this, status); // #6 - status is of type YourStatus //part 2 } } static class EventExtensions { public static void Raise(this EventHandler> theEvent, object sender, T args) { if (theEvent != null) theEvent(sender, new EventArgs(args)); } } 

@ 1) Como iniciar os cálculos “síncronos” e executá-los em segundo plano

@ 2) Como iniciar “asynchronous” e “aguardar”: Aqui o cálculo é executado e completado antes do retorno do método, mas por causa do async / await a UI não é bloqueada ( BTW: tais manipuladores de evento são os únicos válidos) usos do async void como o manipulador de events deve retornar void – use a async Task em todos os outros casos )

@ 3) Em vez de um novo Thread , agora usamos uma Task . Para mais tarde ser capaz de verificar sua conclusão (bem-sucedida), nós a salvamos no membro global calcTask . No fundo isso também inicia um novo thread e executa a ação lá, mas é muito mais fácil de manipular e tem alguns outros benefícios.

@ 4) Aqui também iniciamos a ação, mas desta vez retornamos a tarefa, para que o “manipulador de events asynchronous” possa “aguardar”. Poderíamos também criar uma async Task CalcAsync() e depois await Task.Run(() => calc.TestMethod(input)).ConfigureAwait(false); (FYI: o ConfigureAwait(false) é para evitar deadlocks, você deve ler sobre isso se você usar async / await como seria muito para explicar aqui) o que resultaria no mesmo stream de trabalho, mas como o Task.Run é a única “operação aguardável” e é a última que podemos simplesmente retornar a tarefa e salvar uma opção de contexto, o que economiza tempo de execução.

@ 5) Aqui eu uso agora um “evento genérico fortemente tipado” para que possamos passar e receber nosso “object de status” facilmente

@ 6) Aqui eu uso a extensão definida abaixo, que (além da facilidade de uso) resolve a possível condição de corrida no antigo exemplo. Lá, poderia ter acontecido que o evento ficou null após a verificação if , mas antes da chamada, se o manipulador de events foi removido em outro thread exatamente naquele momento. Isso não pode acontecer aqui, pois as extensões obtêm uma “cópia” do delegado do evento e, na mesma situação, o manipulador ainda está registrado dentro do método Raise .

Eu vou te jogar uma bola curva aqui. Se eu disse isso uma vez eu disse cem vezes. Operações Marshaling como Invoke ou BeginInvoke nem sempre são os melhores methods para atualizar a interface do usuário com o progresso do segmento de trabalho.

Nesse caso, normalmente funciona melhor para que o thread de trabalho publique suas informações de progresso em uma estrutura de dados compartilhada que o thread da interface do usuário pesquisa em intervalos regulares. Isso tem várias vantagens.

  • Ele quebra o acoplamento rígido entre a interface do usuário e o thread de trabalho que Invoke impõe.
  • O thread da interface do usuário começa a ditar quando os controles da interface do usuário são atualizados … da maneira que deveria ser, de qualquer maneira, quando você realmente pensa sobre isso.
  • Não há risco de ultrapassar a fila de mensagens da interface do usuário, como seria o caso se BeginInvoke fosse usado no thread de trabalho.
  • O thread de trabalho não precisa aguardar uma resposta do thread da interface do usuário, como seria o caso de Invoke .
  • Você obtém mais taxa de transferência nos encadeamentos de interface do usuário e de trabalho.
  • Invoke e BeginInvoke são operações caras.

Então, no seu calcClass crie uma estrutura de dados que contenha as informações de progresso.

 public class calcClass { private double percentComplete = 0; public double PercentComplete { get { // Do a thread-safe read here. return Interlocked.CompareExchange(ref percentComplete, 0, 0); } } public testMethod(object input) { int count = 1000; for (int i = 0; i < count; i++) { Thread.Sleep(10); double newvalue = ((double)i + 1) / (double)count; Interlocked.Exchange(ref percentComplete, newvalue); } } } 

Em seguida, na class MainWindow use um DispatcherTimer para pesquisar periodicamente as informações de progresso. Configure o DispatcherTimer para elevar o evento Tick em qualquer intervalo mais apropriado para sua situação.

 public partial class MainWindow : Window { public void YourDispatcherTimer_Tick(object sender, EventArgs args) { YourProgressBar.Value = calculation.PercentComplete; } } 

Você está certo de que deve usar o Dispatcher para atualizar os controles no thread da interface do usuário e também para os processos de execução longa que não devem ser executados no thread da interface do usuário. Mesmo se você executar o processo de longa execução de forma assíncrona no thread da interface do usuário, ele ainda pode causar problemas de desempenho.

Deve-se notar que Dispatcher.CurrentDispatcher retornará o dispatcher para o thread atual, não necessariamente o thread da interface do usuário. Eu acho que você pode usar Application.Current.Dispatcher para obter uma referência ao dispatcher do thread da interface do usuário, se estiver disponível para você, mas se não, você terá que passar o dispatcher de interface do usuário para o thread em segundo plano.

Normalmente eu uso a biblioteca paralela de tarefas para operações de segmentação em vez de um BackgroundWorker . Eu só acho mais fácil de usar.

Por exemplo,

 Task.Factory.StartNew(() => SomeObject.RunLongProcess(someDataObject)); 

Onde

 void RunLongProcess(SomeViewModel someDataObject) { for (int i = 0; i < = 1000; i++) { Thread.Sleep(10); // Update every 10 executions if (i % 10 == 0) { // Send message to UI thread Application.Current.Dispatcher.BeginInvoke( DispatcherPriority.Normal, (Action)(() => someDataObject.ProgressValue = (i / 1000))); } } } 

Tudo o que interage com a interface do usuário deve ser chamado no thread da interface do usuário (a menos que seja um object congelado). Para fazer isso, você pode usar o despachante.

 var disp = /* Get the UI dispatcher, each WPF object has a dispatcher which you can query*/ disp.BeginInvoke(DispatcherPriority.Normal, (Action)(() => /*Do your UI Stuff here*/)); 

Eu uso BeginInvoke aqui, geralmente um backgroundworker não precisa esperar que a interface do usuário seja atualizada. Se você quiser esperar, você pode usar Invoke . Mas você deve ter cuidado para não ligar para o BeginInvoke com rapidez, isso pode ficar realmente desagradável.

By the way, The BackgroundWorker class ajuda com este tipo de taks. Ele permite o relatório de alterações, como uma porcentagem, e envia isso automaticamente do thread de segundo plano para o thread ui. Para a maioria das tarefas thread update, o BackgroundWorker é uma ótima ferramenta.

Se este é um cálculo longo, então eu iria fundo trabalhador. Tem suporte ao progresso. Também tem suporte para cancelar.

 http://msdn.microsoft.com/en-us/library/cc221403(v=VS.95).aspx 

Aqui eu tenho um TextBox ligado ao conteúdo.

  private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { Debug.Write("backgroundWorker_RunWorkerCompleted"); if (e.Cancelled) { contents = "Cancelled get contents."; NotifyPropertyChanged("Contents"); } else if (e.Error != null) { contents = "An Error Occured in get contents"; NotifyPropertyChanged("Contents"); } else { contents = (string)e.Result; if (contentTabSelectd) NotifyPropertyChanged("Contents"); } } 

Você precisará retornar ao seu thread principal (também chamado de UI thread ) para update a interface do usuário. Qualquer outro segmento que tente atualizar sua interface do usuário apenas fará com que exceptions sejam lançadas em todo o lugar.

Então, porque você está no WPF, você pode usar o Dispatcher e, mais especificamente, um beginInvoke neste dispatcher . Isso permitirá que você execute as necessidades necessárias (normalmente Atualizar a interface do usuário) no thread da interface do usuário.

Você também deseja “registrar” a UI do UI em sua business , mantendo uma referência a um controle / formulário, para que possa usar seu dispatcher .

Graças a Deus, a Microsoft descobriu isso no WPF 🙂

Todo Control , como uma barra de progresso, botão, formulário, etc., possui um Dispatcher . Você pode fornecer ao Dispatcher uma Action que precisa ser executada e chamar automaticamente no encadeamento correto (uma Action é como um delegado de function).

Você pode encontrar um exemplo aqui .

Naturalmente, você terá que ter o controle acessível de outras classs, por exemplo, tornando-o public e entregando uma referência à Window para sua outra class, ou talvez passando uma referência apenas para a barra de progresso.

Senti a necessidade de acrescentar essa resposta melhor, já que nada, exceto o BackgroundWorker parecia me ajudar, e a resposta que lida com isso até agora era lamentavelmente incompleta. É assim que você atualizaria uma página XAML chamada MainWindow que possui uma tag Image como esta:

  

com um processo BackgroundWorker para mostrar se você está conectado à rede ou não:

 using System.ComponentModel; using System.Windows; using System.Windows.Controls; public partial class MainWindow : Window { private BackgroundWorker bw = new BackgroundWorker(); public MainWindow() { InitializeComponent(); // Set up background worker to allow progress reporting and cancellation bw.WorkerReportsProgress = true; bw.WorkerSupportsCancellation = true; // This is your main work process that records progress bw.DoWork += new DoWorkEventHandler(SomeClass.DoWork); // This will update your page based on that progress bw.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged); // This starts your background worker and "DoWork()" bw.RunWorkerAsync(); // When this page closes, this will run and cancel your background worker this.Closing += new CancelEventHandler(Page_Unload); } private void bw_ProgressChanged(object sender, ProgressChangedEventArgs e) { BitmapImage bImg = new BitmapImage(); bool connected = false; string response = e.ProgressPercentage.ToString(); // will either be 1 or 0 for true/false -- this is the result recorded in DoWork() if (response == "1") connected = true; // Do something with the result we got if (!connected) { bImg.BeginInit(); bImg.UriSource = new Uri("Images/network_off.jpg", UriKind.Relative); bImg.EndInit(); imgNtwkInd.Source = bImg; } else { bImg.BeginInit(); bImg.UriSource = new Uri("Images/network_on.jpg", UriKind.Relative); bImg.EndInit(); imgNtwkInd.Source = bImg; } } private void Page_Unload(object sender, CancelEventArgs e) { bw.CancelAsync(); // stops the background worker when unloading the page } } public class SomeClass { public static bool connected = false; public void DoWork(object sender, DoWorkEventArgs e) { BackgroundWorker bw = sender as BackgroundWorker; int i = 0; do { connected = CheckConn(); // do some task and get the result if (bw.CancellationPending == true) { e.Cancel = true; break; } else { Thread.Sleep(1000); // Record your result here if (connected) bw.ReportProgress(1); else bw.ReportProgress(0); } } while (i == 0); } private static bool CheckConn() { bool conn = false; Ping png = new Ping(); string host = "SomeComputerNameHere"; try { PingReply pngReply = png.Send(host); if (pngReply.Status == IPStatus.Success) conn = true; } catch (PingException ex) { // write exception to log } return conn; } } 

Para mais informações: https://msdn.microsoft.com/en-us/library/cc221403(v=VS.95).aspx