So aktualisieren Sie die Benutzeroberfläche von einem anderen Thread, der in einer anderen class ausgeführt wird

Ich schreibe gerade mein erstes Programm auf C # und ich bin extrem neu in der Sprache (bisher nur mit C gearbeitet). Ich habe viel geforscht, aber alle Antworten waren zu allgemein und ich konnte es einfach nicht schaffen.

Also hier mein (sehr häufiges) Problem: Ich habe eine WPF-Anwendung, die Eingaben aus einigen Textfeldern nimmt, die vom Benutzer ausgefüllt werden, und dann diese verwendet, um viele Berechnungen mit ihnen durchzuführen. Sie sollten ungefähr 2-3 Minuten dauern, also möchte ich eine Fortschrittsanzeige und einen Textblock aktualisieren, die mir sagen, was der aktuelle Status ist. Außerdem muss ich die UI-Eingaben vom Benutzer speichern und sie dem Thread geben, also habe ich eine dritte class, mit der ich ein Objekt erstelle und dieses Objekt an den Hintergrund-Thread übergeben möchte. Offensichtlich würde ich die Berechnungen in einem anderen Thread ausführen, so dass die Benutzeroberfläche nicht einfriert, aber ich weiß nicht, wie Sie die Benutzeroberfläche aktualisieren, da alle Berechnungsmethoden Teil einer anderen class sind. Nach einer Menge Forschung denke ich, die beste Methode wäre die Verwendung von Dispatcher und TPL und kein Hintergrundarbeiter, aber ehrlich gesagt bin ich mir nicht sicher, wie sie funktionieren und nach etwa 20 Stunden Versuch und Irrtum mit anderen Antworten, entschied ich mich zu fragen eine Frage an mich.

Hier eine sehr einfache Struktur meines Programms:

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

Ich wäre sehr dankbar, wenn jemand eine einfache Erklärung hätte, wie man die Benutzeroberfläche innerhalb der Testmethode aktualisiert. Da ich neu in C # bin und objektorientierte Programmierung, zu komplizierte Antworten werde ich sehr wahrscheinlich nicht verstehen, aber ich werde mein Bestes tun.

Auch wenn jemand im Allgemeinen eine bessere Idee hat (vielleicht Hintergrundarbeiter oder irgendetwas anderes), bin ich offen dafür, es zu sehen.

Zuerst müssen Sie Dispatcher.Invoke , um die Benutzeroberfläche von einem anderen Thread zu ändern, und um dies von einer anderen class zu tun, können Sie Ereignisse verwenden.
Dann können Sie sich für diese Ereignisse in der Hauptklasse registrieren und die Änderungen an der Benutzeroberfläche übermitteln. In der Berechnungsklasse, mit der Sie das Ereignis auslösen, wenn Sie die Benutzeroberfläche benachrichtigen möchten, gilt Folgendes:

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

AKTUALISIEREN:
Wie es scheint, ist dies immer noch eine oft besuchte Frage und Antwort. Ich möchte diese Antwort mit der Art und Weise aktualisieren, wie ich es jetzt machen würde (mit .NET 4.5) – das ist ein bisschen länger, da ich einige verschiedene Möglichkeiten zeigen werde:

 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) Wie starte ich die “synchronen” Berechnungen und führe sie im Hintergrund aus

@ 2) Wie starte ich es “asynchron” und “erwarte es”: Hier wird die Berechnung ausgeführt und beendet, bevor die Methode zurückkehrt, aber wegen der async / await die UI nicht blockiert ( BTW: solche Eventhandler sind die einzigen gültigen Verwendungen von async void da der Event-Handler void muss – in allen anderen Fällen async Task

@ 3) Anstelle eines neuen Thread wir jetzt eine Task . Um später seinen (erfolgreichen) Abschluss überprüfen zu können, speichern wir ihn im globalen calcTask Member. Im Hintergrund startet dies auch einen neuen Thread und führt die Aktion dort aus, aber es ist viel einfacher zu handhaben und hat einige andere Vorteile.

@ 4) Hier starten wir auch die Aktion, aber dieses Mal geben wir die Aufgabe zurück, so dass der “asynchrone Event-Handler” darauf “warten” kann. Wir könnten auch async Task CalcAsync() erstellen und dann await Task.Run(() => calc.TestMethod(input)).ConfigureAwait(false); (FYI: ConfigureAwait(false) soll Deadlocks vermeiden, Sie sollten das hier lesen, wenn Sie async / await wie es hier zu erklären wäre), was den gleichen Workflow zur Folge hätte, aber wie der Task.Run ist die einzige “erwartete Operation” und die letzte, die wir einfach die Aufgabe zurückgeben und einen Kontextwechsel speichern können, was einige Ausführungszeit spart.

@ 5) Hier verwende ich jetzt ein “stark typisiertes generisches Ereignis”, damit wir unser “Statusobjekt” problemlos weitergeben und empfangen können

@ 6) Hier benutze ich die unten definierte Erweiterung, die (abgesehen von der Benutzerfreundlichkeit) die mögliche Wettlaufsituation im alten Beispiel triggers. Da konnte es passieren, dass das Ereignis nach dem if -check, aber vor dem Aufruf null wurde, wenn der Event-Handler gerade in einem anderen Thread entfernt wurde. Dies kann hier nicht passieren, da die Erweiterungen eine “Kopie” des Ereignisdelegaten erhalten und in derselben Situation der Handler immer noch in der Raise Methode registriert ist.

Ich werde dir hier einen Kurvenball casting. Wenn ich es einmal gesagt habe, habe ich es hundertmal gesagt. Marshaling-Operationen wie Invoke oder BeginInvoke sind nicht immer die besten Methoden zum Aktualisieren der Benutzeroberfläche mit Worker-Thread-Fortschritt.

In diesem Fall funktioniert es normalerweise besser, wenn der Worker-Thread seine Fortschrittsinformationen in einer freigegebenen Datenstruktur veröffentlicht, die der UI-Thread anschließend in regelmäßigen Abständen abfragt. Dies hat mehrere Vorteile.

  • Es unterbricht die enge Kopplung zwischen dem UI- und dem Worker-Thread, die von Invoke auferlegt wird.
  • Der UI-Thread wird diktiert, wenn die UI-Steuerelemente aktualisiert werden … so wie es sein sollte, wenn Sie wirklich darüber nachdenken.
  • Es besteht kein Risiko, dass die UI-Nachrichtenwarteschlange BeginInvoke wird, wie dies der Fall wäre, wenn BeginInvoke vom Worker-Thread verwendet würde.
  • Der Worker-Thread muss nicht auf eine Antwort vom Benutzeroberflächenthread warten, wie dies bei Invoke der Fall wäre.
  • Sie erhalten mehr Durchsatz sowohl für die Benutzeroberfläche als auch für die Arbeitsthreads.
  • Invoke und BeginInvoke sind teure Operationen.

Erstellen Sie also in Ihrer calcClass eine Datenstruktur, die die Fortschrittsinformationen enthält.

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

MainWindow dann in Ihrer MainWindow class einen DispatcherTimer um die Fortschrittsinformationen regelmäßig MainWindow . Konfigurieren Sie den DispatcherTimer so, dass das Tick Ereignis in dem für Ihre Situation am besten geeigneten Intervall ausgetriggers wird.

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

Sie haben Recht damit, dass Sie den Dispatcher zum Aktualisieren von Steuerelementen im Benutzeroberflächenthread verwenden sollten und dass processe mit langer Laufzeit nicht im Benutzeroberflächenthread ausgeführt werden sollten. Selbst wenn Sie den process mit langer Laufzeit im Benutzeroberflächen-Thread asynchron ausführen, kann dies weiterhin zu performancesproblemen führen.

Es sollte beachtet werden, dass Dispatcher.CurrentDispatcher den Dispatcher für den aktuellen Thread zurückgibt, nicht unbedingt den UI-Thread. Ich denke, dass Sie Application.Current.Dispatcher , um einen Verweis auf den Dispatcher des UI-Threads abzurufen, wenn dieser für Sie verfügbar ist, aber wenn nicht, müssen Sie den UI-Dispatcher an Ihren Hintergrundthread übergeben.

Normalerweise verwende ich die Task Parallel Library für Threading-Operationen anstelle von BackgroundWorker . Ich finde es einfacher zu benutzen.

Beispielsweise,

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

woher

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

Alles, was mit der Benutzeroberfläche interagiert, muss im UI-Thread aufgerufen werden (es sei denn, es handelt sich um ein eingefrorenes Objekt). Um dies zu tun, können Sie den Dispatcher verwenden.

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

Ich benutze BeginInvoke hier, in der Regel muss ein Hintergrundarbeiter nicht warten, dass die Benutzeroberfläche aktualisiert wird. Wenn Sie warten möchten, können Sie Invoke . Aber Sie sollten vorsichtig sein, BeginInvoke nicht zu schnell zu oft zu nennen, das kann sehr unangenehm werden.

Die BackgroundWorker-class hilft übrigens bei dieser Art von Aufgaben. Es erlaubt Reporting-Änderungen, wie einen Prozentsatz und triggers diese automatisch aus dem Hintergrund-Thread in den ui-Thread aus. Für die meisten Thread <> update ui Aufgaben ist der BackgroundWorker ein großartiges Werkzeug.

Wenn das eine lange Berechnung ist, würde ich Hintergrundarbeiter gehen. Es hat Fortschrittsunterstützung. Es hat auch Unterstützung für Abbrechen.

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

Hier habe ich eine an den Inhalt gebundene TextBox.

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

Sie müssen zu Ihrem Hauptthread (auch als UI thread ) zurückkehren, update die Benutzeroberfläche zu update . Jeder andere Thread, der versucht, Ihre Benutzeroberfläche zu aktualisieren, wird nur dazu führen, dass überall exceptions ausgetriggers werden.

Da Sie sich in WPF befinden, können Sie den Dispatcher und insbesondere einen beginInvoke für diesen dispatcher . Dadurch können Sie die erforderlichen Aufgaben ausführen (normalerweise die Benutzeroberfläche aktualisieren) im Benutzeroberflächenthread.

Sie möchten auch die UI in Ihrem business “registrieren”, indem Sie einen Verweis auf ein Steuerelement / Formular beibehalten, so dass Sie seinen dispatcher .

Gott sei Dank, Microsoft hat das in WPF herausgefunden 🙂

Jedes Control , wie ein Fortschrittsbalken, eine Schaltfläche, ein Formular usw., hat einen Dispatcher . Sie können dem Dispatcher eine Action zuweisen, die ausgeführt werden muss, und er wird automatisch den richtigen Thread aufrufen (eine Action ist wie ein functionsdelegat).

Sie können hier ein Beispiel finden .

Natürlich müssen Sie die Kontrolle von anderen classn aus zugänglich machen, z. B. indem Sie sie public und einen Verweis auf das Window an Ihre andere class weitergeben, oder indem Sie nur eine Referenz nur an die Fortschrittsanzeige übergeben.

Fühlte die Notwendigkeit, diese bessere Antwort hinzuzufügen, da nichts außer BackgroundWorker mir zu helfen schien, und die Antwort, die damit bis jetzt beschäftigt ist, war bekümmert unvollständig. So aktualisieren Sie eine XAML-Seite namens MainWindow mit einem Image-Tag wie diesem:

  

mit einem BackgroundWorker process, der anzeigt, ob Sie mit dem Netzwerk verbunden sind oder nicht:

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

Für weitere Informationen: https://msdn.microsoft.com/en-us/library/cc221403(v=VS.95).aspx