blog.pagesd.info

Aller au contenu | Aller au menu | Aller à la recherche

lundi 26 juillet 2010

Contexte d'une recherche Exalead SDK

Quoiqu'en dise la documentation, il ne faut surtout pas utiliser la forme :

searchQuery.SearchContext = mon_contexte;

Mais obligatoirement la syntaxe :

searchQuery.AddParameter("C", mon_contexte);

En tout cas avec Exalead one:enterprise 4.6.

lundi 7 décembre 2009

Les formulaires de mise à jour en ASP.NET MVC

J'avais prévu de finir la sixième partie du tutoriel NerdDinner la semaine dernière et ça n'a pas marché. Par rapport aux autres parties, je ne peux pas dire que ça soit beaucoup plus compliqué à suivre, c'est juste que c'est un peu plus long. Sans compter que les conditions de travail étaient particulièrement bruyantes cette semaine (quoique ?).

Ce qui fait que j'ai préféré me disperser plutôt que de rester des heures sans pouvoir réellement avancer :

Ces petits écarts ont quand même réussi à me maintenir à peu près d'attaque pour venir à bout du tutoriel. Mais par contre, ça n'a pas été suffisant pour progresser autant sur la traduction.

Le problème, c'est que si dans la journée je patauge pour suivre le tutoriel, ça fait qu'en soirée j'ai beaucoup de mal à avancer sur la traduction puisque c'est des trucs que je n'ai pas encore vu ou suffisamment assimilés... C'est pour cela que malgré mes prévisions pourtant très mesurées, je n'ai pour l'instant réussi à traduire que les 12 premières pages du tutoriel, consacrées majoritairement à la partie Edit de l'application NerdDinner.

Et pour la suite, je n'ai qu'un tout petit plus de la moitié des pages restantes prêtes pour la relecture (et malheureusement, comme j'ai fait ça dans le désordre, je ne suis pas sûr de pouvoir mettre en ligne les pages consacrées au ModelState avant mercredi).

Alors pour me reposer de ma semaine, je me suis laissé aller à programmer pour de vrai. Ca m'a permis de pas mal améliorer le système de rechercher / remplacer qui fait passer le fichier de dans lequel j'enregistre la traduction du format Word au format Wiki de Dotclear. Pour cela, je suis parti de l'application CleanWordHtml de Jeff Atwood. Maintenant, je n'ai plus qu'à exporter la traduction au format HTML filtré et à lancer ma petite application et le résultat obtenu est quasiment prêt à coller sous Dotclear.

Et grâce à ça, j'ai découvert l'existence de deux fonctions trop pratiques : File.ReadAllText() et File.WriteAllText(). C'est des petits trucs comme ça qui devraient suffire à tout le monde pour admettre qu'il y a encore beaucoup de travail avant de pouvoir réaliser proprement une vraie application en ASP.NET 3.5 (MVC ou non). Ou alors, on se contente simplement de refaire plus ou moins bien en .NET 3.5 ou 4.0 ce qu'on a l'habitude de faire en .NET 1.1...

mercredi 25 novembre 2009

VS 2008, SQL Server 2008 Express et Windows 7 64 bits

Depuis que j'ai mon nouveau portable, je galère comme pas possible pour m'y sentir comme chez moi et laisser tomber mon ancien PC. La principale cause de tout ces problèmes (mais pas la seule), c'est que je suis maintenant sous Windows 7 en version 64 bits (alors que j'aurais très bien pu m'en passer vu que je reste scotché à 4 GO de mémoire à cause d'un tranchement entre fromage ou dessert).

Le dernier problème en date c'est SQL Server Express 2008 (avec Service Pack 1 s'il vous plait). J'ai commencé par passer un temps fou à installer / désinstaller / réinstaller / combiner / décombiner / recombiner Visual Studio 2008 Professional Edition, SQL Server Express 2005 et SQL Server Express 2008 pour réussir en début d'après-midi à faire fonctionner mes quelques essais d'applications ASP.NET MVC que j'avais créées sur mon vieux PC sous XP.

Ce matin, j'avais encore le message d'erreur suivant :

The database 'BLABLA.MDF' cannot be opened because it is version 655. This server supports version 612 and earlier. A downgrade path is not supported.

(en fait, mon message d'erreur à moi était en français mais je l'ai perdu, si c'est pas malheureux...)

Je bataille tant que je peux pour désinstaller complètement SQL Server Express 2005 puis installer SQL Server Express 2008 à partir du Web Platform Installer et ça le fait presque : je n'ai plus d'erreur quand je lance mes projets ContactManager et NerdDinner !!!

Mais il reste encore un problème : pas possible de créer une nouvelle base de données. Je vais sur App_Data, je clique-droit, je fais "Ajouter", je fais "Nouvel élément...", je choisis "Base de données SQL Server", je laisse le nom "Database1.mdf" par défaut, je clique sur le bouton "Ajouter" et patatras :

SQL Server Express 2005 doit fonctionner correctement pour permettre les connexions aux fichiers SQL Server (*.mdf). Vérifiez l'installation du composant ou téléchargez-le à partir de l'URL suivante : http://go.microsoft.com/fwlink/?LinkId=49251

ou en anglais pour faciliter les recherches sous Google :

Connections to SQL Server files (*.mdf) require SQL Server Express 2005 to function properly. Please verify the installation of the component of download from the URL: http://go.microsoft.com/fwlink/?LinkId=49251 (http://go.microsoft.com/fwlink/?LinkId=49251)

Et en fait c'est bigrement compliqué pour trouver la bonne réponse sous Google, mais après des tas de liens, je réussi à tomber sur LA solution : c'est une fonctionnalité bien connue (en tout cas depuis janvier 2009) de Visual Studio 2008 SP1 quand on se connecte à une instance 64 bits de SQL Server Express 2008. Y'a même un article n° 957944 tout exprès pour ça dans la Knowledge Base de Microsoft : FIX: Error message when you connect to a 64-bit instance of SQL Server Express 2008 by using Visual Studio 2008 Service Pack 1 (SP1): "Connections to SQL Server files (*.mdf) require SQL Server Express 2005 to function properly".

Comme je suis quelqu'un de très conciliant, je fait tout ce qu'on me demande et j'appelle le support Microsoft. Je me retrouve je ne sais trop où (hello la verte Erin ?) et après quelques interludes musicaux y'a un monsieur qui vérifie une dernière fois que je j'ai bien un problème puis me demande mon adresse mél pour m'envoyer le correctif.

J'aurais lui donner mon adresse GMail parce que ça fait une demi-heure et je ne vois toujours rien venir...

Mise à jour 24 heures après

Toujours pas de nouvelle de Microsoft. Et pourtant, j'ai aussi mis sur le coup un ex-collègue qui a travaillé à Dublin dans un centre d'appel à destination des développeurs MS. Comme j'en ai eu marre d'attendre, j'ai lancé une recherche sur "hotfix 957944" et j'ai eu droit à tout plein de réponses, dont une sur StackOverflow que je me suis empressé de consulter en premier. Et là, ils expliquent même comment télécharger le fameux hotfix 957944 en direct...

Je fais la manipulation indiquée => ça me demande mon adresse mail. Ce coup-ci je donne mon compte gmail et même pas 5 secondes après j'ai un nouveau message dans mon Inbox avec un lien pour aller récupérer ce satané hotfix. Je télécharge, j'installe, ça décompacte un VS90SP1-KB957944-x86.exe que je lance, ça mouline et c'est fini ! Reste plus qu'à lancer VS 2008 pour vérifier que je peux enfin créer une base de données et ça marche !!!

La meilleure façon d'obtenir quelque chose de Microsoft, c'est de demander à Google d'aller rechercher sur StackOverflow :)

jeudi 8 octobre 2009

Tuer du code pour réussir

Dans les mois à venir, je vais avoir à participer à la re-écriture en ASP.NET MVC d'une application existante développée depuis des années en ASP.NET 1.1.

Après de longues tergiversations, il a bien fallu reconnaitre qu'une simple migration en ASP.NET 3.5 et de simples rustines n'étaient ni suffisantes ni envisageables (même pas en rêve mon ami !).

Le problème avec cette application, c'est qu'elle est le résultat d'un empilement continuel de nouvelles fonctionnalités (une fuite en avant perpétuelle ?) et qu'à aucun moment il a été accepté de prendre des pauses pour réaliser les consolidations nécessaires si on voulait que l'ensemble puisse évoluer (voire fonctionner) sereinement.

Ce qui fait qu'aujourd'hui, on a la chance d'avoir une grosse ... avec dans le tas des trucs certainement utiles mais aussi des trucs complètements stupides, des trucs réalisées pour faire plaisir à des gens qui n'ont jamais mis les pieds dans l'application, des trucs développés et toujours pas passés en production 1 an près, des trucs pour que "ça se fait tout seul y a rien à faire", des trucs pour contourner d'autres trucs qui ne marchent pas comme on veut qui faudrait que ça marche, des trucs qu'on est pas sûr que ça serve mais yaka les laisser, des trucs qui prennent une éternité pour faire 3 fois rien, des trucs qui ont été demandés parce qu'on sait jamais ça pourrait toujours servir un jour ou l'autre...

Pour résumer ma pensée, il est plus qu'indispensable de commencer par un énorme travail de réflexion sur le contenu de ce ramassis avant même de penser à démarrer un Visual Studio. Et par réfléchir j'entends supprimer ce qui ne sert pas, supprimer ce qui ne marche pas, supprimer ce qui n’est pas indispensable, supprimer ce qui n’est pas faisable, supprimer ce qui prendra trop de temps à faire, etc… En bref éviter de re-développer des fonctionnalités en pagaille et prendre le risque de re-venir à notre point de départ...

Mon souci c'est que le spectre d'une migration iso-fonctionnelle flotte encore dans l'air : on part de tout ça et au final on veut toujours tout ça (et un peu plus) mais en mieux. Et comme on est très conciliants, on pourrait à la rigueur accepter de sacrifier quelques écrans à condition qu'on retrouve leurs fonctionnalités dans d'autres écrans...

Voilà donc un de mes stress du moment. Mais ce matin, qu'est-ce que je découvre dans mon Bloglines ? Un article des créateurs de Scout qui explique qu'en supprimant des tonnes de code (y compris des fonctionnalités), ils ont amélioré leur application tant d'un point de vue performance (jusqu'à x 10 sur les traitements les plus couteux) que satisfaction de leurs clients. Et en plus ils nous nous livrent les deux leçons que l'on peut en retirer :

1. Arrêter de vouloir faire simple pour l'utilisateur final au prix d'une tarabiscotisation dans le code, mais chercher la simplicité dans la façon de faire (et perso, je pense que si c'est simple à faire, c'est évident à utiliser).

It turns out that simplicity and elegance for the end user can mean some awfully complicated stuff behind the scenes to make it work.

2. Arrêter de perdre son temps sur l'ajout de nouvelles fonctionnalités soi-disant incontournables qui vont cannibaliser notre temps et notre énergie juste pour les mettre au point

If you’re running a web application and the majority of your work is spent delivering the service you advertised to your customers, you’re probably in bad shape.

Ca, ça me plait !

vendredi 5 juin 2009

Ouvrir un document en dehors du navigateur

Quand on vient comme moi de changer de PC et que son nouveau Microsoft Internet Explorer a la facheuse habitude d'ouvrir les fichiers Word dans le navigateur plutôt que de lancer le Microsoft Word qui va bien, il ne faut pas hésiter à aller trifouiller dans le paramètres de l'explorateur de fichiers :

  • cliquer sur le menu "Outils"
  • choisr le sous-menu "Option des dossiers..."
  • aller sur l'onglet "Types de fichiers"
  • rechercher la ligne "DOC - Document Microsoft Word" (c'est dans l'ordre alphabétique)
  • cliquer sur le bouton "Avancé"
  • décocher "Parcourir dans une même fenêtre"
  • valider en cliquent sur le bouton "OK"
  • fermer la fenêtre "Option des dossiers" en cliquant sur le bouton "Fermer"

Pour faire bonne mesure et parce qu'on a rien à perdre, le mieux c'est ensuite de fermer les fenêtres de toutes les applications en cours, d'arrêter l'ordinateur puis de le redémarrer après avoir compté jusqu'à 5 (un, deux, trois, quatre et cinq).

Ou bien alors, avant d'en arriver à cette extrémité, on peut aussi en profiter pour répéter l'opération pour les fichiers Microsoft Excel, Adobe PDF, etcétéra...

jeudi 13 novembre 2008

Installer le client ODBC pour Sybase SQL Anywhere 9

Pour mon super état des marges ultra prioritaire, je dois intégrer des données en provenance de la paye. Le problème c'est que le logiciel de paie utilise une base de données SQL Anywhere 9 et qu'il faut donc que je trouve comment m'y connecter. Et ben c'est pas si évident que ça pour trouver de l'information sur comment faire et encore moins pour arriver à récupérer un client pour Sql Anywhere 9 qui fasse l'affaire. Ou alors il faut être prêt à se tartiner des kilomètres de documentation et installer à tire larigot jusqu'à tomber sur le truc qui va bien.

Mais quand on a autant de temps que moi et qu'on ne veut pas encombrer son PC de tout un tas de trucs inutiles, il faut savoir se débrouiller (et aussi se contenter d'ODBC).

Pour commencer, il faut aller dans le dossier C:\Program Files\Sybase\SQL Anywhere 9\win32 d'une machine où SQL Anywhere 9 est déjà installé.

Une fois là, récupérer les 3 fichiers suivants :

  • dbodbc9.dll
  • dblgen9.dll
  • dbcon9.dll

Eventuellement, on peut aussi prendre les 2 fichiers suivants :

  • dblgfr9.dll : dblgen9.dll en français
  • dbcoen9.chm : fichier d'aide

Créer un dossier "C:\SQL Anywhere 9" et y copier les fichiers.

On dispose maintenant d'un répertoire contenant tous les fichiers nécessaires pour le bon fonctionnement du pilote ODBC de Sybase SQL Anywhere 9. Il reste simplement à configurer la base de registre pour qu'il soit possible de se connecter à Sybase via le pilote ODBC.

On continue et on lance regedit.exe pour ouvrir la base de registre

Aller dans HKEY_LOCAL_MACHINE \ SOFTWARE \ ODBC \ ODBCINST.INI. Là il faut se placer sur la clé "ODBC Drivers" et demander à y créer une nouvelle valeur chaine que l'on appellera "SQL Anywhere 9". Une fois cette valeur créée, double-cliquer dessus pour y enregistrer la valeur "Installed".

Rester dans HKEY_LOCAL_MACHINE \ SOFTWARE \ ODBC \ ODBCINST.INI et y créer une nouvelle clé "SQL Anywhere 9" (le même nom que la chaine créée auparavant). Se placer ensuite sur la clé que l'on vient de créer et ajouter les 2 valeurs chaines suivantes :

  • Driver
  • Setup

Puis pour chacune de ces deux chaines, enregistrer la valeur "C:\SQL Anywhere 9\dbodbc9.dll".

Ca y est, c'est quasiment fini.

On peut maintenant lancer l'administrateur de sources de données ODBC, cliquer sur le bouton "Ajouter" dans l'onglet "Sources de données système" et choisir le pilote "SQL Anywhere 9" pour obtenir la boite de dialogue permettant de configurer la connexion ODBC au serveur SQL Anywhere 9 :

odbc-sql-anywhere-9.png

A partir de là, il suffit de renseigner les différents informations qui vont bien, soit dans mon cas à peu près :

  • Onglet ODBC - Nom de source de données : BasePayeProd
  • Onglet ODBC - Description : Base de données Sybase de production pour la paie
  • Onglet Connexion - ID utilisateur : XXXXXX
  • Onglet Connexion - Mot de passe : XXXXXXXXX
  • Onglet Base de données - Nom du serveur : BasePayeProd
  • Onglet Réseau - TCP/IP : coché

Puis revenir sur l'onglet ODBC pour vérifier que tout est OK en cliquant sur le bouton "Tester la connexion".

Et quand on a comme moi on a la chance d'avoir un ODBCTST.EXE from Oracle, on peut immédiatement se connecter à la base de données pour vérifier que ça marche pour de vrai.

Ensuite, sous Visual Studio, on peut enfin se connecter à la base de donnée Sybase avec la commande suivante :

OdbcConnection db = new OdbcConnection("DSN=BasePayeProd");

lundi 29 septembre 2008

Répéter un formulaire PDF sur plusieurs pages

Pour parvenir à faire un état sur plusieurs pages en remplissant plusieurs fois le même formulaire PDF avec des données différentes, je n'ai finalement pas eu trop à me creuser la tête. J'ai simplement interrogé le tout nouveau site Stack Overflow qui m'a gentiment expliqué How do I programmatically create a PDF in my .NET application?

namespace Altrr.iText {

    using System;
    using System.Collections;
    using System.IO;
    using iTextSharp.text;
    using iTextSharp.text.pdf;

    /// <summary>
    /// Tests du composant iTextSharp
    /// </summary>
    public class Start {

        /// <summary>
        /// Point d'entrée principal de l'application.
        /// </summary>
        [STAThread]
        static void Main (string[] args) {

            string pdfSource = @"D:\Altrr\iText\register_form1.pdf";
            if (args.Length == 1) {
                pdfSource = args[0];
            }
            FillFields(pdfSource);

        }

        /// <summary>
        /// Remplis les différents champs d'un formulaire PDF
        /// </summary>
        static void FillFields (string pdfSource) {

            // Création d'un objet PDF Reader basé sur le formulaire PDF
            Console.WriteLine(pdfSource + " :");

            string pdfRempli = pdfSource.Replace(".pdf", "_test.pdf");
            Document doc = new Document();
            PdfCopy copy = new PdfCopy(doc, new FileStream(pdfRempli, FileMode.Create));
            doc.Open();

            for (int i = 0; i < 10; i++) {

                PdfReader pdfReader = new PdfReader(pdfSource);

                MemoryStream temp = new MemoryStream();
                PdfStamper pdfStamper = new PdfStamper(pdfReader, temp);

                AcroFields fields = pdfStamper.AcroFields;
                fields.SetField("person.name", i.ToString() + "Laura Specimen");
                fields.SetField("person.address", i.ToString() + "Paulo Soares Way 1");
                fields.SetField("person.postal_code", "F00b4R", "FOOBAR");
                fields.SetField("person.email", i.ToString() + "laura@lowagie.com");
                fields.SetField("person.programming", "JAVA");
                fields.SetField("person.language", "FR");
                fields.SetField("person.preferred", "EN");
                fields.SetField("person.knowledge.English", "On");
                fields.SetField("person.knowledge.French", "On");
                fields.SetField("person.knowledge.Dutch", "Off");

                pdfStamper.FormFlattening = true;
                pdfStamper.Close();

                PdfReader tempReader = new PdfReader(temp.ToArray());

                copy.AddPage(copy.GetImportedPage(tempReader, pdfReader.NumberOfPages));
                copy.FreeReader(tempReader);
            }
            doc.Close();
        }

    }
}

Pour que ça marche, j'ai dû ajouter un using iTextSharp.text; sans quoi le type 'Document' n'est pas référencé.

Et j'utilise aussi pdfStamper.FormFlattening = true; pour que le formulaire PDF ne soit plus modifiable. En fait, cela fait que le fichier PDF final redevient un simple PDF sans plus aucun champ saisissable.

Pour l'instant, je vais déjà utiliser ça comme ça parce que l'échéance pour la mise à jour de l'état approche à grands pas. Puis quand j'aurais fini j'essaierai de comprendre un peu mieux ce qui se passe et comment ça marche.

vendredi 26 septembre 2008

Remplir les champs d'un formulaire PDF

Après avoir réussi à lister les champs d'un formulaire PDF, ce coup-ci je fais un premier essai pour mettre des données dans mon formulaire. Pour commencer, je me contente d'y mettre un compteur numérique, ce qui va me permettre de repérer chaque champ dans le formulaire.

namespace Altrr.iText {

    using System;
    using System.Collections;
    using System.IO;
    using iTextSharp.text.pdf;

    /// <summary>
    /// Tests du composant iTextSharp
    /// </summary>
    public class Start {

        /// <summary>
        /// Point d'entrée principal de l'application.
        /// </summary>
        [STAThread]
        static void Main (string[] args) {

            string pdfSource = @"D:\Altrr\iText\register_form1.pdf";
            if (args.Length == 1) {
                pdfSource = args[0];
            }
            FillFields(pdfSource);

        }

        /// <summary>
        /// Remplis les différents champs d'un formulaire PDF
        /// </summary>
        static void FillFields (string pdfSource) {

            // Création d'un objet PDF Reader basé sur le formulaire PDF
            Console.WriteLine(pdfSource + " :");
            PdfReader pdfReader = new PdfReader(pdfSource);

            // Création d'un objet PDF Stamper à partir du formulaire PDF
            string pdfRempli = pdfSource.Replace(".pdf", "_test.pdf");
            PdfStamper pdfStamper = new PdfStamper(pdfReader, new FileStream(pdfRempli, FileMode.Create));
            AcroFields fields = pdfStamper.AcroFields;

            // Boucle pour remplir les différents champs
            int i = 0;
            foreach (DictionaryEntry field in fields.Fields) {
                // nom du champ
                string key = field.Key.ToString();
                // type du champ (et selon le cas liste des valeurs possibles)
                string type = "";
                i++;
                string data = i.ToString();
                string[] list = null;
                switch (fields.GetFieldType(key)) {
                    case AcroFields.FIELD_TYPE_CHECKBOX:
                        type = "CheckBox";
                        list = fields.GetAppearanceStates(key);
                        break;
                    case AcroFields.FIELD_TYPE_COMBO:
                        type = "Combo";
                        list = fields.GetListOptionExport(key);
                        break;
                    case AcroFields.FIELD_TYPE_LIST:
                        type = "List";
                        list = fields.GetListOptionExport(key);
                        break;
                    case AcroFields.FIELD_TYPE_NONE:
                        type = "None";
                        break;
                    case AcroFields.FIELD_TYPE_PUSHBUTTON:
                        type = "PushButton";
                        break;
                    case AcroFields.FIELD_TYPE_RADIOBUTTON:
                        type = "RadioButton";
                        list = fields.GetAppearanceStates(key);
                        break;
                    case AcroFields.FIELD_TYPE_SIGNATURE:
                        type = "Signature";
                        break;
                    case AcroFields.FIELD_TYPE_TEXT:
                        type = "Text";
                        break;
                }
                if (list != null) {
                    data = list[list.Length - 1];
                }
                Console.WriteLine("- " + key + " : " + data);
                fields.SetField(key, data);
            }
            // Fermeture du formulaire PDF rempli
            pdfStamper.Close();
        }

    }
}

Maintenant, il me reste à trouver comment éviter de créer physiquement un nouveau fichier à chaque fois. Etant donné que c'est destiné à être utilisé dans une application ASP.NET, je préfèrerais trouver une méthode qui me permette de renvoyer directement le résultat vers le poste client. Normalement, c'est le genre de chose qui devrait se trouver facilement sur Google (à condition que cela soit réalisable).

Et ce qui serait bien, c'est de voir s'il est possible à partir du formulaire modèle de remplir plusieurs pages avec des données différentes données. De cette façon, je pourrais gérer des impressions groupées de plusieurs documents à la fois.

jeudi 25 septembre 2008

Lister les champs d'un formulaire PDF

Aujourd'hui, j'ai eu besoin de re-faire un état Crystal Report. Il s'agit d'un document officiel dont le format a changé du jour au lendemain et qui doit être en production pour le 1° février. Vu les délais et surtout étant donné que maintenant plus personne ne maitrise Crystal Report chez nous, ça risquait d'être un peu juste.

Mais heureusement, l'organisme qui demande ce nouvel état a eu la bonne idée de le fournir sous forme de formulaire PDF prêt à remplir. Comme ce n'est normalement pas un état trop demandé (ce qui fait qu'on n'a pas vraiment besoin de la puissance de feu de Crystal Report), j'en ai profité pour tester s'il était possible de remplir ce formulaire depuis ASP.NET.

Apparemment, la meilleure solution bon marché pour cela semble être iTextSharp. C'est gratuit et ça semble vraiment très simple à utiliser. Sauf en ce qui concerne la mise à jour des formulaires PDF où il m'a fallu beaucoup creuser pour réussir à trouver des exemples concrets :

Une fois tout ça trouvé, ça a de suite été plus facile de faire un premier essai qui marche :

namespace Altrr.iText {

    using System;
    using System.Collections;
    using iTextSharp.text.pdf;

    /// <summary>
    /// Tests du composant iTextSharp
    /// </summary>
    public class Start {

        /// <summary>
        /// Point d'entrée principal de l'application.
        /// </summary>
        [STAThread]
        static void Main (string[] args) {

            string pdfSource = @"D:\Altrr\iText\register_form1.pdf";
            if (args.Length == 1) {
                pdfSource = args[0];
            }
            ListFieldNames(pdfSource);

        }

        /// <summary>
        /// Affiche la liste des champs d'un formulaire PDF
        /// </summary>
        static void ListFieldNames (string pdfSource) {

            // Création d'un objet PDF Reader basé sur le formulaire PDF
            Console.WriteLine(pdfSource + " :");
            PdfReader pdfReader = new PdfReader(pdfSource);
            AcroFields fields = pdfReader.AcroFields;

            // Boucle sur les différents champs du formulaire
            foreach (DictionaryEntry field in fields.Fields) {
                // nom du champ
                string key = field.Key.ToString();
                // type du champ (et selon le cas liste des valeurs possibles)
                string type = "";
                string data = "";
                switch (fields.GetFieldType(key)) {
                    case AcroFields.FIELD_TYPE_CHECKBOX:
                        type = "CheckBox";
                        data = String.Join(", ", fields.GetAppearanceStates(key));
                        break;
                    case AcroFields.FIELD_TYPE_COMBO:
                        type = "Combo";
                        data = String.Join(", ", fields.GetListOptionExport(key));
                        break;
                    case AcroFields.FIELD_TYPE_LIST:
                        type = "List";
                        data = String.Join(", ", fields.GetListOptionExport(key));
                        break;
                    case AcroFields.FIELD_TYPE_NONE:
                        type = "None";
                        break;
                    case AcroFields.FIELD_TYPE_PUSHBUTTON:
                        type = "PushButton";
                        break;
                    case AcroFields.FIELD_TYPE_RADIOBUTTON:
                        type = "RadioButton";
                        data = String.Join(", ", fields.GetAppearanceStates(key));
                        break;
                    case AcroFields.FIELD_TYPE_SIGNATURE:
                        type = "Signature";
                        break;
                    case AcroFields.FIELD_TYPE_TEXT:
                        type = "Text";
                        break;
                }
                Console.Write("- " + key + " : " + type);
                if (data != "") {
                    Console.WriteLine(" (valeurs possibles : " + data + ")");
                } else {
                    Console.WriteLine("");
                }
            }
        }

    }
}

Ce qui avec le fichier d'exemple me donne :

D:\Altrr\iText\register_form1.pdf :
- person.name : Text
- person.postal_code : Text
- person.knowledge.Dutch : CheckBox (valeurs possibles : Off, On)
- person.language : Combo (valeurs possibles : EN, FR, NL)
- person.knowledge.French : CheckBox (valeurs possibles : Off, On)
- person.programming : List (valeurs possibles : JAVA, C, CS, VB)
- person.email : Text
- person.address : Text
- person.preferred : RadioButton (valeurs possibles : Off, NL, EN, FR)
- person.knowledge.English : CheckBox (valeurs possibles : Off, On)

Il y a quand même un problème, parce qu'avec mon formulaire PDF, j'obtiens une liste des champs totalement dans le désordre, c'est à dire ni dans l'ordre de saisie, ni même dans l'ordre alphabétique. Et comme les noms des champs sont moyennement clairs, c'est assez galère pour savoir à quoi correspond chaque champ.

Ca vaudrait peut être le coup d'essayer la fonction pdfReader.AcroFields.GetFieldPositions(key) qui renvoie la position du champ pour essayer de trier cette liste de haut en bas et de gauche à droite. Ou alors, simplement remplir chaque champ avec une valeur numérique croissante et voir ce que ça donne.

mardi 15 juillet 2008

Créer un fichier Excel en .NET

Mon voisin de bureau avait la désagréable mission de devoir réaliser une interface (lui aussi!) pour exporter des données au format Excel. Sinon y'aurait des têtes qui allaient tomber !

Comme la vie dans un openspace bondé exige une bonne dose de civilité et pour éviter les éclaboussures j'ai fait comme les autres et j'ai donc essayé de le sortir de ce mauvais pas.

Générer de l'Excel

Habituellement, on répond à ce genre de besoin en générant un bête fichier CSV (en tout cas moi c'est ce que je fais à chaque fois) ou dans les cas les plus extrêmes certains d'entre nous utilisent la librairie ExcelXmlWriter pour produire un fichier Excel au format XML.

Mais dans le cas présent, il était nécessaire de fournir un authentique fichier Excel parce que l'application en face s'attendait à lire un "vrai" format Excel bien binaire comme dans le temps, estampillé "Made in Microsoft" (pour un programme en PHP c'est d'ailleurs assez rigolo).

Il ne restait donc que 3 solutions possibles :

  • installer Excel sur le serveur IIS et y aller à coup d'objets COM : sur un serveur de prod! ça va pas la tête ?
  • trouver une librairie qui sache créer de "vrais" fichiers Excel : et pourquoi pas avoir à l'acheter en plus ?
  • vérifier une fois pour toute si le provider OleDb permet aussi d'écrire dans un fichier Excel : ah ouaih ça c'est marrant !

Lire depuis un fichier Excel

Pour commencer, créer vite fait un petit fichier Excel pour se rappeler comment on fait pour lire ce genre de truc :

  • 3 colonnes : Code, Libellé, Date
  • 1° ligne : 1, Un, 01/01/2008
  • 2° ligne : 2, Deux, 02/02/2008
  • 3° ligne : 3, Trois, 03/03/2008

Enregistrer tout ça dans un fichier que l'on nommera Classeur1.xls

Fouiller un peu dans sa mémoire et pas mal sur le disque dur pour retrouver comment coder un petit programme qui va lire le contenu de ce fichier :

using System;
using System.Data.OleDb;

namespace OleDbExcel {

    class Class1 {
        [STAThread]
        static void Main(string[] args) {

            // Défini la chaine de connexion au fichier Excel
            string cnstr = @"Provider=Microsoft.Jet.OLEDB.4.0;Extended Properties=""Excel 8.0;HDR=YES;""";
            cnstr += @";Data Source=D:\Altrr\OleDbExcel\Classeur1.xls";

            // Connexion au fichier Excel
            OleDbConnection cn = new OleDbConnection(cnstr);
            cn.Open();

            // Affiche le contenu
            OleDbCommand cm = cn.CreateCommand();
            cm.CommandText = "SELECT * FROM [Feuil1$]";
            OleDbDataReader dr = cm.ExecuteReader();
            while (dr.Read() == true) {
                Console.WriteLine("{0} : {1}\t{2}", dr[0], dr[1], dr[2]);
            }
            dr.Close();

            // Ferme la connexion
            cn.Close();

            // Fin du test
            Console.WriteLine();
            Console.Write("(Entrée) pour terminer...");
            Console.ReadLine();
        }

    }
}

F5 => ça marche => au suivant!

(une parenthèse pour info : Feuil1 c'est le nom de la 1° feuille dans le classeur Excel et pour l'utiliser en tant que table il faut ajouter un $ au bout et mettre le tout entre crochets)

Ecrire dans un fichier Excel

Et maintenant, le saut dans l'inconnu, à savoir tenter d'écrire dans un fichier Excel via une connexion OleDb (de l'inédit pour moi) :

using System;
using System.Data.OleDb;

namespace OleDbExcel {

    class Class1 {
        [STAThread]
        static void Main(string[] args) {

            // Défini la chaine de connexion au fichier Excel
            string cnstr = @"Provider=Microsoft.Jet.OLEDB.4.0;Extended Properties=""Excel 8.0;HDR=YES;""";
            cnstr += @";Data Source=D:\Altrr\OleDbExcel\Classeur1.xls";

            // Connexion au fichier Excel
            OleDbConnection cn = new OleDbConnection(cnstr);
            cn.Open();

            // Création d'un objet OleDbCommand
            OleDbCommand cm = cn.CreateCommand();

            // Insère une 4° ligne dans le fichier Excel
            cm.CommandText = "INSERT INTO [Feuil1$] ([Code], [Libellé], [Date]) VALUES (4, 'Quatre', '04/04/2008')";
            cm.ExecuteNonQuery();

            // Insère une 5° ligne dans le fichier Excel
            cm.CommandText = "INSERT INTO [Feuil1$] ([Code], [Libellé], [Date]) VALUES (5, 'Cinq', '05/05/2008')";
            cm.ExecuteNonQuery();

            // Affiche le contenu
            cm.CommandText = "SELECT * FROM [Feuil1$]";
            OleDbDataReader dr = cm.ExecuteReader();
            while (dr.Read() == true) {
                Console.WriteLine("{0} : {1}\t{2}", dr[0], dr[1], dr[2]);
            }
            dr.Close();

            // Ferme la connexion
            cn.Close();

            // Fin du test
            Console.WriteLine();
            Console.Write("(Entrée) pour terminer...");
            Console.ReadLine();
        }

    }
}

F5 => ça marche aussi !

On tente la même chose sur le serveur (de prod ! quand y'a des têtes en jeux on a vraiment plus peur de rien) et ça marche encore !!!

Que demander de plus ?

On peut même utiliser des paramètres au lieu de commandes SQL en "dur"

using System;
using System.Data.OleDb;

namespace OleDbExcel {

    class Class1 {
        [STAThread]
        static void Main(string[] args) {

            // Défini la chaine de connexion au fichier Excel
            string cnstr = @"Provider=Microsoft.Jet.OLEDB.4.0;Extended Properties=""Excel 8.0;HDR=YES;""";
            cnstr += @";Data Source=D:\Altrr\OleDbExcel\Classeur1.xls";

            // Connexion au fichier Excel
            OleDbConnection cn = new OleDbConnection(cnstr);
            cn.Open();

            // Création d'un objet OleDbCommand
            OleDbCommand cm = cn.CreateCommand();

            // Insère une 6° ligne dans le fichier Excel
            cm.CommandText = "INSERT INTO [Feuil1$] ([Code], [Libellé], [Date]) VALUES (@Code, @Libelle, @Date)";
            cm.Parameters.Add("@Code", 6);
            cm.Parameters.Add("@Libelle", "Six");
            cm.Parameters.Add("@Date", new DateTime(2008, 6, 6));
            cm.ExecuteNonQuery();

            // Affiche le contenu
            cm.CommandText = "SELECT * FROM [Feuil1$]";
            OleDbDataReader dr = cm.ExecuteReader();
            while (dr.Read() == true) {
                Console.WriteLine("{0} : {1}\t{2}", dr[0], dr[1], dr[2]);
            }
            dr.Close();

            // Ferme la connexion
            cn.Close();

            // Fin du test
            Console.WriteLine();
            Console.Write("(Entrée) pour terminer...");
            Console.ReadLine();
        }

    }
}

On ne peut pas faire un DELETE FROM [Feuil1$] pour vider le fichier avant d'y insérer de nouvelles données : pas très utile donc pas très grave.

Ce qui est un peu plus embêtant, c'est qu'il ne semble pas possible de partir d'un fichier Excel vide et d'y "créer" dynamiquement les colonnes que l'on veut

  • faire un 1° "INSERT INTO [Feuil1$] ([Code], [Caption], [Date]) VALUES ('Code', 'Libellé', 'Date')" plante
  • commencer par un "CREATE TABLE [Feuil1$] ..." plante aussi

Fin de la récréation

C'est ennuyeux, mais comme cela n'a strictement aucune importante dans le cas qui nous occupait au départ, on ne va pas y passer plus que la pause de midi.

jeudi 5 juin 2008

Vérifier que les identifiants automatiques sont à jour

L'objectif est de s'assurer que pour toutes les tables qui ont un identifiant automatique, cet identifiant ne soit pas supérieur à la valeur de la séquence utilisée pour générer cet identifiant. (Pourquoi on se retrouve dans cette situation, c'est un autre problème).

Conventions :

  • tous les noms de tables sont préfixés par LUX_
  • tous les noms de clés primaires sont préfixés par PK_
  • tous les noms de séquences sont préfixés par SEQ_LUX_

Générer un script renvoyant la valeur maximum des identifiants de chaque table

Requête pour générer les SELECT MAX() de chaque table
SELECT 'UNION SELECT ''' || TABLE_NAME || ''' AS TB_NAME, '
       || 'MAX(' || COLUMN_NAME || ') AS TB_IDMAX ' 
       || 'FROM ' || TABLE_NAME
FROM   USER_IND_COLUMNS
WHERE  TABLE_NAME LIKE 'LUX_%'
AND    INDEX_NAME LIKE 'PK_%'
AND    COLUMN_NAME IN (SELECT COLUMN_NAME FROM COLS WHERE DATA_TYPE = 'NUMBER')
ORDER BY TABLE_NAME
Compléter le script généré

Sous TOAD, faire clic droit dans le résultat de la requête et choisir SaveAs et enregistrer dans le presse-papiers, ce qui donne :

UNION SELECT 'LUX_BANQUES' AS TB_NAME, MAX(IDBANQUE) AS TB_IDMAX FROM LUX_BANQUES
UNION SELECT 'LUX_CLIENTS' AS TB_NAME, MAX(IDCLIENT) AS TB_IDMAX FROM LUX_CLIENTS
UNION SELECT 'LUX_COMPTES' AS TB_NAME, MAX(IDCOMPTE) AS TB_IDMAX FROM LUX_COMPTES
UNION SELECT 'LUX_CONTACTS' AS TB_NAME, MAX(IDCONTACT) AS TB_IDMAX FROM LUX_CONTACTS
...

Dans Notepad, enlever le premier UNION et préfixer par un CREATE TABLE

CREATE TABLE TMP_IDMAXIMUMS AS 
SELECT 'LUX_BANQUES' AS TB_NAME, MAX(IDBANQUE) AS TB_IDMAX FROM LUX_BANQUES
UNION SELECT 'LUX_CLIENTS' AS TB_NAME, MAX(IDCLIENT) AS TB_IDMAX FROM LUX_CLIENTS
UNION SELECT 'LUX_COMPTES' AS TB_NAME, MAX(IDCOMPTE) AS TB_IDMAX FROM LUX_COMPTES
UNION SELECT 'LUX_CONTACTS' AS TB_NAME, MAX(IDCONTACT) AS TB_IDMAX FROM LUX_CONTACTS
...

Puis exécuter ce script pour obtenir une première table TMP_IDMAXIMUMS qui enregistrera la valeur maximum de l'identifiant de chaque table

Créer une seconde table pour enregistrer la valeur maximum de chaque séquence

CREATE TABLE TMP_SEQMAXIMUMS AS 
SELECT SUBSTR(SEQUENCE_NAME, 5) AS TB_NAME, LAST_NUMBER AS TB_SEQMAX
FROM   USER_SEQUENCES
WHERE  SEQUENCE_NAME LIKE 'SEQ_LUX_%'

Comparer le contenu des deux tables

SELECT T1.TB_NAME, TB_IDMAX, TB_SEQMAX
FROM   TMP_IDMAXIMUMS  T1,
       TMP_SEQMAXIMUMS T2
WHERE  T1.TB_NAME = T2.TB_NAME
AND    TB_IDMAX > TB_SEQMAX