Come aggiornare l’interfaccia utente da un altro thread in esecuzione in un’altra class

Attualmente sto scrivendo il mio primo programma su C # e sono estremamente nuovo al linguaggio (usato per lavorare solo con C finora). Ho fatto molte ricerche, ma tutte le risposte erano troppo generiche e semplicemente non riuscivo a farlo funzionare.

Quindi qui il mio problema (molto comune): ho un’applicazione WPF che prende input da alcune caselle di testo riempite dall’utente e quindi le usa per fare molti calcoli con loro. Dovrebbero richiedere circa 2-3 minuti, quindi mi piacerebbe aggiornare una barra di avanzamento e un blocco di testo che mi dice quale sia lo stato corrente. Inoltre ho bisogno di memorizzare gli input dell’interfaccia utente dall’utente e darli al thread, quindi ho una terza class, che uso per creare un object e vorrei passare questo object al thread in background. Ovviamente eseguirò i calcoli in un altro thread, quindi l’interfaccia utente non si blocca, ma non so come aggiornare l’interfaccia utente, poiché tutti i metodi di calcolo fanno parte di un’altra class. Dopo un sacco di ricerche, ritengo che il metodo migliore sia usare dispatcher e TPL e non un backgroundworker, ma onestamente non sono sicuro di come funzionano e dopo circa 20 ore di tentativi ed errori con altre risposte, ho deciso di chiedere una domanda anch’io.

Ecco una struttura molto semplice del mio programma:

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); } } } 

Sarei molto grato se qualcuno avesse una semplice spiegazione su come aggiornare l’interfaccia utente all’interno del testmethod. Dato che sono nuovo in C # e nella programmazione orientata agli oggetti, risposte troppo complicate che probabilmente non comprenderò, farò del mio meglio però.

Anche se qualcuno ha un’idea migliore in generale (magari usando il backgroundworker o qualsiasi altra cosa) sono aperto a vederlo.

Per prima cosa devi usare Dispatcher.Invoke per cambiare l’interfaccia utente da un altro thread e farlo da un’altra class, puoi usare gli eventi.
Quindi puoi registrarti a quell’evento (s) nella class principale e inviare le modifiche all’interfaccia utente e nella class di calcolo che si lancia l’evento quando si desidera notificare l’interfaccia utente:

 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 } } 

AGGIORNARE:
A quanto pare questa è ancora una domanda e una risposta spesso visitate Voglio aggiornare questa risposta con come lo farei ora (con .NET 4.5) – questo è un po ‘più lungo perché mostrerò alcune possibilità diverse:

 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) Come avviare i calcoli “sincroni” ed eseguirli in background

@ 2) Come avviarlo “asincrono” e “attenderlo”: Qui il calcolo viene eseguito e completato prima che il metodo ritorni, ma a causa async / await l’UI non sia bloccato ( BTW: tali gestori di eventi sono gli unici validi usi di async void come il gestore di eventi deve restituire void – utilizzare async Task in tutti gli altri casi )

@ 3) Invece di una nuova Thread , ora usiamo un’attività. Per poter successivamente verificare il suo completamento (positivo), lo salviamo nel membro globale calcTask . In secondo piano, viene avviato anche un nuovo thread e viene eseguita l’azione, ma è molto più semplice da gestire e presenta altri vantaggi.

@ 4) Qui iniziamo anche l’azione, ma questa volta restituiamo l’attività, così il “gestore di eventi asincroni” può “attendere”. Potremmo anche creare async Task CalcAsync() e quindi await Task.Run(() => calc.TestMethod(input)).ConfigureAwait(false); (FYI: ConfigureAwait(false) serve per evitare deadlock, dovresti leggere su questo se usi async / await come sarebbe molto da spiegare qui) che risulterebbe nello stesso stream di lavoro, ma come Task.Run è l’unica “operazione attendibile” ed è l’ultima che possiamo semplicemente restituire l’attività e salvare un interruttore di contesto, che consente di risparmiare un po ‘di tempo di esecuzione.

@ 5) Qui ora utilizzo un “evento generico fortemente tipizzato” in modo che possiamo passare e ricevere facilmente il nostro “object stato”

@ 6) Qui utilizzo l’estensione definita di seguito, che (a parte la facilità d’uso) risolve la ansible condizione di competizione nel vecchio esempio. Lì poteva accadere che l’evento diventasse null dopo il comando -check, ma prima della chiamata se il gestore eventi veniva rimosso in un altro thread proprio in quel momento. Questo non può accadere qui, poiché le estensioni ricevono una “copia” dell’evento delegato e nella stessa situazione il gestore è ancora registrato all’interno del metodo Raise .

Sto per lanciarti una palla curva qui. Se l’ho detto una volta l’ho detto centinaia di volte. Le operazioni di marshaling come Invoke o BeginInvoke non sono sempre i metodi migliori per l’aggiornamento dell’interfaccia utente con l’avanzamento del thread di lavoro.

In questo caso, in genere, è meglio che il thread di lavoro pubblichi le informazioni sull’avanzamento in una struttura di dati condivisa, quindi il thread dell’interfaccia utente esegue il polling a intervalli regolari. Questo ha diversi vantaggi.

  • Rompe lo stretto accoppiamento tra l’interfaccia utente e il thread di lavoro imposto da Invoke .
  • Il thread dell’interfaccia utente impone di dettare quando i controlli dell’interfaccia utente vengono aggiornati … nel modo in cui dovrebbe essere comunque quando ci si pensa davvero.
  • Non vi è alcun rischio di sovraccaricare la coda dei messaggi dell’interfaccia utente, come nel caso in cui BeginInvoke venisse utilizzato dal thread di lavoro.
  • Il thread di lavoro non deve attendere una risposta dal thread dell’interfaccia utente, come nel caso di Invoke .
  • Ottieni una maggiore velocità sia sull’interfaccia utente sia sui thread di lavoro.
  • Invoke e BeginInvoke sono operazioni costose.

Quindi nel tuo calcClass creare una struttura dati che terrà le informazioni sullo stato di avanzamento.

 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); } } } 

Quindi nella class MainWindow utilizzare un DispatcherTimer per eseguire periodicamente il polling delle informazioni sull'avanzamento. Configura DispatcherTimer per aumentare l'evento Tick in qualsiasi intervallo sia più appropriato per la tua situazione.

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

Hai ragione che dovresti utilizzare il Dispatcher per aggiornare i controlli sul thread dell’interfaccia utente e anche che i processi a esecuzione prolungata non devono essere eseguiti sul thread dell’interfaccia utente. Anche se si esegue il processo di lunga durata in modo asincrono sul thread dell’interfaccia utente, è comunque ansible causare problemi di prestazioni.

Va notato che Dispatcher.CurrentDispatcher restituirà il dispatcher per il thread corrente, non necessariamente il thread dell’interfaccia utente. Penso che tu possa usare Application.Current.Dispatcher per ottenere un riferimento al dispatcher del thread dell’interfaccia utente, se questo è disponibile, ma in caso contrario dovrai passare il dispatcher dell’interfaccia utente al tuo thread in background.

In genere utilizzo la Libreria parallela attività per operazioni di filettatura anziché un BackgroundWorker . Lo trovo più facile da usare.

Per esempio,

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

dove

 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))); } } } 

Tutto ciò che interagisce con l’interfaccia utente deve essere chiamato nel thread dell’interfaccia utente (a meno che non sia un object congelato). Per farlo, puoi usare il dispatcher.

 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*/)); 

Io uso BeginInvoke qui, di solito un backgroundworker non ha bisogno di aspettare che gli aggiornamenti dell’interfaccia utente. Se vuoi aspettare, puoi usare Invoke . Ma dovresti fare attenzione a non chiamare BeginInvoke per digiunare spesso, questo può diventare davvero cattivo.

A proposito, la class BackgroundWorker aiuta con questo tipo di tak. Permette di riportare le modifiche, come una percentuale e lo invia automaticamente dal thread in background nel thread ui. Per la maggior parte dei thread <> aggiorna i task ui, BackgroundWorker è un ottimo strumento.

Se si tratta di un calcolo lungo, farei l’assistente in background. Ha supporto per il progresso. Ha anche il supporto per annullare.

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

Qui ho un TextBox legato ai contenuti.

  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"); } } 

Dovrai tornare al thread principale (chiamato anche UI thread ) per update l’interfaccia utente. Qualsiasi altro thread che tenti di aggiornare l’interfaccia utente causerà semplicemente il lancio di exceptions ovunque.

Quindi, poiché ci si trova in WPF, è ansible utilizzare il Dispatcher e in particolare un beginInvoke su questo dispatcher . Ciò ti consentirà di eseguire ciò che è necessario (in genere Aggiorna l’interfaccia utente) nel thread dell’interfaccia utente.

Inoltre, si desidera “registrare” l’ UI nella propria business , mantenendo un riferimento a un controllo / modulo, in modo da poter utilizzare il dispatcher .

Grazie a Dio, Microsoft lo ha capito in WPF 🙂

Ogni Control , come una barra di avanzamento, un pulsante, un modulo, ecc. Ha un Dispatcher su di esso. Puoi dare al Dispatcher Action che deve essere eseguita e la chiamerà automaticamente sul thread corretto ( Action è come una funzione delegata).

Puoi trovare un esempio qui .

Ovviamente, dovrete avere il controllo accessibile da altre classi, ad esempio rendendolo public e consegnando un riferimento alla Window all’altra class, o magari passando un riferimento solo alla barra di avanzamento.

Sentivo la necessità di aggiungere questa risposta migliore, poiché nulla, tranne che in BackgroundWorker sembrava aiutarmi, e la risposta che trattava finora era tristemente incompleta. Ecco come si aggiornerebbe una pagina XAML chiamata MainWindow che ha un tag Image come questo:

  

con un processo di BackgroundWorker per mostrare se si è connessi alla rete o no:

 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; } } 

Per maggiori informazioni: https://msdn.microsoft.com/en-us/library/cc221403(v=VS.95).aspx