web-dev-qa-db-fra.com

Comment mettre à jour l'interface graphique à partir d'un autre thread?

Quel est le moyen le plus simple de mettre à jour un Label à partir d'un autre thread?

J'ai un Form sur thread1, et à partir de cela, je commence un autre thread (thread2). Pendant que thread2 traite certains fichiers, je souhaite mettre à jour un Label sur le Form avec l’état actuel du travail de thread2.

Comment puis je faire ça?

1304
CruelIO

Pour .NET 2.0, voici un joli morceau de code que j'ai écrit qui fait exactement ce que vous voulez et qui fonctionne pour toutes les propriétés d'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 });
  }
}

Appelez ça comme ça:

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

Si vous utilisez .NET 3.0 ou une version ultérieure, vous pouvez réécrire la méthode ci-dessus en tant que méthode d'extension de la classe Control, ce qui simplifierait ensuite l'appel à:

myLabel.SetPropertyThreadSafe("Text", status);

PDATE 05/10/2010:

Pour .NET 3.0, vous devez utiliser ce code:

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

qui utilise les expressions LINQ et lambda pour permettre une syntaxe plus propre, plus simple et plus sûre:

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

Non seulement le nom de la propriété est maintenant vérifié lors de la compilation, mais le type de la propriété l'est également, il est donc impossible d'affecter (par exemple) une valeur de chaîne à une propriété booléenne et donc de provoquer une exception d'exécution.

Malheureusement, cela n'empêche personne de faire des choses stupides telles que transmettre la propriété et la valeur d'une autre Control, aussi les éléments suivants seront-ils compilés avec bonheur:

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

Par conséquent, j’ai ajouté les contrôles d’exécution pour s’assurer que la propriété transmise appartient bien à la variable Control à laquelle la méthode est appelée. Pas parfait, mais toujours beaucoup mieux que la version .NET 2.0.

Si quelqu'un a d'autres suggestions sur la façon d'améliorer ce code pour la sécurité au moment de la compilation, veuillez commenter!

751
Ian Kemp

La méthode la plus simple est une méthode anonyme passée à 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
_

Notez que Invoke bloque l'exécution jusqu'à la fin. Il s'agit d'un code synchrone. La question ne concerne pas le code asynchrone, mais il y a beaucoup de contenu sur le dépassement de pile à propos de l'écriture de code asynchrone lorsque vous souhaitez en savoir plus.

1033
Marc Gravell

Gérer un long travail

Depuis . NET 4.5 et C # 5. , vous devez utiliser Modèle asynchrone basé sur des tâches (TAP) avec async - wait mots-clés dans tous les domaines (y compris l'interface graphique ):

TAP est le modèle de conception asynchrone recommandé pour les nouveaux développements.

au lieu de modèle de programmation asynchrone (APM) et modèle asynchrone basé sur des événements (EAP) (ce dernier inclut le BackgroundWorker Class ).

Ensuite, la solution recommandée pour les nouveaux développements est la suivante:

  1. Implémentation asynchrone d'un gestionnaire d'événements (oui, c'est tout):

    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. Implémentation du deuxième thread notifiant le thread d'interface utilisateur:

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

Notez ce qui suit:

  1. Code court et propre écrit de manière séquentielle, sans rappels ni threads explicites.
  2. Tâche au lieu de Fil .
  3. async mot-clé, qui permet d'utiliser wait qui empêche à son tour le gestionnaire d'événements d'atteindre l'état d'achèvement jusqu'à la fin de la tâche et qui, entre-temps, ne bloque pas le thread d'interface utilisateur.
  4. Classe de progression (voir IProgress Interface ) prenant en charge le principe de conception Separation of Concerns (SoC) et ne nécessitant pas de fonction explicative de répartition et d'appel. Il utilise le courant SynchronizationContext à partir de son lieu de création (ici, le thread d'interface utilisateur).
  5. TaskCreationOptions.LongRunning qui suggère de ne pas mettre la tâche en file d'attente dans ThreadPool .

Pour des exemples plus verbeux, voir: L'avenir de C #: les bonnes choses vont à ceux qui 'attendent' by Joseph Albahari .

Voir aussi à propos de modèle d'interface utilisateur concept.

Gestion des exceptions

L'extrait ci-dessous est un exemple de la façon de gérer les exceptions et d'utiliser la propriété Enabled du bouton bascule pour empêcher les clics multiples pendant l'exécution en arrière-plan.

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...");
    }
}
379
Ryszard Dżegan

Variation de solution la plus simple de Marc Gravell pour .NET 4:

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

Ou utilisez plutôt Action delegate:

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

Voir ici pour une comparaison des deux: MethodInvoker vs Action pour Control.BeginInvoke

227
Zaid Masud

Méthode d'extension Fire and Forgot pour .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();
        }
    }
}

Ceci peut être appelé en utilisant la ligne de code suivante:

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

C'est la manière classique de faire ceci:

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

Votre thread de travail a un événement. Votre thread d'interface utilisateur démarre un autre thread pour effectuer le travail et connecte cet événement de travail afin que vous puissiez afficher l'état du thread de travail.

Ensuite, dans l'interface utilisateur, vous devez traverser les threads pour modifier le contrôle réel ... comme une étiquette ou une barre de progression.

65
Hath

La solution simple consiste à utiliser Control.Invoke.

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

void updateGUI() {
    // update gui here
}
60
OregonGhost

Le code de threading est souvent bogué et toujours difficile à tester. Il n'est pas nécessaire d'écrire du code de thread pour mettre à jour l'interface utilisateur à partir d'une tâche en arrière-plan. Utilisez simplement la classe BackgroundWorker pour exécuter la tâche et sa méthode ReportProgress pour mettre à jour l'interface utilisateur. Généralement, vous ne signalez qu'un pourcentage achevé, mais il existe une autre surcharge qui inclut un objet d'état. Voici un exemple qui signale simplement un objet string:

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

C'est bien si vous voulez toujours mettre à jour le même champ. Si vous avez des mises à jour plus complexes à effectuer, vous pouvez définir une classe représentant l'état de l'interface utilisateur et la transmettre à la méthode ReportProgress.

Une dernière chose, assurez-vous de définir le drapeau WorkerReportsProgress, sinon la méthode ReportProgress sera complètement ignorée.

46
Don Kirkby

La grande majorité des réponses utilise Control.Invoke qui est un condition de concurrence imminente . Par exemple, considérons la réponse acceptée:

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

Si l'utilisateur ferme le formulaire juste avant que this.Invoke ne soit appelé (rappelez-vous, this est l'objet Form,), un ObjectDisposedException sera probablement déclenché.

La solution consiste à utiliser SynchronizationContext, plus précisément SynchronizationContext.Current comme hamilton.danielb suggère (d'autres réponses s'appuient sur des implémentations spécifiques de SynchronizationContext, ce qui est totalement inutile). Je modifierais légèrement son code pour utiliser SynchronizationContext.Post plutôt que SynchronizationContext.Send bien que (comme il n'y a généralement pas besoin d'attendre du thread de travail):

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

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

Notez que sur .NET 4.0 et supérieur, vous devriez vraiment utiliser des tâches pour les opérations asynchrones. Voir la réponse de n-san pour l’approche équivalente basée sur la tâche (utilisant TaskScheduler.FromCurrentSynchronizationContext).

Enfin, sur .NET 4.5 et supérieur, vous pouvez également utiliser Progress<T> (qui capture essentiellement SynchronizationContext.Current lors de sa création), comme le montre Ryszard Dardegan's dans les cas où l'opération de longue durée doit exécuter le code d'interface utilisateur tout en travaillant.

41
Ohad Schneider

Vous devrez vous assurer que la mise à jour se produit sur le bon thread; le fil de l'interface utilisateur.

Pour ce faire, vous devrez invoquer le gestionnaire d'événements au lieu de l'appeler directement.

Vous pouvez le faire en élevant votre événement comme ceci:

(Le code est tapé ici dans ma tête, donc je n'ai pas vérifié la syntaxe, etc., mais ça devrait vous aider.)

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

Notez que le code ci-dessus ne fonctionnera pas sur les projets WPF, car les contrôles WPF n'implémentent pas l'interface ISynchronizeInvoke.

Pour vous assurer que le code ci-dessus fonctionne avec Windows Forms, WPF et toutes les autres plates-formes, vous pouvez consulter les classes AsyncOperation, AsyncOperationManager et SynchronizationContext.

Pour créer facilement des événements de cette manière, j'ai créé une méthode d'extension, ce qui me permet de simplifier la création d'événement en appelant simplement:

MyEvent.Raise(this, EventArgs.Empty);

Bien entendu, vous pouvez également utiliser la classe BackGroundWorker, qui résumera cette question pour vous.

35
Frederik Gheysels

Vous devrez invoquer la méthode sur le fil de l'interface graphique. Vous pouvez le faire en appelant Control.Invoke.

Par exemple:

delegate void UpdateLabelDelegate (string message);

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

    MyLabelControl.Text = message;
}
29
Kieron

En raison de la trivialité du scénario, j'aurais en fait l'interrogation du statut d'interface utilisateur. Je pense que vous constaterez que cela peut être assez élégant.

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

Cette approche évite l'opération de marshaling requise lors de l'utilisation des méthodes ISynchronizeInvoke.Invoke et ISynchronizeInvoke.BeginInvoke. Il n’ya rien de mal à utiliser la technique du marshaling, mais il ya quelques mises en garde dont vous devez être conscient.

  • Assurez-vous de ne pas appeler BeginInvoke trop souvent, sinon la pompe à messages pourrait être dépassée.
  • L'appel de Invoke sur le thread de travail est un appel bloquant. Cela arrêtera temporairement le travail effectué dans ce fil.

La stratégie que je propose dans cette réponse inverse les rôles de communication des threads. Au lieu que le thread de travail pousse les données, le thread d'interface utilisateur les interroge. Ceci est un modèle commun utilisé dans de nombreux scénarios. Puisque tout ce que vous voulez faire, c'est afficher les informations de progression du thread de travail, vous constaterez que cette solution est une excellente alternative à la solution de marshaling. Il présente les avantages suivants.

  • L'interface utilisateur et les threads de travail restent faiblement couplés, contrairement à l'approche Control.Invoke ou Control.BeginInvoke qui les couple étroitement.
  • Le thread d'interface utilisateur n'empêchera pas la progression du thread de travail.
  • Le thread de travail ne peut pas dominer le temps que le thread d'interface utilisateur met à jour.
  • Les intervalles auxquels les opérations d'interface utilisateur et de travail effectuent des opérations peuvent rester indépendants.
  • Le thread de travail ne peut pas dépasser la pompe de messages du thread d'interface utilisateur.
  • Le fil d'interface utilisateur détermine quand et à quelle fréquence l'interface utilisateur est mise à jour.
28
Brian Gideon

Aucun des éléments Invoke dans les réponses précédentes n'est nécessaire.

Vous devez examiner 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
}
27
Jon H

Celui-ci est similaire à la solution ci-dessus utilisant .NET Framework 3.0, mais il a résolu le problème de support de sécurité au moment de la compilation.

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

Utiliser:

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

Le compilateur échouera si l'utilisateur transmet un type de données incorrect.

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

Salvete! Après avoir recherché cette question, j’ai trouvé les réponses à FrankG et à Oregon Ghost être le plus facile le plus utile pour moi. Maintenant, je code dans Visual Basic et ai exécuté cet extrait via un convertisseur; donc je ne sais pas trop comment ça se passe.

J'ai un formulaire de dialogue appelé form_Diagnostics, qui a une zone de texte riche, appelée updateDiagWindow, que j'utilise comme une sorte d'affichage de journalisation. Je devais être en mesure de mettre à jour son texte à partir de tous les sujets. Les lignes supplémentaires permettent à la fenêtre de faire défiler automatiquement les lignes les plus récentes.

Et ainsi, je peux maintenant mettre à jour l’affichage avec une seule ligne, à partir de n’importe où dans l’ensemble du programme, de la manière qui, à votre avis, fonctionnerait sans aucun filetage:

  form_Diagnostics.updateDiagWindow(whatmessage);

Code principal (mettez ceci à l'intérieur du code de classe de votre formulaire):

#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
23
bgmCoder
Label lblText; //initialized elsewhere

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

   lblText.Text = text;           
}

Notez que BeginInvoke() est préférable à Invoke() parce qu'il est moins susceptible de provoquer des blocages (toutefois, ce n'est pas un problème ici lorsque vous affectez du texte à une étiquette):

Lorsque vous utilisez Invoke(), vous attendez le retour de la méthode. Maintenant, il se peut que vous fassiez quelque chose dans le code appelé qui devra attendre le fil, ce qui peut ne pas être immédiatement évident s'il est enterré dans certaines fonctions que vous appelez, ce qui peut se produire indirectement via des gestionnaires d'événements. Donc, vous attendriez le fil, le fil vous attendrait et vous êtes dans une impasse.

Cela a en fait causé le blocage de certains de nos logiciels publiés. Il était assez facile de réparer en remplaçant Invoke() par BeginInvoke(). Sauf si vous avez besoin d'un fonctionnement synchrone, ce qui peut être le cas si vous avez besoin d'une valeur de retour, utilisez BeginInvoke().

21
ILoveFortran

Ceci dans ma variante C # 3.0 de la solution de 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);
}

Vous l'appelez comme ça:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
  1. Il ajoute la vérification de nullité au résultat de "as MemberExpression".
  2. Il améliore la sécurité de type statique.

Sinon, l'original est une très bonne solution.

21
Rotaerk

Pour de nombreuses raisons, c'est aussi simple que cela:

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

"serviceGUI ()" est une méthode de niveau d'interface graphique au sein du formulaire (this) qui peut modifier autant de contrôles que vous le souhaitez. Appelez "updateGUI ()" à partir de l'autre thread. Des paramètres peuvent être ajoutés pour transmettre des valeurs, ou (probablement plus rapidement) utiliser des variables de portée de classe avec des verrous si nécessaire en cas de conflit susceptible de provoquer une instabilité entre les threads. Utilisez BeginInvoke au lieu de Invoke si le thread non GUI est critique pour le temps (en gardant à l'esprit l'avertissement de Brian Gideon).

21
Frankg

Lorsque j'ai rencontré le même problème, j'ai demandé l'aide de Google, mais plutôt que de me donner une solution simple, cela m'a encore plus déconcerté en donnant des exemples de MethodInvoker et de bla bla bla. J'ai donc décidé de le résoudre moi-même. Voici ma solution:

Faites un délégué comme ceci:

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
}

Vous pouvez appeler cette fonction dans un nouveau fil comme celui-ci

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

Ne soyez pas confondu avec Thread(() => .....). J'utilise une fonction anonyme ou une expression lambda lorsque je travaille sur un thread. Pour réduire les lignes de code, vous pouvez également utiliser la méthode ThreadStart(..), que je ne suis pas censé expliquer ici.

20
ahmar

Utilisez simplement quelque chose comme ceci:

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

Vous pouvez utiliser le délégué déjà existant Action:

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

Ma version est d'insérer ne ligne de "mantra" récursif:

Pour aucun argument:

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

        // Your code!
    }

Pour une fonction qui a des arguments:

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

c'est ça.


Quelques arguments: En règle générale, il est mauvais pour la lisibilité du code de mettre {} après une instruction if () sur une ligne. Mais dans ce cas, il s’agit d’une "mantra" de routine. La lisibilité du code ne sera pas compromise si cette méthode est cohérente sur le projet. Et cela économise votre code des déchets (une ligne de code au lieu de cinq).

Lorsque vous voyez if(InvokeRequired) {something long}, vous savez juste que "cette fonction peut être appelée en toute sécurité depuis un autre thread".

14
MajesticRa

Essayez d’actualiser l’étiquette en utilisant ceci

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

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

Créez une variable de classe:

SynchronizationContext _context;

Définissez-le dans le constructeur qui crée votre interface utilisateur:

var _context = SynchronizationContext.Current;

Lorsque vous souhaitez mettre à jour l'étiquette:

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

Vous devez utiliser invoke et delegate

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

La plupart des autres réponses sont un peu complexes pour moi sur cette question (je suis nouveau en C #), alors j'écris la mienne:

J'ai une application WPF et j'ai défini un ouvrier comme ci-dessous:

Problème:

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

Solution:

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

Je suis encore à savoir ce que la ligne ci-dessus signifie, mais cela fonctionne.

Pour WinForms:

Solution:

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

La façon la plus simple je pense:

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

Par exemple, accédez à un contrôle autre que dans le thread actuel:

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

Là, la lblThreshold est une étiquette et Speed_Threshold est une variable globale.

8
Da Xiong

Je viens de lire les réponses et cela semble être un sujet d'actualité. J'utilise actuellement .NET 3.5 SP1 et Windows Forms.

La formule bien connue décrite dans les réponses précédentes et qui utilise la propriété InvokeRequired couvre la plupart des cas, mais pas la totalité du pool.

Et si le Handle n'a pas encore été créé?

La propriété InvokeRequired , telle que décrite ici (référence de la propriété Control.InvokeRequired à MSDN) renvoie true si l'appel a été effectué à partir d'un thread qui n'est pas le thread d'interface graphique, false si l'appel a été effectué à partir du thread d'interface graphique ou si le descripteur n'a pas encore été créé.

Vous pouvez rencontrer une exception si vous souhaitez qu'un formulaire modal soit affiché et mis à jour par un autre thread. Parce que vous voulez afficher ce formulaire sous forme modale, vous pouvez procéder comme suit:

private MyForm _gui;

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

Et le délégué peut mettre à jour une étiquette sur l'interface graphique:

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

Cela peut provoquer une InvalidOperationException si les opérations avant la mise à jour de l'étiquette "prennent moins de temps" (lisez-le et interprétez-le comme une simplification) que le temps qu'il prend pour que le fil d’interface graphique crée le handle du formulaire . Cela se produit dans la méthode ShowDialog () .

Vous devriez également vérifier le Handle comme ceci:

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

Vous pouvez gérer l'opération à effectuer si le Handle n'a pas encore été créé: vous pouvez simplement ignorer la mise à jour de l'interface graphique (comme indiqué dans le code ci-dessus) ou vous pouvez attendre (plus risqué). Cela devrait répondre à la question.

Contenu optionnel: Personnellement, j'ai codé les éléments suivants:

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

Je nourris mes formulaires qui sont mis à jour par un autre thread avec une instance de ce ThreadSafeGuiCommand , et je définis des méthodes qui mettent à jour l'interface graphique (dans mon formulaire) comme ceci :

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

De cette façon, je suis à peu près sûr que mon interface graphique sera mise à jour quel que soit le thread qui passera l'appel, en attendant éventuellement une durée bien définie (le délai d'expiration).

8
Sume

Lorsque vous êtes dans le thread d'interface utilisateur, vous pouvez lui demander son planificateur de tâches de contexte de synchronisation. Cela vous donnerait un TaskScheduler qui planifie tout sur le thread d'interface utilisateur.

Vous pouvez ensuite chaîner vos tâches de manière à ce que, lorsque le résultat est prêt, une autre tâche (planifiée sur le thread d'interface utilisateur) la sélectionne et l'assigne à une étiquette.

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

Cela fonctionne pour les tâches (pas les threads) qui sont le moyen préféré d'écrire du code concurrent maintenant .

8
nosalan

Même si l'opération prend du temps (thread.sleep dans mon exemple) - Ce code ne verrouille PAS votre interface utilisateur:

 private void button1_Click(object sender, EventArgs e)
 {

      Thread t = new Thread(new ThreadStart(ThreadJob));
      t.IsBackground = true;
      t.Start();         
 }

 private void ThreadJob()
 {
     string newValue= "Hi";
     Thread.Sleep(2000); 

     this.Invoke((MethodInvoker)delegate
     {
         label1.Text = newValue; 
     });
 }
7
Yuliia Ashomok

Le moyen le plus simple dans les applications WPF est:

this.Dispatcher.Invoke((Action)(() =>
{
    // This refers to a form in a WPF application 
    val1 = textBox.Text; // Access the UI 
}));
7
Basheer AL-MOMANI

Je ne pouvais pas comprendre la logique de Microsoft derrière cette implémentation moche, mais vous devez avoir deux fonctions:

void setEnableLoginButton()
{
  if (InvokeRequired)
  {
    // btn_login can be any conroller, (label, button textbox ..etc.)

    btn_login.Invoke(new MethodInvoker(setEnable));

    // OR
    //Invoke(new MethodInvoker(setEnable));
  }
  else {
    setEnable();
  }
}

void setEnable()
{
  btn_login.Enabled = isLoginBtnEnabled;
}

Ces extraits fonctionnent pour moi, je peux donc faire quelque chose sur un autre thread, puis je mets à jour l'interface graphique:

Task.Factory.StartNew(()=>
{
    // THIS IS NOT GUI
    Thread.Sleep(5000);
    // HERE IS INVOKING GUI
    btn_login.Invoke(new Action(() => DoSomethingOnGUI()));
});

private void DoSomethingOnGUI()
{
   // GUI
   MessageBox.Show("message", "title", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}

Encore plus simple:

btn_login.Invoke(new Action(()=>{ /* HERE YOU ARE ON GUI */ }));
6
MBH

Je voulais ajouter un avertissement car j'ai remarqué que certaines solutions simples omettent la vérification InvokeRequired.

J'ai remarqué que si votre code s'exécute avant que le handle de fenêtre du contrôle ait été créé (par exemple avant que le formulaire ne soit affiché), Invoke lève une exception. Je recommande donc de toujours vérifier sur InvokeRequired avant d'appeler Invoke ou BeginInvoke.

6
Jos Bosmans

Voici un nouveau regard sur un problème séculaire utilisant un style plus fonctionnel. Si vous conservez la classe TaskXM dans tous vos projets, vous ne disposez que d'une ligne de code pour ne plus jamais vous soucier des mises à jour multithreads.

public class Example
{
    /// <summary>
    /// No more delegates, background workers, etc. Just one line of code as shown below.
    /// Note it is dependent on the Task Extension method shown next.
    /// </summary>
    public async void Method1()
    {
        // Still on the GUI thread here if the method was called from the GUI thread
        // This code below calls the extension method which spins up a new task and calls back.
        await TaskXM.RunCodeAsync(() =>
        {
            // Running an asynchronous task here
            // Cannot update the GUI thread here, but can do lots of work
        });
        // Can update GUI on this line
    }
}


/// <summary>
/// A class containing extension methods for the Task class
/// </summary>
public static class TaskXM
{
    /// <summary>
    /// RunCodeAsyc is an extension method that encapsulates the Task.run using a callback
    /// </summary>
    /// <param name="Code">The caller is called back on the new Task (on a different thread)</param>
    /// <returns></returns>
    public async static Task RunCodeAsync(Action Code)
    {
        await Task.Run(() =>
        {
            Code();
        });
        return;
    }
}
5
John Peters

Peut-être un peu une overdose, mais c'est le genre de solution que je résous normalement:

Les appels ne sont pas requis ici à cause de la synchronisation. BasicClassThreadExample est pour moi une sorte de mise en page. Modifiez-le pour l'adapter à vos besoins réels.

C'est simple parce que vous n'avez pas besoin de gérer les éléments dans le fil de l'interface utilisateur!

public partial class Form1 : Form
{
    BasicClassThreadExample _example;

    public Form1()
    {
        InitializeComponent();
        _example = new BasicClassThreadExample();
        _example.MessageReceivedEvent += _example_MessageReceivedEvent;
    }

    void _example_MessageReceivedEvent(string command)
    {
        listBox1.Items.Add(command);
    }

    private void button1_Click(object sender, EventArgs e)
    {
        listBox1.Items.Clear();
        _example.Start();
    }
}

public class BasicClassThreadExample : IDisposable
{
    public delegate void MessageReceivedHandler(string msg);

    public event MessageReceivedHandler MessageReceivedEvent;

    protected virtual void OnMessageReceivedEvent(string msg)
    {
        MessageReceivedHandler handler = MessageReceivedEvent;
        if (handler != null)
        {
            handler(msg);
        }
    }

    private System.Threading.SynchronizationContext _SynchronizationContext;
    private System.Threading.Thread _doWorkThread;
    private bool disposed = false;

    public BasicClassThreadExample()
    {
        _SynchronizationContext = System.ComponentModel.AsyncOperationManager.SynchronizationContext;
    }

    public void Start()
    {
        _doWorkThread = _doWorkThread ?? new System.Threading.Thread(dowork);

        if (!(_doWorkThread.IsAlive))
        {
            _doWorkThread = new System.Threading.Thread(dowork);
            _doWorkThread.IsBackground = true;
            _doWorkThread.Start();
        }
    }

    public void dowork()
    {
        string[] retval = System.IO.Directory.GetFiles(@"C:\Windows\System32", "*.*", System.IO.SearchOption.TopDirectoryOnly);
        foreach (var item in retval)
        {
            System.Threading.Thread.Sleep(25);
            _SynchronizationContext.Post(new System.Threading.SendOrPostCallback(delegate(object obj)
            {
                OnMessageReceivedEvent(item);
            }), null);
        }
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                _doWorkThread.Abort();
            }
            disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~BasicClassThreadExample() { Dispose(false); }

}
5
Carsten R.

Autre exemple sur le sujet: j'ai créé une classe abstraite, UiSynchronizeModel, qui contient une implémentation de méthode commune:

public abstract class UiSynchronizeModel
{
    private readonly TaskScheduler uiSyncContext;
    private readonly SynchronizationContext winformsOrDefaultContext;

    protected UiSynchronizeModel()
    {
        this.winformsOrDefaultContext = SynchronizationContext.Current ?? new SynchronizationContext();
        this.uiSyncContext = TaskScheduler.FromCurrentSynchronizationContext();
    }

    protected void RunOnGuiThread(Action action)
    {
        this.winformsOrDefaultContext.Post(o => action(), null);
    }

    protected void CompleteTask(Task task, TaskContinuationOptions options, Action<Task> action)
    {
        task.ContinueWith(delegate
        {
            action(task);
            task.Dispose();
        }, CancellationToken.None, options, this.uiSyncContext);
    }
}

Votre modèle ou classe de contrôleur doit être dérivé de cette classe abstraite. Vous pouvez utiliser n’importe quel modèle (tâches ou threads d’arrière-plan gérés manuellement) et utiliser les méthodes suivantes:

public void MethodThatCalledFromBackroundThread()
{
   this.RunOnGuiThread(() => {
       // Do something over UI controls
   });
}

Exemple de tâches:

var task = Task.Factory.StartNew(delegate
{
    // Background code
    this.RunOnGuiThread(() => {
        // Do something over UI controls
    });
});

this.CompleteTask(task, TaskContinuationOptions.OnlyOnRanToCompletion, delegate
{
    // Code that can safely use UI controls
});
5
Alexander Egorov

Le moyen de résoudre ce problème, quelle que soit la version de l'infrastructure ou le type de bibliothèque sous-jacente de l'interface graphique, consiste à enregistrer le contrôle en créant le contexte de synchronisation du thread pour le thread de travail, afin de marshaler l'interaction du contrôle entre le contrôle et la file de messages du thread.

Exemple:

SynchronizationContext ctx = SynchronizationContext.Current; // From control
ctx.Send\Post... // From worker thread
4
Roman Ambinder

Et encore un autre générique Contrôle extension aproach ..

Ajoutez d’abord une méthode d’extension pour les objets de type Control

public static void InvokeIfRequired<T>(this T c, Action<T> action) where T : Control
{
    if (c.InvokeRequired)
    {
        c.Invoke(new Action(() => action(c)));
    }
    else
    {
        action(c);
    }
}

et appelez comme ceci depuis un autre thread pour accéder à un contrôle nommé object1 dans UI-thread:

object1.InvokeIfRequired(c => { c.Visible = true; });
object1.InvokeIfRequired(c => { c.Text = "ABC"; });

..ou comme ça

object1.InvokeIfRequired(c => 
  { 
      c.Text = "ABC";
      c.Visible = true; 
  }
);
3
flodis

Commencez par obtenir l'instance de votre formulaire (dans ce cas mainForm), puis utilisez simplement ce code dans un autre thread.

mainForm.Invoke(new MethodInvoker(delegate () 
{
    // Update things in my mainForm here
    mainForm.UpdateView();
}));
3
Musculaa

Je préfère celui-ci:

private void UpdateNowProcessing(string nowProcessing)
{
    if (this.InvokeRequired)
    {
        Action<string> d = UpdateNowProcessing;
        Invoke(d, nowProcessing);
    }
    else
    {
        this.progressDialog.Next(nowProcessing);
    }            
}
3
user523650

Placez une variable commune dans une classe séparée pour contenir la valeur.

Exemple:

public  class data_holder_for_controls
{
    // It will hold the value for your label
    public string status = string.Empty;
}

class Demo
{
    public static  data_holder_for_controls d1 = new data_holder_for_controls();

    static void Main(string[] args)
    {
        ThreadStart ts = new ThreadStart(perform_logic);
        Thread t1 = new Thread(ts);
        t1.Start();
        t1.Join();
        //your_label.Text=d1.status; --- can access it from any thread
    }

    public static void perform_logic()
    {
        // Put some code here in this function
        for (int i = 0; i < 10; i++)
        {
            // Statements here
        }
        // Set the result in the status variable
        d1.status = "Task done";
    }
}
3
Saurabh

il suffit d'utiliser le contexte de synchronisation de l'interface utilisateur

using System.Threading;

// ...

public partial class MyForm : Form
{
    private readonly SynchronizationContext uiContext;

    public MyForm()
    {
        InitializeComponent();
        uiContext = SynchronizationContext.Current; // get ui thread context
    }

    private void button1_Click(object sender, EventArgs e)
    {
        Thread t = new Thread(() =>
            {// set ui thread context to new thread context                            
             // for operations with ui elements to be performed in proper thread
             SynchronizationContext
                 .SetSynchronizationContext(uiContext);
             label1.Text = "some text";
            });
        t.Start();
    }
}
2
user53373

Dans mon cas (WPF) la solution est simple comme ceci:

private void updateUI()
{
    if (!Dispatcher.CheckAccess())
    {
        Dispatcher.BeginInvoke(updateUI);
        return;
    }

    // Update any number of controls here
}
2
ipe

L'approche générale est comme:

using System;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        int clickCount = 0;

        public Form1()
        {
            InitializeComponent();
            label1.SetText("0");
        }

        private void button1_Click(object sender, EventArgs e)
        {
            new Thread(() => label1.SetText((++clickCount).ToString())).Start();
        }
    }

    public static class ControlExtensions
    {
        public static void SetText(this Control control, string text)
        {
            if (control.InvokeRequired)
                control.Invoke(setText, control, text);
            else
                control.Text = text;
        }

        private static readonly Action<Control, string> setText =
            (control, text) => control.Text = text;
    }
}

Explication :

La réponse est jolie comme celui-ci . Mais utilise une syntaxe plus soignée (comme pour moi) et plus récente. Le point est InvokeRequired propriété de control. Il obtient une valeur indiquant si l'appelant doit appeler une méthode invoke lors d'appels de méthode au contrôle, car l'appelant se trouve sur un autre thread que celui sur lequel le contrôle a été créé. Donc, si nous appelons control.SetText("some text") sur le même thread control a été créé, il est correct de définir Text comme ce control.Text = text. Mais sur n'importe quel autre thread, il cause System.InvalidOperationException, il faut donc appeler une méthode via control.Invoke(...) pour définir Text sur le thread control a été créé.

1
Alex

Le moyen le plus simple consiste à appeler comme suit:

 Application.Current.Dispatcher.Invoke(new Action(() =>
             {
                    try
                    {
                        ///
                    }
                    catch (Exception)
                    {
                      //
                    }


                    }
     ));
0
Lankan