web-dev-qa-db-fra.com

Comment mettre à jour l'interface utilisateur à partir d'un autre thread s'exécutant dans une autre classe

Je suis actuellement en train d'écrire mon premier programme sur C # et je suis extrêmement novice dans ce langage (utilisé jusqu'à présent uniquement avec C). J'ai fait beaucoup de recherches, mais toutes les réponses étaient trop générales et je ne pouvais tout simplement pas y arriver.

Voici donc mon problème (très courant): J'ai une application WPF qui prend en charge les entrées de quelques zones de texte remplies par l'utilisateur, puis les utilise pour effectuer de nombreux calculs. Cela devrait prendre environ 2 à 3 minutes, je voudrais donc mettre à jour une barre de progression et un bloc texte m'indiquant quel est le statut actuel .. .. Je dois également stocker les entrées de l'interface utilisateur de l'utilisateur et les transmettre au fil, j'ai donc une troisième classe, que j'utilise pour créer un objet et souhaite passer cet objet au thread d'arrière-plan . Évidemment, j'exécuterais les calculs dans un autre thread, pour que l'interface utilisateur ne se fige pas, mais je ne Je ne sais pas comment mettre à jour l'interface utilisateur, car toutes les méthodes de calcul font partie d'une autre classe . Après de nombreuses recherches, je pense que la meilleure méthode serait d'utiliser des répartiteurs et TPL et non un arrière-plan, mais honnêtement je je ne sais pas trop comment ils fonctionnent et après environ 20 heures d’essais et d’erreurs avec d’autres réponses, j’ai décidé de poser une question moi-même.

Voici une structure très simple de mon programme:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        Initialize Component();
    }

    private void startCalc(object sender, RoutedEventArgs e)
    {
        inputValues input = new inputValues();

        calcClass calculations = new calcClass();

        try
        {
             input.pota = Convert.ToDouble(aVar.Text);
             input.potb = Convert.ToDouble(bVar.Text);
             input.potc = Convert.ToDouble(cVar.Text);
             input.potd = Convert.ToDouble(dVar.Text);
             input.potf = Convert.ToDouble(fVar.Text);
             input.potA = Convert.ToDouble(AVar.Text);
             input.potB = Convert.ToDouble(BVar.Text);
             input.initStart = Convert.ToDouble(initStart.Text);
             input.initEnd = Convert.ToDouble(initEnd.Text);
             input.inita = Convert.ToDouble(inita.Text);
             input.initb = Convert.ToDouble(initb.Text);
             input.initc = Convert.ToDouble(initb.Text);
         }
         catch
         {
             MessageBox.Show("Some input values are not of the expected Type.", "Wrong Input", MessageBoxButton.OK, MessageBoxImage.Error);
         }
         Thread calcthread = new Thread(new ParameterizedThreadStart(calculations.testMethod);
         calcthread.Start(input);
    }

public class inputValues
{
    public double pota, potb, potc, potd, potf, potA, potB;
    public double initStart, initEnd, inita, initb, initc;
}

public class calcClass
{
    public void testmethod(inputValues input)
    {
        Thread.CurrentThread.Priority = ThreadPriority.Lowest;
        int i;
        //the input object will be used somehow, but that doesn't matter for my problem
        for (i = 0; i < 1000; i++)
        {
            Thread.Sleep(10);
        }
    }
}

Je serais très reconnaissant si quelqu'un avait une explication simple sur la façon de mettre à jour l'interface utilisateur à partir de la méthode de test. Étant donné que je suis nouveau en C # et en programmation orientée objet, des réponses trop complexes que je ne comprendrai probablement pas, je ferai de mon mieux cependant.

Aussi, si quelqu'un a une meilleure idée en général (peut-être en utilisant des antécédents ou toute autre chose), je suis ouvert à la voir.

34
phil13131

Vous devez d’abord utiliser Dispatcher.Invoke pour changer l’UI d’un autre thread et, pour ce faire, d’une autre classe, vous pouvez utiliser des événements.
Ensuite, vous pouvez vous inscrire à cet événement dans la classe principale et transmettre les modifications apportées à l'interface utilisateur et à la classe de calcul pour générer l'événement lorsque vous souhaitez notifier l'interface utilisateur:

class MainWindow : Window
{
    private void startCalc()
    {
        //your code
        CalcClass calc = new CalcClass();
        calc.ProgressUpdate += (s, e) => {
            Dispatcher.Invoke((Action)delegate() { /* update UI */ });
        };
        Thread calcthread = new Thread(new ParameterizedThreadStart(calc.testMethod));
        calcthread.Start(input);
    }
}

class CalcClass
{
    public event EventHandler ProgressUpdate;

    public void testMethod(object input)
    {
        //part 1
        if(ProgressUpdate != null)
            ProgressUpdate(this, new YourEventArgs(status));
        //part 2
    }
}

METTRE À JOUR:
Comme il semble que ce soit toujours une question fréquemment consultée, je souhaite mettre à jour cette réponse avec la façon dont je le ferais maintenant (avec .NET 4.5) - ceci est un peu plus long car je montrerai différentes possibilités: 

class MainWindow : Window
{
    Task calcTask = null;

    void buttonStartCalc_Clicked(object sender, EventArgs e) { StartCalc(); } // #1
    async void buttonDoCalc_Clicked(object sender, EventArgs e) // #2
    {
        await CalcAsync(); // #2
    }

    void StartCalc()
    {
        var calc = PrepareCalc();
        calcTask = Task.Run(() => calc.TestMethod(input)); // #3
    }
    Task CalcAsync()
    {
        var calc = PrepareCalc();
        return Task.Run(() => calc.TestMethod(input)); // #4
    }
    CalcClass PrepareCalc()
    {
        //your code
        var calc = new CalcClass();
        calc.ProgressUpdate += (s, e) => Dispatcher.Invoke((Action)delegate()
            {
                // update UI
            });
        return calc;
    }
}

class CalcClass
{
    public event EventHandler<EventArgs<YourStatus>> ProgressUpdate; // #5

    public TestMethod(InputValues input)
    {
        //part 1
        ProgressUpdate.Raise(this, status); // #6 - status is of type YourStatus
        //part 2
    }
}

static class EventExtensions
{
    public static void Raise<T>(this EventHandler<EventArgs<T>> theEvent,
                                object sender, T args)
    {
        if (theEvent != null)
            theEvent(sender, new EventArgs<T>(args));
    }
}

@ 1) Comment démarrer les calculs "synchrones" et les exécuter en tâche de fond 

@ 2) Comment le démarrer "asynchrone" et "l'attendre": Ici, le calcul est exécuté et terminé avant le retour de la méthode, mais en raison de la variable asyncawait, l'interface utilisateur n'est pas bloquée (_/BTW: de tels gestionnaires d'événements sont les Seules les utilisations valides de async void en tant que gestionnaire d'événements doivent renvoyer void - utilisez async Task dans tous les autres cas

@ 3) Au lieu d'un nouveau Thread, nous utilisons maintenant un Task. Pour pouvoir vérifier ultérieurement son achèvement (réussi), nous le sauvegardons dans le membre global calcTask. En arrière-plan, cela commence également un nouveau thread et exécute l'action ici, mais il est beaucoup plus facile à gérer et présente d'autres avantages. 

@ 4) Ici, nous commençons également l'action, mais cette fois nous renvoyons la tâche afin que le "gestionnaire d'événement async" puisse "l'attendre". Nous pourrions également créer async Task CalcAsync(), puis await Task.Run(() => calc.TestMethod(input)).ConfigureAwait(false); (pour info: la ConfigureAwait(false) permet d'éviter les blocages, lisez ce qui suit si vous utilisez async/await car il serait trop compliqué d'expliquer ici), ce qui entraînerait le même flux Le Task.Run est la seule "opération attendue" et c'est la dernière que nous pouvons simplement renvoyer la tâche et enregistrer un commutateur de contexte, ce qui économise du temps d'exécution. 

@ 5) Ici, j'utilise maintenant un "événement générique fortement typé" afin que nous puissions passer et recevoir notre "objet d'état" facilement 

@ 6) Ici, j'utilise l'extension définie ci-dessous, qui (mis à part la facilité d'utilisation) résout l'éventualité d'une situation de concurrence critique dans l'ancien exemple. Là, il aurait pu arriver que l'événement ait eu null après la vérification if-, mais avant l'appel si le gestionnaire d'événements a été supprimé dans un autre thread à ce moment-là. Cela ne peut pas arriver ici, car les extensions reçoivent une "copie" du délégué à l'événement et, dans la même situation, le gestionnaire est toujours enregistré dans la méthode Raise.

54
ChrFin

Je vais vous lancer une balle courbe ici. Si je l'ai dit une fois, je l'ai dit cent fois. Les opérations de marshaling telles que Invoke ou BeginInvoke ne sont pas toujours les meilleures méthodes pour mettre à jour l'interface utilisateur avec la progression du thread de travail.

Dans ce cas, il est généralement préférable que le thread de travail publie ses informations de progression dans une structure de données partagée que le thread d'interface utilisateur interroge ensuite à intervalles réguliers. Cela a plusieurs avantages.

  • Il rompt le couplage étroit entre l'interface utilisateur et le thread de travail que Invoke impose.
  • Le fil d'interface utilisateur dicte quand les contrôles d'interface utilisateur sont mis à jour ... comme cela devrait être le cas quand vous y réfléchissez vraiment.
  • Il n'y a aucun risque de dépasser la file d'attente de messages de l'interface utilisateur, comme ce serait le cas si BeginInvoke était utilisé à partir du thread de travail.
  • Le thread de travail ne doit pas nécessairement attendre une réponse du thread d'interface utilisateur, comme ce serait le cas avec Invoke.
  • Vous obtenez plus de débit à la fois sur l'interface utilisateur et sur les threads de travail.
  • Invoke et BeginInvoke sont des opérations coûteuses.

Donc, dans votre calcClass, créez une structure de données qui contiendra les informations de progression.

public class calcClass
{
  private double percentComplete = 0;

  public double PercentComplete
  {
    get 
    { 
      // Do a thread-safe read here.
      return Interlocked.CompareExchange(ref percentComplete, 0, 0);
    }
  }

  public testMethod(object input)
  {
    int count = 1000;
    for (int i = 0; i < count; i++)
    {
      Thread.Sleep(10);
      double newvalue = ((double)i + 1) / (double)count;
      Interlocked.Exchange(ref percentComplete, newvalue);
    }
  }
}

Dans votre classe MainWindow, utilisez ensuite une DispatcherTimer pour interroger périodiquement les informations de progression. Configurez DispatcherTimer pour déclencher l'événement Tick à l'intervalle le plus approprié à votre situation.

public partial class MainWindow : Window
{
  public void YourDispatcherTimer_Tick(object sender, EventArgs args)
  {
    YourProgressBar.Value = calculation.PercentComplete;
  }
}
28
Brian Gideon

Vous avez raison de dire que vous devez utiliser la variable Dispatcher pour mettre à jour les contrôles sur le thread d'interface utilisateur et que les processus de longue durée ne doivent pas s'exécuter sur le thread d'interface utilisateur. Même si vous exécutez le processus de longue durée de manière asynchrone sur le thread d'interface utilisateur, il peut toujours entraîner des problèmes de performances.

Il est à noter que Dispatcher.CurrentDispatcher renverra le répartiteur pour le fil actuel, pas nécessairement le fil d'interface utilisateur. Je pense que vous pouvez utiliser Application.Current.Dispatcher pour obtenir une référence au répartiteur du fil d'interface utilisateur, s'il est disponible, mais sinon, vous devrez passer le répartiteur d'interface utilisateur à votre fil d'arrière-plan.

En règle générale, j'utilise Bibliothèque parallèle de tâches pour les opérations de filetage au lieu de BackgroundWorker. Je trouve juste que c'est plus facile à utiliser.

Par exemple,

Task.Factory.StartNew(() => 
    SomeObject.RunLongProcess(someDataObject));

void RunLongProcess(SomeViewModel someDataObject)
{
    for (int i = 0; i <= 1000; i++)
    {
        Thread.Sleep(10);

        // Update every 10 executions
        if (i % 10 == 0)
        {
            // Send message to UI thread
            Application.Current.Dispatcher.BeginInvoke(
                DispatcherPriority.Normal,
                (Action)(() => someDataObject.ProgressValue = (i / 1000)));
        }
    }
}
5
Rachel

Tout ce qui interagit avec l'interface utilisateur doit être appelé dans le thread d'interface utilisateur (à moins qu'il ne s'agisse d'un objet gelé). Pour ce faire, vous pouvez utiliser le répartiteur.

var disp = /* Get the UI dispatcher, each WPF object has a dispatcher which you can query*/
disp.BeginInvoke(DispatcherPriority.Normal,
        (Action)(() => /*Do your UI Stuff here*/));

J'utilise BeginInvoke ici, généralement un backgroundworker n'a pas besoin d'attendre que l'interface utilisateur soit mise à jour. Si vous voulez attendre, vous pouvez utiliser Invoke. Mais vous devez faire attention à ne pas appeler BeginInvoke trop souvent, cela peut être très désagréable.

À propos, la classe BackgroundWorker aide avec ce genre de tâches. Il permet de signaler les modifications, telles qu'un pourcentage, et les envoie automatiquement du fil d'arrière-plan au fil de l'interface utilisateur. Pour la plupart des tâches <> de mise à jour de l'interface utilisateur, BackgroundWorker est un excellent outil.

4
dowhilefor

Vous allez devoir revenir à votre thread principal (également appelé UI thread) afin de update l'interface utilisateur. Tout autre thread essayant de mettre à jour votre interface utilisateur provoquera simplement le transfert de exceptions dans tous les sens.

Donc, puisque vous êtes dans WPF, vous pouvez utiliser le Dispatcher et plus précisément un beginInvoke sur ce dispatcher. Cela vous permettra d'exécuter ce qui est nécessaire (généralement, mettez à jour l'interface utilisateur) dans le thread d'interface utilisateur. 

Vous voudrez peut-être aussi "enregistrer" la UI dans votre business, en maintenant une référence à un contrôle/formulaire afin de pouvoir utiliser sa dispatcher.

1
squelos

S'il s'agit d'un calcul long, j'irais bien travailler en arrière-plan. Il a un support de progrès. Il a également un support pour annuler. 

http://msdn.Microsoft.com/en-us/library/cc221403(v=VS.95).aspx

Ici, j'ai une TextBox liée au contenu.

    private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        Debug.Write("backgroundWorker_RunWorkerCompleted");
        if (e.Cancelled)
        {
            contents = "Cancelled get contents.";
            NotifyPropertyChanged("Contents");
        }
        else if (e.Error != null)
        {
            contents = "An Error Occured in get contents";
            NotifyPropertyChanged("Contents");
        }
        else
        {
            contents = (string)e.Result;
            if (contentTabSelectd) NotifyPropertyChanged("Contents");
        }
    }
1
paparazzo

J'ai ressenti le besoin d'ajouter cette meilleure réponse, car rien d'autre que BackgroundWorker ne semblait m'aider, et la réponse à cette question jusqu'à présent était terriblement incomplète. Voici comment mettre à jour une page XAML appelée MainWindow qui comporte une balise Image comme celle-ci:

<Image Name="imgNtwkInd" Source="Images/network_on.jpg" Width="50" />

avec un processus BackgroundWorker pour indiquer si vous êtes connecté ou non au réseau:

using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;

public partial class MainWindow : Window
{
    private BackgroundWorker bw = new BackgroundWorker();

    public MainWindow()
    {
        InitializeComponent();

        // Set up background worker to allow progress reporting and cancellation
        bw.WorkerReportsProgress = true;
        bw.WorkerSupportsCancellation = true;

        // This is your main work process that records progress
        bw.DoWork += new DoWorkEventHandler(SomeClass.DoWork);

        // This will update your page based on that progress
        bw.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged);

        // This starts your background worker and "DoWork()"
        bw.RunWorkerAsync();

        // When this page closes, this will run and cancel your background worker
        this.Closing += new CancelEventHandler(Page_Unload);
    }

    private void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        BitmapImage bImg = new BitmapImage();
        bool connected = false;
        string response = e.ProgressPercentage.ToString(); // will either be 1 or 0 for true/false -- this is the result recorded in DoWork()

        if (response == "1")
            connected = true;

        // Do something with the result we got
        if (!connected)
        {
            bImg.BeginInit();
            bImg.UriSource = new Uri("Images/network_off.jpg", UriKind.Relative);
            bImg.EndInit();
            imgNtwkInd.Source = bImg;
        }
        else
        {
            bImg.BeginInit();
            bImg.UriSource = new Uri("Images/network_on.jpg", UriKind.Relative);
            bImg.EndInit();
            imgNtwkInd.Source = bImg;
        }
    }

    private void Page_Unload(object sender, CancelEventArgs e)
    {
        bw.CancelAsync();  // stops the background worker when unloading the page
    }
}


public class SomeClass
{
    public static bool connected = false;

    public void DoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker bw = sender as BackgroundWorker;

        int i = 0;
        do 
        {
            connected = CheckConn();  // do some task and get the result

            if (bw.CancellationPending == true)
            {
                e.Cancel = true;
                break;
            }
            else
            {
                Thread.Sleep(1000);
                // Record your result here
                if (connected)
                    bw.ReportProgress(1);
                else
                    bw.ReportProgress(0);
            }
        }
        while (i == 0);
    }

    private static bool CheckConn()
    {
        bool conn = false;
        Ping png = new Ping();
        string Host = "SomeComputerNameHere";

        try
        {
            PingReply pngReply = png.Send(Host);
            if (pngReply.Status == IPStatus.Success)
                conn = true;
        }
        catch (PingException ex)
        {
            // write exception to log
        }
        return conn;
    }
}

Pour plus d'informations: https://msdn.Microsoft.com/en-us/library/cc221403(v=VS.95).aspx

0
vapcguy

Dieu merci, Microsoft a que a découvert WPF :)

Chaque Control, comme une barre de progression, un bouton, un formulaire, etc., porte une Dispatcher. Vous pouvez donner à la Dispatcher une Action qui doit être exécutée et l'appelera automatiquement sur le bon thread (une Action est comme un délégué de fonction).

Vous pouvez trouver un exemple ici .

Bien entendu, le contrôle devra être accessible à partir d'autres classes, par exemple. en le faisant public et en transférant une référence à Window à votre autre classe, ou peut-être en passant une référence uniquement à la barre de progression.

0
Vladislav Zorov