Mise à jour de l'IHM depuis un Thread
|
Un petit tutoriel d'initiation à la problématique fréquente de mise à jour d'une IHM pour rendre compte de l'évolution d'un traitement longuet.
|
|
Vu
972
fois
|
Scénario type
On souhaite effectuer un traitement potentiellement long, en informant l'utilisateur du déroulement de celui-ci.
Approche 1 : ça bloque !
Le principe de base est d'effectuer les tâches et d'intercaler des raffraichissement de l'IHM.
private void MaMethode() { this.EffectuerTache1(); this.MettreAJour(champ1, message1, valeur1); this.Refresh(); this.EffectuerTache2(); this.MettreAJour(champ2, message2, valeur2); this.Refresh(); this.EffectuerTache3(); this.MettreAJour(champ3, message3, valeur3); this.Refresh(); ... }
private void MettreAJour(Control champ, string message, int valeur) { // Le contenu de cette méthode est volontairement non compilatoire // A vous de déterminer quelle signature vous voulez utiliser pour mettre à jour vos contrôles champ.Text = message; champ.Value = valeur; this.Refresh(); }
|
L'inconvénient de cette méthode est que pendant toute la durée du traitement, l'IHM est verrouillée, et il est impossible d'interrompre quoi que ce soit.
Approche 2 : on bricole...
Pour permettre à l'utilisateur d'interragir pendant le traitement, on teste régulièrement les évènements d'annulation.
private void MaMethode() { this.EffectuerTache1(); this.MettreAJour(champ1, message1, valeur1); this.Refresh(); Application.DoEvents(); this.TesterAnnulation(); this.EffectuerTache2(); this.MettreAJour(champ2, message2, valeur2); this.Refresh(); Application.DoEvents(); this.TesterAnnulation(); this.EffectuerTache3(); this.MettreAJour(champ3, message3, valeur3); this.Refresh(); Application.DoEvents(); this.TesterAnnulation(); ... } |
L'IHM n'est ici bloquée que pendant les traitements "unitaires" et l'utilisateur peut donc annuler. L'aspect visuel sera moyen, dans la mesure ou l'écran restera figé entre chaque appel à DoEvents(). Il faut donc cliquer sur les boutons (une seule fois) sans que le clic soit pris en compte immédiatement...
Approche 3 : On effectue le traitement dans un Thread
L'objectif est ici de conserver le thread principal à disposition de l'utilisateur, et d'effectuer les traitements dans un autre thread.
private void MaMethode() { ThreadStart methodeTraitement = new ThreadStart(this.EffectuerTraitement); Thread traitement = new Thread(methodeTraitement); traitement.Start(); }
private void EffectuerTraitement() { this.EffectuerTache1(); this.MettreAJour(champ1, message1, valeur1); this.Refresh(); this.TesterAnnulation(); this.EffectuerTache2(); this.MettreAJour(champ2, message2, valeur2); this.Refresh(); this.TesterAnnulation(); this.EffectuerTache3(); this.MettreAJour(champ3, message3, valeur3); this.Refresh(); this.TesterAnnulation(); ... } |
L'IHM est tout à fait disponible, on peut minimiser la fenêtre, la déplacer, cliquer sur le bouton d'annulation, en temps réel. L'annulation n'est prise en compte qu'à la fin d'une tâche élémentaire. On a donc le résultat escompté, sauf que ça compile, mais ça ne marche pas...
Approche 4 : On corrige notre gros bug
Selon les versions du Framework, les résultats obtenus vont varier. Dans le Framework 1.x, ça va marcher dans 95% des cas, et figer l'application (complètement) de temps en temps ! Dans le Framework 2.0 on obtiendra systématiquement une exception (ce qui est quand même plus propre).
Ce problème est du à l'interdiction (car trop risqué) de mettre à jour un Control créé dans un thread depuis un autre thread.
Pour corriger ce problème, on va utiliser la méthode Control.Invoke(Delegate target, object[] args).
// On déclare le delegate que l´on souhaite en fonction des champs à mettre à jour public void Delegate MettreAJourHandler(Control champ, string message, int valeur);
private void MaMethode() { ThreadStart methodeTraitement = new ThreadStart(this.EffectuerTraitement); Thread traitement = new Thread(methodeTraitement); traitement.Start(); }
private void EffectuerTraitement() { this.EffectuerTache1(); this.RaffraichirDepuisThread(champ1, message1, valeur1); this.TesterAnnulation(); this.EffectuerTache2(); this.RaffraichirDepuisThread(champ2, message2, valeur2); this.TesterAnnulation(); this.EffectuerTache3(); this.RaffraichirDepuisThread(champ3, message3, valeur3); this.TesterAnnulation();
... }
private void RaffraichirDepuisThread(Control champ, string message, int valeur) { // On regroupe les arguments dans un tableau object[] args = new object[3]; args[0] = champ; args[1] = message; args[2] = valeur; // On appelle la méthode dans le Thread Principal base.Invoke(new MettreAJourHandler(this.MettreAJour), args); }
private void MettreAJour(Control champ, string message, int valeur) { // Le contenu de cette méthode est volontairement non compilatoire // A vous de déterminer quelle signature vous voulez utiliser pour mettre à jour vos contrôles champ.Text = message; champ.Value = valeur; this.Refresh(); } |
Remarques
Avec l'arrivée du Framework 2.0 et du mot clé params, une nouvelle syntaxe rend l'ensemble plus lisible.
La méthode exposée est désormais :
Control.Invoke(Delegate methode, params object[] args) |
Ce qui signifie qu'il n'est plus nécessaire de regrouper les différents paramètres dans un tableau, mais qu'on peut se contenter des les énumérer. La méthode de mise à jour depuis le thread devient donc :
private void RaffraichirDepuisThread(Control champ, string message, int valeur) { // On appelle la méthode dans le Thread Principal base.Invoke(new MettreAJourHandler(this.MettreAJour), champ, message, valeur); } |
On peut donc supprimer cette sous-méthode, et appeler le Invoke directement :
private void EffectuerTraitement() { MettreAJourHandler methode = new MettreAJourHandler(this.MettreAJour); this.EffectuerTache1(); base.Invoke(methode, champ1, message1, valeur1); this.TesterAnnulation(); this.EffectuerTache2(); base.Invoke(methode, champ2, message2, valeur2); this.TesterAnnulation(); this.EffectuerTache3(); base.Invoke(methode, champ3, message3, valeur3); this.TesterAnnulation();
... }
|
En .NET Compact Framework, il n'est malheureusement possible de passer des arguments qu'à partir de la version 2.0. Dans la version 1.x, la signature ne prend pas de deuxième paramètre de type object[].
|