Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Les class helper : s'en servir pour gérer l'invocation des composants GUI en multithread

Les class helper dont j'ai déjà parlé ici peuvent servir à beaucoup de choses, si on se limite à des services assez génériques et qu'on ne s'en sert pas pour éclater le code d'une application qui deviendra alors très difficile à maintenir. C'est l'opinion que j'exprimais dans cet ancien billet et que je conserve toujours. Dès lors trouver des utilisations pertinentes des class helpers n'est pas forcément chose aisée, pourtant il ne faudrait pas les diaboliser non plus et se priver des immenses services qu'ils peuvent rendent lorsqu'ils sont utilisés à bon escient. Dans le blog de Richard on trouve un exemple assez intéressant à plus d'un titre. D'une part il permet de voir que les class helpers sont des alliés d'une grande efficacité dès qu'il s'agit de trouver une solution globale à un problème répétitif. Mais d'autre part cet exemple, par l'utilisation des génériques et des expressions lambda, a l'avantage de mettre en scène tout un ensemble de nouveautés syntaxiques de C# 3.0 en quelques lignes de code. Et les  de ce genre sont toujours formateurs. Pour ceux qui lisent l'anglais, allez directement sur le billet original en cliquant ici. Pour les autres, voici non pas un résumé mais une interprétation libre sur le même sujet : Le problème à résoudre : l'invocation en multithread. Lorsqu'un thread doit mettre à jour des composants détenus par le thread principal cela doit passer par un appel à Invoke car seul le thread principal peut mettre à jour les contrôles qu'il possède. Cette situation est courante. Par exemple un traitement en tâche de fond qui doit mettre à jour une barre de progression. Bien entendu il ne s'agit pas de bricoler directement les composants d'une form depuis un thread secondaire, ce genre de programmation est à proscrire, mais même en créant dans la form une propriété publique accessible au thread, la modification de cette propriété se fera à l'intérieur du thread secondaire et non pas dans le thread principal... Il faut alors détecter cette situation et trouver un moyen de faire la modification de façon "détournée", c'est à dire de telle façon à ce que ce soit le thread principal qui s'en charge. Les Windows Forms et les contrôles conçus pour cette librairie mettent à la disposition du développeur la méthode InvokeRequired qui permet justement de savoir si le contrôle nécessite l'indirection que j'évoquais plus haut ou bien s'il est possible de le modifier directement. Le premier cas correspond à une modification depuis un thread secondaire, le dernier à une modification du contrôle depuis le thread principal, cas le plus habituel. La méthode classique Sous .NET 1.1 le framework ne détectait pas le conflit et les applications mal conçues pouvait planter aléatoirement si des modifications de contrôles étaient effectuées depuis des threads secondaires. Le framework 2.0 a ajouté plus de sécurité en détectant la situation qui déclenche une exception, ce qui est bien préférable aux dégâts aléatoires... Donc, pour s'en sortir on doit écrire un code du genre de celui-ci : [...] NetworkChange.NetworkAddressChanged += new NetworkAddressChangedEventHandler(NetworkChange_NetworkAddressChanged); [...] delegate void SetStatus(bool status); void NetworkChange_NetworkAddressChanged(object sender, EventArgs e) {       bool isConnected = IsConnected();       if (InvokeRequired)         Invoke(new SetStatus(UpdateStatus), new object[] { isConnected });       else         UpdateStatus(isConnected); } void UpdateStatus(bool connected) {       if (connected)          this.connectionPictureBox.ImageLocation = @"..\bullet.green.gif";       else          this.connectionPictureBox.ImageLocation = @"..\bullet.red.gif"; } [...]  Cette solution classique impose la création d'un délégué et beaucoup de code pour passer d'une modification directe à une modification indirecte selon le cas. Bien entendu le code en question doit être dupliqué pour chaque contrôle pouvant être modifié par un thread secondaire... C'est assez lourd, convenons-en... (l'exemple ci-dessus provient d'un vieux post de 2006 d'un blog qui n'est plus actif mais que vous pouvez toujours visiter en cliquant ici). Pour la compréhension, le code ci-dessus change l'image d'une PictureBox pour indiquer l'état (vert ou rouge) d'une connexion à un réseau et le code appelant cette mise à jour de l'affichage peut émaner d'un thread secondaire. Comme on le voit, la méthode est fastidieuse et va avoir tendance à rendre le code plus long, moins fiable (coder plus pour bugger plus...), et moins maintenable. C'est ici que l'idée d'utiliser un class helper prend tout son intérêt... La solution via un class helper La question qu'on peut se poser est ainsi "n'existe-t-il pas un moyen générique de résoudre le problème ?". De base pas vraiment. Mais avec l'intervention d'un class helper, si, c'est possible (© Hassan Céhef - joke pour les amateurs des "nuls"). Voici le class helper en question : public static TResult Invoke<T, TResult>(this T controlToInvokeOn, Func<TResult> code) where T : Control {    if (controlToInvokeOn.InvokeRequired)    {     return (TResult)controlToInvokeOn.Invoke(code);    }    else    {     return (TResult)code();    } } Il s'agit d'ajouter à toutes les classes dérivées de Control (et à cette dernière aussi) la méthode "Invoke". Le class helper, tel que conçu ici, prend en charge le retour d'une valeur, ce qui est pratique si on désire lire la chaîne d'un textbox par exemple. Le premier paramètre "ne compte pas", il est le marqueur syntaxique des class helpers en quelque sorte. Le second paramètre qui apparaitra comme le seul et unique lors de l'utilisation de la méthode est de type Func<TResult>, il s'agit ici d'un prototype de méthode. Il sera donc possible de passer à Invoke directement un bout de code, voire une expression lambda, et de récupérer le résultat. Un exemple d'utilisation :  string value = this.Invoke(() => button1.Text); Ici on va chercher la valeur de la propriété Text de "button1" via un appel à Invoke sur "this", supposée ici être la form. Le résultat est récupéré dans la variable "value". On note l'utilisation d'une expression lambda en paramètre de Invoke. Mais si le code qu'on désire appeler ne retourne pas de résultat ? Le class helper, tel que défini ici, ne fonctionnera pas puisqu'il attend en paramètre du code retournant une valeur (une expression). Il est donc nécessaire de créer un overload de Invoke pour gérer ce cas particulier : public static void Invoke(this Control controlToInvokeOn, Func code) {     if (controlToInvokeOn.InvokeRequired)     {        controlToInvokeOn.Invoke(code);     }     else     {        code();     } } Avec cet overload la solution est complète et gère aussi bien le code retournant une valeur que le code "void". On peut écrire alors: this.Invoke(() => progressBar1.Value = i); Sachant que pour simplifier l'appel est ici effectué dans la form elle-même (this). L'appel à Invoke contient une expression lambda qui modifie la valeur d'une barre de progression. Mais peu importe les détails, c'est l'esprit qui compte. Conclusion Les class helpers peuvent fournir des solutions globales à des problèmes récurrents. Utilisés dans un tel cadre ils prennent tout leur sens et au lieu de rendre le code plus complexe et moins maintenable, au contraire, il le simplifie et centralise sa logique. L'utilisation des génériques, des prototypes de méthodes et des expressions lambda montrent aussi que les nouveautés syntaxiques de C#, loin d'être des gadgets dont on peut se passer forment la base d'un style de programmation totalement nouveau, plus efficace, plus sobre et plus ... générique. L'exemple étudié ici illustre parfaitement cette nouvelle façon de coder de C# 3.0 et les avantages qu'elle proccure à ceux qui la maîtrise. Bon dev ! (et Stay Tuned, of course !)

Quizz C#. Vous croyez connaître le langage ? et bien regardez ce qui suit !

C# est un langage merveilleux, plein de charme... et de surprises ! En effet, s'écartant des chemins battus et intégrant de nombreuses extensions comme Linq aujourd'hui ou les méthodes anonymes hier, il est en perpétuelle évolution. Mais ces "extensions" transforment progressivement un langage déjà subtile à la base (plus qu'il n'y paraît) en une jungle où le chemin le plus court n'est pas celui que les explorateurs que nous sommes auraient envisagé... Pour se le prouver, 6 quizz qui vous empêcheront de bronzer idiot ou de vous endormir au boulot ! Un petit projet exemple avec les questions et les réponses sera téléchargeable en fin d'article demain... [EDIT 4-7-08] Le lien se trouve désormais en fin d'article. Le code source contient les réponses au quizz. Quizz 1 Etant données les déclarations suivantes : class ClasseDeBase {     public virtual void FaitUnTruc(int x)     { Console.WriteLine("Base.FaitUnTruc(int)"); } } class ClasseDérivée : ClasseDeBase {     public override void FaitUnTruc(int x)    { Console.WriteLine("Dérivée.FaitUnTruc(int)"); }    public void FaitUnTruc(object o)   { Console.WriteLine("Dérivée.FaitUnTruc(object)"); } } Pouvez-vous prédire l'affiche du code suivant et expliquer la sortie réelle :     ClasseDérivée d = new ClasseDérivée();     int i = 10;     d.FaitUnTruc(i); Gratt' Gratt' Gratt'..... Quizz 2 Pouvez-vous prédire l'affichage de cette séquence et expliquer l'affichage réel ? double d1a = 1.00001; double d2a = 0.00001; Console.WriteLine((d1a - d2a) == 1.0); double d1b = 1.000001; double d2b = 0.000001; Console.WriteLine((d1b - d2b) == 1.0); double d1c = 1.0000001; double d2c = 0.0000001; Console.WriteLine((d1c - d2c) == 1.0); Je vous laisse réfléchir ... Quizz 3 Toujours le même jeu : prédire ce qui va être affiché et expliquer ce qui est affiché réellement... c'est forcément pas la même chose sinon le quizz n'existerait pas :-) List<Travail> travaux = new List<Travail>(); Console.WriteLine("Init de la liste de delegates"); for (int i = 0; i < 10; i++) { travaux.Add(delegate { Console.WriteLine(i); }); } Console.WriteLine("Activation de chaque delegate de la liste"); foreach (Travail travail in travaux) travail();  Les apparences sont trompeuses, méfiez-vous ! Quizz 4 Ce code compile-t-il ? public enum EtatMoteur { Marche, Arrêt } public static void DoQuizz() {     EtatMoteur etat = 0.0;     Console.WriteLine(etat); } Quizz 5  Etant données les déclarations suivantes : private static void Affiche(object o) { Console.WriteLine("affichage <object>"); } private static void Affiche<T>(params T[] items) { Console.WriteLine("Affichage de <params T[]>"); }  Pouvez-vous prédire et expliquer la sortie de l'appel suivant :   Affiche("Qui va m'afficher ?");  Je sens que ça chauffe :-) Quizz 6  Etant données les déclarations suivantes :  delegate void FaitLeBoulot(); private static FaitLeBoulot FabriqueLeDélégué() {    Random r = new Random();    Console.WriteLine("Fabrique. r = "+r.GetHashCode());    return delegate {                             Console.WriteLine(r.Next());                             Console.WriteLine("delegate. r = "+r.GetHashCode());                          }; }  Quelle sera la sortie de la séquence suivante :     FaitLeBoulot action = FabriqueLeDélégué();     action();     action();   Conclusion C# est un langage d'une grande souplesse et d'une grande richesse. Peut-être qu'à devenir trop subtile il peut en devenir dangereux, comme C++ était dangereux car trop permissif. Avec C#, même de très bons développeurs peuvent restés interloqués par la sortie réelle d'une séquence qui n'a pourtant pas l'air si compliquée. Ses avantages sont tels qu'il mérite l'effort de compréhension supplémentaire pour le maîtriser réellement, mais tout développeur C# (les moyens comme ceux qui pensent être très bons!) doit être mis au moins une fois dans sa vie face à un tel quizz afin de lui faire toucher la faiblesse de son savoir et les risques qu'il prend à coder sans vraiment connaître le langage. Comme toujours l'intelligence est ce bel outil qui nous permet de mesurer à quel point nous ne savons rien. Nous sommes plutôt habitués à envisager cette question sous un angle métaphysique, le développement nous surprend lorsque lui aussi nous place face à cette réalité... Pour le projet exemple dont le source contient les réponses cliquez sur le lien en fin de billet. So (et plus que jamais!) .. Stay Tuned ! Quizz.rar (103,41 kb)