update - textbox cross thread c#




Come aggiorno la GUI da un altro thread? (20)

Gestire un lavoro lungo

Dal momento che .NET 4.5 e C # 5.0 è necessario utilizzare il modello asincrono basato su attività (TAP) insieme async - await parole chiave in tutte le aree (compresa la GUI):

TAP è il modello di progettazione asincrono consigliato per il nuovo sviluppo

anziché il modello di programmazione asincrono (APM) e il modello asincrono basato sugli eventi (EAP) (quest'ultimo include la classe BackgroundWorker ).

Quindi, la soluzione consigliata per il nuovo sviluppo è:

  1. Implementazione asincrona di un gestore di 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 il 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 callback e thread espliciti.
  2. Task invece di Thread .
  3. async keyword, che consente di utilizzare await 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. Progresso della classe (vedi Interfaccia IProgress ) che supporta il principio di progettazione Separation of Concerns (SoC) e che non richiede esplicitamente dispatcher e invocazioni. Usa SynchronizationContext corrente dalla sua posizione di creazione (qui il thread dell'interfaccia utente).
  5. TaskCreationOptions.LongRunning che suggerisce di non accodare l'attività in ThreadPool .

Per un esempio più dettagliato vedi: The Future of C #: Le cose buone arrivano a coloro che "attendono" Joseph Albahari .

Vedi anche sul concetto del modello di threading UI .

Gestire le eccezioni

Il frammento di seguito è un esempio di come gestire le eccezioni e attivare la proprietà Enabled 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...");
    }
}

Qual è il modo più semplice per aggiornare Label da un altro thread?

Ho un Form su thread1 e da quello sto avviando un altro thread ( thread2 ). Mentre thread2 sta elaborando alcuni file vorrei aggiornare Label sul Form con lo stato corrente del thread2 di thread2 .

Come lo posso fare?


A causa della banalità dello scenario, avrei effettivamente il polling del thread dell'interfaccia utente per lo stato. Penso che troverai 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.Invoke e ISynchronizeInvoke.BeginInvoke . Non c'è niente di sbagliato nell'usare la tecnica di marshalling, ma ci sono un paio di avvertenze da tenere presente.

  • Assicurati di non chiamare BeginInvoke troppo frequentemente o che potrebbe sovraccaricare la pompa dei messaggi.
  • Chiamare Invoke sul thread di lavoro è una chiamata bloccante. Interromperà temporaneamente il lavoro svolto in quel thread.

La strategia che propongo in questa risposta ribalta i ruoli comunicativi dei thread. Invece del thread worker che inserisce i dati, il thread UI esegue il polling per esso. Questo è un modello comune utilizzato in molti scenari. Dal momento che tutto ciò che si vuole fare è visualizzare le informazioni di avanzamento dal thread di lavoro, penso che si troverà che questa soluzione è un'ottima alternativa alla soluzione di marshalling. Presenta i seguenti vantaggi.

  • L'interfaccia utente e i thread di lavoro rimangono accoppiati liberamente anziché l'approccio Control.Invoke o Control.BeginInvoke che li accoppia strettamente.
  • Il thread dell'interfaccia utente non impedirà l'avanzamento del thread di lavoro.
  • Il thread di lavoro non può dominare il momento in cui il thread dell'interfaccia utente spende 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ò superare la pompa dei messaggi del thread dell'interfaccia utente.
  • Il thread dell'interfaccia utente consente di stabilire quando e quanto spesso l'interfaccia utente viene aggiornata.

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

Per fare ciò, dovrai richiamare il gestore di eventi invece di chiamarlo direttamente.

Puoi farlo alzando il tuo evento in questo modo:

(Il codice è stato digitato qui fuori dalla mia testa, quindi non ho controllato 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 non funzionerà su progetti WPF, poiché i controlli WPF non implementano l'interfaccia ISynchronizeInvoke .

Per assicurarti che il codice sopra AsyncOperation con Windows Form e WPF e tutte le altre piattaforme, puoi dare un'occhiata alle AsyncOperation , AsyncOperationManager e SynchronizationContext .

Al fine di aumentare facilmente gli eventi in questo modo, ho creato un metodo di estensione, che mi consente di semplificare l'innalzamento di un evento semplicemente chiamando:

MyEvent.Raise(this, EventArgs.Empty);

Naturalmente, puoi anche utilizzare la classe BackGroundWorker, che estrae l'argomento per te.


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

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 Invoke blocca l'esecuzione fino al suo completamento - questo è il codice sincrono. La domanda non riguarda il codice asincrono, ma c'è molto contenuto su sulla scrittura di codice asincrono quando si vuole imparare a riguardo.


La maggior parte delle risposte utilizza Control.Invoke una condizione di competizione 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 immediatamente prima di questo. this.Invoke viene chiamato (ricordare, this è l'oggetto Form ), verrà probabilmente ObjectDisposedException .

La soluzione è utilizzare SynchronizationContext , in particolare SynchronizationContext.Current come suggerito da hamilton.danielb (altre risposte si basano su specifiche implementazioni di SynchronizationContext che è completamente inutile). Vorrei modificare leggermente il suo codice per utilizzare SynchronizationContext.Post piuttosto che SynchronizationContext.Send anche se (in genere non è necessario attendere il thread worker):

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. Vedere n-san's risposta n-san's per l'approccio basato TaskScheduler.FromCurrentSynchronizationContext equivalente (utilizzando TaskScheduler.FromCurrentSynchronizationContext ).

Infine, su .NET 4.5 e versioni successive è possibile utilizzare Progress<T> (che in pratica cattura SynchronizationContext.Current momento 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.


Non è necessaria nessuna delle voci Invoke nelle risposte precedenti.

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

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 precedente come metodo di estensione della classe Control , che quindi semplificherà 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 ||
      [email protected]().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 usa le espressioni LINQ e lambda per consentire una sintassi molto più pulita, più semplice e più sicura:

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

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

Sfortunatamente questo non impedisce a nessuno di fare cose stupide come passare la proprietà e il valore di un altro Control , quindi il seguente compilerà felicemente:

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

Quindi ho aggiunto i controlli di runtime per assicurarmi che la proprietà passata appartenga effettivamente al Control cui il metodo è stato chiamato. Non perfetto, ma ancora 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!


Questo è il modo classico per 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 tuo thread UI inizia da un altro thread per eseguire il lavoro e collega quell'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.


Questo è simile alla soluzione sopra utilizzando .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");

Salvete! Dopo aver cercato questa domanda, ho trovato che le risposte di FrankG e Oregon Ghost sono le più facili per me. Ora, codice in Visual Basic e ho eseguito questo snippet tramite un convertitore; quindi non sono sicuro di come andrà a finire.

Ho una finestra di dialogo chiamata form_Diagnostics, che ha una casella di testo in estensione, chiamata updateDiagWindow, che sto usando come una sorta di display di registrazione. Dovevo essere in grado di aggiornare il suo testo da tutti i thread. Le linee aggiuntive consentono alla finestra di scorrere automaticamente verso le righe più recenti.

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

  form_Diagnostics.updateDiagWindow(whatmessage);

Codice principale (inserisci questo all'interno del codice di classe del tuo 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


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

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

Esiste lblThresholdun'etichetta ed Speed_Thresholdè una variabile globale.


Basta usare qualcosa come questo:

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

Il modo più semplice che penso:

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

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

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

QUELLO è IT .

Alcune argomentazioni : in genere è dannoso che la leggibilità del codice metta {} dopo if ()un'istruzione in una riga. Ma in questo caso è lo stesso "mantra" di routine. Non infrange la leggibilità del codice se questo metodo è coerente sul progetto. E salva il tuo codice dai rifiuti (una riga di codice anziché cinque).

Come vedi, if(InvokeRequired) {something long}puoi solo sapere "questa funzione è sicura per chiamare da un altro thread".


Puoi utilizzare il delegato già esistente Action:

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

Quando si è nel thread dell'interfaccia utente, è possibile chiedergli il relativo programma di pianificazione del contesto di sincronizzazione. Ti darebbe un TaskScheduler che pianifica tutto sul thread dell'interfaccia utente.

Quindi puoi concatenare le tue attività in modo che quando il risultato è pronto, un'altra attività (che è pianificata sul thread dell'interfaccia utente) la preleva e la assegna 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;
  }
}

Questo funziona per le attività (non i thread) che sono il modo preferito di scrivere codice simultaneo ora .


Volevo aggiungere un avviso perché ho notato che alcune delle semplici soluzioni omettevano il InvokeRequiredcontrollo.

Ho notato che se il codice viene eseguito prima che l'handle della finestra del controllo sia stato creato (ad esempio prima che venga visualizzato il modulo), Invokegenera un'eccezione. Quindi consiglio sempre di verificare InvokeRequiredprima di chiamare Invokeo BeginInvoke.





user-interface