Question Comment puis-je mettre à jour l'interface graphique d'un autre thread?


Quelle est la manière la plus simple de mettre à jour un Label d'un autre fil?

j'ai un Form sur thread1, et à partir de là je commence un autre fil (thread2). Tandis que thread2 traite des fichiers que je voudrais mettre à jour un Label sur le Form avec le statut actuel de thread2du travail.

Comment puis je faire ça?


1144
2018-03-19 09:37


origine


Réponses:


Pour .NET 2.0, voici un petit code que j'ai écrit qui fait exactement ce que vous voulez, et qui 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 comme méthode d'extension de Control classe, 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 chaîne à une propriété booléenne et donc une exception d'exécution.

Malheureusement, cela n'empêche personne de faire des choses stupides comme de passer dans un autre Control's propriété et la valeur, donc ce qui suit sera heureux de compiler:

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

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


688
2018-03-19 10:37



le le 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

Remarquerez que Invoke bloque l'exécution jusqu'à la fin - c'est un code synchrone. La question ne pose pas de code asynchrone, mais il y a beaucoup de contenu sur Stack Overflow à propos de l'écriture de code asynchrone lorsque vous voulez en apprendre davantage.


938
2018-03-19 10:17



Manipuler un long travail

Depuis .NET 4.5 et C # 5.0 Tu devrais utiliser Modèle asynchrone basé sur des tâches (TAP) de même que async-attendre 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 comprend le BackgroundWorker Class).

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 UI:

    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. Un code court et propre écrit de manière séquentielle sans callback ni thread explicite.
  2. Tâche au lieu de Fil.
  3. async mot-clé, qui permet d'utiliser attendre ce 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 ne bloque pas le thread d'interface utilisateur.
  4. Classe de progrès (voir Interface IProgress) qui soutient Séparation des préoccupations (SoC) principe de conception et ne nécessite pas de répartiteur explicite et d'invocation. Il utilise le courant SynchronizationContext de son lieu de création (ici le fil de l'interface utilisateur).
  5. TaskCreationOptions.LongRunning que des conseils pour ne pas mettre en file d'attente la tâche dans ThreadPool.

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

Voir aussi à propos de Modèle de filetage de l'interface utilisateur concept.

Gestion des exceptions

L'extrait ci-dessous est un exemple de la façon de gérer les exceptions et les boutons de bascule Enabled propriété pour éviter 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...");
    }
}

333
2017-08-03 13:09



Variation de Marc Gravell le plus simple Solution pour .NET 4:

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

Ou utilisez Action Delegate à la place:

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

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


195
2018-05-29 18:51



Ignorer et ignorer 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é en utilisant la ligne de code suivante:

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

115
2017-08-27 21:10



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


57
2018-03-19 10:31



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
}

50
2018-03-19 09:46