Frage Wie aktualisiere ich die GUI von einem anderen Thread?


Was ist der einfachste Weg, um ein Update durchzuführen? Label von einem anderen Thread?

Ich habe ein Form auf thread1und von diesem starte ich einen anderen Thread (thread2). Während thread2 verarbeitet einige Dateien, die ich gerne aktualisieren würde Label auf der Form mit dem aktuellen Status von thread2Arbeit.

Wie kann ich das machen?


1144
2018-03-19 09:37


Ursprung


Antworten:


Für .NET 2.0, hier ist ein schönes Stück Code, den ich geschrieben habe, das genau das tut, was Sie wollen, und für jede Eigenschaft auf einem funktioniert 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 });
  }
}

Nenn es so:

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

Wenn Sie .NET 3.0 oder höher verwenden, können Sie die obige Methode als eine Erweiterungsmethode für die Control Klasse, die dann den Aufruf vereinfachen würde:

myLabel.SetPropertyThreadSafe("Text", status);

UPDATE 05/10/2010:

Für .NET 3.0 sollten Sie diesen Code verwenden:

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

Das verwendet LINQ und Lambda-Ausdrücke, um viel sauberer, einfacher und sicherer Syntax zu ermöglichen:

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

Der Eigenschaftsname wird jetzt nicht nur zur Kompilierzeit überprüft, sondern auch der Typ der Eigenschaft. Daher ist es beispielsweise nicht möglich, einer booleschen Eigenschaft einen Zeichenfolgenwert zuzuweisen und somit eine Laufzeitausnahme zu verursachen.

Leider hindert das niemanden daran, dumme Dinge zu tun, wie zum Beispiel in einem anderen vorbei zu kommen Control's Eigenschaft und Wert, so wird Folgendes glücklich kompilieren:

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

Daher habe ich die Laufzeitprüfungen hinzugefügt, um sicherzustellen, dass die übergebene Eigenschaft tatsächlich zu der gehört Control dass die Methode aufgerufen wird. Nicht perfekt, aber immer noch viel besser als die .NET 2.0-Version.

Wenn jemand weitere Vorschläge zur Verbesserung dieses Codes für die Kompilierungssicherheit hat, bitte Kommentar!


688
2018-03-19 10:37



Das einfachste Weg ist eine anonyme Methode, in die übergeben wird 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

Beachte das Invoke blockiert die Ausführung, bis sie abgeschlossen ist - das ist synchroner Code. Die Frage fragt nicht nach asynchronem Code, aber es gibt viele Inhalt auf Stack Overflow über das Schreiben von asynchronem Code, wenn Sie mehr darüber erfahren möchten.


938
2018-03-19 10:17



Lange Arbeit erledigen

Schon seit .NET 4.5 und C # 5.0 du solltest benutzen Task-basiertes asynchrones Muster (TAP) zusammen mit asynchron-erwarten Schlüsselwörter In allen Bereichen (einschließlich der GUI):

TAP ist das empfohlene asynchrone Entwurfsmuster für Neuentwicklungen

Anstatt von Asynchrones Programmiermodell (APM) und Ereignisbasiertes asynchrones Muster (EAP) (Letzteres beinhaltet die BackgroundWorker Klasse).

Dann ist die empfohlene Lösung für die Neuentwicklung:

  1. Asynchrone Implementierung eines Event-Handlers (Ja, das ist alles):

    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. Implementierung des zweiten Threads, der den UI-Thread benachrichtigt:

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

Beachten Sie Folgendes:

  1. Kurzer und sauberer Code, der sequentiell ohne Rückrufe und explizite Threads geschrieben wird.
  2. Aufgabe Anstatt von Faden.
  3. asynchron Schlüsselwort, das zu verwenden erlaubt erwarten Dies wiederum verhindert, dass der Event-Handler den Abschlusszustand erreicht, bis die Task beendet ist und in der Zwischenzeit den UI-Thread nicht blockiert.
  4. Fortschrittsklasse (siehe IProgress-Schnittstelle) das unterstützt Trennung von Anliegen (SoC) Design-Prinzip und erfordert keinen expliziten Dispatcher und Aufruf. Es benutzt den Strom Synchronisierungskontext von seinem Erstellungsort (hier der UI-Thread).
  5. TaskCreationOptions.LongRunning das deutet an, die Aufgabe nicht in die Warteschlange zu stellen ThreadPool.

Für ausführlichere Beispiele siehe: Die Zukunft von C #: Gute Dinge kommen denen, die "erwarten" durch Joseph Albahari.

Siehe auch über UI-Threading-Modell Konzept.

Ausnahmen behandeln

Das folgende Snippet ist ein Beispiel für die Behandlung von Ausnahmen und Schaltflächen Enabled Eigenschaft, um mehrere Klicks während der Ausführung im Hintergrund zu verhindern.

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

333
2017-08-03 13:09



Variation von Marc Gravell einfachste Lösung für .NET 4:

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

Oder verwenden Sie stattdessen den Aktionsdelegaten:

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

Sehen Sie hier für einen Vergleich der beiden: MethodInvoker vs Aktion für Control.BeginInvoke


195
2018-05-29 18:51



Fire and Forget-Erweiterungsmethode für .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();
        }
    }
}

Dies kann mit der folgenden Codezeile aufgerufen werden:

this.UIThread(() => this.myLabel.Text = "Text Goes Here");

115
2017-08-27 21:10



Dies ist der klassische Weg, wie Sie dies tun sollten:

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

Ihr Arbeitsthread hat ein Ereignis. Ihr UI-Thread beginnt mit einem anderen Thread, um die Arbeit auszuführen, und verknüpft dieses Arbeitsereignis, sodass Sie den Status des Arbeitsthreads anzeigen können.

Dann müssen Sie in der Benutzeroberfläche Threads überqueren, um das eigentliche Steuerelement zu ändern ... wie eine Beschriftung oder eine Fortschrittsleiste.


57
2018-03-19 10:31



Die einfache Lösung ist zu verwenden Control.Invoke.

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

void updateGUI() {
    // update gui here
}

50
2018-03-19 09:46



Threading-Code ist oft fehlerhaft und immer schwer zu testen. Sie müssen Threading-Code nicht schreiben, um die Benutzeroberfläche von einer Hintergrundaufgabe zu aktualisieren. Benutze einfach die Hintergrundarbeiter Klasse, um die Aufgabe und ihre auszuführen BerichtProgress Methode, um die Benutzeroberfläche zu aktualisieren. Normalerweise geben Sie nur einen Prozentsatz als abgeschlossen an, aber es gibt noch eine weitere Überladung, die ein Zustandsobjekt enthält. Hier ist ein Beispiel, das nur ein String-Objekt meldet:

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

Das ist in Ordnung, wenn Sie immer dasselbe Feld aktualisieren möchten. Wenn Sie komplexere Aktualisierungen vornehmen müssen, können Sie eine Klasse definieren, die den UI-Status darstellt, und sie an die ReportProgress-Methode übergeben.

Eine letzte Sache, stellen Sie sicher, die WorkerReportsProgress Flagge oder die ReportProgress Methode wird komplett ignoriert.


41
2017-08-27 20:42