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

1393

Quelle est la façon la plus simple de mettre à jour un à Labelpartir d'un autre Thread?

  • Je suis en Formcours d'exécution thread1, et à partir de là, je commence un autre thread ( thread2).

  • Tout en thread2est le traitement de certains fichiers , je voudrais mettre à jour un Labelsur la Formavec l'état actuel des thread2travaux de ».

Comment pourrais-je faire ça?

CruelIO
la source
25
.Net 2.0+ n'a pas la classe BackgroundWorker juste pour cela. Ce thread d'interface utilisateur est au courant. 1. Créez un BackgroundWorker 2. Ajoutez deux délégués (un pour le traitement et un pour l'achèvement)
Preet Sangha
13
peut-être un peu tard: codeproject.com/KB/cs/Threadsafe_formupdating.aspx
MichaelD
4
Voir la réponse pour .NET 4.5 et C # 5.0: stackoverflow.com/a/18033198/2042090
Ryszard Dżegan
5
Cette question ne s'applique pas à l'interface graphique Gtk #. Pour Gtk # voir ceci et cette réponse.
hlovdal
Attention: les réponses à cette question sont maintenant un désordre encombré d'OT ("voici ce que j'ai fait pour mon application WPF") et d'artefacts historiques .NET 2.0.
Marc L.

Réponses:

768

Pour .NET 2.0, voici un joli morceau de code que j'ai écrit qui fait exactement ce que vous voulez, et fonctionne pour n'importe quelle propriété sur 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 ceci:

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

Si vous utilisez .NET 3.0 ou supérieur, vous pouvez réécrire la méthode ci-dessus en tant que méthode d'extension de la Controlclasse, ce qui simplifierait alors l'appel à:

myLabel.SetPropertyThreadSafe("Text", status);

MISE À JOUR 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 ||
      !@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 });
  }
}

qui utilise les expressions LINQ et lambda pour permettre une syntaxe beaucoup 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é au moment de la compilation, mais le type de la propriété l'est également, il est donc impossible (par exemple) d'attribuer 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 Controlla propriété et la valeur d'un autre, donc les éléments suivants seront joyeusement compilés:

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

Par conséquent, j'ai ajouté les vérifications d'exécution pour garantir que la propriété transmise appartient réellement à la propriété 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é à la compilation, veuillez commenter!

Ian Kemp
la source
3
Il y a des cas où this.GetType () a la même valeur que propertyInfo.ReflectedType (par exemple LinkLabel sur WinForms). Je n'ai pas une grande expérience C #, mais je pense que la condition d'exception devrait être: if (propertyInfo == null || ([email protected] (). IsSubclassOf (propertyInfo.ReflectedType) && @ this.GetType ( )! = propertyInfo.ReflectedType) || @ this.GetType (). GetProperty (propertyInfo.Name, propertyInfo.PropertyType) == null)
Corvin
9
@lan peut SetControlPropertyThreadSafe(myLabel, "Text", status)être appelé depuis un autre module ou classe ou formulaire
Smith
71
La solution proposée est inutilement complexe. Voir la solution de Marc Gravell, ou la solution de Zaid Masud, si vous appréciez la simplicité.
Frank Hileman
8
Cette solution gaspille des tonnes de ressources si vous mettez à jour plusieurs propriétés car chaque invocation coûte beaucoup de ressources. Je ne pense pas que c'est de cette façon que la fonctionnalité de Thread Safety a été conçue. Faites encapsuler vos actions de mise à jour de l'interface utilisateur et invoquez-le UNE FOIS (et non par propriété)
Console
4
Pourquoi diable utiliseriez-vous ce code sur le composant BackgroundWorker?
Andy
1080

La manière la plus simple est une méthode anonyme passée dans 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 Invokebloque l'exécution jusqu'à ce qu'elle se termine - c'est du code synchrone. La question ne concerne pas le code asynchrone, mais il y a beaucoup de contenu sur Stack Overflow sur l'écriture de code asynchrone lorsque vous voulez en savoir plus.

Marc Gravell
la source
8
Étant donné que l'OP n'a mentionné aucune classe / instance sauf la forme, ce n'est pas un mauvais défaut ...
Marc Gravell
39
N'oubliez pas que le mot clé "this" fait référence à une classe "Control".
AZ.
8
@codecompleting c'est sûr de toute façon, et nous savons déjà que nous sommes sur un travailleur, alors pourquoi vérifier quelque chose que nous savons?
Marc Gravell
4
@Dragouf pas vraiment - l'un des avantages de cette méthode est que vous savez déjà quelles parties s'exécutent sur l'ouvrier et lesquelles s'exécutent sur le thread d'interface utilisateur. Pas besoin de vérifier.
Marc Gravell
3
@ Joan.bdm il n'y a pas assez de contexte pour que je puisse commenter cela
Marc Gravell
401

Gérer un long travail

Depuis .NET 4.5 et C # 5.0, vous devez utiliser le modèle asynchrone basé sur les tâches (TAP) avec async - attendez les 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 du modèle de programmation asynchrone (APM) et du modèle asynchrone basé sur les événements (EAP) (ce dernier inclut la classe BackgroundWorker ).

Ensuite, la solution recommandée pour un nouveau développement est:

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

Remarquez ce qui suit:

  1. Code court et propre écrit de manière séquentielle sans rappels et threads explicites.
  2. Tâche au lieu de Thread .
  3. mot-clé async , qui permet d'utiliser l' attente qui à son tour empêche le gestionnaire d'événements d'atteindre l'état d'achèvement jusqu'à la fin de la tâche et en attendant ne bloque pas le thread d'interface utilisateur.
  4. Classe de progression (voir Interface IProgress ) qui prend en charge le principe de conception de séparation des préoccupations (SoC) et ne nécessite pas de répartiteur explicite ni d'appel. Il utilise le SynchronizationContext actuel depuis son lieu de création (ici le thread d'interface utilisateur).
  5. TaskCreationOptions.LongRunning qui indique de ne pas mettre la tâche en file d'attente dans ThreadPool .

Pour des exemples plus verbeux, voir: L'avenir de C #: de bonnes choses viennent à ceux qui «attendent» par Joseph Albahari .

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

Gestion des exceptions

L'extrait ci-dessous est un exemple de la façon de gérer les exceptions et de basculer la Enabledpropriété du bouton pour empêcher plusieurs clics lors de 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...");
    }
}
Ryszard Dżegan
la source
2
Si SecondThreadConcern.LongWork()lève une exception, peut-elle être interceptée par le thread d'interface utilisateur? C'est un excellent article, au fait.
kdbanman
2
J'ai ajouté une section supplémentaire à la réponse pour répondre à vos besoins. Cordialement.
Ryszard Dżegan
3
La classe ExceptionDispatchInfo est responsable de ce miracle de relance de l'exception d'arrière-plan sur le thread d'interface utilisateur dans un modèle d'attente asynchrone.
Ryszard Dżegan
1
Est-ce juste moi en pensant que cette façon de faire est beaucoup plus verbeuse que de simplement invoquer Invoke / Begin?!
MeTitus
2
Task.Delay(500).Wait()? Quel est l'intérêt de créer une tâche pour simplement bloquer le thread actuel? Vous ne devez jamais bloquer un thread de pool de threads!
Yarik
236

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

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

Ou utilisez plutôt délégué d'action:

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

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

Zaid Masud
la source
1
qu'est-ce que le «contrôle» dans cet exemple? Mon contrôle de l'interface utilisateur? Essayer d'implémenter cela dans WPF sur un contrôle d'étiquette, et Invoke n'est pas membre de mon étiquette.
Dbloom
Qu'en est-il de la méthode d'extension comme @styxriver stackoverflow.com/a/3588137/206730 ?
Kiquenet
déclarer 'Action y;' à l'intérieur de la classe ou de la méthode en changeant la propriété text et mettez à jour le texte avec ce morceau de code 'yourcontrol.Invoke (y = () => yourcontrol.Text = "new text");'
Antonio Leite
4
@Dbloom ce n'est pas un membre car c'est seulement pour WinForms. Pour WPF, vous utilisez Dispatcher.Invoke
sLw
1
Je suivais cette solution mais parfois mon interface utilisateur n'était pas mise à jour. J'ai trouvé que je dois this.refresh()forcer l'invalidation et repeindre l'interface graphique .. si cela est utile ..
Rakibul Haq
137

Déclenchez et oubliez la méthode d'extension 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();
        }
    }
}

Cela peut être appelé à l'aide de la ligne de code suivante:

this.UIThread(() => this.myLabel.Text = "Text Goes Here");
StyxRiver
la source
5
Quel est l'intérêt de @this usage? Le «contrôle» ne serait-il pas équivalent? Y a-t-il des avantages à @this?
argyle
14
@jeromeyers - Le @thisest simplement le nom de la variable, dans ce cas la référence au contrôle actuel appelant l'extension. Vous pouvez le renommer en source, ou tout ce qui fait flotter votre bateau. J'utilise @this, car il fait référence à «ce contrôle» qui appelle l'extension et est cohérent (dans ma tête, au moins) avec l'utilisation du mot-clé «this» dans le code normal (sans extension).
StyxRiver
1
C'est génial, facile et pour moi la meilleure solution. Vous pouvez inclure tout le travail que vous avez à faire dans le fil d'interface utilisateur. Exemple: this.UIThread (() => {txtMessage.Text = message; listBox1.Items.Add (message);});
Auto
1
J'aime vraiment cette solution. Nit mineur: je nommerais cette méthode OnUIThreadplutôt que UIThread.
ToolmakerSteve
2
C'est pourquoi j'ai nommé cette extension RunOnUiThread. Mais c'est juste un goût personnel.
Grisgram
66

Voici la manière classique de procéder:

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 croiser les fils pour changer le contrôle réel ... comme une étiquette ou une barre de progression.

Hath
la source
62

La solution simple est d'utiliser Control.Invoke.

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

void updateGUI() {
    // update gui here
}
OregonGhost
la source
bravo pour la simplicité! non seulement simple, mais fonctionne aussi bien! Je ne comprenais vraiment pas pourquoi Microsoft ne pouvait pas le simplifier comme il se doit! pour appeler 1 ligne sur le thread principal, nous devons écrire quelques fonctions!
MBH
1
@MBH D'accord. BTW, avez-vous remarqué stackoverflow.com/a/3588137/199364 réponse ci-dessus, qui définit une méthode d'extension? Faites-le une fois dans une classe d'utilitaires personnalisés, alors ne vous souciez plus que Microsoft ne l'a pas fait pour nous :)
ToolmakerSteve
@ToolmakerSteve C'est exactement ce que cela signifiait! vous avez raison, nous pouvons trouver un moyen, mais je veux dire du point de vue SEC (ne vous répétez pas), le problème qui a une solution commune, peut être résolu par eux avec un minimum d'effort par Microsoft, ce qui économisera beaucoup de temps pour programmeurs :)
MBH
47

Le code de threading est souvent bogué et toujours difficile à tester. Vous n'avez pas besoin 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. Habituellement, vous signalez simplement un pourcentage achevé, mais il y a une autre surcharge qui inclut un objet d'état. Voici un exemple qui rapporte simplement un objet chaîne:

    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 compliquées à effectuer, vous pouvez définir une classe pour représenter l'état de l'interface utilisateur et la transmettre à la méthode ReportProgress.

Une dernière chose, assurez-vous de définir l' WorkerReportsProgressindicateur, sinon la ReportProgressméthode sera complètement ignorée.

Don Kirkby
la source
2
En fin de traitement, il est également possible de mettre à jour l'interface utilisateur via backgroundWorker1_RunWorkerCompleted.
DavidRR
41

La grande majorité des réponses utilisent Control.Invokece qui est une condition de concurrence qui attend de se produire . Par exemple, considérez 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 this.Invokeson appel (rappelez-vous, thisest l' Formobjet), unObjectDisposedException sera probablement renvoyé.

La solution est d'utiliser SynchronizationContext, en particulier SynchronizationContext.Currentcomme le suggère hamilton.danielb (les autres réponses reposent sur des SynchronizationContextimplémentations spécifiques, ce qui est complètement inutile). Je modifierais légèrement son code pour l'utiliser SynchronizationContext.Postplutôt que de le faire SynchronizationContext.Send(car il n'est généralement pas nécessaire d'attendre le 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 devez vraiment utiliser des tâches pour les opérations asynchrones. Voir la réponse de n-san pour l'approche équivalente basée sur les tâches (en utilisantTaskScheduler.FromCurrentSynchronizationContext ).

Enfin, sur .NET 4.5 et plus, vous pouvez également utiliser Progress<T>(qui capture essentiellement SynchronizationContext.Currentlors de sa création) comme démontré par Ryszard Dżegan pour les cas où l'opération de longue durée doit exécuter du code d'interface utilisateur tout en continuant à fonctionner.

Ohad Schneider
la source
37

Vous devrez vous assurer que la mise à jour a lieu sur le bon thread; le thread d'interface utilisateur.

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

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

(Le code est tapé ici de ma tête, donc je n'ai pas vérifié la syntaxe correcte, etc., mais cela 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' ISynchronizeInvokeinterface.

Afin de vous assurer que le code ci - dessus fonctionne avec Windows Forms et WPF, et toutes les autres plates - formes, vous pouvez consulter les AsyncOperation, AsyncOperationManageret les SynchronizationContextclasses.

Afin de déclencher facilement des événements de cette façon, j'ai créé une méthode d'extension, qui me permet de simplifier le déclenchement d'un événement en appelant simplement:

MyEvent.Raise(this, EventArgs.Empty);

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

Frederik Gheysels
la source
En effet, mais je n'aime pas «encombrer» mon code GUI avec cette question. Mon interface graphique ne devrait pas se soucier si elle doit être invoquée ou non. En d'autres termes: je ne pense pas que c'est la responsabilité de l'interface graphique d'effectuer le contexte-swithc.
Frederik Gheysels
1
Briser le délégué, etc. semble exagéré - pourquoi pas simplement: SynchronizationContext.Current.Send (delegate {MyEvent (...);}, null);
Marc Gravell
Avez-vous toujours accès au SynchronizationContext? Même si votre classe est dans une bibliothèque de classe?
Frederik Gheysels
29

Vous devrez appeler la méthode sur le thread GUI. 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;
}
Kieron
la source
1
La ligne d'invocation me donne une erreur de compilation. La meilleure correspondance de méthode surchargée pour 'System.Windows.Forms.Control.Invoke (System.Delegate, object [])' contient des arguments non valides
CruelIO
28

En raison de la banalité du scénario, j'aurais en fait le sondage du thread d'interface utilisateur pour le statut. Je pense que vous constaterez qu'il 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...";
    }
  }
}

L'approche évite l'opération de marshaling requise lors de l'utilisation des méthodes ISynchronizeInvoke.Invokeet ISynchronizeInvoke.BeginInvoke. Il n'y a rien de mal à utiliser la technique de marshaling, mais il y a quelques mises en garde dont vous devez être conscient.

  • Assurez-vous de ne pas appeler BeginInvoketrop fréquemment, car cela pourrait dépasser la pompe de messages.
  • L'appel Invokesur le thread de travail est un appel bloquant. Cela interrompra temporairement le travail en cours dans ce thread.

La stratégie que je propose dans cette réponse inverse les rôles de communication des threads. Au lieu du thread de travail poussant les données, le thread d'interface utilisateur l'interroge. C'est un modèle commun utilisé dans de nombreux scénarios. Puisque tout ce que vous voulez faire est d'afficher les informations de progression du thread de travail, je pense que 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 par opposition à Control.InvokeouControl.BeginInvoke approche qui les couple étroitement.
  • Le thread d'interface utilisateur n'entravera pas la progression du thread de travail.
  • Le thread de travail ne peut pas dominer le temps que le thread d'interface utilisateur passe à mettre à jour.
  • Les intervalles auxquels l'interface utilisateur et les threads 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 thread d'interface utilisateur doit dicter quand et à quelle fréquence l'interface utilisateur est mise à jour.
Brian Gideon
la source
3
Bonne idée. La seule chose que vous n'avez pas mentionnée est de savoir comment disposer correctement la minuterie une fois le WorkerThread terminé. Notez que cela peut provoquer des problèmes à la fin de l'application (c'est-à-dire que l'utilisateur ferme l'application). Avez-vous une idée de comment résoudre ce problème?
Matt
@Matt Au lieu d'utiliser un gestionnaire anonyme pour les Elapsedévénements, vous utilisez une méthode membre afin de pouvoir supprimer le minuteur lorsque le formulaire est supprimé ...
Phil1970
@ Phil1970 - Bon point. Vous vouliez dire aimer System.Timers.ElapsedEventHandler handler = (s, a) => { MyProgressLabel.Text = m_Text; };et l'assigner via m_Timer.Elapsed += handler;, plus tard dans le contexte de disposition, fais m_Timer.Elapsed -= handler;-je un bon choix? Et pour l'élimination / fermeture suivant les conseils comme discuté ici .
Matt
27

Aucune des choses Invoke dans les réponses précédentes n'est nécessaire.

Vous devez regarder 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
}
Jon H
la source
4
que pensez-vous que la méthode Post utilise sous le capot? :)
increddibelly
23

Celle-ci est similaire à la solution ci-dessus utilisant .NET Framework 3.0, mais elle a résolu le problème de la prise en charge de la 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 le mauvais type de données.

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

Salvete! Après avoir cherché cette question, j'ai trouvé les réponses de FrankG et Oregon Ghost étaient les plus simples et les plus utiles pour moi. Maintenant, je code en Visual Basic et j'ai exécuté cet extrait de code 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 boîte de texte riche, appelé updateDiagWindow,que j'utilise comme une sorte d'affichage de journalisation. J'avais besoin de pouvoir mettre à jour son texte à partir de toutes les discussions. Les lignes supplémentaires permettent à la fenêtre de défiler automatiquement jusqu'aux lignes les plus récentes.

Et donc, je peux maintenant mettre à jour l'affichage avec une seule ligne, de n'importe où dans le programme entier de la manière dont vous pensez que cela fonctionnerait sans filetage:

  form_Diagnostics.updateDiagWindow(whatmessage);

Code principal (insérez-le dans le 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
bgmCoder
la source
21

À de nombreuses fins, 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 GUI dans le formulaire (this) qui peut changer 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 sur eux s'il y a une possibilité de conflit entre les threads y accédant qui pourrait provoquer une instabilité. Utilisez BeginInvoke au lieu d'Invoke si le thread non GUI est critique en temps (en gardant à l'esprit l'avertissement de Brian Gideon).

Frankg
la source
21

Ceci dans ma variation C # 3.0 de la solution d'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 ceci:

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

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

Rotaerk
la source
21
Label lblText; //initialized elsewhere

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

   lblText.Text = text;           
}

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

Lors de l'utilisation Invoke() vous vous attendez le retour de la méthode. Maintenant, il se peut que vous fassiez quelque chose dans le code invoqué qui devra attendre le thread, 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. Vous attendriez donc le fil, le fil vous attendrait et vous êtes dans une impasse.

Cela a en fait provoqué le blocage de certains de nos logiciels publiés. Il était assez facile à corriger 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().

ILoveFortran
la source
20

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 davantage troublé en donnant des exemples de MethodInvokeret bla bla bla. J'ai donc décidé de le résoudre par 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 ThreadStart(..)méthode que je ne suis pas censé expliquer ici.

ahmar
la source
17

Utilisez simplement quelque chose comme ceci:

 this.Invoke((MethodInvoker)delegate
            {
                progressBar1.Value = e.ProgressPercentage; // runs on UI thread
            });
Hassan Shouman
la source
Si c'est le cas e.ProgressPercentage, n'êtes-vous pas déjà dans le thread d'interface utilisateur de la méthode que vous appelez cela?
LarsTech
L'événement ProgressChanged s'exécute sur le thread d'interface utilisateur. C'est l'une des commodités de l'utilisation de BackgroundWorker. L'événement Terminé s'exécute également sur l'interface graphique. La seule chose qui s'exécute dans le thread non UI est la méthode DoWork.
LarsTech
15

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

private void UpdateMethod()
{
    if (InvokeRequired)
    {
        Invoke(new Action(UpdateMethod));
    }
}
Embedd_Khurja
la source
14

Ma version consiste à insérer une 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 : Habituellement, il est mauvais pour la lisibilité du code de mettre {} après une if ()instruction sur une seule ligne. Mais dans ce cas, c'est tout de même un "mantra" de routine. Il ne rompt pas la lisibilité du code si cette méthode est cohérente sur le projet. Et il sauve votre code des ordures (une ligne de code au lieu de cinq).

Comme vous le voyez, if(InvokeRequired) {something long}vous savez simplement que "cette fonction est sûre à appeler depuis un autre thread".

MajesticRa
la source
13

Essayez de rafraîchir 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);
    }
}
Ivaylo Slavov
la source
Est-ce pour Windows Forms ?
Kiquenet
13

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);
esprit noir
la source
12

Vous devez utiliser invoke et delegate

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

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

J'ai une application WPF et j'ai défini un travailleur 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 ne sais pas encore ce que signifie la ligne ci-dessus, mais cela fonctionne.

Pour WinForms :

Solution:

txtLog.Invoke((MethodInvoker)delegate
{
    txtLog.Text = "my message";
});
Manohar Reddy Poreddy
la source
La question concernait Winforms, pas WPF.
Marc L.
Merci. Ajout de la solution WinForms ci-dessus.
Manohar Reddy Poreddy
... qui n'est qu'une copie des nombreuses autres réponses à cette même question, mais d'accord. Pourquoi ne pas faire partie de la solution et simplement supprimer votre réponse?
Marc L.
hmm, correct vous êtes, si seulement, vous lisez ma réponse avec attention, la partie début (la raison pour laquelle j'ai écrit la réponse), et j'espère qu'avec un peu plus d'attention, vous voyez qu'il y a quelqu'un qui avait exactement le même problème et a voté aujourd'hui pour ma réponse simple, et avec encore plus d'attention si vous pouviez prévoir la véritable histoire sur la raison de tout cela, que Google m'envoie ici même lorsque je recherche wpf. Bien sûr, puisque vous avez raté ces 3 raisons plus ou moins évidentes, je peux comprendre pourquoi vous ne supprimez pas votre downvote. Au lieu de nettoyer le bon, créez quelque chose de nouveau qui est beaucoup plus difficile.
Manohar Reddy Poreddy
8

La façon la plus simple, je pense:

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

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

Il lblThresholdy a une étiquette et Speed_Thresholdune variable globale.

Da Xiong
la source
8

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.

Ensuite, vous pouvez enchaîner vos tâches de sorte que lorsque le résultat est prêt, une autre tâche (planifiée sur le thread d'interface utilisateur) le sélectionne et l'affecte à 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 pour les threads) qui sont le moyen préféré d'écrire du code simultané maintenant .

nosalan
la source
1
Appeler Task.Startn'est généralement pas une bonne pratique blogs.msdn.com/b/pfxteam/archive/2012/01/14/10256832.aspx
Ohad Schneider
8

Je viens de lire les réponses et cela semble être un sujet très brûlant. J'utilise actuellement .NET 3.5 SP1 et Windows Forms.

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

Que faire si la poignée n'a pas encore été créée?

La propriété InvokeRequired , comme décrit ici (référence de propriété Control.InvokeRequired à MSDN) renvoie true si l'appel a été effectué à partir d'un thread qui n'est pas le thread GUI, false soit si l'appel a été effectué à partir du thread GUI, ou si le handle a été pas encore créé.

Vous pouvez rencontrer une exception si vous souhaitez qu'un formulaire modal soit affiché et mis à jour par un autre thread. Étant donné que vous souhaitez que ce formulaire s'affiche de manière modale, vous pouvez effectuer les opérations suivantes:

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 « prennent moins de temps » de l'étiquette ( le lire et l' interpréter comme une simplification) que le temps qu'il faut pour le thread GUI pour créer le formulaire de poignée . Cela se produit dans la méthode ShowDialog () .

Vous devez également vérifier la poignée 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.

Trucs optionnels: Personnellement, je suis venu coder ce qui suit:

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 sûr que mon interface graphique sera mise à jour quel que soit le thread qui fera l'appel, en attendant éventuellement une durée bien définie (le délai d'expiration).

Sume
la source
1
Je suis venu ici pour trouver cela, car je vérifie également IsHandleCreated. Une autre propriété à vérifier est IsDisposed. Si votre formulaire est supprimé, vous ne pouvez pas appeler Invoke () dessus. Si l'utilisateur a fermé le formulaire avant que votre thread d'arrière-plan puisse se terminer, vous ne voulez pas qu'il essaie de rappeler à l'interface utilisateur lorsque le formulaire est supprimé.
Jon
Je dirais que c'est une mauvaise idée de commencer ... Normalement, vous montrez le formulaire enfant immédiatement et avez une barre de progression ou d'autres commentaires lors du traitement en arrière-plan. Ou vous feriez d'abord tout le traitement, puis vous transmettriez le résultat au nouveau formulaire lors de la création. Faire les deux en même temps aurait généralement des avantages marginaux mais un code beaucoup moins facile à gérer.
Phil1970
Le scénario décrit prend en compte une forme modale utilisée comme vue de progression d'un travail d'arrière-plan. Parce qu'il doit être modal, il doit être affiché en appelant la méthode Form.ShowDialog () . En faisant cela, vous empêchez votre code qui suit l'appel d'être exécuté jusqu'à ce que le formulaire soit fermé. Donc, à moins que vous ne puissiez démarrer le thread d'arrière-plan différemment de l'exemple donné (et, bien sûr, vous pouvez) ce formulaire doit être affiché de manière modale après le démarrage du thread d'arrière-plan. Dans ce cas, vous devez vérifier la poignée à créer. Si vous n'avez pas besoin d'une forme modale, c'est une autre histoire.
Sume