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 !)

Comment passer outre la limitation du DISTINCT sur un SELECT contenant un champ text sous SQL Server

Voici un billet moins "hi-tech" que d'habitude. Point de LINQ dans tout çà, juste un bon server SQL SERVER, une table contenant un champ Text ou NText et une bête requête qui peut retourner plusieurs fois le même enregistrement. Et comme on ne désire pas voir les copies éventuelles, bien entendu on place instinctivement une clause DISTINCT dans le SELECT.

C'est beau, simple comme SQL... Sauf que... SQL Server n'aime pas du tout la clause DISTINCT s'il y a un champ texte dans le SELECT. Et pour cause, il ne sait pas comment comparer les contenus, du coup point de DISTINCT possible.

Quelques bonnes âmes vous conseilleront peut-être :

a) de changer tous vos champs texte en varchar
b) de vous passer du champ texte dans le SELECT et de l'obtenir à part dans une autre requête

Les conseilleurs ne sont pas les codeurs ! Les varchar ont une limite de 8000 caractères et il n'est pas toujours possible de les substituer à Text/NText. De plus modifier tous les champs de ce type dans une application peut être un énorme travail (code, procédures stockées, vues, tout cela à mettre à jour). Solution bidon donc.

Quant à faire le SELECT DISTINCT sans les champs texte puis à faire une seconde requêtre derrière pour les obtenir, c'est franchement lourd et pas forcément sans conséquence sur le code appelant (qui doit faire autant de sous requêtes que de records pour obtenir les champs texte, et les stocker, etc).

Non, il existe plus simple, et un peu plus vicieux : faire une jointure de la table sur elle même.

Oui, tout simplement. L'astuce consiste à faire un select dans la table de tous les champs (y compris les champs texte) mais sans la clause DISTINCT, table qu'on lie à elle même mais ce coup ci en faisant un SELECT DISTINCT qui lui omet les champs texte... La feinte est bonne, mais elle ne saurait faire de miracle, en effet, les champs texte ne seront pas utilisés pour faire la DISCTINCTion entre les records. Cette solution n'a donc qu'un seul hic : elle ne peut pas fonctionner si vous voulez vraiment que le DISTINCT prenne en compte les champs texte. Là je n'ai pas vraiment d'astuce à vous proposer. Mais pour les autres, voici un bout de code SQL fictif qui vous montrera la syntaxe à utiliser :

SELECT t1.c1, t1.c2, t1.LeChampTexte
FROM MaTable t1 JOIN
     (SELECT DISTINCT c1,c2 FROM MaTable WHERE Desc LIKE 'Test%') AS t2
     ON t1.c1 = t2.c1

on suppose ici que le champ "c1" représente la clé (il peut s'agir de plusieurs champs, of course, à vous d'adapter le code), que le champ "c2" est un autre champ sur lequel le DISTINCT portera et que "LeChampTexte" ... est le champ texte qui pose problème. On voit mieux ici que le DISTINCT ne sera effectué que sur "c1" et "c2". Mais cela permet bien de faire un DISTINCT tout en retournant les champs texte, le tout en une seule requête...

Bon SQL et... Stay Tuned !

Les 101 exemples de LINQ à portée de clic !

LINQ.. Je vous en parle très souvent car cela en vaut largement la peine. L'essayer c'est l'adopter, surtout que ses diverses moutures (Linq to XML, Linq to SQL, Linq to Object...) peuvent trouver leur place et rendre service dans tout type de développement.

Articles, astuces, j'ai beaucoup écrit sur LINQ et ce n'est certainement pas terminé. Mais il ne faudrait pas oublier les "101 exemples pour LINQ" créés par Microsoft.

A ma connaissance il n'existe pas de traduction, cela n'existe qu'en anglais. Mais c'est une belle visite de la syntaxe de LINQ et certainement un complément indispensable à une autoformation sur le sujet.

Ces exemples ne sont pas très récents mais je me suis rendu compte que peu de développeurs les connaissaient, au moins en France. D'où l'intérêt de rappeler leur existence.

Pour la petite histoire, le chef de projet qui a écrit ces exemples a touché $202 de la part de Anders... En effet, lorsqu'il a fallu écrire les exemples, Anders était assez pressé de voir le travail fini et avait promis $2 pour chaque exemple qui serait terminé à la dead-line, une sorte de plaisenterie. Le jour fatidique, arrivé en réunion, le chef de projet en avait réalisé 101. Anders a alors fouillé ses poches et a fait l'appoint, 101x2 = $202 qu'il a remis au gars un peu gêné, ne sachant s'il devait accepter ou pas... Ce bon chef de projet a utilisé la somme pour inviter le soir même les développeurs de son équipe au resto. Une belle histoire non ? :-)

PS: Vous pouvez aussi télécharger d'autres exemples Linq en cliquant ici [edit: en novembre 2012 le site pointé n'existe plus, sorry]. Ce n'est pas très récent (Ctp de mars 2007) mais pour ceux qui prennent Linq au vol plutôt qu'à ses débuts, il est fort probable, si vous êtes dans ce cas, que vous soyez passé à côté de ce téléchargement...

PS2: Si vous voulez faire tourner le code des 101 exemples LINQ, vous trouverez sur le site pointé par le lien donné en début d'article le code de la méthode GetProductList(). Ce code est assez exotique et ne compile pas. Il faut le modifier pour en tirer quelque chose. Pour vous simplifier la vous trouverez ci-après une unité de code C# contenant une définition correcte de cette fonction, encapsulée dans une classe abstraite. Cela devrait vous aider... Data.rar (2,26 kb)

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)

Adios XP !

Et oui, nous sommes le 1er juillet et nous entrons dans une ère nouvelle, depuis ce matin XP ne peut plus être acheté en magasin...

Microsoft prévoit quelques dérogations de ci de là et a repoussé la fin du support à 2014 au lieu de 2009.

Mais il va falloir s'y faire, l'OS d'un PC s'appelle Vista, plus XP. A moins de passer à Linux, réponse extrême et peu rentable puisque dans ce cas ce ne sont pas quelques incompatibilités avec les programmes qu'on possède qu'il faudra gérer, mais une incompatibilité totale...

Il n'en reste pas moins vrai que XP a été la première version de Windows aussi complète et aussi stable. Ceci explique cela puisque c'est aussi celle qui a connu la durée de vie la plus longue sur le marché.

C'est donc bien une époque qui s'achève et une autre qui commence.

O'lé com'tche !*

(* "c'est comme çà !" en patois charentais :-) )

De toute façon toutes les machines récentes sont livrées avec Vista et seuls les possesseurs de dinosaures (machines de plus d'un an donc en jargon d'informaticien) auront à gérer la transition... Pour ma part la machine spécialisée pour le Home Studio restera en XP, mes portables sont déjà sous Vista et ma machine principale est en double boot XP/Vista, la question est donc réglée depuis un moment  :-)

Hasta la .. Vista !

Mieux utiliser les "Master Page" de ASP.NET

Quelle belle invention que les pages maîtres (Master Page) introduites sous ASP.NET 2.0 ! Quand on pense aux contorsions nécessaires pour avoir un menu ou des bandeaux lattéraux sans cet ajout décisif, et pire quand on sait comment il fallait faire ( et qu'il faut encore faire) sous d'autres environnements...

Bref les pages maître c'est super. Mais voilà, il arrive que les pages de contenu doivent accéder à des champs de la page maître, soit pour y lire certaines informations, soit pour en écrire (cas d'un label d'info se trouvant sur une MP dont le contenu est changé par la page courante par exemple).

La (mauvaise) méthode habituelle 

Prenons l'exemple du label (que vous remplacerez mentalement par ce que vous voulez du moment qu'il s'agit d'un objet de la MP). On rencontre très souvent un code qui ressemble à cela dans les pages détails :

Label info = this.Master.Page.FindControl("labelInfo") as Label;
if (info != null) { info.Text = "information sur la page en cours..."; }

 Et encore, le test sur le null est plutôt un luxe qui ne se voit pas dans tous les codes !

Bien entendu cette méthode fonctionne, mais elle n'est franchement pas propre. Au moins pour une raison, c'est qu'une simple erreur de frappe dans le FindControl et c'est le bug, pas trop grave si le test sur null est ajouté comme dans l'exemple de code ci-dessus, mais bug quand même. En aucun cas le compilateur ne peut détecter l'erreur. Dommage pour un environnement fortement typé !

Mais ne riez pas trop vite, on rencontre des horreurs bien pire que le code montré ici : certains n'hésitent pas à transtyper la propriété this.Master.Page pour accéder directement à l'objet ! Et comme un mauvais chemin ne peut mener qu'à faire des âneries de plus en plus grosses, les éléments d'interface étant protected par défaut, pour que ça passe (en force!) les mêmes sont donc contraints de modifier la visibilité de l'objet convoité en le faisant passer en public. Dire que c'est du bricolage est très largement en dessous de la réalité.

Heureusement il existe une solution très simple qui marche et qui permet d'écrire un code de bien meilleur qualité, c'est la directive MasterType, trop peu connue et encore moins utilisée (il y a même des pervers qui en ont entendu parlé mais qui ne s'en servent pas, si, ça existe!).

La (bonne) méthode à utiliser

Elle se décompose en deux temps :

  • Temps 1 : Créer des propriétés dans la page maître
  • Temps 2 : utiliser la directive MasterType

La propriété 

Continuons sur notre exemple du label, il conviendra alors d'écrire quelque chose du genre (dans le code de la page maître donc) :

 public Label InfoLabel { get { return labelInfo; } }

Honnêtement il y a encore mieux à faire, personnellement je n'aime pas voir des objets exposés de la sorte. On créera plutôt une propriété de ce type :

public string Info { get { return labelInfo.Text; } set { labelInfo.Text=value;} }

Chacun ses préférences et ses justifications, nous venons en tout cas de répondre à la première exigence pour faire un code plus propre.

La directive

Dans le fichier .aspx de la page de contenu, il suffit d'ajouter la directive suivante :

<%@ MasterType VirtualPath="~/blabla/LaMasterPageDeMonSite.master" %>

Cette simple directement va instruire l'environnement du vrai type de la page master, et, Ô magie !, c'est la propriété this.Master qui est automatiquement typée correctement !

On peut dès lors reprendre le même code que celui proposé en introduction (celui utilisant FindControl) et le réécrire proprement en bénéficiant d'un typage fort et de tous les contrôles dignes d'un environnement comme ASP.NET :

this.Master.InfoLabel.Text = "info affiché sur la page maître";

On suppose ici avoir utiliser la méthode consistant à publier le label via une propriété. Avec la seconde méthode proposée, encore plus propre, le code sera encore plus simple et lisible :

this.Master.Info = "info affiché sur la page maître";

Conclusion

C'est pas plus joli comme ça ? ! Si, forcément...

Alors maintenant que vous savez que MasterType existe, n'hésitez pas à vous en servir... et ...

Stay Tuned !