webentwicklung-frage-antwort-db.com.de

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

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

Also hier mein (sehr häufiges) Problem: Ich habe eine WPF-Anwendung, die Eingaben aus einigen vom Benutzer ausgefüllten Textfeldern entnimmt und diese dann verwendet, um mit ihnen viele Berechnungen durchzuführen. Sie sollten ungefähr 2-3 Minuten dauern, daher würde ich gerne einen Fortschrittsbalken und einen Textblock aktualisieren, in denen der aktuelle Status angezeigt wird .. Außerdem muss ich die UI-Eingaben des Benutzers speichern und sie dem Thread übergeben. Ich habe also eine dritte Klasse, die ich zum Erstellen eines Objekts verwende, und möchte dieses Objekt an den Hintergrund-Thread übergeben. Natürlich würde ich die Berechnungen in einem anderen Thread ausführen, sodass die Benutzeroberfläche nicht einfriert, aber ich nicht Ich weiß nicht, wie man die Benutzeroberfläche aktualisiert, da alle Berechnungsmethoden Teil einer anderen Klasse sind. Nach vielen Recherchen denke ich, dass die beste Methode die Dispatcher und TPL ist und kein Hintergrundarbeiter, aber ehrlich gesagt ich Ich bin nicht sicher, wie sie funktionieren, und nach etwa 20 Stunden Versuch und Irrtum mit anderen Antworten habe ich mich entschieden, selbst eine Frage zu stellen.

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 die Benutzeroberfläche von der Testmethode aus aktualisiert werden kann. Da ich mit C # und objektorientierter Programmierung noch nicht vertraut bin, werde ich wahrscheinlich zu komplizierte Antworten nicht verstehen, aber ich werde mein Bestes geben.

Auch wenn jemand im Allgemeinen eine bessere Idee hat (vielleicht mit Backgroundworker oder irgendetwas anderem), bin ich offen, sie zu sehen.

34
phil13131

Zuerst müssen Sie Dispatcher.Invoke verwenden, um die Benutzeroberfläche von einem anderen Thread zu ändern. Um dies in einer anderen Klasse 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 versenden und in der Berechnungsklasse das Ereignis auslösen, wenn Sie die Benutzeroberfläche benachrichtigen möchten:

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

UPDATE:
Wie es scheint, ist dies immer noch eine häufig besuchte Frage und Antwort. Ich möchte diese Antwort dahingehend aktualisieren, wie ich es jetzt tun würde (mit .NET 4.5) - dies ist etwas länger, da ich verschiedene Möglichkeiten aufzeigen 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<EventArgs<YourStatus>> 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<T>(this EventHandler<EventArgs<T>> theEvent,
                                object sender, T args)
    {
        if (theEvent != null)
            theEvent(sender, new EventArgs<T>(args));
    }
}

@ 1) So starten Sie die "synchronen" Berechnungen und führen Sie sie im Hintergrund aus 

@ 2) So starten Sie "asynchron" und "wait it": Hier wird die Berechnung ausgeführt und abgeschlossen, bevor die Methode zurückkehrt. Aufgrund der asyncawait ist die Benutzeroberfläche jedoch nicht blockiert (_/BTW: solche Ereignishandler sind die Nur gültige Verwendungen von async void als Ereignishandler müssen void zurückgeben - in allen anderen Fällen async Task verwenden

@ 3) Anstelle eines neuen Thread verwenden wir jetzt ein Task. Um später den (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, ist jedoch viel einfacher zu handhaben und hat einige andere Vorteile. 

@ 4) Hier starten wir auch die Aktion, aber diesmal geben wir die Task zurück, sodass der "async-Event-Handler" darauf "warten kann". Wir könnten auch async Task CalcAsync() und dann await Task.Run(() => calc.TestMethod(input)).ConfigureAwait(false); erstellen (FYI: Die ConfigureAwait(false) dient dazu, Deadlocks zu vermeiden. Wenn Sie async/await verwenden, da dies hier zu viel zu erklären wäre, sollten Sie sich darüber informieren.) Der Task.Run ist die einzige "zu erwartende Operation" und ist die letzte, die wir einfach 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 übergeben und empfangen können 

@ 6) Hier verwende ich die unten definierte Erweiterung, die (neben der Benutzerfreundlichkeit) die mögliche Race-Bedingung im alten Beispiel löst. Dort hätte es passieren können, dass das Ereignis null nach der if- Prüfung erhalten wurde, aber vor dem Aufruf, wenn der Ereignishandler gerade in einem anderen Thread entfernt wurde. Dies kann hier nicht passieren, da die Erweiterungen eine "Kopie" des Ereignisdelegierten erhalten, und in derselben Situation ist der Handler immer noch in der Raise-Methode registriert.

54
ChrFin

Ich werde dir hier einen Kurvenball werfen. 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 ist es in der Regel besser, wenn der Worker-Thread seine Fortschrittsinformationen in einer gemeinsam genutzten Datenstruktur veröffentlicht, die der UI-Thread dann in regelmäßigen Abständen abruft. Dies hat mehrere Vorteile.

  • Es löst die enge Kopplung zwischen der Benutzeroberfläche und dem Arbeitsthread, die Invoke auferlegt.
  • Der UI-Thread muss bestimmen, wann die Steuerelemente der Benutzeroberfläche aktualisiert werden. So wie es sein sollte, wenn Sie wirklich darüber nachdenken.
  • Es besteht kein Risiko, die UI-Nachrichtenwarteschlange zu überschreiben, wie dies bei der Verwendung von BeginInvoke aus dem Worker-Thread der Fall wäre.
  • Der Worker-Thread muss nicht auf eine Antwort vom UI-Thread 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 kostspielige 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);
    }
  }
}

Verwenden Sie dann in Ihrer MainWindow-Klasse eine DispatcherTimer, um die Fortschrittsinformationen regelmäßig abzufragen. Konfigurieren Sie die Variable DispatcherTimer so, dass das Ereignis Tick in dem für Ihre Situation am besten geeigneten Intervall ausgelöst wird.

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

Sie haben Recht, dass Sie die Dispatcher verwenden sollten, um die Steuerelemente im UI-Thread zu aktualisieren, und auch, dass langlaufende Prozesse nicht im UI-Thread ausgeführt werden sollten. Selbst wenn Sie den lang laufenden Prozess asynchron auf dem UI-Thread ausführen, kann dies dennoch zu Leistungsproblemen führen.

Es ist zu beachten, dass Dispatcher.CurrentDispatcher den Dispatcher für den aktuellen Thread zurückgibt, nicht unbedingt den UI-Thread. Ich denke, Sie können Application.Current.Dispatcher verwenden, um einen Verweis auf den Dispatcher des UI-Threads zu erhalten, falls Ihnen dieser zur Verfügung steht. Andernfalls müssen Sie den UI-Dispatcher an Ihren Hintergrund-Thread übergeben.

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

Zum Beispiel,

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

Alles, was mit der Benutzeroberfläche interagiert, muss im Benutzeroberflächen-Thread aufgerufen werden (es sei denn, es handelt sich um ein eingefrorenes Objekt). Dazu 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 verwende BeginInvoke hier. Normalerweise muss ein Hintergrundarbeiter nicht warten, bis die Benutzeroberfläche aktualisiert wird. Wenn Sie warten möchten, können Sie Invoke verwenden. Sie sollten jedoch darauf achten, BeginInvoke nicht zu schnell zu oft aufzurufen. Dies kann sehr unangenehm werden.

Übrigens hilft die BackgroundWorker-Klasse bei dieser Art von Aufgaben. Es ermöglicht das Melden von Änderungen, z. B. eines Prozentsatzes, und sendet diese automatisch vom Hintergrund-Thread in den UI-Thread. Für die meisten Thread-Aktualisierungsaufgaben ist der BackgroundWorker ein großartiges Werkzeug.

4
dowhilefor

Sie müssen zu Ihrem Haupt-Thread (auch UI thread genannt) zurückkehren, um die Benutzeroberfläche update zu machen. Jeder andere Thread, der versucht, Ihre Benutzeroberfläche zu aktualisieren, führt dazu, dass exceptions überallhin geworfen wird.

Da Sie sich in WPF befinden, können Sie Dispatcher und insbesondere ein beginInvoke für dieses dispatcher verwenden. Auf diese Weise können Sie im Benutzeroberflächenthread ausführen, was getan werden muss (in der Regel die Benutzeroberfläche aktualisieren).

Sie möchten möglicherweise auch UI in Ihrem business "registrieren", indem Sie einen Verweis auf ein Steuerelement/Formular pflegen, damit Sie dessen dispatcher verwenden können.

1
squelos

Wenn dies eine lange Berechnung ist, würde ich im Hintergrund arbeiten. Es hat Fortschrittsunterstützung. Es hat auch Unterstützung für den Abbruch. 

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

Hier habe ich eine an Inhalte 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");
        }
    }
1
paparazzo

Ich fühlte das Bedürfnis, diese bessere Antwort hinzuzufügen, da nichts außer BackgroundWorker mir zu helfen schien und die Antwort, die sich bisher damit beschäftigte, war kläglich unvollständig. So würden Sie eine XAML-Seite mit dem Namen MainWindow aktualisieren, die ein Image-Tag wie das folgende hat:

<Image Name="imgNtwkInd" Source="Images/network_on.jpg" Width="50" />

mit einem BackgroundWorker-Prozess, um anzuzeigen, 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;
    }
}

Weitere Informationen: https://msdn.Microsoft.com/en-us/library/cc221403(v=VS.95).aspx

0
vapcguy

Gott sei Dank hat Microsoft herausgefunden, dass in WPF herausgefunden wurde :)

Jeder Control, wie ein Fortschrittsbalken, eine Schaltfläche, ein Formular usw., enthält ein Dispatcher. Sie können Dispatcher eine Action geben, die ausgeführt werden muss, und es wird automatisch im richtigen Thread aufgerufen (eine Action ist wie ein Funktionsdelegat).

Sie finden ein Beispiel hier .

Natürlich müssen Sie die Kontrolle auch von anderen Klassen aus haben, z. indem Sie public machen und der anderen Klasse einen Verweis auf Window übergeben, oder vielleicht, indem Sie nur einen Verweis auf die Fortschrittsleiste übergeben.

0
Vladislav Zorov