Création d'un portail web ASP.NET pour générer des rapports avec Crystal ReportsDate de publication : 18/08/2005
Par
David Pédehourcq (Mon blog) Dans cet article, nous allons voir comment créer un portail ASP.NET qui génèrera des rapports au format Word, PDF ou Excel à l'aide de Crystal Reports. Ce portail web sera modulable et entièrement configurable via un fichier xml afin de pouvoir rajouter ou enlever des états sans avoir à recompiler l'application. Avant-propos I. Mécanismes de génération d'un état II. Description d'un état dans un fichier xml III. Récupération des informations XML des états IV. Création des contrôles utilisateurs web A. Un contrôle utilisateur web simple B. Un contrôle utilisateur web plus complexe : saisie d'une période C. Un contrôle utilisateur web avec accès aux données V. Chargement des contrôles utilisateur web VI. Création d'un dataset fortement typé de comptoir.mdb VII. Extraction des données VIII. Création d'un état IX. Génération de l'état X. Ajouter un nouvel état sans recompiler le projet XI. Critiques et améliorations de la solution proposée 1. Les controles utilisateurs web 2. L'extraction des données 3. Utilisation de Crystal Reports 4. Gestion des exceptions Conclusion Avant-propos
La génération de rapports est devenue une pierre angulaire dans tous les systèmes d'information. La centralisation des données nécessite très souvent
un applicatif de reporter qui se chargera d'extraire et de formater des données inexploitables, en l'état, au coeur du SGBD. Le choix d'une technologie web pour le reporting présente l'intérêt non négligeable
d'un déploiement unique sur un serveur qui permettra à de nombreux utilisateurs d'accéder au service à l'aide d'un simple navigateur web.
Cet article va essayer de répondre à deux problèmatiques :
Ce dernier point est à mon avis essentiel. Tout système est amené à évoluer, mais particulièrement les systèmes de reporting.
Les utilisateurs finaux demandent souvent l'ajout et la modification de rapports et il peut vite devenir très fastidieux de recompiler l'application à chaque modification. Un scénario catastrophe serait d'avoir des versions compilées différentes suivant les états que l'on souhaite déployer Nous allons donc mettre en place un système qui permettra de modifier et d'ajouter la plupart des états sans recompiler l'application.
La base de données utilisée dans cet article sera "comptoir.mdb" pour des raisons de simplicité.
I. Mécanismes de génération d'un état
Nous allons commencer par analyser rapidement les différentes étapes de génération d'un état. Un schéma simple valant de long discours :
Pour générer un état, il nous faut donc :
Finalement, les critères d'extraction de données sont souvent redondants dans la génération d'états : une période, une date, une liste déroulante pour sélectionner des types, un case à cocher,...
On peut découper ces paramètres en contrôles utilisateurs. On aurait ainsi 1 contrôle utilisateur web = 1 paramètre pour extraire les données.
Concernant la méthode qui va se charger d'extraire les données, celle-ci retournera toujours un dataset fortement typé pour être exploitable par l'état Crystal Reports.
On aura donc un dataset fortement typé représentant la structure de la base qui sera rempli différemment suivant les critères et la méthode d'extraction des données.
A partir du fichier .XSD de ce dataset fortement typé, on peut créer autant de modèle Crystal Reports que l'on souhaite. Et chaque modèle pourra générer un état à partir d'un méthode d'extraction de données.
On peut donc facilement décrire un état dans un fichier xml... et c'est ce que nous allons faire !!!
II. Description d'un état dans un fichier xml
Nous allons séparer notre application en 2 couches, il faut donc faire 2 projets : "Etats" qui englobera toute la partie affichage et génération et "MetierEtats" qui s'occupera de l'accès aux données et de la description des états.
Une fois les 2 nouveaux projets crés ajoutons un fichier "descetats.xml" au projet "MetierEtats" :
Explication des balises :
On a donc ici tout ce qu'il nous faut pour paramétrer un ensemble d'états d'un portail web.
Ici, nous avons une description assez minimaliste de nos états. Considérez cette vision du portail comme une base de travail qu'il faudra adapter à vos besoins ;).
Comme je suis plutôt fainéant (et c'est pas avec Visuel Studio .NET 2005 que ça va s'améliorer !), je génère automatiquement le schéma correspondant à mon XML en faisant Clic droit => "Créer un Schéma" sous Visual Studio .NET 2003.
Voici la structure du XSD :
On retrouve bien en en-tête de notre fichier "descetats.xml" :
III. Récupération des informations XML des états
Créons maintenant une classe "DescEtatsFactory" qui se trouvera dans un fichier "DescEtatsFactory.cs". Cette classe va utiliser le fameux pattern Singleton pour garder en cache la description des états du fichier xml.
Nous allons stocker les données du fichier xml dans un dataset qui sera une instance de la classe "descetats" (automatiquement générée par Visual Studio .NET 2003 lors de la création du schéma xsd à partir de descetats.xml).
Cette classe a pour rôle de retourner constamment une instance de descetats, si elle est déjà crée, la méthode statique retourne l'instance courante sinon elle lit le fichier xml et en crée une.
Avec ce singleton, on évite de lire le fichier xml à chaque fois qu'on a besoin d'une instance de descetats.
Par contre, quand on modifiera le fichier xml, il faut prévoir un mécanisme qui appellera la méthode "KillInstance()" afin que le prochain appel à "descetats" charge la nouvelle configuration xml des états. La solution qui m'a semblé la plus judicieuse est de mettre un SystemFileWatcher qui va surveiller toute modification du fichier "descetats.xml" et appeler "KillInstance()".
Ce SystemFileWatcher devra commencer son travail de surveillance dès le démarrage de l'application. Donc dans le global.asax de notre projet "Etats" :
Voilà, maintenant nos états sont décrits dans un fichier xml et nous avons les méthodes adéquates pour accéder à ces informations dans notre application.
IV. Création des contrôles utilisateurs web
Maintenant passons à la création des contrôles utilisateurs qui vont permettre de saisir les différents critères pour la récupération des données. 1 critère = 1 paramètre pour extraire les données = 1 contrôle utilisateur.
Tous nos contrôles utilisateurs devront implémenter l'interface IControlEtat suivante :
A. Un contrôle utilisateur web simple
Commençons par un contrôle simple qui permet de saisir une date. On crée un répertoire "Contrôles" dans le projet "Etats". Dans ce répertoire, on ajoute un contrôle utilisateur web que l'on nommera "CtrlDate.cs". Ce contrôle permettra la saisie d'une date :
On implémente ensuite la méthode et la propriété de l'interface :
B. Un contrôle utilisateur web plus complexe : saisie d'une période
Cette fois nous allons créer un contrôle plus complexe qui ne renverra pas un type primaire mais un type que nous aurons défini.
Dans le projet "MetierEtats", nous allons créer un fichier "Structs.cs" qui rassemblera tous les types que nous aurons cré.
Une recherche se fait plus fréquemment sur une période que sur une date précise, nous allons donc créer une structure Période :
On crée ensuite le contrôle utilisateur "CtrlPeriode" qui sera conçu comme "CtrlDate" mais avec 2 contrôles de saisie de date :
Et on implémente notre interface IControlEtat :
Nous venons donc de créer un contrôle utilisateur web qui retourne une période "Periode".
On peut bien sur définir des contrôles utilisateurs plus complexes qui retourneront des structures plus complexes.
C. Un contrôle utilisateur web avec accès aux données
Maintenant nous allons créer un contrôle utilisateur web qui sera une liste déroulante qui listera le nom des employés.
Pour extraire des données d'un base et les mettre dans une liste déroulante, la plupart du temps on a besoin :
Voici donc une petite classe, que l'on mettra dans FillDDL.cs (dans le projet "EtatsMetier") qui va me permettre de remplir facilement tous mes contrôles de liste déroulante :
Maintenant la création d'un contrôle utilisateur web avec une liste déroulante accédant à la base devient un jeux d'enfant :
Et le code Csharp :
V. Chargement des contrôles utilisateur web
Maintenant que nous avons cré quelques contrôles utilisateurs web voyons comment les charger et comment va s'architecturer la page principale de notre portail. Commençons par modifier descetats.xml :
Maintenant voici comment va se présenter la page : sur la gauche on aura un menu qui présentera tous les "libcourt" de chaque état.
En haut, on aura un Libellé qui représentera le "liblong" de l'état et au centre, on aura un placeholder dans lequel on chargera tous les contrôles utilisateur web de l'état. Voici le code de la page "Default.aspx" du projet "Etats" :
Pour le chargement des contrôles utilisateur web, j'ai préféré isoler ce code dans une classe séparée. Si les contrôles et leur description se complexifient, le code risque de vite devenir illisible.
On va donc créer une classe ControlFactory qui va charger et initialiser un contrôle utilisateur web et retourner une instance prête à être placée dans le placeHolder.
Maintenant il nous reste plus qu'à récupérer descetats, générer le menu, et charger les contrôles dans le placeHolder avec "LoadControlEtat" suivant dans quel état on se trouve.
Le code est relativement simple : on génère le menu et on teste si on a un paramètre "etat" dans l'url.
Si on a un paramètre "etat", on affiche les contrôles utilisateur associés dans le PlaceHolder. Vous pouvez compiler et exécuter, vous verrez bien les contrôles utilisateur web s'afficher lorsque vous cliquez sur "Etat1" dans le menu. VI. Création d'un dataset fortement typé de comptoir.mdb
Maintenant nous allons créer un dataset fortement typé représentant "comptoir.mdb". Pour que nos états puissent consommer les données de notre dataset, celui-ci doit être fortement typé. En effet, lors de la création de l'état, il faut bien que Crystal Reports puisse déterminer le nom et le type des champs pour les données qui lui serviront à générer le rapport.
C'est le .xsd d'un dataset fortement typé qui contient toutes ces informations. Rien de plus simple sous Visual Studio .NET :
Ce qui devrait vous donner :
Dataset fortement typé de comptoir.mdb
VII. Extraction des données
Autant vous prévenir de suite, cette partie ne sera pas trop développée : si je devais m'attaquer maintenant aux différentes problématiques d'accès aux données, cet article deviendrait fort indigeste et je risquerais de perdre un bon nombre de lecteurs après quelques lignes. Pour bien traiter le sujet, il faudrait faire un article entier dessus, ça sera peut-être le cas dans le futur mais pour l'instant nous allons nous contenter du minimum pour avancer dans la construction de notre portail.
Reprenons notre schéma de départ : Pour l'instant, nous avons les critères qui vont être saisis avec nos contrôles utilisateurs web et le conteneur de nos données qui sera le dateset fortement typé que nous venons de créer DataEtats. Nous allons maintenant créer une classe "ExtractionDataEtats" qui ne contiendra que des méthodes statiques lesquelles prendront en paramètre le résultat des saisies sur les différents contrôles utilisateurs.
Nous avons à notre disposition une liste déroulante des employés et un contrôle permettant la saisie d'une période. Nous allons extraire des données pour générer des états sur les commandes passées par les employés sur des périodes données.
Nous allons ajouter un nouveau fichier de type "ClassComponment" à notre projet "MetierEtats". Nous l'appellerons "gestData.cs".
Nous allons créer rapidement quatre adapters pour remplir les tables "Employés", "Produits", "Commandes" et "Détails commandes" en faisant un drag & drop de OleDbDataAdapter sur le designer. On suit les étapes du wizard et on génère le code pour remplir ces trois tables. On obtient donc ceci :
Dans cette classe, nous allons ajouter quatre méthodes : "FillEmploye", "FillCommandeByPeriode", "FillAllProduits" et FillDetailCommandes :
Maintenant, créons la classe "ExtractionDataEtats" qui regroupera l'ensemble des méthodes statiques qui permettront l'extraction des données.
Toutes ces méthodes retourneront bien évidement une instance de "DataEtats". Nous avons donc :
Passons maintenant à la création d'un état...
VIII. Création d'un état
Créez un répertoire "EtatsCR" dans le projet "Etats". Ajoutez y un nouvel élément de type "Etat Crystal Reports" que l'on appellera "Etat1".
Lors du choix de la source de données, nous choisirons biensûr le dataset "DataEtats" : Nous allons sélectionner les tables : "Employés", "Produits", "Commandes" et "Détails commandes".
Comme promis, Crystal Reports retrouve les relations entre les tables :
Je ne détaillerais pas ici la création de l'état. Dans l'exemple que je mettrais en téléchargement, cet état affichera la référence, le nom, le nombre et le prix unitaire de chaque produit, groupé par commande et par date de commande et par pays de livraison.
Une fois l'état créé, dans les propriétés de l'état, mettre "Action de génération" à "Aucun" :
Notre état est créé, il ne nous reste plus qu'à écrire le code pour lui faire consommer notre dataset rempli par notre méthode d'extraction des données.
IX. Génération de l'état
Nous approchons du but. Il ne nous manque plus qu'à réaliser la génération de l'état, mais nous avons tous les éléments nécessaires à notre disposition. Commençons par modifier notre page Default.aspx en lui rajoutant un bouton "Générer" et une liste déroulante qui déterminera le format de sortie.
Nous allons ajouter ceci au fichier Default.aspx :
Le panel sert à masquer le bouton générer et le choix du format tant que l'on a pas sélectionné un état. On rajoutera donc :
Au chargement des contrôles utilisateurs web, juste après :
Maintenant nous avons deux options
J'ai choisi la deuxième option, on peut ainsi garder les critères de génération du document et générer un même état en différents formats. Si on génère le document dans la même page (Default.aspx), on perdra systématiquement toutes les valeurs des formulaires à chaque génération.
La solution retenue présente cependant un inconvénient : les tueurs de popups ...
Voilà comment nous allons procéder : quand l'utilisateur va cliquer sur générer, nous allons placer la valeur de chaque contrôle utilisateur web en session, puis ouvrir genetat.aspx dans une nouvelle fenêtre. Nous passerons en paramètre de l'url l'identifiant de l'état à générer et le format de sortie souhaité. genetats.aspx génèrera l'état au format demandé et videra la session.
Voici la méthode déclenchée lors du clic sur le bouton "générer" :
Maintenant passons à la génération de l'état :
Le code me semble suffisamment commenté pour que vous puissiez le comprendre. On récupère la valeur des objets en session et on les met dans une liste d'objets. Ensuite on récupère la méthode d'extraction des données de "ExtractionDataEtats"
en fonction du nom défini dans le fichier xml. On exécute cette méthode en lui passant en paramètre la liste des objets de session pour récupérer le dataset fortement typé rempli.
Ensuite, on charge un état Crystal Reports en fonction du chemin défini dans le fichier xml et on lui fait consommer les données de notre dataset rempli. On finit par la génération du flux souhaité.
Maintenant modifions notre fichier "descetats.xml"
Compilez et exécutez, sélectionnez l'état "Bilan Commandes" dans le menu et testez !
Nous allons voir maintenant comment rajouter un nouvel état à notre application sans recompiler le projet.
X. Ajouter un nouvel état sans recompiler le projet
Commencez par ouvrir une nouvelle instance de Visual Studio.NET et créer un projet vide. Dans ce nouveau projet, ajoutez une référence à l'assembly "EtatsMetier.dll". Cette référence contient les informations du dataset fortement typé "DataEtats". Créez un état comme décrit brièvement dans la partie VIII de l'article : Création d'un état.
Une fois l'état créé, enregistrez tout et fermez Visual Studio.NET. Copier/Coller "Etat2.rpt" dans le répertoire "EtatsCR" de l'application web et modifiez le fichier descetats.xml :
Enregistrez le fichier et sans recompiler rouvrez "Default.aspx". Le nouvel état est en place et il est opérationnel. Nous avons donc ajouter un nouvel état sans recompiler l'application et sans interrompre le service en rajoutant seulement le modèle .rpt et en renseignant les paramètres de l'état dans le fichier descetats.xml
XI. Critiques et améliorations de la solution proposée
La solution proposée est loin d'être parfaite. Cette partie va nous permettre de critiquer et expliquer les choix qui ont été fait. Nous aborderons aussi dans les grandes lignes, les améliorations qu'il faudrait apporter à ce portail. Cet article étant assez dense, j'ai préféré simplifier le portail afin de concentrer mes explications sur les concepts de base. 1. Les controles utilisateurs web
Commençons par nos contrôles utilisateurs web. Comme vous avez pu le constater, aucun mécanisme de validation de formulaire n'a été implémenté. Rien ne nous empêche de saisir une période invalide par exemple. Plusieurs approches sont envisageables. On pourrait utiliser les validator d'ASP.NET pour valider les webforms de chaque contrôle utilisateur web. Si un contrôle du formulaire n'est pas valide, la page ne sera pas postée et on bloque alors tout mécanisme de génération de l'état. Une autre solution serait de rajouter une méthode IsValid() et une propriétée LibError à l'interface IControlEtat. Avant la génération de l'état, il suffirait de vérifier que chaque contrôle utilisateur web retourne bien true avec sa méthode IsValid() et si ce n'est pas le cas, afficher les erreurs de validation au travers de la propriété LibError de chaque contrôle.
Concernant le contrôle utilisateur web permettant de sélectionner un employé. Nous avons mis au point une méthode d'extraction des données assez générique. Pour mieux l'exploiter, on pourrait imaginer que les paramètres de FillDDL.GetData() fassent parti de la description xml du contrôle. On aurait ainsi un contrôle utilisateur web de liste déroulante qui pourrait permettre la saisie de n'importe quel champ de la base.
On peut imaginer une multitude d'évolutions sur les controles utilisateur web. Mais la solution proposée permet de mettre en place toutes ces évolutions de manière assez simple. Nous avons une classe pour charger nos controles, une interface pour unifier leurs méthodes et propriétés et une structure xml permettant de complexifier leur description. Faire évoluer vos contrôles utilisateurs web selon vos besoins ne devrait donc pas présenter de problèmes particuliers.
2. L'extraction des données
L'accés aux données est un peu le "maillon faible" de notre portail. Mais comme je l'ai dit précédemment, cette partie mériterait à mon avis un article à elle seule.
Je vous conseille tout d'abord d'utiliser un composant d'accés aux données qui vous permettra de factoriser tout le code répétitif dès que l'on veut utiliser une requête. Je vous conseille l'article de R. Chapuis : Construire un composant d'accès aux données.
Ensuite je vois deux approches différentes suivant les besoins.
La première est d'enrichir ExtractionDataEtats de méthodes statiques différentes qui répondront aux différents besoins d'extraction de données. La flexibilitée de notre portail dépendra de la richesse de la classe ExtractionDataEtats en méthodes d'extraction. C'est la méthode la plus simple et qui conviendra dans la plupart des cas. Seulement contrairement à ce qui a été promis, il faudra recompiler le portail et ajouter une méthode statique d'extraction lors de l'ajout de certains états.
La deuxième approche est beaucoup plus complexe à mettre en place mais ouvre de plus larges perspectives. Finalement l'extraction des données revient à extraire un ensemble de tables. Ces tables seront extraites :
La "liste de contraintes sur des champs placés dans la clause Where de la rêquete select" correspond à nos contrôles utilisateurs web : si l'on complexifie la description du contrôle utilisateur web, on pourrait ajouter une table cible et un champ de contrainte. Si l'on place les DataRelations dans notre DataSet fortement typé, il nous suffit d'avoir l'ordre de chargement des tables et les contraintes à appliquer à ces chargements pour générer toutes les requêtes SELECT dynamiquement. On aurait alors une méthode statique qui pourrait nous retourner tout type d'extraction de données.
Cette deuxième solution est bien plus complexe à mettre en place mais amène une grande souplesse à l'application qui deviendrait entièrement configurable en xml. Je n'ai pas encore mis en place cette solution, mais je pense que c'est faisable. Cela sera peut-être le sujet d'un prochain article.
3. Utilisation de Crystal Reports
Pourquoi ai-je utilisé Crystal Reports ? Tout d'abord rappellons que Crystal Reports est livré avec Visual Studio.NET et peut être utilisé en production aprés un enregistrement gratuit sur le site de Business Objects. Comme vous avez pu le voir l'essentiel de l'article n'est pas la génération du rapport en elle-même étant donné que la création de l'état prend moins de 5 min (pour un état simple) et que la génération proprement dite prend à peinne quelques lignes de code une fois que l'on a rempli notre DataSet. Donc l'argument qui a le plus orienté mon choix vers Crystal Reports est la productivitée. Biensûr on peut générer du pdf, du word et du excel sans Crystal Reports, mais pas aussi facilement. Coté performances, Crystal Reports utilise une mise en cache intelligente des états générés et l'ensemble de l'application supporte bien la montée en charge. J'ai fait des test de montée en charge sur une application de production et j'ai été agréablement surpris. Je ne vous donnerais pas de chiffres de bench qui seraient insignifiants mais pour générer un rapport pdf de 60 pages avec plus de 2 000 utilisateurs simultanés, l'ouverture d'Acrobat Reader et l'envoi du pdf prennent plus de temps que la génération de celui-ci. Par contre, prévoyez un serveur dédié car la génération consomme pas mal de ressources CPU sur votre serveur.
Le seul inconvénient de Crystal Reports va être le déploiement, pas toujours évident mais quand on a pris le coup de main, on finit par y arriver assez facilement. Je vous propose de lire : Déploiement d'un état Crystal Reports pour plus d'informations. L'article parle du déploiement d'applications windows mais le déploiement web est similaire.
4. Gestion des exceptions
Je n'ai volontairement pas traité une seule exception. La gestion des exceptions est plutôt complexe et peut-être abordée de bien des manières. Mieux vaut ne pas les gérer que mal les gérer. Il faudra cependant prendre la gestion des exceptions en considération sur une application en production sachant que la moindre erreur sur le fichier xml de description des états peut entraîner des erreurs en cascade.
Conclusion
Nous voici donc arrivé à la fin de cet article.
|
Copyright © 2005 David Pédehourcq. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.