• WF4 – Afficher / Cacher des informations dans le designer

    Je consulte depuis quelques jours pas mal de blogs concernant Workflow Foundation dont celui de Jérémy Jeanson qui contient des tonnes d’informations super utiles. En voyant son billet intitulé [WF4] Activity avec vue détail masquée par défaut, vive WPF!, je me suis demandé si il n’existait pas une solution me permettant, à moi en tant que développeur, de faire la même chose sans trop empiéter sur le boulot du designer.

    Donc, le but du jeu, c’est de permettre, dans le concepteur de workflow, de gérer le mode collapse/expand des activités, donc de passer de ca :

    image

    à ça (enfin, avec des informations utiles, pas simplement avec du texte…):

    image

    Comme il ne s’agit de modifier l’apparence du contrôle mais bien d’implémenter une fonctionnalité, je ne suis pas fan d’une approche basée sur des ControlTemplates. Je sais bien qu’il ne s’agit que d’un concepteur pour une activité et donc qu’il y a peu de chance qu’un designer passe par là, mais bon, c’est un reflexe :)

    Pour faire la même chose, écrivons d’abord un convertisseur qui permet de prendre transformer un booleen en Visibility. En plus, ça pourrait resservir dans plein d’autres cas :

       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5: using System.Windows.Data;
       6: using System.Windows;
       7:  
       8: namespace WorkflowConsoleApplication3
       9: {
      10:     public class VisibleIfTrue : IValueConverter
      11:     {
      12:         #region IValueConverter Members
      13:  
      14:         public object Convert(object value, Type targetType, 
      15:             object parameter, System.Globalization.CultureInfo culture)
      16:         {
      17:             if (value is bool)
      18:                 return (bool)value ? 
         Visibility.Visible : Visibility.Collapsed;
      19:             if (value is bool?)
      20:                 return ((bool?)value).GetValueOrDefault() ?
      21:           Visibility.Visible : Visibility.Collapsed;
      22:             return Visibility.Collapsed;
      23:         }
      24:  
      25:         public object ConvertBack(object value, Type targetType, 
      26:             object parameter, System.Globalization.CultureInfo culture)
      27:         {
      28:             throw new NotImplementedException();
      29:         }
      30:  
      31:         #endregion
      32:     }
      33:  
      34:  
      35: }

    L’implémentation est un peu basique, mais on s’en contentera pour les besoins de la démonstration.

    Une fois ce convertisseur réalisé, on va pouvoir s’en servir pour afficher ou cacher le contenu du concepteur :

       1: <sap:ActivityDesigner 
    x:Class
    ="WorkflowConsoleApplication3.ExempleDesigner"
       2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       4:     x:Name="This"
       5:     xmlns:local="clr-namespace:WorkflowConsoleApplication3"
       6:     xmlns:sap="clr-namespace:System.Activities.Presentation;
         assembly=System.Activities.Presentation"
       7:     xmlns:sapv="clr-namespace:System.Activities.Presentation.View;
          assembly=System.Activities.Presentation"
    >
       8:     
       9:     <sap:ActivityDesigner.Resources>
      10:         <local:VisibleIfTrue x:Key="VisibleIfTrue" />
      11:     </sap:ActivityDesigner.Resources>
      12:     <StackPanel>
      13:         <Grid>
      14:             <TextBlock Text="La version minimale du designer" 
    TextWrapping
    ="Wrap" />
      15:         </Grid>
      16:         
      17:         <Grid Visibility="{Binding ElementName=This, 
         Path=ShowExpanded, Converter={StaticResource VisibleIfTrue}}"
    >
      18:             <Label Content="des informations supplémentaires"/>
      19:         </Grid>
      20:  
      21:     </StackPanel>
      22: </sap:ActivityDesigner>

    Voila ! évidemment, si vous voulez intervertir les deux blocs au lieu de cacher seulement le second, il vous faudra faire le même travail (mais avec un VisibleIfFalse) sur la première grid.

  • The infamous case of IQueryable.First()

    Préambule : Ceci n’est absolument pas un billet destiné à râler sur un bug ou sur un défaut de conception honteux dans LINQ / C# 3.0 : le comportement décrit ci-dessous est une erreur de programmation de notre coté (voir Préambule N°2) et non quelque chose d’induit par LINQ/IQueryable. Cela nous apprendra à ne pas lire la documentation d’une méthode et à ne pas faire attention à la méthode juste en dessous dans la boite de dialogue Intellisense.

    Préambule N°2: Nous connaissons IQueryable.FirstOrDefault() qui constitue la réponse aux mots maux (raaaah des fois c’est bien de se relire, ca évite de passer pour un c…ne sachant même pas faire la différence entre “mots” et “maux”) décrits ci dessous.

    Cette fin de matinée, un samedi qui plus est, a été source de grande joie pour moi : je viens de passer un peu plus de deux heures sur un bug d’une incroyable stupidité…

    L’un des nos clients, en ces derniers jours de préparation de colis pour Noël, a eu besoin de l’écriture en urgence d’un outil destiné à remplacer des terminaux défectueux. Aussi tôt dit, presque (il fallait quand arriver à trouver le temps de programmer…) aussitôt fait : nous avons réalisé une mini-application à partir de WinForms et LINQ. Celle-ci, bien qu’elle n’est pas près de figurer dans la catégorie des applications les mieux conçues, remplissait son métier… enfin… pendant quelques heures : ce matin, catastrophe, il y a beaucoup de messages d’erreurs et cela ralenti considérablement une logistique déjà sous pression.

    Les symptômes sont assez curieux : lors du traitement, les gens obtiennent souvent une erreur correspondant à des règles de gestion internes alors qu’ils sont sûr que celles-ci sont bien respectées. Chose encore plus étrange, parfois un simple nouvel essai permet de corriger le problème, d’autre fois il suffit d’attendre un peu et de re-essayer et cela fonctionne. Après examen du code, nous réduisons le code “fautif” à ces quelques lignes :

    var produits = from ... in linqContext. ...
          where .....
         select ....;
     
    try
    {
       var produit = produits.First();
       if(produit == null)
       {
           //... affiche l'erreur ...
       }
       else
       {
          // ...fait le traitement nécessaire...
       }
    }
    catch // plusieurs blocs catch pour traiter les différents cas
    {
        //... affiche l'erreur ...
    }

     

    Code qui me semble parfaitement valide, j’entreprends donc de monter Visual studio sur l’environnement de production et de commencer à tester en production. Après quelques ratés, nous trouvons des cas où, effectivement, l’erreur apparaît sans raison. Celle ci est affichée par une InvalidOperationException lors de l’appel à IQueryable.First() et dont le message est “le résultat ne contient aucune ligne” (ou quelque chose du genre, je n’ai plus le message en tête). Et la, donc, drame : IQueryable.First() renvoie sous forme d’erreur quelque chose qui n’est, pour moi, pas de l’ordre de l’exception mais bien d’un traitement “normal”.

    Hormis le fait que la documentation MSDN ne liste pas cette exception dans celles possibles pour cette méthode (pour la première fois de ma vie, l’obligation de déclaration des exceptions propre à java m’a semblé utile… comparé aux milliers de fois où il m’a énervé lorsque j’en faisait ^^), le retour sous forme d’exception m’a quelque peu choqué. Combien de fois, en effet, avez vous écrit en (T-) SQL : select top 1 … from …. ? Dans ce genre de cas, car c’est bien cela que semble exprimer IQueryable.First(), on peux raisonnablement s’attendre à ce que le résultat “il n’y a pas de première ligne”, ne soit pas de type “exceptionnel”.

    Tout cela pour dire que, si vous mettez à disposition d’une équipe de développeurs un composant, il est primordial de bien comprendre et de bien définir le contrat que votre composant propose, et en particulier d’évaluer le plus possible ce qui tient des postulats “implicites” de vos fonctions (dans le cas présent, le postulat étant qu’un IQueryable ne peut pas contenir 0 éléments) et de les documenter au fur et à mesure que ceux-ci apparaissent (certain n’apparaîtront en effet que longtemps après l’implémentation, lorsqu’un développeur aura la mauvaise idée de ne pas le(s) respecter).

    Comme je le disais en préambule, l’équipe chargée de cette partie de LINQ/C# 3.0 a parfaitement géré le problème : il existe deux méthodes : First() et FirstOrDefault() (conservant même la notion de …OrDefault propre aux objets “nullables”) qui permettent de choisir le type de comportement que l’on souhaite.

    Pour la petite histoire, l’erreur ne provenait absolument pas de cette erreur. Nous n’affichions pas le message d’erreur exact, peut-être, mais le message d’erreur était proche de ce qu’il devait être. Le fautif a fini par être trouvé : les outils de saisie, parfois, transmettaient des informations incohérentes (quand je vous disais que le bug était d’un incroyable stupidité…).

    Post-Scriptum : Ce billet ne doit son existence qu’à “de saines lectures”, en l’occurrence Eric Lippert. L’un de ses billets, lu par hasard il y a quelques jours, a très fortement résonné dans mon esprit ce matin pendant cette phase de “debug en production” et, sans celui-ci, j’aurais probablement haussé les épaules et continué. Bien que le sujet ne soit pas exactement le même, il y parle du même type de problématique : l’implémentation d’une méthode, ainsi que les postulats implicites de l’équipe développant un composant, font partie intégrante du contrat.

  • Conseils pour développeurs (asp.net) du dimanche

    Cela fait quelques semaines que j'interviens de façon ponctuelle sur la maintenance d'un extranet pour un client, et que je m'énerve régulièrement sur l'incapacité de la plupart des développeurs à comprendre qu'un code doit être un minimum propre... L'application, écrite en asp.net, est un ensemble de morceaux de code réalisés, probablement, à la va-vite par des CDD si j'en juge par la "qualité" très médiocre.

    Voici donc quelque conseils à retenir si vous faites du développement ASP.net et que vous souhaitez voir votre code être maintenu...

    N'accèdez jamais, directement dans votre page, à des variables de sessions ou d'application : c'est probablement le B-A-BA du développement avec des langages typés... Il n'est pas normal de voir dans une page ASP.net (sauf cas exceptionnels) des accès du type :

    (int) Session["MEMBRE_ID"] // non !!!!!

    Pourquoi cela n'est-il pas bien ? eh bien tout simplement parce que si une autre page accède (ou encore plus catastrophique met à jour) votre valeur, il y a 1 chance sur 2 pour que celle-ci ne manipule pas le même type de données (dans l'exemple de l'application extranet, l'une des pages traitait l'identifiant client comme un decimal et l'autre comme un int...). Pour réaliser un code maintenable, il vous reste à faire une class "Helper" qui se charge de masquer l'accès à la variable de session et le typage :

    public class SessionHelper
    {
        public int MembreId
        {
            get
            {
                object o = Session["MEMBRE_ID"];
                if (o == null || !(o is int))
                    return 0;
                return (int)o;
            }
            set
            {
                Session["MEMBRE_ID"] = value;
            }
        }
    }

    Avec ça, plus jamais de "InvalidCastException" - enfin, si je ne me suis pas trompé dans l'écriture de la classe :) - et vous risquez moins de problème qu'à tout stocker en string (je pense principalement à des soucis d'injection sql...). Sans parler du fait que vous ne chercherez plus si vous avez appelé votre variable "MEMBRE_ID", "IdMembre", "MEMBREID", ou tout autre variation...

    Dans le même ordre d'idée, ne réalisez jamais les updates sans typer vos variables : à moins de réaliser une application poubelle et donc d'utiliser le RAD à 100%, vous avez probablement écrit des méthodes (si ce n'est des objets) pour l'implémentation de vos règles métiers et de vos accès aux données. Bien ! mais pitié, n'utilisez pas des signatures du type :

    public static Membre GetMembre(string membreId)
    {
         // non !!!!!
    }
    

    lorsque vous savez que membreId est un int, cela fait désordre. Lorsque je vois ce genre de signature, cela m'effraie toujours un peu : si le développeur n'a même pas été capable de typer ses variables, il y a peu de chance qu'il ai fait des requêtes paramétrées, et c'est donc une porte ouverte à l'injection SQL...

    Si vous avez des cas complexes dans vos règles de gestion, découpez le problème en différentes méthodes axées chacune sur la résolution d'un problème simple. Ce conseil la, je pensais vraiment que tout le monde l'avait en tête, mais je me trompais lourdement... Toujours sur le même exemple (oui, cela pourrait être un cas d'école cet extranet, pour le cours "programmez comme des pieds, c'est mieux !"), l'application présente une menu général dont les fonctionnalités changent en fonction de critères multiples (droits accordé à un client, type de structure dont il fait partie, etc.), voici (extrêmement simplifiée) la façon dont cela est traité :

    string matricePossibilite = "";
    matricePossiblite += Session["MEMBRE_TYPE"].ToString();
    matricePossibilite += Session["..."].ToString();
    // il y a encore 3 autres criteres comme ceci...
    
    switch (matricePossibilite)
    {
        case "11012":
            // une bonne cinquentaine de lignes de code
            break;
    
        case "21012":
            // d'autres lignes...
            break;
    }

    Non, non, vous ne rêvez pas, c'est bel est bien écrit comme cela, avec un minimum de commentaires, pour la plupart inutiles d'ailleurs, histoire de simplifier l'affaire... Hormis le fait que tout le code se trouve dans la même méthode, c'est aussi typiquement le cas ou un switch est une très mauvaise idée : cela rends plus difficile à lire. Admettons que l'on garde le switch pour une premiere ré-écriture, il est déjà plus qu'obligatoire de virer toutes ces lignes de codes :

    string matricePossibilite = "";
    matricePossibilite += Session["MEMBRE_TYPE"].ToString();
    matricePossibilite += Session["..."].ToString();
    // il y a encore 3 autres critères comme ceci...
    
    switch (matricePossibilite)
    {
        case "11012":
            NomDuCasNumero1();
            break;
    
        case "21012":
            NomDuCasNumero2();
            break;
    }
    

    C'est déjà un poil plus lisible... Il reste ensuite, soit à bien documenter ce que signifie chacun des cas :

    case "11012":
    // l'utilisateur est de type ...
    // dans une structure ....
    // etc

    Ou même encore mieux : ne pas utiliser de variable bizarre pour déterminer les droits mais des variables avec des noms compréhensibles :

    bool estAdmin = SessionHelper.MembreType == MembreType.Admin;
    TypeStruct type = SessionHelper.TypeStructure;
    // les autres critères selon le même principe
    
    if(estAdmin)
    {
        if(...) // un autre critère
            NomDuCasNumero1();
        else
            NomDuCasNumero2();
    }
    else if(!estAdmin && type==TypeStruct.Type1)
    {
        NomDuCasNumero3();
    }
    // etc.
    

    C'est un peu plus verbeux, et cela demandera certainement un peu de réflexion pour ne pas tomber dans une liste de if/else if ou une cascade de if imbriqués - pensez de nouveau à découper en plusieurs méthodes si vous avez trop de if... - mais qu'est-ce que c'est plus simple à comprendre !

    Si vous êtes adepte des procédure stockées (ce n'est pas mon cas, mais bon, tous les goûts sont dans la nature...), n'hésitez pas à les nommer proprement : mbr_s c'est bien comme nom mais MembreSuppr c'est carrément plus lisible... et tant qu'à faire, pensez à définir des méthodes-metier dont les noms sont en relation avec celui de la proc-stock.

     

    Voila, ce sont les quelques conseils/remarques qui me sont venus à l'esprit au cours des heures passées à me battre avec cette application. Pour résumer :

    • la plupart des langages actuels sont fortement typés (cela n'a pas que des avantages, mais évite certains dérapages...), assurez vous donc que votre code en soit conscient et compense automatiquement les cas où le typage est plus léger (pour asp.net, il faut comprendre par là : tout ce qui à été conservé compatible avec asp...)
    • votre code doit pouvoir être compris dans son ensemble par un autre développeur sans avoir à lire 2000 pages de document Word ou sans avoir besoin de lire dans vos pensées, un peu de commentaires et surtout un code lisible qui peut se comprendre facilement est très souvent préférable à un code plus optimisé - ou du moins qui vous paraît plus optimisé.
    • ah...oui... pour avoir eu aussi un problème de code source livré différents de l'application en production : utilisez un système de gestion de source : il est rageant de devoir décompiler la version de production pour retrouver des bouts de code...

    Et pour thierry, si il passe sur ce billet : essayez de trouver un prestataire qui ne soit pas en carton pour la prochaine fois :)

  • Tiens, un générateur de code qui sait parler anglais...

    Bon, c'est pas hyper passionnant, mais je viens de voir une petite chose amusante dans LINQ. J'ai deux tables Categories et MetaCategories dans mon schéma de base de données (bon, je vous fait pas l'affront de vous dire qu'il y a une relation parent-enfant entre les deux tables... ah bah si tiens, je l'ai dit...) et LINQ To SQL m'a automatiquement crée les liens entre les tables dans les classes qu'il a créé.

    Jusque là, rien de bien exceptionnel me direz vous... Oui, mais ce qui m'a amusé c'est que les objets de données ont été appelés "Category" et "MetaCategory", avec un "y" : LINQ a detecté une forme plurielle et a fait de lui même la conversion... Comme il est sympa !

    Pour naviguer dans mes tables je peut donc par exemple :

    • faire un from ... MetaCategories ... select ...
    • ce select me renvoi un MetaCategory[] (enfin pour les puristes, pas tout à fait, mais on peut simplifier en disant cela)
    • sur un objet MetaCategory, la propriété Categories permet d'obtenir obtenir ses enfants

    J'espère que MS à prévu l'internationalisation de ce truc, ca serait vraiment bien !

  • Conversion chiffres vers lettres

    Voici une petite classe qui permet de convertir un nombre donnée en lettres.Le code est en C#, et devrait donc fonctionner sur toute la plateforme .net

    /// <summary>
    /// Conversion de chiffres vers lettres.
    /// </summary>
    public class Convertisseur
    {
      /// <summary>
      /// Converti une valeur en euros
      /// </summary>
      /// <param name="valeur">
      /// La valeur à convertir</param>
      /// <returns>Une chaine correspondant au nombre</returns>
      /// <remarks>format xxx euro et yyy cent(s)</remarks>
      public static string ConvertirEuro(decimal valeur)
      {
        return Convertir(valeur, 2, " euro", " cent(s)", " et ");
      }
    
      /// <summary>
      /// Conversion d'une valeur decimal en lettres
      /// </summary>
      /// <param name="valeur">Valeur à convertir</param>
      /// <param name="nbDecimales">
      /// Nombre de décimale à conserver&lt;/param>
      /// <returns>Une chaine correspondant au nombre</returns>
      /// <remarks>Pas d'unités, séparateur = ","</remarks>
      public static string Convertir(decimal valeur,
        int nbDecimales)
      {
        return Convertir(valeur, nbDecimales, "", "", ",");
      }
    
      /// <summary>
      /// Conversion d'une valeur decimal en lettres
      /// </summary>
      /// <param name="valeur">La valeur à convertir</param>
      /// <param name="nbDecimales">
      /// Le nombre de decimales à conserver</param>
      /// <param name="uniteEntiere">
      /// Le nom des unités de la partie entière</param>
      /// <param name="uniteDecimale">
      /// Le nom des unité de la partie décimale</param>
      /// <param name="separateur">le séparateur entre les parties</param>
      /// <returns>Une chaine correspondant au nombre</returns>
      public static string Convertir(decimal valeur, 
        int nbDecimales, 
        string uniteEntiere, 
        string uniteDecimale, 
        string separateur)
      {
        valeur = Math.Round(valeur,nbDecimales);
    
        int val = (int) Math.Floor((double) valeur);
        string ret = Convertir(val) + uniteEntiere;
    
        valeur = valeur - val;
        valeur = valeur * (int) (Math.Pow(10,nbDecimales));
        val = (int) Math.Floor((double) valeur);
        if(val>0)
          ret += separateur + Convertir(val) + uniteDecimale;
        
        return ret;
      }
    
      /// <summary>
      /// Conversion d'un entier en lettre
      /// </summary>
      /// <param name="nombre">
      /// L'entier à convertir</param>
      /// <returns>Une chaine correspondant au nombre</returns>
      public static string Convertir(int nombre)
      {
        StringBuilder lettre = new StringBuilder();
        int centaine, dizaine, unite, reste, y;
        reste = nombre;
        
        for(int i=1000000000; i>=1; i/=1000)
        {
          y = reste/i;
          if(y!=0)
          {
            centaine = y/100;
            dizaine  = (y - centaine*100)/10;
            unite = y-(centaine*100)-(dizaine*10);
            switch(centaine)
            {
              case 0:
                break;
              case 1:
                lettre.Append(Convert(centaine*100));
                lettre.Append(" ");
                break;
              default :
                lettre.Append(Convert(centaine));
                lettre.Append(" ");
                lettre.Append(Convert(100));
                if((dizaine == 0)&&(unite == 0)) lettre.Append("s ");
                else lettre.Append(" ");
                break;
            }
                
            switch(dizaine)
            {
              case 0:
                if(unite!=1 || (unite==1 && i!=1000) )
                {
                  lettre.Append(Convert(unite));
                  if(unite!=0) lettre.Append(" ");
                }
                break;
              case 1:
                lettre.Append(Convert(dizaine*10+unite));
                lettre.Append(" ");
                break;
              case 7:
                goto case 1;
              case 9:
                goto case 1;
              default :
    	    lettre.Append(Convert(dizaine*10));
                if(unite==1 && dizaine!=8) lettre.Append("-et-");
                else lettre.Append(" ");
                lettre.Append(Convert(unite));
                lettre.Append(" ");
                break;
    
            } 
            switch (i)
            {
              case 1000000000:
                if(y>1) lettre.Append("milliards ");
                else lettre.Append("milliard ");
                break;
              case 1000000:
                if(y>1) lettre.Append("millions ");
                else lettre.Append("million ");
                break;
              case 1000:
                lettre.Append("mille ");
                break;
            }
          } 
          reste -= y*i;
        } // end for
        if(lettre.Length ==0) return "zero"; 
        
        return lettre.ToString().Trim();  
      }
    
      private static string Convert(int nb)
      {
        switch(nb)
        {
          case 0: return "";
          case 1: return "un";
          case 2: return "deux";
          case 3: return "trois";
          case 4: return "quatre";
          case 5: return "cinq";
          case 6: return "six";
          case 7: return "sept";
          case 8: return "huit";
          case 9: return "neuf";
          case 10: return "dix";
          case 11: return "onze";
          case 12: return "douze";
          case 13: return "treize";
          case 14: return "quatorze";
          case 15: return "quinze";
          case 16: return "seize";
          case 17: return "dix-sept";
          case 18: return "dix-huit";
          case 19: return "dix-neuf";
          case 20: return "vingt";
          case 30: return "trente";
          case 40: return "quarante";
          case 50: return "cinquante";
          case 60: return "soixante";
          case 70: return "soixante-dix";
          case 71: return "soixante-onze";
          case 72: return "soixante-douze";
          case 73: return "soixante-treize";
          case 74: return "soixante-quatorze";
          case 75: return "soixante-quinze";
          case 76: return "soixante-seize";
          case 77: return "soixante-dix-sept";
          case 78: return "soixante-dix-huit";
          case 79: return "soixante-dix-neuf";
          case 80: return "quatre-vingt";
          case 90: return "quatre-vingt-dix";
          case 91: return "quatre-vingt-onze";
          case 92: return "quatre-vingt-douze";
          case 93: return "quatre-vingt-treize";
          case 94: return "quatre-vingt-quatorze";
          case 95: return "quatre-vingt-quinze";
          case 96: return "quatre-vingt-seize";
          case 97: return "quatre-vingt-dix-sept";
          case 98: return "quatre-vingt-dix-huit";
          case 99: return "quatre-vingt-dix-neuf";
          case 100: return "cent";
          case 1000: return "mille";
        }
        return "";
      }
    }