Come posso aggiornare la GUI da un altro thread?


1393

Qual è il modo più semplice per aggiornare un Labelda un altro Thread?

  • Ho una Formcorsa thread1, e da quello sto iniziando un altro thread ( thread2).

  • Mentre thread2è l'elaborazione di alcuni file vorrei aggiornare un Labelsullo Formcon lo stato attuale del thread2lavoro di 's.

Come potrei farlo?


25
.Net 2.0+ non ha la classe BackgroundWorker proprio per questo. È al corrente dell'interfaccia utente. 1. Crea un BackgroundWorker 2. Aggiungi due delegati (uno per l'elaborazione e uno per il completamento)
Preet Sangha


4
Vedere la risposta per .NET 4.5 e C # 5.0: stackoverflow.com/a/18033198/2042090
Ryszard Dżegan

5
Questa domanda non si applica a Gtk # GUI. Per Gtk # vedi questa e questa risposta.
hlovdal,

Attenzione: le risposte a questa domanda sono ora un disordine disordinato di OT ("ecco cosa ho fatto per la mia app WPF") e manufatti storici di .NET 2.0.
Marc L.

Risposte:


768

Per .NET 2.0, ecco un bel po 'di codice che ho scritto che fa esattamente quello che vuoi e funziona per qualsiasi proprietà su un 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 });
  }
}

Chiamalo così:

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

Se si utilizza .NET 3.0 o versione successiva, è possibile riscrivere il metodo sopra come metodo di estensione della Controlclasse, che semplificherebbe quindi la chiamata a:

myLabel.SetPropertyThreadSafe("Text", status);

AGGIORNAMENTO 05/10/2010:

Per .NET 3.0 dovresti usare questo codice:

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

che utilizza le espressioni LINQ e lambda per consentire una sintassi molto più chiara, semplice e sicura:

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

Non solo il nome della proprietà ora viene verificato al momento della compilazione, ma anche il tipo della proprietà, quindi è impossibile (ad esempio) assegnare un valore di stringa a una proprietà booleana e quindi causare un'eccezione di runtime.

Sfortunatamente ciò non impedisce a nessuno di fare cose stupide come passare in Controlproprietà e valore di un altro, quindi compileranno felicemente:

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

Quindi ho aggiunto i controlli di runtime per garantire che la proprietà passata appartenga effettivamente a Control cui viene chiamato il metodo. Non perfetto, ma comunque molto meglio della versione .NET 2.0.

Se qualcuno ha ulteriori suggerimenti su come migliorare questo codice per la sicurezza in fase di compilazione, si prega di commentare!


3
Ci sono casi in cui this.GetType () restituisce lo stesso di propertyInfo.ReflectedType (ad esempio LinkLabel su WinForms). Non ho una grande esperienza con C #, ma penso che la condizione per l'eccezione dovrebbe essere: if (propertyInfo == null || (!@this.GetType (). IsSubclassOf (propertyInfo.ReflectedType) && @ this.GetType ( )! = propertyInfo.ReflectedType) || @ this.GetType (). GetProperty (propertyInfo.Name, propertyInfo.PropertyType) == null)
Corvin,

9
@lan può SetControlPropertyThreadSafe(myLabel, "Text", status)essere chiamato da un altro modulo o classe o modulo
Smith,

71
La soluzione fornita è inutilmente complessa. Vedi la soluzione di Marc Gravell, o la soluzione di Zaid Masud, se apprezzi la semplicità.
Frank Hileman,

8
Questa soluzione spreca tonnellate di risorse se aggiorni più proprietà poiché ogni Invoke costa molte risorse. Non penso che sia così che la funzionalità di Thread Safety è stata pensata. Incapsula le tue azioni di aggiornamento dell'interfaccia utente e invocalo UNA VOLTA (e non per proprietà)
Console

4
Perché mai useresti questo codice sul componente BackgroundWorker?
Andy,

1080

Il modo più semplice è un metodo anonimo passato in 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

Si noti che Invokeblocca l'esecuzione fino al completamento: si tratta di codice sincrono. La domanda non si pone sul codice asincrono, ma su Stack Overflow c'è molto contenuto sulla scrittura di codice asincrono quando si desidera conoscerlo.


8
Visto che l'OP non ha menzionato alcuna classe / istanza tranne il modulo, che non è un difetto di default ...
Marc Gravell

39
Non dimenticare che la parola chiave "this" fa riferimento a una classe "Control".
AZ.

8
@codecompleting è sicuro in entrambi i casi e sappiamo già che stiamo lavorando, quindi perché controllare qualcosa che sappiamo?
Marc Gravell

4
@Dragouf non proprio - uno dei punti di utilizzo di questo metodo è che sai già quali parti vengono eseguite sul lavoratore e quali vengono eseguite sul thread dell'interfaccia utente. Non c'è bisogno di controllare.
Marc Gravell

3
@ Joan.bdm non c'è nessun contesto abbastanza vicino per farmi commentare
Marc Gravell

401

Gestire lavori lunghi

Dal momento che .NET 4.5 e C # 5.0 è necessario utilizzare Task-based del modello asincrono (TAP) con async - attendono le parole chiave in tutte le aree (tra cui il GUI):

TAP è il modello di progettazione asincrono raccomandato per i nuovi sviluppi

invece di Asynchronous Programming Model (APM) e Event-based Asynchronous Pattern (EAP) (quest'ultimo include la classe BackgroundWorker ).

Quindi, la soluzione consigliata per il nuovo sviluppo è:

  1. Implementazione asincrona di un gestore eventi (Sì, tutto qui):

    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. Implementazione del secondo thread che notifica al thread dell'interfaccia utente:

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

Si noti quanto segue:

  1. Codice breve e pulito scritto in modo sequenziale senza richiamate e thread espliciti.
  2. Attività anziché Thread .
  3. parola chiave asincrona , che consente di utilizzare wait che a sua volta impedisce al gestore di eventi di raggiungere lo stato di completamento fino al completamento dell'attività e nel frattempo non blocca il thread dell'interfaccia utente.
  4. Classe di avanzamento (vedi Interfaccia IProgress ) che supporta il principio di progettazione Separation of Concerns (SoC) e non richiede il dispatcher esplicito e l'invocazione. Utilizza l'attuale SynchronizationContext dal suo luogo di creazione (qui il thread dell'interfaccia utente).
  5. TaskCreationOptions.LongRunning che suggerisce di non mettere in coda l'attività in ThreadPool .

Per esempi più dettagliati vedi: Il futuro di C #: le cose buone arrivano a coloro che "attendono" di Joseph Albahari .

Vedi anche sul modello di threading dell'interfaccia utente concetto di .

Gestire le eccezioni

Lo snippet di seguito è un esempio di come gestire le eccezioni e attivare la Enabledproprietà del pulsante per impedire più clic durante l'esecuzione in background.

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

2
Se SecondThreadConcern.LongWork()genera un'eccezione, può essere catturato dal thread dell'interfaccia utente? Questo è un post eccellente, a proposito.
kdbanman,

2
Ho aggiunto una sezione aggiuntiva alla risposta per soddisfare le tue esigenze. Saluti.
Ryszard Dżegan,

3
La classe ExceptionDispatchInfo è responsabile di quel miracolo della riqualificazione dell'eccezione di sfondo sul thread dell'interfaccia utente nel modello async-waitit.
Ryszard Dżegan,

1
Sono solo io a pensare che questo modo di fare questo sia molto più prolisso rispetto al solo invocare Invoke / Begin ?!
MeTitus,

2
Task.Delay(500).Wait()? Qual è lo scopo di creare un'attività per bloccare il thread corrente? Non devi mai bloccare un thread del pool di thread!
Yarik,

236

Variazione della soluzione più semplice di Marc Gravell per .NET 4:

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

O utilizzare invece il delegato di azione:

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

Vedi qui per un confronto dei due: MethodInvoker vs Action for Control.BeginInvoke


1
cos'è il "controllo" in questo esempio? Il mio controllo dell'interfaccia utente? Sto cercando di implementare questo in WPF su un controllo etichetta e Invoke non è un membro della mia etichetta.
Dbloom

Cosa c'è su metodo di estensione come @styxriver stackoverflow.com/a/3588137/206730 ?
Kiquenet,

dichiarare "Azione y;" all'interno della classe o del metodo modificando la proprietà text e aggiornando il testo con questo pezzo di codice 'yourcontrol.Invoke (y = () => yourcontrol.Text = "new text");'
Antonio Leite,

4
@Dbloom non è un membro perché è solo per WinForms. Per WPF usi Dispatcher.Invoke
sLw

1
Stavo seguendo questa soluzione ma a volte la mia interfaccia utente non veniva aggiornata. Ho scoperto che devo this.refresh()forzare l'annullamento e ridipingere la GUI .. se è utile ..
Rakibul Haq

137

Attiva e dimentica il metodo di estensione per .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();
        }
    }
}

Questo può essere chiamato usando la seguente riga di codice:

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

5
Qual è lo scopo dell'utilizzo di @this? Il "controllo" non sarebbe equivalente? Ci sono dei vantaggi con @this?
Argyle,

14
@jeromeyers - @thisÈ semplicemente il nome della variabile, in questo caso il riferimento al controllo corrente che chiama l'estensione. Potresti rinominarlo alla fonte, o qualunque cosa galleggi la tua barca. Uso @this, perché si riferisce a "questo controllo" che chiama l'estensione ed è coerente (almeno nella mia testa) con l'uso della parola chiave "questo" nel codice normale (non estensione).
StyxRiver

1
Questa è ottima, facile e per me la soluzione migliore. Puoi includere tutto il lavoro che devi fare nel thread dell'interfaccia utente. Esempio: this.UIThread (() => {txtMessage.Text = message; listBox1.Items.Add (message);});
Auto

1
Mi piace molto questa soluzione. Minor nit: chiamerei questo metodo OnUIThreadpiuttosto che UIThread.
ToolmakerSteve

2
Ecco perché ho chiamato questa estensione RunOnUiThread. Ma questo è solo un gusto personale.
Grisgram

66

Questo è il modo classico in cui dovresti farlo:

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

Il tuo thread di lavoro ha un evento. Il thread dell'interfaccia utente inizia un altro thread per eseguire il lavoro e collega l'evento worker in modo da poter visualizzare lo stato del thread worker.

Quindi nell'interfaccia utente è necessario incrociare i thread per modificare il controllo effettivo ... come un'etichetta o una barra di avanzamento.


62

La soluzione semplice è usare Control.Invoke.

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

void updateGUI() {
    // update gui here
}

ben fatto per la semplicità! non solo semplice, ma funziona anche bene! Non capivo davvero perché Microsoft non potesse renderlo più semplice come dovrebbe essere! per chiamare 1 linea sul thread principale, dovremmo scrivere un paio di funzioni!
MBH,

1
@MBH Accetto. A proposito, hai notato stackoverflow.com/a/3588137/199364 risposta sopra, che definisce un metodo di estensione? Fallo una volta in una classe di utility personalizzate, quindi non devi più preoccuparti che Microsoft non l'abbia fatto per noi :)
ToolmakerSteve

@ToolmakerSteve Questo è esattamente ciò che significava essere! hai ragione possiamo trovare un modo, ma intendo dal punto di vista DRY (non ripeterti), il problema che ha una soluzione comune, può essere risolto da loro con il minimo sforzo di Microsoft che farà risparmiare molto tempo per programmatori :)
MBH,

47

Il codice di threading è spesso errato e sempre difficile da testare. Non è necessario scrivere il codice di threading per aggiornare l'interfaccia utente da un'attività in background. Basta usare la classe BackgroundWorker per eseguire l'attività e il suo metodo ReportProgress per aggiornare l'interfaccia utente. Di solito, devi solo segnalare una percentuale completa, ma c'è un altro sovraccarico che include un oggetto stato. Ecco un esempio che riporta solo un oggetto stringa:

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

Va bene se vuoi sempre aggiornare lo stesso campo. Se hai aggiornamenti più complicati da eseguire, puoi definire una classe per rappresentare lo stato dell'interfaccia utente e passarlo al metodo ReportProgress.

Un'ultima cosa, assicurati di impostare il WorkerReportsProgressflag, altrimenti il ReportProgressmetodo verrà completamente ignorato.


2
Al termine dell'elaborazione, è anche possibile aggiornare l'interfaccia utente tramite backgroundWorker1_RunWorkerCompleted.
DavidRR

41

La stragrande maggioranza delle risposte utilizza Control.Invokeuna condizione di razza in attesa di accadere . Ad esempio, considera la risposta accettata:

string newText = "abc"; // running on worker thread
this.Invoke((MethodInvoker)delegate { 
    someLabel.Text = newText; // runs on UI thread
});

Se l'utente chiude il modulo appena prima che this.Invokevenga chiamato (ricorda, thisè l' Formoggetto), unObjectDisposedException probabilmente verrà generato.

La soluzione è usare SynchronizationContext, in particolare SynchronizationContext.Currentcome suggerisce hamilton.danielb (altre risposte si basano su SynchronizationContextimplementazioni specifiche che sono completamente inutili). Modificherei leggermente il suo codice per usarlo SynchronizationContext.Postpiuttosto che SynchronizationContext.Sendperò (poiché in genere non è necessario che il thread di lavoro aspetti):

public partial class MyForm : Form
{
    private readonly SynchronizationContext _context;
    public MyForm()
    {
        _context = SynchronizationContext.Current
        ...
    }

    private MethodOnOtherThread()
    {
         ...
         _context.Post(status => someLabel.Text = newText,null);
    }
}

Nota che su .NET 4.0 e versioni successive dovresti davvero utilizzare le attività per le operazioni asincrone. Vedi la risposta di n-san per l'approccio equivalente basato sull'attività (utilizzo TaskScheduler.FromCurrentSynchronizationContext).

Infine, su .NET 4.5 e versioni successive è anche possibile utilizzare Progress<T>(che sostanzialmente acquisisce al SynchronizationContext.Currentmomento della sua creazione) come dimostrato da Ryszard Dżegan per i casi in cui l'operazione di lunga durata deve eseguire il codice UI mentre è ancora in funzione.


37

Dovrai assicurarti che l'aggiornamento avvenga sul thread corretto; il thread dell'interfaccia utente.

Per fare ciò, dovrai invocare il gestore dell'evento invece di chiamarlo direttamente.

Puoi farlo sollevando il tuo evento in questo modo:

(Il codice viene digitato qui fuori dalla mia testa, quindi non ho verificato la sintassi corretta, ecc., Ma dovrebbe farti andare.)

if( MyEvent != null )
{
   Delegate[] eventHandlers = MyEvent.GetInvocationList();

   foreach( Delegate d in eventHandlers )
   {
      // Check whether the target of the delegate implements 
      // ISynchronizeInvoke (Winforms controls do), and see
      // if a context-switch is required.
      ISynchronizeInvoke target = d.Target as ISynchronizeInvoke;

      if( target != null && target.InvokeRequired )
      {
         target.Invoke (d, ... );
      }
      else
      {
          d.DynamicInvoke ( ... );
      }
   }
}

Si noti che il codice sopra riportato non funzionerà sui progetti WPF, poiché i controlli WPF non implementano ISynchronizeInvoke interfaccia.

Per assicurarti che il codice sopra funzioni con Windows Forms e WPF e tutte le altre piattaforme, puoi dare un'occhiata a AsyncOperation, AsyncOperationManagereSynchronizationContext classi.

Per generare facilmente eventi in questo modo, ho creato un metodo di estensione, che mi consente di semplificare la generazione di un evento semplicemente chiamando:

MyEvent.Raise(this, EventArgs.Empty);

Naturalmente, puoi anche utilizzare la classe BackGroundWorker, che renderà astratta questa faccenda per te.


Anzi, ma non mi piace ingombrare il mio codice GUI con questa faccenda. La mia interfaccia grafica non dovrebbe importare se deve invocare o meno. In altre parole: non penso che sia la responsabilità della GUI a eseguire lo swithc di contesto.
Frederik Gheysels,

1
Rompere il delegato a parte ecc. Sembra eccessivo - perché non solo: SynchronizationContext.Current.Send (delegate {MyEvent (...);}, null);
Marc Gravell

Hai sempre accesso a SynchronizationContext? Anche se la tua classe è in una classe lib?
Frederik Gheysels,

29

Dovrai invocare il metodo sul thread della GUI. Puoi farlo chiamando Control.Invoke.

Per esempio:

delegate void UpdateLabelDelegate (string message);

void UpdateLabel (string message)
{
    if (InvokeRequired)
    {
         Invoke (new UpdateLabelDelegate (UpdateLabel), message);
         return;
    }

    MyLabelControl.Text = message;
}

1
La riga invoke mi dà un errore del compilatore. La migliore corrispondenza del metodo sovraccarico per "System.Windows.Forms.Control.Invoke (System.Delegate, object [])" ha alcuni argomenti non validi
CruelIO

28

A causa della banalità dello scenario, avrei effettivamente il thread del thread dell'interfaccia utente per lo stato. Penso che scoprirai che può essere abbastanza elegante.

public class MyForm : Form
{
  private volatile string m_Text = "";
  private System.Timers.Timer m_Timer;

  private MyForm()
  {
    m_Timer = new System.Timers.Timer();
    m_Timer.SynchronizingObject = this;
    m_Timer.Interval = 1000;
    m_Timer.Elapsed += (s, a) => { MyProgressLabel.Text = m_Text; };
    m_Timer.Start();
    var thread = new Thread(WorkerThread);
    thread.Start();
  }

  private void WorkerThread()
  {
    while (...)
    {
      // Periodically publish progress information.
      m_Text = "Still working...";
    }
  }
}

L'approccio evita l'operazione di marshalling richiesta quando si utilizzano i metodi ISynchronizeInvoke.Invokee ISynchronizeInvoke.BeginInvoke. Non c'è niente di sbagliato nell'usare la tecnica di marshalling, ma ci sono un paio di avvertenze che devi conoscere.

  • Assicurati di non chiamare BeginInvoketroppo frequentemente o potrebbe sovraccaricare il pump dei messaggi.
  • Chiamare Invokeil thread di lavoro è una chiamata di blocco. Interromperà temporaneamente il lavoro svolto in quel thread.

La strategia che propongo in questa risposta inverte i ruoli comunicativi dei thread. Invece del thread di lavoro che spinge i dati il ​​thread dell'interfaccia utente esegue il polling per esso. Questo è un modello comune usato in molti scenari. Dal momento che tutto ciò che vuoi fare è visualizzare le informazioni sullo stato di avanzamento dal thread di lavoro, penso che troverai che questa soluzione è un'ottima alternativa alla soluzione di marshalling. Ha i seguenti vantaggi.

  • L'interfaccia utente e i thread di lavoro rimangono vagamente accoppiati rispetto all'approccio Control.Invokeo Control.BeginInvokeche li unisce strettamente.
  • Il thread dell'interfaccia utente non impedisce l'avanzamento del thread di lavoro.
  • Il thread di lavoro non può dominare il tempo in cui il thread dell'interfaccia utente trascorre l'aggiornamento.
  • Gli intervalli in cui l'interfaccia utente e i thread di lavoro eseguono operazioni possono rimanere indipendenti.
  • Il thread di lavoro non può sovraccaricare il pump dei messaggi del thread dell'interfaccia utente.
  • Il thread dell'interfaccia utente viene dettato quando e con quale frequenza viene aggiornata l'interfaccia utente.

3
Buona idea. L'unica cosa che non hai menzionato è come smaltire correttamente il timer una volta terminato WorkerThread. Si noti che ciò può causare problemi al termine dell'applicazione (ovvero l'utente chiude l'applicazione). Hai idea di come risolverlo?
Matt

@Matt Invece di utilizzare un gestore anonimo per l' Elapsedevento, si utilizza un metodo membro in modo da poter rimuovere il timer quando il modulo viene eliminato ...
Phil1970

@ Phil1970 - Buon punto. Volevi dire come System.Timers.ElapsedEventHandler handler = (s, a) => { MyProgressLabel.Text = m_Text; };e assegnarlo tramite m_Timer.Elapsed += handler;, più tardi nel contesto di smaltimento facendo un m_Timer.Elapsed -= handler;vero? E per lo smaltimento / chiusura seguendo i consigli come discusso qui .
Matt,

27

Nessuna delle cose Invoke nelle risposte precedenti è necessaria.

Devi guardare WindowsFormsSynchronizationContext:

// In the main thread
WindowsFormsSynchronizationContext mUiContext = new WindowsFormsSynchronizationContext();

...

// In some non-UI Thread

// Causes an update in the GUI thread.
mUiContext.Post(UpdateGUI, userData);

...

void UpdateGUI(object userData)
{
    // Update your GUI controls here
}

4
cosa pensi che usi il metodo Post sotto il cofano? :)
increddibelly

23

Questo è simile alla soluzione precedente usando .NET Framework 3.0, ma ha risolto il problema del supporto di sicurezza in fase di compilazione .

public  static class ControlExtension
{
    delegate void SetPropertyValueHandler<TResult>(Control souce, Expression<Func<Control, TResult>> selector, TResult value);

    public static void SetPropertyValue<TResult>(this Control source, Expression<Func<Control, TResult>> selector, TResult value)
    {
        if (source.InvokeRequired)
        {
            var del = new SetPropertyValueHandler<TResult>(SetPropertyValue);
            source.Invoke(del, new object[]{ source, selector, value});
        }
        else
        {
            var propInfo = ((MemberExpression)selector.Body).Member as PropertyInfo;
            propInfo.SetValue(source, value, null);
        }
    }
}

Usare:

this.lblTimeDisplay.SetPropertyValue(a => a.Text, "some string");
this.lblTimeDisplay.SetPropertyValue(a => a.Visible, false);

Il compilatore fallirà se l'utente passa il tipo di dati errato.

this.lblTimeDisplay.SetPropertyValue(a => a.Visible, "sometext");

23

Salvete! Dopo aver cercato questa domanda, ho trovato le risposte di FrankG e Oregon Ghost per me le più facili e utili. Ora codice in Visual Basic ed eseguivo questo frammento tramite un convertitore; quindi non sono sicuro di come sia andata a finire.

Ho un modulo di dialogo chiamato form_Diagnostics,che ha una casella richtext, chiamatoupdateDiagWindow, che sto usando come una sorta di display di registrazione. Avevo bisogno di poter aggiornare il suo testo da tutti i thread. Le righe extra consentono alla finestra di scorrere automaticamente fino alle righe più recenti.

E così, ora posso aggiornare il display con una riga, da qualsiasi parte dell'intero programma nel modo in cui pensi che funzionerebbe senza alcun thread:

  form_Diagnostics.updateDiagWindow(whatmessage);

Codice principale (inseriscilo nel codice di classe del modulo):

#region "---------Update Diag Window Text------------------------------------"
// This sub allows the diag window to be updated by all threads
public void updateDiagWindow(string whatmessage)
{
    var _with1 = diagwindow;
    if (_with1.InvokeRequired) {
        _with1.Invoke(new UpdateDiagDelegate(UpdateDiag), whatmessage);
    } else {
        UpdateDiag(whatmessage);
    }
}
// This next line makes the private UpdateDiagWindow available to all threads
private delegate void UpdateDiagDelegate(string whatmessage);
private void UpdateDiag(string whatmessage)
{
    var _with2 = diagwindow;
    _with2.appendtext(whatmessage);
    _with2.SelectionStart = _with2.Text.Length;
    _with2.ScrollToCaret();
}
#endregion

21

Per molti scopi è semplice come questo:

public delegate void serviceGUIDelegate();
private void updateGUI()
{
  this.Invoke(new serviceGUIDelegate(serviceGUI));
}

"serviceGUI ()" è un metodo a livello di GUI all'interno del modulo (questo) che può modificare tutti i controlli che desideri. Chiama "updateGUI ()" dall'altro thread. I parametri possono essere aggiunti per passare valori o (probabilmente più velocemente) utilizzare variabili di ambito di classe con blocchi su di essi come richiesto se esiste la possibilità di uno scontro tra i thread che accedono ad essi che potrebbe causare instabilità. Usa BeginInvoke invece di Invoke se il thread non GUI è critico in termini di tempo (tenendo presente l'avvertimento di Brian Gideon).


21

Questo nella mia variante C # 3.0 della soluzione di Ian Kemp:

public static void SetPropertyInGuiThread<C,V>(this C control, Expression<Func<C, V>> property, V value) where C : Control
{
    var memberExpression = property.Body as MemberExpression;
    if (memberExpression == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    var propertyInfo = memberExpression.Member as PropertyInfo;
    if (propertyInfo == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    if (control.InvokeRequired)
        control.Invoke(
            (Action<C, Expression<Func<C, V>>, V>)SetPropertyInGuiThread,
            new object[] { control, property, value }
        );
    else
        propertyInfo.SetValue(control, value, null);
}

Lo chiami così:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
  1. Aggiunge il controllo null al risultato di "as MemberExpression".
  2. Migliora la sicurezza statica del tipo.

Altrimenti, l'originale è una soluzione molto bella.


21
Label lblText; //initialized elsewhere

void AssignLabel(string text)
{
   if (InvokeRequired)
   {
      BeginInvoke((Action<string>)AssignLabel, text);
      return;
   }

   lblText.Text = text;           
}

Si noti che BeginInvoke()è preferibile Invoke()perché è meno probabile che causi deadlock (tuttavia, questo non è un problema qui quando si assegna il testo a un'etichetta):

Quando si utilizza, Invoke()si attende il ritorno del metodo. Ora, potrebbe essere che tu faccia qualcosa nel codice invocato che dovrà attendere il thread, il che potrebbe non essere immediatamente evidente se è sepolto in alcune funzioni che stai chiamando, cosa che potrebbe accadere indirettamente tramite i gestori di eventi. Quindi aspetteresti il ​​thread, il thread ti aspetterebbe e sarai bloccato.

Ciò ha causato la sospensione di alcuni dei nostri software rilasciati. È stato abbastanza facile da risolvere sostituendolo Invoke()con BeginInvoke(). A meno che non sia necessario un funzionamento sincrono, che può essere il caso se è necessario un valore di ritorno, utilizzare BeginInvoke().


20

Quando ho riscontrato lo stesso problema ho cercato aiuto da Google, ma piuttosto che darmi una soluzione semplice mi ha confuso di più dando esempi di MethodInvokere blah blah blah. Quindi ho deciso di risolverlo da solo. Ecco la mia soluzione:

Crea un delegato come questo:

Public delegate void LabelDelegate(string s);

void Updatelabel(string text)
{
   if (label.InvokeRequired)
   {
       LabelDelegate LDEL = new LabelDelegate(Updatelabel);
       label.Invoke(LDEL, text);
   }
   else
       label.Text = text
}

Puoi chiamare questa funzione in un nuovo thread come questo

Thread th = new Thread(() => Updatelabel("Hello World"));
th.start();

Non essere confuso con Thread(() => .....). Uso una funzione anonima o un'espressione lambda quando lavoro su un thread. Per ridurre le righe di codice puoi usare anche il ThreadStart(..)metodo che non dovrei spiegare qui.


17

Usa semplicemente qualcosa del genere:

 this.Invoke((MethodInvoker)delegate
            {
                progressBar1.Value = e.ProgressPercentage; // runs on UI thread
            });

Se sì e.ProgressPercentage, non sei già nel thread dell'interfaccia utente dal metodo che stai chiamando?
LarsTech,

L'evento ProgressChanged viene eseguito sul thread dell'interfaccia utente. Questa è una delle comodità dell'utilizzo di BackgroundWorker. Anche l'evento Completato viene eseguito sulla GUI. L'unica cosa in esecuzione nel thread non UI è il metodo DoWork.
LarsTech,

15

È possibile utilizzare il delegato già esistente Action:

private void UpdateMethod()
{
    if (InvokeRequired)
    {
        Invoke(new Action(UpdateMethod));
    }
}

14

La mia versione è quella di inserire una riga di "mantra" ricorsivo:

Per nessun argomento:

    void Aaaaaaa()
    {
        if (InvokeRequired) { Invoke(new Action(Aaaaaaa)); return; } //1 line of mantra

        // Your code!
    }

Per una funzione che ha argomenti:

    void Bbb(int x, string text)
    {
        if (InvokeRequired) { Invoke(new Action<int, string>(Bbb), new[] { x, text }); return; }
        // Your code!
    }

QUESTO È .


Qualche argomentazione : di solito è male per la leggibilità del codice mettere {} dopo unif () un'istruzione in una riga. Ma in questo caso è sempre lo stesso "mantra" di routine. Non interrompe la leggibilità del codice se questo metodo è coerente con il progetto. E salva il tuo codice dai rifiuti (una riga di codice anziché cinque).

Come vedi if(InvokeRequired) {something long}sai "questa funzione è sicura da chiamare da un altro thread".


13

Prova ad aggiornare l'etichetta usando questo

public static class ExtensionMethods
{
    private static Action EmptyDelegate = delegate() { };

    public static void Refresh(this UIElement uiElement)
    {
        uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
    }
}

È per Windows Form ?
Kiquenet,

13

Crea una variabile di classe:

SynchronizationContext _context;

Impostalo nel costruttore che crea la tua UI:

var _context = SynchronizationContext.Current;

Quando si desidera aggiornare l'etichetta:

_context.Send(status =>{
    // UPDATE LABEL
}, null);

12

È necessario utilizzare invoke e delegate

private delegate void MyLabelDelegate();
label1.Invoke( new MyLabelDelegate(){ label1.Text += 1; });

12

La maggior parte delle altre risposte sono un po 'complesse per me su questa domanda (sono nuovo di C #), quindi sto scrivendo la mia:

Ho un'applicazione WPF e ho definito un lavoratore come di seguito:

Problema:

BackgroundWorker workerAllocator;
workerAllocator.DoWork += delegate (object sender1, DoWorkEventArgs e1) {
    // This is my DoWork function.
    // It is given as an anonymous function, instead of a separate DoWork function

    // I need to update a message to textbox (txtLog) from this thread function

    // Want to write below line, to update UI
    txt.Text = "my message"

    // But it fails with:
    //  'System.InvalidOperationException':
    //  "The calling thread cannot access this object because a different thread owns it"
}

Soluzione:

workerAllocator.DoWork += delegate (object sender1, DoWorkEventArgs e1)
{
    // The below single line works
    txtLog.Dispatcher.BeginInvoke((Action)(() => txtLog.Text = "my message"));
}

Devo ancora scoprire cosa significa la riga sopra, ma funziona.

Per WinForms :

Soluzione:

txtLog.Invoke((MethodInvoker)delegate
{
    txtLog.Text = "my message";
});

La domanda riguardava Winforms, non WPF.
Marc L.

Grazie. Aggiunta la soluzione WinForms sopra.
Manohar Reddy Poreddy l'

... che è solo una copia di quante altre risposte su questa stessa domanda, ma va bene. Perché non far parte della soluzione ed eliminare la tua risposta?
Marc L.

hmm, corretto sei, se non altro, leggi la mia risposta con attenzione, la parte iniziale (il motivo per cui ho scritto la risposta), e si spera con un po 'più di attenzione vedi che c'è qualcuno che ha avuto lo stesso problema e votato oggi per la mia semplice risposta, e con ancora più attenzione se potessi prevedere la vera storia sul perché tutto ciò è accaduto, quel google mi manda qui anche quando cerco wpf. Sicuramente da quando hai perso questi 3 motivi più o meno ovvi, posso capire perché non rimuoverai il tuo voto negativo. Invece di pulire quello a posto, crea qualcosa di nuovo che è molto più difficile.
Manohar Reddy Poreddy l'

8

Il modo più semplice penso:

   void Update()
   {
       BeginInvoke((Action)delegate()
       {
           //do your update
       });
   }

8

Ad esempio, accedi a un controllo diverso dal thread corrente:

Speed_Threshold = 30;
textOutput.Invoke(new EventHandler(delegate
{
    lblThreshold.Text = Speed_Threshold.ToString();
}));

C'è lblThresholdun'etichetta ed Speed_Thresholdè una variabile globale.


8

Quando ti trovi nel thread dell'interfaccia utente, potresti richiederlo per il relativo programma di pianificazione del contesto di sincronizzazione. Ti darebbe un TaskScheduler che pianifica tutto sul thread dell'interfaccia utente.

Quindi è possibile concatenare le attività in modo che quando il risultato è pronto, un'altra attività (pianificata sul thread dell'interfaccia utente) la raccolga e la assegni a un'etichetta.

public partial class MyForm : Form
{
  private readonly TaskScheduler _uiTaskScheduler;
  public MyForm()
  {
    InitializeComponent();
    _uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
  }

  private void buttonRunAsyncOperation_Click(object sender, EventArgs e)
  {
    RunAsyncOperation();
  }

  private void RunAsyncOperation()
  {
    var task = new Task<string>(LengthyComputation);
    task.ContinueWith(antecedent =>
                         UpdateResultLabel(antecedent.Result), _uiTaskScheduler);
    task.Start();
  }

  private string LengthyComputation()
  {
    Thread.Sleep(3000);
    return "47";
  }

  private void UpdateResultLabel(string text)
  {
    labelResult.Text = text;
  }
}

Funziona con attività (non thread) che sono il modo preferito di scrivere codice simultaneo ora .


1
La chiamata in Task.Startgenere non è una buona pratica blogs.msdn.com/b/pfxteam/archive/2012/01/14/10256832.aspx
Ohad Schneider

8

Ho appena letto le risposte e questo sembra essere un argomento molto caldo. Attualmente sto usando .NET 3.5 SP1 e Windows Form.

La nota formula ampiamente descritta nelle risposte precedenti che fa uso della proprietà InvokeRequired copre la maggior parte dei casi, ma non l'intero pool.

Cosa succede se l' handle non è stato ancora creato?

La proprietà InvokeRequired , come descritto qui (riferimento alla proprietà Control.InvokeRequired a MSDN) restituisce true se la chiamata è stata effettuata da un thread che non è il thread GUI, false se la chiamata è stata effettuata dal thread GUI o se l' handle era non ancora creato.

Puoi imbatterti in un'eccezione se vuoi che un modulo modale sia mostrato e aggiornato da un altro thread. Perché vuoi che il modulo sia mostrato in modo modale, puoi fare quanto segue:

private MyForm _gui;

public void StartToDoThings()
{
    _gui = new MyForm();
    Thread thread = new Thread(SomeDelegate);
    thread.Start();
    _gui.ShowDialog();
}

E il delegato può aggiornare un'etichetta sulla GUI:

private void SomeDelegate()
{
    // Operations that can take a variable amount of time, even no time
    //... then you update the GUI
    if(_gui.InvokeRequired)
        _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
    else
        _gui.Label1.Text = "Done!";
}

Ciò può causare un InvalidOperationException se le operazioni prima di aggiornamento dell'etichetta "richiedono meno tempo" (lo leggono e lo interpretano come una semplificazione) rispetto al tempo necessario per il thread GUI per creare il modulo s' Handle . Questo accade all'interno di ShowDialog () metodo .

Dovresti anche controllare la maniglia in questo modo:

private void SomeDelegate()
{
    // Operations that can take a variable amount of time, even no time
    //... then you update the GUI
    if(_gui.IsHandleCreated)  //  <---- ADDED
        if(_gui.InvokeRequired)
            _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
        else
            _gui.Label1.Text = "Done!";
}

Puoi gestire l'operazione da eseguire se l' handle non è stato ancora creato: puoi semplicemente ignorare l'aggiornamento della GUI (come mostrato nel codice sopra) o puoi aspettare (più rischioso). Questo dovrebbe rispondere alla domanda.

Elementi opzionali: Personalmente mi è venuta in mente la seguente codifica:

public class ThreadSafeGuiCommand
{
  private const int SLEEPING_STEP = 100;
  private readonly int _totalTimeout;
  private int _timeout;

  public ThreadSafeGuiCommand(int totalTimeout)
  {
    _totalTimeout = totalTimeout;
  }

  public void Execute(Form form, Action guiCommand)
  {
    _timeout = _totalTimeout;
    while (!form.IsHandleCreated)
    {
      if (_timeout <= 0) return;

      Thread.Sleep(SLEEPING_STEP);
      _timeout -= SLEEPING_STEP;
    }

    if (form.InvokeRequired)
      form.Invoke(guiCommand);
    else
      guiCommand();
  }
}

Nutro i miei moduli che vengono aggiornati da un altro thread con un'istanza di questo ThreadSafeGuiCommand e definisco i metodi che aggiornano la GUI (nel mio modulo) in questo modo:

public void SetLabeTextTo(string value)
{
  _threadSafeGuiCommand.Execute(this, delegate { Label1.Text = value; });
}

In questo modo sono abbastanza sicuro che avrò aggiornato la mia GUI qualunque thread effettuerà la chiamata, opzionalmente aspettando un periodo di tempo ben definito (il timeout).


1
Sono venuto qui per trovare questo, mentre controllo anche IsHandleCreated. Un'altra proprietà da verificare è IsDisposed. Se il modulo è eliminato, non è possibile chiamare Invoke () su di esso. Se l'utente ha chiuso il modulo prima del completamento del thread in background, non si desidera che tenti di richiamare l'interfaccia utente quando il modulo viene eliminato.
Jon,

Direi che è una cattiva idea iniziare con ... Normalmente, mostreresti immediatamente il modulo figlio e avresti una barra di avanzamento o qualche altro feedback mentre esegui l'elaborazione in background. Oppure eseguiresti prima tutte le elaborazioni, quindi passerai il risultato al nuovo modulo al momento della creazione. Fare entrambi allo stesso tempo avrebbe generalmente benefici marginali ma molto meno codice gestibile.
Fil 1970,

Lo scenario descritto tiene conto di un modulo modale utilizzato come vista di avanzamento di un processo con thread in background. Poiché deve essere modale, deve essere mostrato chiamando il metodo Form.ShowDialog () . In questo modo, si impedisce l'esecuzione del codice che segue la chiamata fino alla chiusura del modulo. Pertanto, a meno che non sia possibile avviare il thread in background in modo diverso dall'esempio fornito (e, ovviamente, è possibile) questo modulo deve essere mostrato modalmente dopo l'avvio del thread in background. In questo caso, è necessario verificare la maniglia da creare. Se non hai bisogno di una forma modale, allora è un'altra storia.
Sume il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.