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 (Version VB.Net)

Date de publication : 14/08/2005

Par David Pédehourcq (Mon blog)
 Ronald VASSEUR (autres articles)
 

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">
info 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.vb". 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.vb
Imports System
Imports System.Xml
Namespace EtatsMetier
    ' Description résumée de descEtatsFactory.
    Public Class DescEtatsFactory
        Private Shared m_dataDescEtats As descetats
        Public Shared Function GetEtatDesc() As descetats
            ' s'il n'y a pas d'instance du dataset m_dataDescEtats, on en crée une
            If m_dataDescEtats Is Nothing Then
               m_dataDescEtats = CreateListEtatsDesc
            End If
            Return (m_dataDescEtats)
        End Function
        ' on crée une instance de dataDescEtats qu'on remplit
        Private Shared Function CreateListEtatsDesc() As descetats
            Dim dataDescEtats As descetats
            dataDescEtats = New descetats
            Dim m_doc As XmlDataDocument = New XmlDataDocument(dataDescEtats)
            m_doc.Load(System.AppDomain.CurrentDomain.BaseDirectory + "EtatsMetier/EtatsMetier/descetats.xml")
            Return (dataDescEtats)
        End Function
        ' On détruit l'instance courante de descetats
        Public Shared Sub KillInstance()
            m_dataDescEtats = Nothing
        End Sub
    End Class
End Namespace
idea 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.vb
Imports System.Web
Imports System.Web.SessionState
Namespace Etats
Public Class Global
    Inherits System.Web.HttpApplication
    'Private components As System.ComponentModel.IContainer = Nothing
    Public m_watcher As System.IO.FileSystemWatcher
#Region " Code généré par le Concepteur de composants "
        Public Sub New()
            MyBase.New()
            'Cet appel est requis par le Concepteur de composants.
            InitializeComponent()
            m_watcher = New System.IO.FileSystemWatcher(System.AppDomain.CurrentDomain.BaseDirectory & _
                        "EtatsMetier/EtatsMetier/")
            m_watcher.NotifyFilter = System.IO.NotifyFilters.LastWrite
            m_watcher.Filter = "descetats.xml"
            'm_watcher.Changed += New System.IO.FileSystemEventHandler(m_watcher_Changed)
            AddHandler m_watcher.Changed, AddressOf m_watcher_Changed
            m_watcher.EnableRaisingEvents = True
            'Ajoutez une initialisation quelconque après l'appel InitializeComponent()
        End Sub
        'Requis par le Concepteur de composants
        Private components As System.ComponentModel.IContainer
        'REMARQUE : la procédure suivante est requise par le Concepteur de composants
        'Elle peut être modifiée à l'aide du Concepteur de composants.  
        'Ne la modifiez pas en utilisant l'éditeur de code.
        <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
            components = New System.ComponentModel.Container
        End Sub
		Private Sub m_watcher_Changed(ByVal sender As Object, _
                    ByVal e As System.IO.FileSystemEventArgs)
        	EtatsMetier.DescEtatsFactory.KillInstance()
	    End Sub
End Class
End Namespace
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.vb
Imports 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
        Function GetData() As Object
        ' Propriété gérant le libellé du contrôle
        Property Libelle() As String
    End Interface
End Namespace
info 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.vb".
Ce contrôle permettra la saisie d'une date :

CtrlDate.ascx
<%@ Control="vb" AutoEventWireup="false" Codebehind="CtrlDate.ascx.vb" 
Inherits="Etats.Controles.CtrlDate" 
TargetSchema="http://schemas.microsoft.com/intellisense/ie5"%>
<@ Register="ew" Namespace="eWorld.UI" Assembly="eWorld.UI" >
<asp:Label id="lblLibelle" runat="server"></asp:Label>
<ew:CalendarPopup id="DTP" runat="server" Width="88px"></ew:CalendarPopup>
info 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 : faq 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.vb
Imports System.Web.UI.HtmlControls
Imports System.Web.UI.WebControls
Imports System.Web
Imports System.Drawing
Imports System.Data
Imports System
Namespace Etats.Controles
    Public Class CtrlDate
        Inherits System.Web.UI.UserControl
        Implements IControlEtat
        Protected DTP As eWorld.UI.CalendarPopup
        Protected lblLibelle As System.Web.UI.WebControls.Label
        Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
        End Sub
        Protected Overloads Overrides Sub OnInit(ByVal e As EventArgs)
            InitializeComponent()
            MyBase.OnInit(e)
        End Sub
        Private Sub InitializeComponent()
            AddHandler Me.Load, AddressOf Me.Page_Load
        End Sub
        Public Function GetData() As Object Implements IControlEtat.GetData
            Return (Me.DTP.SelectedDate)
        End Function
        Public Property Libelle() As String Implements IControlEtat.Libelle
            Get
                Return Me.lblLibelle.Text
            End Get
            Set(ByVal Value As String)
                Me.lblLibelle.Text = Value
            End Set
        End Property
    End Class
End Namespace

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.vb" 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.vb
Imports System
Namespace EtatsMetier
    Public Structure Periode
        Public DateD As DateTime
        Public DateF As DateTime
    End Structure
End Namespace
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
<%@ Control Language="vb" AutoEventWireup="false" Codebehind="CtrlPeriode.ascx.vb" 
Inherits="Etats.Controles.CtrlPeriode" 
TargetSchema="http://schemas.microsoft.com/intellisense/ie5"%>
<%@ Register TagPrefix="ew" Namespace="eWorld.UI" Assembly="eWorld.UI" %>
<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>
Et on implémente notre interface IControlEtat :

CtrlPeriode.ascx.vb
Imports EtatsMetier
Imports System.Web.UI.HtmlControls
Imports System.Web.UI.WebControls
Imports System.Web
Imports System.Drawing
Imports System.Data
Imports System
Namespace Etats.Controles
    Public Class CtrlPeriode
        Inherits System.Web.UI.UserControl
        Implements IControlEtat
        Protected DTPDebut As eWorld.UI.CalendarPopup
        Protected lblLibelle As System.Web.UI.WebControls.Label
        Protected DTPFin As eWorld.UI.CalendarPopup
        Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
        End Sub
        Protected Overloads Overrides Sub OnInit(ByVal e As EventArgs)
            InitializeComponent()
            MyBase.OnInit(e)
        End Sub
        Private Sub InitializeComponent()
            AddHandler Me.Load, AddressOf Me.Page_Load
        End Sub
        Public Function GetData() As Object Implements IControlEtat.GetData
            Dim p As Periode
            p.DateD = Me.DTPDebut.SelectedDate
            p.DateF = Me.DTPFin.SelectedDate
            Return (p)
        End Function
        Public Property Libelle() As String Implements IControlEtat.Libelle
            Get
                Return Me.lblLibelle.Text
            End Get
            Set(ByVal Value As String)
                Me.lblLibelle.Text = Value
            End Set
        End Property
    End Class
End Namespace
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.vb (dans le projet "EtatsMetier") qui va me permettre de remplir facilement tous mes contrôles de liste déroulante :

FillDDL.vb
Imports System
Imports System.Data
Imports System.Data.OleDb
Imports System.Collections
Namespace EtatsMetier
    Public Structure champDDL
        Public ID As String
        Public Libelle As String
    End Structure
    Public Class FillDDL
        Public Shared Function GetData(ByVal ChampCle As String, _
               ByVal ChampLib As String, ByVal TableName As String) As ArrayList
            Dim m_champ As champDDL
            Dim m_result As ArrayList = New ArrayList
            Dim m_reader As OleDbDataReader
            Dim m_conn As OleDbConnection
            ' Using 
            m_conn = New OleDbConnection("Jet OLEDB:Database Password=; _
                   Data Source=""" & System.AppDomain.CurrentDomain.BaseDirectory & _
                   "\Comptoir.mdb"";Password=;Provider=""Microsoft.Jet.OLEDB.4.0"";")
            Try
                Dim m_comm As OleDbCommand = m_conn.CreateCommand
                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)
                End While
                m_reader.Close()
                m_conn.Close()
            Finally
                ' could not find variable declaration 
                ' TODO : Dispose object 
            End Try
            ' on retourne un arrayList prête à remplir une liste déroulante
            Return (m_result)
        End Function
    End Class
End Namespace
info 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="vb" AutoEventWireup="false" Codebehind="CtrlDDLEmploye.ascx.vb" 
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>
Et le code VB.Net :

CtrlDDLEmploye.ascx.vb
Imports EtatsMetier
Imports System.Web.UI.HtmlControls
Imports System.Web.UI.WebControls
Imports System.Web
Imports System.Drawing
Imports System
Namespace Etats.Controles
    Public Class CtrlDDLEmploye
        Inherits System.Web.UI.UserControl
        Implements IControlEtat
        Protected lblLibelle As System.Web.UI.WebControls.Label
        Protected DDLEmploye As System.Web.UI.WebControls.DropDownList
        Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
            ' On charge la liste déroulante
            For Each m_champ As champDDL In _
                          FillDDL.GetData("[N° employé]", "[Nom]", "[Employés]")
               Me.DDLEmploye.Items.Add(New ListItem(m_champ.Libelle, m_champ.ID))
            Next
        End Sub
        Protected Overloads Overrides Sub OnInit(ByVal e As EventArgs)
            InitializeComponent()
            MyBase.OnInit(e)
        End Sub
        Private Sub InitializeComponent()
            AddHandler Me.Load, AddressOf Me.Page_Load
        End Sub
        Public Function GetData() As Object Implements IControlEtat.GetData
            Return (System.Convert.ToInt32(Me.DDLEmploye.SelectedValue))
        End Function
        Public Property Libelle() As String Implements IControlEtat.Libelle
            Get
                Return Me.lblLibelle.Text
            End Get
            Set(ByVal Value As String)
                Me.lblLibelle.Text = Value
            End Set
        End Property
    End Class
End Namespace
warning 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 : fr 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="vb" Codebehind="Default.aspx.vb" 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="vb">
 <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.vb
Imports System
Imports System.Web
Imports System.Web.UI
Imports EtatsMetier
Namespace Etats
    Public Class ControlFactory
        ' Initialise un contrôle à partir de sa description.
        Public Shared Function LoadControlEtat(ByVal ControlEtats _
               As descetats.controlRow, ByVal page As Page) As Control
            ' On passe par l'interface IControl afin d'initialiser les propriétés 
            ' spécifiques au contrôle utilisateur web des états
            Dim m_control As IControlEtat
            m_control = CType(page.LoadControl(ControlEtats.pathcontrol), IControlEtat)
            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 CType(m_control, Control)
        End Function
    End Class
End Namespace
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.vb
Imports System
Imports System.Collections
Imports System.ComponentModel
Imports System.Data
Imports System.Drawing
Imports System.Web
Imports System.Web.SessionState
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Web.UI.HtmlControls
Imports EtatsMetier
Namespace Etats
    Public Class _Default
        Inherits System.Web.UI.Page
        Protected placeControls As System.Web.UI.WebControls.PlaceHolder
        Protected TitrePage As System.Web.UI.WebControls.Label
        Protected menu As System.Web.UI.HtmlControls.HtmlTable
        Protected TRmenu As System.Web.UI.HtmlControls.HtmlTableRow
        Protected TDmenu As System.Web.UI.HtmlControls.HtmlTableCell
        Protected btnGenerer As System.Web.UI.WebControls.Button
        Protected ddlFormat As System.Web.UI.WebControls.DropDownList
        Protected PanelOptions As System.Web.UI.WebControls.Panel
        Protected m_descEtats As descetats
        Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
            m_descEtats = DescEtatsFactory.GetEtatDesc
            ' Génération du menu
            ' i sert a affecter un identifiant numérique à chaque état
            Dim i As Integer = 0
            ' Pour chaque état
            For Each m_etat As descetats.etatRow 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)
                System.Math.Min(System.Threading.Interlocked.Increment(i), i - 1)
            Next
            ' 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 Not (Request.Params("etat") Is Nothing) Then
                PanelOptions.Visible = True
                Dim m_indexEtat As Integer = _
                    System.Convert.ToInt32(Request.Params("etat"))
                ' Pour chaque contrôle de l'état sélectionné
                For Each m_descControle As descetats.controlRow _
                    In m_descEtats.etat(m_indexEtat).GetcontrolRows
                    ' On charge les contrôles
                    Me.placeControls.Controls.Add(ControlFactory_
                             .LoadControlEtat(m_descControle, Me))
                Next
                ' On change le titre de la page qu'on remplace par le liblong de l'état
                Me.TitrePage.Text = m_descEtats.etat(m_indexEtat).liblong
            End If
        End Sub
        Protected Overloads Overrides Sub OnInit(ByVal e As EventArgs)
            InitializeComponent()
            MyBase.OnInit(e)
        End Sub
        Private Sub InitializeComponent()
            AddHandler Me.btnGenerer.Click, AddressOf Me.btnGenerer_Click
            AddHandler Me.Load, AddressOf Me.Page_Load
        End Sub
        Private Sub btnGenerer_Click(ByVal sender As Object, _
                               ByVal e As System.EventArgs)
            Dim i As Integer = 0
            For Each m_controle As IControlEtat In placeControls.Controls
                HttpContext.Current.Session.Add(i.ToString, m_controle.GetData)
                System.Math.Min(System.Threading.Interlocked.Increment(i), i - 1)
            Next
            Dim url As String = "\genetat.aspx?etat=" & Request.Params("etat").ToString
            url += "&format=" & Me.ddlFormat.SelectedValue
            Response.Write("<body><script>window.open(""" & url _
                          & """,""_blank"");</script></body>")
        End Sub
    End Class
End Namespace
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
info 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.vb".
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.vb[design]

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

GestData.vb
    Public Function FillEmploye(ByVal dataEtats As DataEtats, _
                    ByVal numEmploye As Integer) As DataEtats
        ' On rajoute une clause where à la requete select pour 
        ' ne selectionner que les employés voulus
        Me.AdapterEmployes.SelectCommand.CommandText += " WHERE [N° employé]=?"
        Me.AdapterEmployes.SelectCommand.Parameters.Add("numEmp", numEmploye)
        Me.AdapterEmployes.Fill(dataEtats)
        Return (dataEtats)
    End Function
    Public Function FillCommandesByPeriode(ByVal dataEtats As DataEtats, _
                    ByVal periode As Periode) As DataEtats
        ' On rajoute une clause where à la requete select pour 
        ' ne selectionner que la période voulue
        Me.AdapterCommandes.SelectCommand.CommandText += " WHERE _
                   [Date commande]>=? AND [Date commande]<=?"
        Me.AdapterCommandes.SelectCommand.Parameters.Add("dateD", periode.DateD)
        Me.AdapterCommandes.SelectCommand.Parameters.Add("dateF", periode.DateF)
        Me.AdapterCommandes.Fill(dataEtats)
        Return (dataEtats)
    End Function
    Public Function FillAllProduits(ByVal dataEtats As DataEtats) As DataEtats
        Me.AdapterProduits.Fill(dataEtats)
        Return (dataEtats)
    End Function
    Public Function FillAllDetailsCommandes(ByVal dataEtats As DataEtats) As DataEtats
        Me.AdapterDetailCommandes.Fill(dataEtats)
        Return (dataEtats)
    End Function
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.vb
Imports System
Namespace EtatsMetier
    Public Class ExtractionDataEtats
        Public Shared Function GetEmployeCommandByPeriode(ByVal numEmploye As _
                  Integer, ByVal periode As Periode) As DataEtats
            ' On instancie la classe qui va remplir le dataset
            Dim m_gestData As gestData = New gestData
            ' On instancie un dataset fortement typé
            Dim m_dataEtats As 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)
        End Function
    End Class
End Namespace
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 not (Request.Params["etat"]=nothing)
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.vb
        Private Sub btnGenerer_Click(ByVal sender As Object, ByVal e As System.EventArgs)
            Dim i As Integer = 0
            ' Pour chaque contrôle du placeholder
            For Each m_controle As IControlEtat In placeControls.Controls
                ' On ajoute le couple Propriété à mapper/valeur en session
                HttpContext.Current.Session.Add(i.ToString, m_controle.GetData)
                System.Math.Min(System.Threading.Interlocked.Increment(i), i - 1)
            Next
            ' On crée l'url en passant en paramètre l'état + le format de sortie
            Dim url As String = "\genetat.aspx?etat=" & Request.Params("etat").ToString
            url += "&format=" & Me.ddlFormat.SelectedValue
            ' On ouvre une nouvelle fenetre dans le navigateur
            Response.Write("<body><script>window.open(""" & url & """,""_blank"");</script></body>")
        End Sub
Maintenant passons à la génération de l'état :

genetat.aspx.vb
Imports System
Imports System.Collections
Imports System.ComponentModel
Imports System.Data
Imports System.Drawing
Imports System.Web
Imports System.Web.SessionState
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Web.UI.HtmlControls
Imports System.Reflection
Imports System.IO
Imports EtatsMetier
Namespace Etats
    Public Class genetat
        Inherits System.Web.UI.Page
        ' Etat Crystal Reports
        Protected m_report As CrystalDecisions.CrystalReports.Engine.ReportDocument
        ' Description des etats
        Private m_descEtats As descetats
        ' Dataset fortement typé représentant les données de notre état
        Private m_dataEtats As DataEtats
        ' Index de l'état sélectionné
        Private m_indexEtat As Integer
        ' Nombre de paramètres en session
        Private nbrParam As Integer
        ' Liste des paramètres de la méthode d'extraction
        Private listeParam As Object()
        Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
            ' 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 - 1) {}
            Dim i As Integer
            i = 0
            ' On remplit la liste de paramètres
            While i < nbrParam
                listeParam(i) = HttpContext.Current.Session(i)
                System.Math.Min(System.Threading.Interlocked.Increment(i), i - 1)
            End While
            ' 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
            Dim t As Type = GetType(ExtractionDataEtats)
            Dim m As MethodInfo = _
                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 = CType(m.Invoke(Nothing, listeParam), DataEtats)
            ' 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é
            Dim m_stream As MemoryStream = New MemoryStream
            If Request.Params("format") = "excel" Then
                m_stream = CType(m_report.ExportToStream(CrystalDecisions_
                           .Shared.ExportFormatType.Excel), MemoryStream)
            Else
                If Request.Params("format") = "word" Then
                    m_stream = CType(m_report.ExportToStream(CrystalDecisions_
                           .Shared.ExportFormatType.WordForWindows), MemoryStream)
                Else
                    m_stream = CType(m_report.ExportToStream(CrystalDecisions_
                           .Shared.ExportFormatType.PortableDocFormat), MemoryStream)
                End If
            End If
            ' Envoi du flux 
            Response.Clear()
            Response.Buffer = True
            If Request.Params("format") = "excel" Then
                Response.ContentType = "application/vnd.ms-excel"
            Else
                If Request.Params("format") = "word" Then
                    Response.ContentType = "application/doc"
                Else
                    Response.ContentType = "application/pdf"
                End If
            End If
            Response.BinaryWrite(m_stream.ToArray)
        End Sub
        Protected Overloads Overrides Sub OnInit(ByVal e As EventArgs)
            InitializeComponent()
            MyBase.OnInit(e)
        End Sub
        Private Sub InitializeComponent()
            AddHandler Me.Load, AddressOf Me.Page_Load
        End Sub
    End Class
End Namespace
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 !

warning 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 : fr 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 fr 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 : fr 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.



Valid XHTML 1.1!Valid CSS!

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.