IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Création d'un portail web ASP.NET pour générer des rapports avec Crystal Reports

Date 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 :
  • Créer un système de reporting web supportant divers formats (Word, PDF, Excel).
  • Créer un système de reporting web assez flexible pour permettre l'ajout, la suppression et la modification d'états sans recompiler l'application
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 :

génération d'un état


    Pour générer un état, il nous faut donc :
  • Des critères d'extraction de données.
  • Une méthode qui va extraire les données.
  • Un modèle d'état : un chemin vers un modèle d'état Crystal Reports (.rpt).
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" :

<?xml version="1.0" encoding="ISO-8859-1"?> <descetats> <etat> <libcourt>Etat1</libcourt> <liblong>Lib Long Etat1</liblong> <pathreport>EtatsCR\report1.rpt</pathreport> <methodgetdata>GetDataReport</methodgetdata> <control> <Libcontrol>param1</Libcontrol> <pathcontrol>Controles/control1.ascx</pathcontrol> </control> <control> <Libcontrol>param2</Libcontrol> <pathcontrol>Controles/control1.ascx</pathcontrol> </control> <control> <Libcontrol>param3</Libcontrol> <pathcontrol>Controles/control3.ascx</pathcontrol> </control> </etat> </descetats>



    Explication des balises :
  • <etat></etat> : la description d'un etat.
  • <libcourt></libcourt> : c'est le nom qu'aura l'état dans le menu de la page web.
  • <liblong></liblong> : c'est le titre qu'aura la page web de l'état.
  • <pathreport></pathreport> : c'est le chemin du modèle Crystal Reports de l'état.
  • <methodgetdata></methodgetdata> : c'est le nom de la méthode qui se chargera d'extraire les données.
  • <control></control> : un contrôle utilisateur de saisie.
  • <libcontrol></libcontrol> : le libellé se trouvant devant le contrôle de saisie.
  • <pathcontrol></pathcontrol> : le chemin du contrôle utilisateur de saisie.
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 :

<?xml version="1.0" ?> <xs:schema id="descetats" targetNamespace="http://tempuri.org/descetats.xsd" xmlns:mstns="http://tempuri.org/descetats.xsd" xmlns="http://tempuri.org/descetats.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" attributeFormDefault="qualified" elementFormDefault="qualified"> <xs:element name="descetats" msdata:IsDataSet="true" msdata:Locale="fr-FR" msdata:EnforceConstraints="False"> <xs:complexType> <xs:choice maxOccurs="unbounded"> <xs:element name="etat"> <xs:complexType> <xs:sequence> <xs:element name="libcourt" type="xs:string" minOccurs="0" /> <xs:element name="liblong" type="xs:string" minOccurs="0" /> <xs:element name="pathreport" type="xs:string" minOccurs="0" /> <xs:element name="control" minOccurs="0" maxOccurs="unbounded"> <xs:complexType> <xs:sequence> <xs:element name="Libcontrol" type="xs:string" minOccurs="0" /> <xs:element name="mapproperty" type="xs:string" minOccurs="0" /> <xs:element name="pathcontrol" type="xs:string" minOccurs="0" /> </xs:sequence> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> </xs:element> </xs:choice> </xs:complexType> </xs:element> </xs:schema>
On retrouve bien en en-tête de notre fichier "descetats.xml" :

<?xml version="1.0" encoding="ISO-8859-1"?> <descetats xmlns="http://tempuri.org/descetats.xsd">
J'ai choisi d'utiliser un dataset fortement typé sur le xml de description des états pour faciliter l'écriture du code permettant de récupérer les informations du fichier. Ce choix est certainement discutable mais rien ne vous empêche de procéder différemment. L'impact sur les performances est, à mon avis, négligeable car les informations du XML seront mises en cache et non récupérées à chaque requête du portail web.

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

DescEtatsFactory.cs
using System; using System.Xml; namespace EtatsMetier { /// <summary> /// Description résumée de descEtatsFactory. /// </summary> public class DescEtatsFactory { private static descetats m_dataDescEtats; public static descetats GetEtatDesc() { //s'il n'y a pas d'instance du dataset m_dataDescEtats, on en crée une if(m_dataDescEtats == null) { m_dataDescEtats = CreateListEtatsDesc(); } return(m_dataDescEtats); } //on crée une instance de dataDescEtats qu'on remplit private static descetats CreateListEtatsDesc() { descetats dataDescEtats; dataDescEtats = new descetats(); XmlDataDocument m_doc = new XmlDataDocument(dataDescEtats); m_doc.Load(System.AppDomain.CurrentDomain.BaseDirectory + "EtatsMetier/descetats.xml"); return(dataDescEtats); } //On détruit l'instance courante de descetats public static void KillInstance() { m_dataDescEtats = null; } } }
Pour faire simple, j'ai considéré ici que la couche "EtatsMetier" sera toujours à la racine de mon application web. Pour plus de flexibilité, on peut placer le chemin du fichier "descetats.xml" dans un fichier .config.
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" :

Global.asax.cs
public class Global : System.Web.HttpApplication { /// <summary> /// Variable nécessaire au concepteur. /// </summary> private System.ComponentModel.IContainer components = null; public System.IO.FileSystemWatcher m_watcher; public Global() { InitializeComponent(); m_watcher = new System.IO.FileSystemWatcher(System.AppDomain.CurrentDomain.BaseDirectory + "EtatsMetier/"); m_watcher.NotifyFilter = System.IO.NotifyFilters.LastWrite; m_watcher.Filter="descetats.xml"; m_watcher.Changed+=new System.IO.FileSystemEventHandler(m_watcher_Changed); m_watcher.EnableRaisingEvents = true; } private void m_watcher_Changed(object sender, System.IO.FileSystemEventArgs e) { EtatsMetier.DescEtatsFactory.KillInstance(); } }
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 :

IControlEtat.cs
using System; namespace Etats { //Interface que doit implémenter tout contrôle utilisateur de Etats public interface IControlEtat { //Méthode qui va retourner la valeur saisie sur le contrôle object GetData(); //Propriété gérant le libellé du contrôle string Libelle{get;set;} } }
Compte tenu de la simplicité de l'interface, vous vous dites sûrement qu'on aurait pu s'en passer. Mais les contrôles utilisateurs de cette application seront peut-être emmenés à se complexifier. Autant prévoir dès le début, une interface commune à tous ces contrôles.

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 :

CtrlDate.ascx
<%@ Register="ew" Namespace="eWorld.UI" Assembly="eWorld.UI" %> <%@ Control="c#" AutoEventWireup="false" Codebehind="CtrlDate.ascx.cs" Inherits="Etats.Controles.CtrlDate" TargetSchema="http://schemas.microsoft.com/intellisense/ie5"%> <asp:Label id="lblLibelle" runat="server"></asp:Label> <ew:CalendarPopup id="DTP" runat="server" Width="88px"></ew:CalendarPopup> <br><br>
J'utilise ici un contrôle date time piquer qui n'est pas présent dans le framework .NET mais que vous pouvez retrouver ici :  contrôle Date Time picker. Je vous recommande de le tester, il remplace avantageusement le calendaire d'ASP.NET.
On implémente ensuite la méthode et la propriété de l'interface :

CtrlDate.ascx.cs
namespace Etats.Controles { using System; using System.Data; using System.Drawing; using System.Web; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; /// <summary> /// Description résumée de CtrlDate. /// </summary> public class CtrlDate : System.Web.UI.UserControl, IControlEtat { protected eWorld.UI.CalendarPopup DTP; protected System.Web.UI.WebControls.Label lblLibelle; private void Page_Load(object sender, System.EventArgs e) { // Placer ici le code utilisateur pour initialiser la page } #region Code généré par le Concepteur Web Form override protected void OnInit(EventArgs e) { // // CODEGEN : Cet appel est requis par le Concepteur Web Form ASP.NET. // InitializeComponent(); base.OnInit(e); } /// <summary> /// Méthode requise pour la prise en charge du concepteur - ne modifiez pas /// le contenu de cette méthode avec l'éditeur de code. /// </summary> private void InitializeComponent() { this.Load += new System.EventHandler(this.Page_Load); } #endregion #region Membres de IControlEtat public object GetData() { return (this.DTP.SelectedDate); } public string Libelle { get { return this.lblLibelle.Text; } set { this.lblLibelle.Text = value; } } #endregion } }

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 :

Structs.cs
using System; namespace MetierEtats { public struct Periode { public DateTime DateD; public DateTime DateF; } }
On crée ensuite le contrôle utilisateur "CtrlPeriode" qui sera conçu comme "CtrlDate" mais avec 2 contrôles de saisie de date :

CtrlPeriode.ascx
<%@ Register TagPrefix="ew" Namespace="eWorld.UI" Assembly="eWorld.UI" %> <%@ Control Language="c#" AutoEventWireup="false" Codebehind="CtrlPeriode.ascx.cs" Inherits="Etats.Controles.CtrlPeriode" TargetSchema="http://schemas.microsoft.com/intellisense/ie5"%> <asp:Label id="lblLibelle" runat="server"></asp:Label>&nbsp;du&nbsp; <ew:CalendarPopup id="DTPDebut" runat="server"></ew:CalendarPopup>&nbsp;au&nbsp; <ew:CalendarPopup id="DTPFin" runat="server"></ew:CalendarPopup> <br><br>
Et on implémente notre interface IControlEtat :

CtrlPeriode.ascx.cs
namespace Etats.Controles { using System; using System.Data; using System.Drawing; using System.Web; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; using EtatsMetier; /// <summary> /// Description résumée de CtrlPeriode. /// </summary> public class CtrlPeriode : System.Web.UI.UserControl, IControlEtat { protected eWorld.UI.CalendarPopup DTPDebut; protected System.Web.UI.WebControls.Label lblLibelle; protected eWorld.UI.CalendarPopup DTPFin; private void Page_Load(object sender, System.EventArgs e) { // Placer ici le code utilisateur pour initialiser la page } #region Code généré par le Concepteur Web Form override protected void OnInit(EventArgs e) { // // CODEGEN : Cet appel est requis par le Concepteur Web Form ASP.NET. // InitializeComponent(); base.OnInit(e); } /// <summary> /// Méthode requise pour la prise en charge du concepteur - ne modifiez pas /// le contenu de cette méthode avec l'éditeur de code. /// </summary> private void InitializeComponent() { this.Load += new System.EventHandler(this.Page_Load); } #endregion #region Membres de IControlEtat public object GetData() { Periode p; p.DateD = this.DTPDebut.SelectedDate; p.DateF=this.DTPFin.SelectedDate; return (p); } public string Libelle { get { return this.lblLibelle.Text; } set { this.lblLibelle.Text = value; } } #endregion } }
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 :

  • Du nom de la table où l'on va chercher les enregistrements.
  • Du libellé d'un enregistrement.
  • De la clé de l'enregistrement.
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 :

FillDDL.cs
using System; using System.Data; using System.Data.OleDb; using System.Collections; namespace EtatsMetier { /// <summary> /// Description résumée de FillDDL. /// </summary> //structure d'un champ d'une liste déroulante public struct champDDL { public string ID; public string Libelle; } public class FillDDL { public static ArrayList GetData(string ChampCle, string ChampLib, string TableName) { champDDL m_champ; ArrayList m_result = new ArrayList(); OleDbDataReader m_reader; OleDbConnection m_conn; using (m_conn= new OleDbConnection(@"Jet OLEDB:Database Password=;Data Source=" + System.AppDomain.CurrentDomain.BaseDirectory + @"\Comptoir.mdb";Password=;Provider="Microsoft.Jet.OLEDB.4.0";")) { OleDbCommand m_comm = m_conn.CreateCommand(); //on crée la requete m_comm.CommandText="SELECT "+ ChampCle +"," + ChampLib +"FROM"+ TableName; m_conn.Open(); m_reader= m_comm.ExecuteReader(); //on remplis m_result de m_champ while(m_reader.Read()) { m_champ.ID = m_reader.GetValue(0).ToString(); m_champ.Libelle = m_reader.GetValue(1).ToString(); m_result.Add(m_champ); } m_reader.Close(); m_conn.Close(); } //on retourne un arrayList prête à remplir une liste déroulante return(m_result); } } }
Pour éviter de trop surcharger l'article, je ferais abstraction de toutes les problématiques liées à l'accès aux données. Il est évident qu'il ne faut jamais mettre une chaîne de connection en dur dans le code. Des liens vers des articles connexes permettant de mieux appréhender ces problématiques seront fournis en fin d'article.
Maintenant la création d'un contrôle utilisateur web avec une liste déroulante accédant à la base devient un jeux d'enfant :

CtrlDDLEmploye.ascx
<%@ Control="c#" AutoEventWireup="false" Codebehind="CtrlDDLEmploye.ascx.cs" Inherits="Etats.Controles.CtrlDDLEmploye" TargetSchema="http://schemas.microsoft.com/intellisense/ie5"%> <asp:Label id="lblLibelle" runat="server"></asp:Label> <asp:DropDownList id="DDLEmploye" runat="server"></asp:DropDownList> <br><br>
Et le code Csharp :

CtrlDDLEmploye.ascx.cs
namespace Etats.Controles { using System; using System.Drawing; using System.Web; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; using EtatsMetier; /// <summary> /// Description résumée de CtrlDDLEmploye. /// </summary> public class CtrlDDLEmploye : System.Web.UI.UserControl,IControlEtat { protected System.Web.UI.WebControls.Label lblLibelle; protected System.Web.UI.WebControls.DropDownList DDLEmploye; private void Page_Load(object sender, System.EventArgs e) { //On charge la liste déroulante foreach(champDDL m_champ in FillDDL.GetData("[N° employé]","[Nom]","[Employés]")) { this.DDLEmploye.Items.Add(new ListItem(m_champ.Libelle,m_champ.ID)); } } #region Code généré par le Concepteur Web Form override protected void OnInit(EventArgs e) { // // CODEGEN : Cet appel est requis par le Concepteur Web Form ASP.NET. // InitializeComponent(); base.OnInit(e); } /// <summary> /// Méthode requise pour la prise en charge du concepteur - ne modifiez pas /// le contenu de cette méthode avec l'éditeur de code. /// </summary> private void InitializeComponent() { this.Load += new System.EventHandler(this.Page_Load); } #endregion #region Membres de IControlEtat public object GetData() { return(System.Convert.ToInt32(this.DDLEmploye.SelectedValue)); } public string Libelle { get { return this.lblLibelle.Text; } set { this.lblLibelle.Text = value; } } #endregion } }
Ici, Microsoft nous montre bien ce qu'il ne faut surtout pas faire pour le nommage des champs d'une table le champ "N° employé" est une abomination (et je pèse mes mots) les "[" et "]" sont des caractères spécifiques à ACCESS qui permettent de spécifier les bornes d'un champ et de ne pas faire échouer le moteur SQL; sur le champ "N° employé", on peut relever 3 erreurs sur ce champ : Un caractère spécial "°" Un espace dans le nom du champ Un caractère accentué Je vous conseille de lire :  les règles de nomage du SQL

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 :

descetats.xml
<?xml version="1.0" encoding="ISO-8859-1"?> <descetats xmlns="http://tempuri.org/descetats.xsd"> <etat> <libcourt>Etat1</libcourt> <liblong>Lib Long Etat1</liblong> <pathreport>EtatsCR\report1.rpt</pathreport> <methodgetdata>GetDataReport</methodgetdata> <control> <Libcontrol>une date :</Libcontrol> <pathcontrol>Controles/CtrlDate.ascx</pathcontrol> </control> <control> <Libcontrol>une période :</Libcontrol> <pathcontrol>Controles/CtrlPeriode.ascx</pathcontrol> </control> <control> <Libcontrol>un employés :</Libcontrol> <pathcontrol>Controles/CtrlDDLEmploye.ascx</pathcontrol> </control> </etat> </descetats>
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" :

Default.aspx
<%@ Page="c#" Codebehind="Default.aspx.cs" AutoEventWireup="false" Inherits="Etats.Default" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <HTML> <HEAD> <title>WebForm1</title> <meta name="GENERATOR" Content="Microsoft Visual Studio .NET 7.1"> <meta name="CODE_LANGUAGE" Content="C#"> <meta name="vs_defaultClientScript" content="JavaScript"> <meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5"> </HEAD> <body> <form id="Form1" method="post" runat="server"> <div align="center"><asp:Label ID="TitrePage" Runat="server">Bienvenue </asp:Label></div> <br><br><br><br> <table> <tr> <td valign="top"> <table id="menu" runat="server" cellspacing="5" cellpadding="5" border="1"> </table> </td> <td> <asp:placeholder id="placeControls" Runat="server"></asp:placeholder> </td> </tr> </table> </form> </body> </HTML>
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.

ControlFactory.cs
using System; using System.Web; using System.Web.UI; using EtatsMetier; namespace Etats { /// <summary> /// Description résumée de ControlFactory. /// </summary> public class ControlFactory { //Initialise un contrôle à partir de sa description. public static Control LoadControlEtat(descetats.controlRow ControlEtats, Page page) { //On passe par l'interface IControl afin d'initialiser les propriétés //spécifiques au contrôle utilisateur web des états IControlEtat m_control; m_control = (IControlEtat)page.LoadControl(ControlEtats.pathcontrol); m_control.Libelle = ControlEtats.Libcontrol; //On retourne le control initialisé en le castant en "Control" //pour qu'on puisse directement l'insérer dans le place Holder. return((Control)m_control); } } }
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.

Default.aspx.cs
using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Drawing; using System.Web; using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; using EtatsMetier; namespace Etats { /// <summary> /// Description résumée de WebForm1. /// </summary> public class Default : System.Web.UI.Page { protected System.Web.UI.WebControls.PlaceHolder placeControls; protected System.Web.UI.WebControls.Label TitrePage; //Element de HTMLTable pour générer le menu protected System.Web.UI.HtmlControls.HtmlTable menu; protected System.Web.UI.HtmlControls.HtmlTableRow TRmenu; protected System.Web.UI.HtmlControls.HtmlTableCell TDmenu; protected System.Web.UI.WebControls.Button btnGenerer; protected System.Web.UI.WebControls.DropDownList ddlFormat; protected System.Web.UI.WebControls.Panel PanelOptions; //Description des états protected descetats m_descEtats; private void Page_Load(object sender, System.EventArgs e) { //On récupère la description des états m_descEtats = DescEtatsFactory.GetEtatDesc(); /////////////////////////// ///Génération du menu///// ////////////////////////// //i sert a affecter un identifiant numérique à chaque état int i = 0; //Pour chaque état foreach(descetats.etatRow m_etat in m_descEtats.etat) { TRmenu = new HtmlTableRow(); TDmenu = new HtmlTableCell(); //On crée dynamiquement un lien qui passera le numéro de l'état en paramètre TDmenu.InnerHtml ="<a href=\"?etat=" + i + "\">" + m_etat.libcourt.ToString() + "</a>"; TRmenu.Cells.Add(TDmenu); menu.Rows.Add(TRmenu); i++; } ///////////////////////////////////////////////////////////////// ///Chargement des controles utilisateurs en fontion de l'état//// ///////////////////////////////////////////////////////////////// ///Si on a un paramètre "état" dans l'url, on a cliqué sur un lien du menu ///donc on charge les contrôles associés à l'état sélectionné if (Request.Params["etat"]!=null) { PanelOptions.Visible=true; int m_indexEtat = System.Convert.ToInt32(Request.Params["etat"]); //Pour chaque contrôle de l'état sélectionné foreach(descetats.controlRow m_descControle in m_descEtats.etat[m_indexEtat].GetcontrolRows()) { //On charge les contrôles this.placeControls.Controls.Add(ControlFactory.LoadControlEtat(m_descControle,this)); } //On change le titre de la page qu'on remplace par le liblong de l'état this.TitrePage.Text = m_descEtats.etat[m_indexEtat].liblong; } } #region Code généré par le Concepteur Web Form override protected void OnInit(EventArgs e) { // // CODEGEN : Cet appel est requis par le Concepteur Web Form ASP.NET. // InitializeComponent(); base.OnInit(e); } /// <summary> /// Méthode requise pour la prise en charge du concepteur - ne modifiez pas /// le contenu de cette méthode avec l'éditeur de code. /// </summary> private void InitializeComponent() { this.Load += new System.EventHandler(this.Page_Load); } #endregion } }
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 :

  • Créer une nouvelle connexion de données sur comptoir.mdb dans la fenêtre "explorateur de serveurs" : clic droit sur connexion de données => ajouter une connexion
  • Ajouter un dataset fortement typé au projet : dans l'explorateur de solution, sur le projet "EtatsMetier" => clic droit sur le projet => Ajouter => Ajouter un nouvel élément => Dataset. On l'appellera "DataEtats.xsd"
  • Dans l'explorateur de serveur, développer l'arbre de la connexion à la base Access => développer Tables => Glisser/Déposer toutes les tables dans le concepteur de dataset.
Ce qui devrait vous donner :

Dataset fortement typé de comptoir.mdb
Dataset fortement typé de comptoir.mdb
Ici, nous n'avons pas besoin de recréer les relations entre les tables : les clés des différentes tables sont correctement nommées donc Crystal Reports se chargera de faire le liaisons en fonction du nom des champs.

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 :
génération d'un état
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 :
gestData.cs[design]

Dans cette classe, nous allons ajouter quatre méthodes : "FillEmploye", "FillCommandeByPeriode", "FillAllProduits" et FillDetailCommandes :

GestData.cs
public DataEtats FillEmploye(DataEtats dataEtats, int numEmploye) { //On rajoute une clause where à la requete select pour ne selectionner que les employés voulus this.AdapterEmployes.SelectCommand.CommandText+=" WHERE [N° employé]=?"; this.AdapterEmployes.SelectCommand.Parameters.Add("numEmp",numEmploye); this.AdapterEmployes.Fill(dataEtats); return(dataEtats); } public DataEtats FillCommandesByPeriode(DataEtats dataEtats, Periode periode) { //On rajoute une clause where à la requete select pour ne selectionner que la période voulue this.AdapterCommandes.SelectCommand.CommandText+=" WHERE [Date commande]>=? AND [Date commande]<=?"; this.AdapterCommandes.SelectCommand.Parameters.Add("dateD",periode.DateD); this.AdapterCommandes.SelectCommand.Parameters.Add("dateF",periode.DateF); this.AdapterCommandes.Fill(dataEtats); return(dataEtats); } public DataEtats FillAllProduits(DataEtats dataEtats) { this.AdapterProduits.Fill(dataEtats); return(dataEtats); } public DataEtats FillAllDetailsCommandes(DataEtats dataEtats) { this.AdapterDetailCommandes.Fill(dataEtats); return(dataEtats); }
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 :

ExtractionDataEtats.cs
using System; namespace EtatsMetier { /// <summary> /// Description résumée de ExtractionDataEtats. /// </summary> public class ExtractionDataEtats { public static DataEtats GetEmployeCommandByPeriode(int numEmploye, Periode periode) { //on instancie la classe qui va remplir le dataset gestData m_gestData = new gestData(); //on instancie un dataset fortement typé DataEtats m_dataEtats = new DataEtats(); m_dataEtats = m_gestData.FillEmploye(m_dataEtats,numEmploye); m_dataEtats = m_gestData.FillCommandesByPeriode(m_dataEtats,periode); m_dataEtats = m_gestData.FillAllProduits(m_dataEtats); m_dataEtats = m_gestData.FillAllDetailsCommandes(m_dataEtats); return (m_dataEtats); } } }
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" :


Sélectionner le dataset dataEtat du projet


Nous allons sélectionner les tables : "Employés", "Produits", "Commandes" et "Détails commandes".

Comme promis, Crystal Reports retrouve les relations entre les tables :

Relations entre les tables sous Crystal Reports

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" :

Propriétés de Etat1

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 :

Default.aspx
<asp:panel id="PanelOptions" runat="server" Visible="false"> <TABLE> <TR> <TD> <asp:button id="btnGenerer" Runat="server" Text="Générer l'état"></asp:button></TD> <TD : <asp:DropDownList id="ddlFormat" Runat="server"> <asp:ListItem Value="pdf">pdf</asp:ListItem> <asp:ListItem Value="word">word</asp:ListItem> <asp:ListItem Value="excel">excel</asp:ListItem> </asp:DropDownList> </TD> </TR> </TABLE> <BR> </asp:panel>
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 :

PanelOptions.Visible=true;
Au chargement des contrôles utilisateurs web, juste après :

if (Request.Params["etat"]!=null) {
    Maintenant nous avons deux options
  • Soit nous générons le document dans Default.aspx.
  • Soit la génération de document se fait dans une nouvelle fenêtre que nous ouvrirons.
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" :

Default.aspx.cs
private void btnGenerer_Click(object sender, System.EventArgs e) { int i=0; //pour chaque contrôle du placeholder foreach (IControlEtat m_controle in placeControls.Controls) { //On ajoute le couple Propriété à mapper/valeur en session HttpContext.Current.Session.Add(i.ToString(),m_controle.GetData()); i++; } //on crée l'url en passant en paramètre l'état + le format de sortie string url=@"\genetat.aspx?etat=" + Request.Params["etat"].ToString(); url+="&format=" + this.ddlFormat.SelectedValue; //on ouvre une nouvelle fenetre dans le navigateur Response.Write("<body><script>window.open(\" + url + "\",\"_blank\");</script></body>"); }
Maintenant passons à la génération de l'état :

genetat.aspx.cs
using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Drawing; using System.Web; using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; using System.Reflection; using System.IO; using EtatsMetier; namespace Etats { /// <summary> /// Description résumée de genetat. /// </summary> public class genetat : System.Web.UI.Page { //Etat Crystal Reports protected CrystalDecisions.CrystalReports.Engine.ReportDocument m_report; //Description des etats private descetats m_descEtats; //Dataset fortement typé représentant les données de notre état private DataEtats m_dataEtats; //Index de l'état sélectionné private int m_indexEtat; //Nombre de paramètres en session private int nbrParam; //Liste des paramètres de la méthode d'extraction private object[] listeParam; private void Page_Load(object sender, System.EventArgs e) { //On récupère l'index de l'état passé en paramètre m_indexEtat = System.Convert.ToInt32(Request.Params["etat"]); //On récupère le descriptif des états m_descEtats = DescEtatsFactory.GetEtatDesc(); //On compte le nombre de paramètres en session nbrParam = HttpContext.Current.Session.Keys.Count; //on initialise la liste de paramètres en conséquence listeParam = new object[nbrParam]; int i; //On remplit la liste de paramètres for (i=0;i<nbrParam;i++) { listeParam[i] = HttpContext.Current.Session[i]; } //On vide la session HttpContext.Current.Session.Clear(); //On localise la méthode dans ExtractionDataEtats grâce à son nom défini //dans la balise methodgetdata de notre fichier xml Type t = typeof(ExtractionDataEtats); MethodInfo m = t.GetMethod(m_descEtats.etat[m_indexEtat].methodgetdata); //On invoque la méthode et on lui passe en paramètre la liste des objets de session //On récupère donc les données extraites dans notre dataset m_dataEtats = (DataEtats)m.Invoke(null,listeParam); //On instancie l'état m_report = new CrystalDecisions.CrystalReports.Engine.ReportDocument(); //On charge l'état en fonction du chemin défini dans la balise pathreport //du fichier xml m_report.Load(HttpContext.Current.Request.PhysicalApplicationPath + m_descEtats.etat[m_indexEtat].pathreport); //On passe la source de données à l'état m_report.SetDataSource(m_dataEtats); //Création du flux au format souhaité MemoryStream m_stream = new MemoryStream(); if(Request.Params["format"]=="excel") m_stream = (MemoryStream)m_report.ExportToStream(CrystalDecisions.Shared.ExportFormatType.Excel); else if (Request.Params["format"]=="word") m_stream = (MemoryStream)m_report.ExportToStream(CrystalDecisions.Shared.ExportFormatType.WordForWindows); else m_stream = (MemoryStream)m_report.ExportToStream(CrystalDecisions.Shared.ExportFormatType.PortableDocFormat); //Envoi du flux Response.Clear(); Response.Buffer = true; if(Request.Params["format"]=="excel") Response.ContentType = "application/vnd.ms-excel"; else if (Request.Params["format"]=="word") Response.ContentType = "application/doc"; else Response.ContentType = "application/pdf"; Response.BinaryWrite(m_stream.ToArray()); } #region Code généré par le Concepteur Web Form override protected void OnInit(EventArgs e) { // // CODEGEN : Cet appel est requis par le Concepteur Web Form ASP.NET. // InitializeComponent(); base.OnInit(e); } /// <summary> /// Méthode requise pour la prise en charge du concepteur - ne modifiez pas /// le contenu de cette méthode avec l'éditeur de code. /// </summary> private void InitializeComponent() { this.Load += new System.EventHandler(this.Page_Load); } #endregion } }
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"

descetats.xml
<?xml version="1.0" encoding="ISO-8859-1"?> <descetats xmlns="http://tempuri.org/descetats1.xsd"> <etat> <libcourt>Bilan Commandes</libcourt> <liblong>Bilan des commandes par employé et par période</liblong> <pathreport>EtatsCR\Etat1.rpt</pathreport> <methodgetdata>GetEmployeCommandByPeriode</methodgetdata> <control> <Libcontrol>un employés :</Libcontrol> <pathcontrol>Controles/CtrlDDLEmploye.ascx</pathcontrol> </control> <control> <Libcontrol>une période :</Libcontrol> <pathcontrol>Controles/CtrlPeriode.ascx</pathcontrol> </control> </etat> </descetats>
Compilez et exécutez, sélectionnez l'état "Bilan Commandes" dans le menu et testez !

Les dates des commandes de la base comptoir.mdb sont comprises entre l'année 1996 et l'année 1998.
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 :

descetats.xml
<?xml version="1.0" encoding="ISO-8859-1" ?> <descetats xmlns="http://tempuri.org/descetats.xsd"> <etat> <libcourt>Bilan Commandes</libcourt> <liblong>Bilan des commandes par employé et par période</liblong> <pathreport>EtatsCR\Etat1.rpt</pathreport> <methodgetdata>GetEmployeCommandByPeriode</methodgetdata> <control> <Libcontrol>un employés :</Libcontrol> <pathcontrol>Controles/CtrlDDLEmploye.ascx</pathcontrol> </control> <control> <Libcontrol>une période :</Libcontrol> <pathcontrol>Controles/CtrlPeriode.ascx</pathcontrol> </control> </etat> <etat> <libcourt>Bilan Commandes 2</libcourt> <liblong>Bilan des commandes par employé et par période 2</liblong> <pathreport>EtatsCR\Etat2.rpt</pathreport> <methodgetdata>GetEmployeCommandByPeriode</methodgetdata> <control> <Libcontrol>un employés :</Libcontrol> <pathcontrol>Controles/CtrlDDLEmploye.ascx</pathcontrol> </control> <control> <Libcontrol>une période :</Libcontrol> <pathcontrol>Controles/CtrlPeriode.ascx</pathcontrol> </control> </etat> </descetats>
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 :

  • Soit en totalité.
  • Soit en fonction d'une liste de contraintes sur des champs placés dans la clause Where de la rêquete select.
  • Soit en fonction des enregistrements chargés dans une table "père".
  • Soit en fonction d'une liste de contraintes et des enregistrements de la table "père"
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.