Skip to content

6. Exemples

Nous nous proposons dans ce chapitre d'illustrer ce qui a été vu précédemment par une série d'exemples.

6.1. Exemple 1

6.1.1. Le problème

Cette application doit permettre à un utilisateur de calculer son impôt. On se place dans le cas simplifié d'un contribuable n'ayant que son seul salaire à déclarer (chiffres 2004 pour revenus 2003) :

  • on calcule le nombre de parts du salarié nbParts=nbEnfants/2 +1 s'il n'est pas marié, nbEnfants/2+2 s'il est marié, où nbEnfants est son nombre d'enfants.
  • s'il a au moins trois enfants, il a une demi part de plus
  • on calcule son revenu imposable R=0.72*S où S est son salaire annuel
  • on calcule son coefficient familial QF=R/nbParts
  • on calcule son impôt I. Considérons le tableau suivant :

4262        

0

0

8382

0.0683

291.09

14753

0.1914

1322.92

23888

0.2826

2668.39

38868

0.3738

4846.98

47932

0.4262

6883.66

0                

0.4809

9505.54

Chaque ligne a 3 champs. Pour calculer l'impôt I, on recherche la première ligne où QF<=champ1. Par exemple, si QF=5000 on trouvera la ligne

    8382        0.0683      291.09

L'impôt I est alors égal à 0.0683*R - 291.09*nbParts. Si QF est tel que la relation QF<=champ1 n'est jamais vérifiée, alors ce sont les coefficients de la dernière ligne qui sont utilisés. Ici :

    0               0.4809  9505.54

ce qui donne l'impôt I=0.4809*R - 9505.54*nbParts.

6.1.2. La structure MVC de l'application

La structure MVC de l'application sera la suivante :

Image

Le rôle du contrôleur sera joué par la page [main.aspx]. Il y aura trois actions possibles :

  • init : correspond à la première requête du client. Le contrôleur affichera la vue [formulaire.aspx]
  • calcul : correspond à la demande de calcul de l'impôt. Si les données du formulaire de saisie sont correctes, l'impôt est calculé grâce à la classe métier [impots]. Le contrôleur retourne au client la vue [formulaire.aspx] telle qu'elle avait été validée avec de plus l'impôt calculé. Si les données du formulaire de saisie sont incorrectes, le contrôleur retournera la vue [erreurs.aspx] avec la liste des erreurs et un lien pour retourner au formulaire.
  • retour : correspond au retour au formulaire après une erreur. Le contrôleur affiche la vue [formulaire.aspx] telle qu'elle a été validée avant l'erreur.

Le contrôleur [main.aspx] ne connaît rien au calcul d'impots. Il est simplement en charge de gérer le dialogue client-serveur et de faire exécuter les actions demandées par le client. Pour l'action [calcul], il s'appuiera sur la classe métier [impot].

6.1.3. La classe métier

La classe impot sera définie comme suit :


' espaces de noms importés
Imports System

' classe
Namespace st.istia.univangers.fr
   Public Class impot
       Private limites(), coeffR(), coeffN() As Decimal

       ' constructeur
       Public Sub New(ByRef source As impotsData)
           ' les données nécessaires au calcul de l'impôt
           ' proviennent d'une source extérieure [source]
           ' on les récupère - il peut y avoir une exception
           Dim data() As Object = source.getData
           limites = CType(data(0), Decimal())
           coeffR = CType(data(1), Decimal())
           coeffN = CType(data(2), Decimal())
       End Sub

       ' calcul de l'impôt
       Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Long) As Long
           ' calcul du nombre de parts
           Dim nbParts As Decimal
           If marié Then
               nbParts = CDec(nbEnfants) / 2 + 2
           Else
               nbParts = CDec(nbEnfants) / 2 + 1
           End If
           If nbEnfants >= 3 Then
               nbParts += 0.5D
           End If
           ' calcul revenu imposable & Quotient familial
           Dim revenu As Decimal = 0.72D * salaire
           Dim QF As Decimal = revenu / nbParts
           ' calcul de l'impôt
           limites((limites.Length - 1)) = QF + 1
           Dim i As Integer = 0
           While QF > limites(i)
               i += 1
           End While
           Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
       End Function
   End Class
End Namespace

Un objet impôt en fournissant à son constructeur une source de données de type [impotsData]. Cette classe a une méthode publique [getData] qui permet d'obtenir les trois tableaux de données nécessaires au calcul de l'impôt et qui ont été présentés précédemment. Cette méthode peut gérer une exception si les donnnées n'on pu être acquises ou si elles s'avèrent incorrectes. Une fois l'objet [impot] créé, on peut appeler de façon répétée sa méthode calculer qui calcule l'impôt du contribuable à partir de son statut marital (marié ou non), son nombre d'enfants et son salaire annuel.

6.1.4. La classe d'accès aux données

La classe [impotsData] est la classe qui permet d'accéder aux données. C'est une classe abstraite. On doit créer une classe dérivée pour chaque nouvelle source de données possible (tableaux, fichiers plats, bases de données, console, ...). Sa définition est la suivante :


Imports System.Collections

Namespace st.istia.univangers.fr
   Public MustInherit Class impotsData
       Protected limites() As Decimal
       Protected coeffr() As Decimal
       Protected coeffn() As Decimal
       Protected checked As Boolean
       Protected valide As Boolean

       ' méthode d'accès aux données
       Public MustOverride Function getData() As Object()

       ' méthode de vérification des données
       Protected Function checkData() As Integer
           ' vérifie les données acquises
           ' on doit avoir des données
           valide = Not limites Is Nothing AndAlso Not coeffr Is Nothing AndAlso Not coeffn Is Nothing
           If Not valide Then Return 1
           ' on doit avoir 3 tableaux de même taille
           If valide Then valide = limites.Length = coeffr.Length AndAlso limites.Length = coeffn.Length
           If Not valide Then Return 2
           ' les tableaux doivent être non vides
           valide = limites.Length <> 0
           If Not valide Then Return 3
           ' chaque tableau doit contenir des éléments >=0 et en ordre croissant
           valide = check(limites, limites.Length - 1) AndAlso check(coeffr, coeffr.Length) AndAlso check(coeffn, coeffn.Length)
           If Not valide Then Return 4
           ' tout est bon
           Return 0
       End Function

       ' vérifie la validité du contenu d'un tableau
       Protected Function check(ByRef tableau() As Decimal, ByVal n As Integer) As Boolean
           ' tableau doit avoir ses n premiers éléments >=0 et en ordre strictement croissant
           If tableau(0) < 0 Then Return False
           For i As Integer = 1 To n - 1
               If tableau(i) <= tableau(i - 1) Then Return False
           Next
           ' c'est bon
           Return True
       End Function
   End Class
End Namespace

La classe a les attributs protégés suivants :


limites

tableau des limites de tranches d'impôts


coeffr

tableau des coefficients appliqués au revenu imposable


coeffn

tableau des coefficients appliqués au nombre de parts


checked

booléen indiquant si les données (limites, coeffr, coeffn) ont été vérifiées


valide

booléen indiquant si les données (limites, coeffr, coeffn) sont valides

La classe n'a pas de constructeur. Elle a une méthode abstraite [getData] que les classes dérivées devront implémenter. Cette méthode a pour rôle de :

  • affecter des valeurs aux trois tableaux limites, coeffr, coeffn
  • de lancer une exception si les données n'ont pu être acquises ou si elles se sont avérées invalides.

La classe fournit les méthodes protégées [checkData] et [check] qui vérifient la validité des attributs (limites, coeffr, coeffn). Cela dispense les classes dérivées de les implémenter. Elles n'auront qu'à les utiliser.

La première classe dérivée que nous utiliserons sera la suivante :


Imports System.Collections
Imports System

Namespace st.istia.univangers.fr
   Public Class impotsArray
       Inherits impotsData

       ' constructeur sans argument
       Public Sub New()
           ' initialisations des tableaux avec des constantes
           limites = New Decimal() {4262D, 8382D, 14753D, 23888D, 38868D, 47932D, 0D}
           coeffr = New Decimal() {0D, 0.0683D, 0.1914D, 0.2826D, 0.3738D, 0.4262D, 0.4809D}
           coeffn = New Decimal() {0D, 291.09D, 1322.92D, 2668.39D, 4846.98D, 6883.66D, 9505.54D}
           checked = True
           valide = True
       End Sub

       ' constructeur avec trois tableaux en entrée
       Public Sub New(ByRef limites() As Decimal, ByRef coeffr() As Decimal, ByRef coeffn() As Decimal)
           ' on mémorise les données
           Me.limites = limites
           Me.coeffr = coeffr
           Me.coeffn = coeffn
           checked = False
       End Sub

       Public Overrides Function getData() As Object()
           ' on vérifie éventuellement les données
           Dim erreur As Integer
           If Not checked Then erreur = checkData() : checked = True
           ' si pas valide, alors on lance une exception
           If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
           ' sinon on rend les trois tableaux
           Return New Object() {limites, coeffr, coeffn}
       End Function
   End Class
End Namespace

Cette classe appelée [impotsArray] a deux constructeurs :

  • un constructeur sans argument qui initialise les attributs (limites,coeffr,coeffn) de la classe de base avec des tableaux codés en "dur"
  • un constructeur qui initialise les attributs (limites,coeffr,coeffn) de la classe de base avec des tableaux qui lui sont passés en paramètres

La méthode [getData] qui permettra aux classes externes d'obtenir les tableaux (limites,coeffr,coeffn) se contente de vérifier la validité des trois tableaux à l'aide de la méthode [checkData] de la classe de base. Elle lance une exception si les données sont invalides.

6.1.5. Tests des classes métier et des classes d'accès aux données

Il est important de n'inclure dans une application web que des classes métier et d'accès aux données certifiées correctes. Ainsi la phase de débogage de l'application web pourra se concentrer sur la partie contrôleur et vues. Un programme de test pourait être le suivant :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System
Imports Microsoft.VisualBasic

Namespace st.istia.univangers.fr
   Module test
       Sub Main()
           ' programme interactif de calcul d'impôt
           ' l'utilisateur tape trois données au clavier : marié nbEnfants salaire
           ' le programme affiche alors l'impôt à payer
           Const syntaxe As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"

           ' création d'un objet impôt
           Dim objImpôt As impot = Nothing
           Try
               objImpôt = New impot(New impotsArray)
           Catch ex As Exception
               Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
               Environment.Exit(1)
           End Try
           ' boucle infinie
           Dim marié As String
           Dim nbEnfants As Integer
           Dim salaire As Long
           While True
               ' on demande les paramètres du calcul de l'impôt
               Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
               Dim paramètres As String = Console.In.ReadLine().Trim()
               ' qq chose à faire ?
               If paramètres Is Nothing OrElse paramètres = "" Then
                   Exit While
               End If
               ' vérification du nombre d'arguments dans la ligne saisie
               Dim erreur As Boolean = False
               Dim args As String() = paramètres.Split(Nothing)
               Dim nbParamètres As Integer = args.Length
               If nbParamètres <> 3 Then
                   Console.Error.WriteLine(syntaxe)
                   erreur = True
               End If
               ' vérification de la validité des paramètres
               If Not erreur Then
                   ' marié
                   marié = args(0).ToLower()
                   If marié <> "o" And marié <> "n" Then
                       erreur = True
                   End If
                   ' nbEnfants
                   Try
                       nbEnfants = Integer.Parse(args(1))
                       If nbEnfants < 0 Then
                           Throw New Exception
                       End If
                   Catch
                       erreur = True
                   End Try
                   ' salaire
                   Try
                       salaire = Integer.Parse(args(2))
                       If salaire < 0 Then
                           Throw New Exception
                       End If
                   Catch
                       erreur = True
                   End Try
               End If
               ' si les paramètres sont corrects - on calcule l'impôt
               If Not erreur Then
                   Console.Out.WriteLine(("impôt=" & objImpôt.calculer(marié = "o", nbEnfants, salaire) & " euro(s)"))
               Else
                   Console.Error.WriteLine(syntaxe)
               End If
           End While
       End Sub
   End Module
End Namespace

L'application demande à l'utilisateur de taper au clavier les trois informations dont on a besoin pour calculer son impôt :

  • son statut marital : o pour marié, n pour non marié
  • son nombre d'enfants
  • son salaire annuel

Le calcul de l'impôt est fait à l'aide d'un objet de type [impot] créé dès le lancement de l'application :


           ' création d'un objet impôt
           Dim objImpôt As impot = Nothing
           Try
               objImpôt = New impot(New impotsArray)
           Catch ex As Exception
               Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
               Environment.Exit(1)
           End Try

Comme source de données, on utilise un objet de type [impotsArray]. C'est le constructeur sans argument de cette classe qui est utilisé et qui fournit les trois tableaux (limites, coeffr, coeffn) avec des valeurs en "dur". La création d'un objet [impot] peut en théorie créer une exception puisque pour se créer, l'objet va demander les données (limites,coeffr,coeffn) à sa source de données qu'on lui a passé en paramètre, et que cette acquisition de données peut lancer une exception. Il se trouve qu'ici, la méthode d'obtention des données (codage en dur) ne peut provoquer d'exception. Nous avons cependant laissé la gestion de celle-ci afin d'attirer l'attention du lecteur sur cette possibilité que l'objet [impot] se construise mal.

Voici un exemple d'exécution du programme précédent :

dos>dir
05/04/2004  13:28                1 337 impots.vb
21/04/2004  08:23                1 311 impotsArray.vb
21/04/2004  08:26                1 634 impotsData.vb
21/04/2004  08:42                2 490 testimpots1.vb

Nous compilons l'ensemble des classes [impot, impotsData, impotsArray] dans un assembage [impot.dll] :

dos>vbc /t:library /out:impot.dll impotsData.vb impotsArray.vb impots.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4
dos>dir
05/04/2004  13:28                1 337 impots.vb
21/04/2004  08:23                1 311 impotsArray.vb
21/04/2004  08:26                1 634 impotsData.vb
21/04/2004  08:42                2 490 testimpots1.vb
21/04/2004  09:21                5 632 impot.dll

Nous compilons le programme de test :

dos>vbc /r:impot.dll testimpots1.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4
dos>dir
05/04/2004  13:28                1 337 impots.vb
21/04/2004  08:23                1 311 impotsArray.vb
21/04/2004  08:26                1 634 impotsData.vb
21/04/2004  08:42                2 490 testimpots1.vb
21/04/2004  09:21                5 632 impot.dll
21/04/2004  09:23                4 608 testimpots1.exe

Nous pouvons faire les tests :

dos>testimpots1
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 60000
impôt=4300 euro(s)
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :n 2 60000
impôt=6872 euro(s)
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :

6.1.6. Les vues de l'application web

L'application aura deux vues : [formulaire.aspx] et [erreurs.aspx]. Illustrons le fonctionnement de l'application par des copies d'écran. La vue [formulaire.aspx] est présentée lorsque l'url [main.aspx] est demandée la première fois :

Image

L'utilisateur renseigne le formulaire :

Image

et utilise le bouton [Calculer] pour obtenir la réponse suivante :

Image

Il peut se tromper dans les données saisies :

Image

L'utilisation du bouton [Calculer] amène alors une autre réponse [erreurs.aspx] :

Image

Il peut utiliser le lien [Retour au formulaire] ci-dessus pour retrouver la vue [formulaire.aspx] telle qu'il l'a validée avant l'erreur :

Image

6.1.7. La vue [formulaire.aspx]

La page [formulaire.aspx] sera la suivante :


<%@ page src="formulaire.aspx.vb" inherits="formulaire" AutoEventWireup="false"%>
<html>
   <head>
       <title>Impôt</title>
   </head>
   <body>
       <P>Calcul de votre impôt</P>
       <HR>
       <form method="post" action="main.aspx?action=calcul">
           <TABLE border="0">
               <TR>
                   <TD>Etes-vous marié(e)</TD>
                   <TD>
                       <INPUT type="radio" value="oui" name="rdMarie" <%=rdouichecked%>>Oui 
                   <INPUT type="radio"  value="non" name="rdMarie" <%=rdnonchecked%>>Non
                   </TD>
               </TR>
               <TR>
                   <TD>Nombre d'enfants</TD>
                   <TD><INPUT type="text" size="3" maxLength="3" name="txtEnfants" value="<%=txtEnfants%>"></TD>
               </TR>
               <TR>
                   <TD>Salaire annuel (euro)</TD>
                   <TD><INPUT type="text" maxLength="12" size="12" name="txtSalaire" value="<%=txtSalaire%>"></TD>
               </TR>
               <TR>
                   <TD>Impôt à payer :
                   </TD>
                   <TD><%=txtImpot%></TD>
               </TR>
           </TABLE>
           <hr>
           <P>
               <INPUT type="submit" value="Calculer">
           </P>
       </form>
       <form method="post" action="main.aspx?action=effacer">
               <INPUT type="submit" value="Effacer">
       </form>
   </body>
</html>

Les champs dynamiques de cette page sont les suivants :


rdouichecked

"checked" si la case [oui] doit être cochée, "" sinon


rdnonchecked

idem pour la case [non]


txtEnfants

valeur à placer dans le champ de saisie [txtEnfants]


txtSalaire

valeur à placer dans le champ de saisie [txtSalaire]


txtImpot

valeur à placer dans le champ de saisie [txtImpot]

La page a deux formulaires, chacun ayant un bouton [submit]. Le bouton [Calculer] est le bouton [submit] du formulaire suivant :


       <form method="post" action="main.aspx?action=calcul">
...
           <P>
               <INPUT type="submit" value="Calculer">
           </P>
       </form>

On voit que les paramètres du formulaire seront postés au contrôleur avec [action=calcul]. Le bouton [Effacer] est le bouton [submit] du formulaire suivant :


       <form method="post" action="main.aspx?action=effacer">
               <INPUT type="submit" value="Effacer">
       </form>

On voit que les paramètres du formulaire seront postés au contrôleur avec [action=effacer]. Ici, le formulaire n'a aucun paramètre. Il n'y a que l'action qui importe.

Les champs de [formulaire.aspx] sont calculés par [formulaire.aspx.vb] :


Imports System.Collections.Specialized

Public Class formulaire
   Inherits System.Web.UI.Page

   ' champs de la page
   Protected rdouichecked As String
   Protected rdnonchecked As String
   Protected txtEnfants As String
   Protected txtSalaire As String
   Protected txtImpot As String

   Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
       ' on récupère la précédente requête dans le contexte
       Dim form As NameValueCollection = Context.Items("formulaire")
       ' on prépare la page à afficher
       ' boutons radio
       rdouichecked = ""
       rdnonchecked = "checked"
       If form("rdMarie").ToString = "oui" Then
           rdouichecked = "checked"
           rdnonchecked = ""
       End If
       ' le reste
       txtEnfants = CType(form("txtEnfants"), String)
       txtSalaire = CType(form("txtSalaire"), String)
       txtImpot = CType(Context.Items("txtImpot"), String)
   End Sub
End Class

Le calcul des champs de [main.aspx] se fait à partir de deux informations placées par le contrôleur dans le contexte de la page :

  • Context.Items("formulaire") : dictionnaire de type NameValueCollection contenant les valeurs des champs HTML [rdmarie,txtEnfants,txtSalaire]
  • Context.Items("txtImpot") : valeur de l'impôt

6.1.8. La vue [erreurs.aspx]

La vue [erreurs.aspx] est celle qui affiche les erreurs éventuelles qui peuvent se produire lors de la vie de l'application. Son code de présentation est le suivant :


<%@ page src="erreurs.aspx.vb" inherits="erreurs" AutoEventWireup="false"%>
<HTML>
   <HEAD>
       <title>Impôt</title>
   </HEAD>
   <body>
       <P>Les erreurs suivantes se sont produites :</P>
       <HR>
       <ul>
           <%=erreursHTML%>
       </ul>
       <a href="<%=href%>">
           <%=lien%>
       </a>
   </body>
</HTML>

La page a trois champs dynamiques :


erreursHTML

code HTML d'une liste d'erreurs


href

url d'un lien


lien

texte du lien

Ces champs sont calculés par la partie contrôleur de la page dans [erreurs.aspx.vb] :


Imports System.Collections
Imports Microsoft.VisualBasic

Public Class erreurs
   Inherits System.Web.UI.Page

   ' paramètre de page
   Protected erreursHTML As String = ""
   Protected href As String
   Protected lien As String

   Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
       ' on récupère les éléments du contexte
       Dim erreurs As ArrayList = CType(context.Items("erreurs"), ArrayList)
       href = context.Items("href").ToString
       lien = context.Items("lien").ToString
       ' on génère le code HTML de la liste
       Dim i As Integer
       For i = 0 To erreurs.Count - 1
           erreursHTML += "<li> " + erreurs(i).ToString + "</li>" + ControlChars.CrLf
       Next
   End Sub

End Class

Le contrôleur de la page récupère des informations placées par le contrôleur de l'application dans le contexte de la page :


Context.Items("erreurs"

objet ArrayList contenant la liste des messages d'erreurs à afficher


Context.Items("href"

url d'un lien


Context.Items("lien"

texte du lien

Maintenant que nous savons ce que voit l'utilisateur de l'application, nous pouvons passer à l'écriture du contrôleur de celle-ci.

6.1.9. Les contrôleurs [global.asax, main.aspx]

Rappelons le schéma MVC de notre application :

Image

Le contrôleur [main.aspx] a à traiter trois actions :

  • init : correspond à la première requête du client. Le contrôleur affiche la vue [formulaire.aspx]
  • calcul : correspond à la demande de calcul de l'impôt. Si les données du formulaire de saisie sont correctes, l'impôt est calculé grâce à la classe métier [impots]. Le contrôleur retourne au client la vue [formulaire.aspx] telle qu'elle avait été validée avec de plus l'impôt calculé. Si les données du formulaire de saisie sont incorrectes, le contrôleur retourne la vue [erreurs.aspx] avec la liste des erreurs et un lien pour retourner au formulaire.
  • retour : correspond au retour au formulaire après une erreur. Le contrôleur affiche la vue [formulaire.aspx] telle qu'elle a été validée avant l'erreur.

On sait par ailleurs, que toute requête vers l'application transite via le contrôleur [global.asax] s'il existe. Nous avons alors à l'entrée de l'application, une chaîne de deux contrôleurs :

  • [global.asax] qui de par l'architecture ASP.NET reçoit toute requête vers l'application
  • [main.aspx] qui de par la décision du développeur reçoit également toute requête vers l'application

La nécessité de [main.aspx] vient du fait que nous aurons une session à gérer. Nous avons vu que [global.asax] ne convenait pas comme contrôleur dans ce cas. On pourrait ici se passer totalement de [global.asax]. Nous allons cependant l'utiliser pour exécuter du code au démarrage de l'application. Le schéma MVC ci-dessus montre que nous allons avoir besoin de créer un objet [impot] pour calculer l'impôt. Il est inutile de créer celui-ci plusieurs fois, une fois suffit. Nous allons donc le créer au démarrage de l'application lors de l'événement [Application_Start] géré par le contrôleur [global.asax]. Le code de celui-ci est le suivant :

[global.asax]

<%@ Application src="Global.asax.vb" Inherits="Global" %>

[global.asax.vb]


Imports System
Imports System.Web
Imports System.Web.SessionState
Imports st.istia.univangers.fr

Public Class Global
    Inherits System.Web.HttpApplication

   Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
       ' on crée un objet impot
       Dim objImpot As impot
       Try
           objImpot = New impot(New impotsArray)
           ' on met l'objet dans l'application
           Application("objImpot") = objImpot
           ' pas d'erreur
           Application("erreur") = False
       Catch ex As Exception
           'il y a eu erreur, on le note dans l'application
           Application("erreur") = True
       End Try
   End Sub
End Class

Une fois créé, l'objet de type [impot] est mis dans l'application. C'est là que les différentes requêtes des différents clients iront le chercher. Comme la construction de l'objet [impot] peut échouer, nous gérons l'éventuelle exception et plaçons une clé [erreur] dans l'application pour signaler s'il y a eu ou non une erreur lors de la création de l'objet [impot].

Le code du contrôleur [main.aspx, main.aspx.vb] sera le suivant :

[main.aspx]

<%@ page src="main.aspx.vb" inherits="main" AutoEventWireup="false"%>

[main.aspx.vb]


Imports System
Imports System.Collections.Specialized
Imports System.Collections
Imports st.istia.univangers.fr

Public Class main
   Inherits System.Web.UI.Page

   Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
       ' avant tout, on regarde si l'application a pu s'initialiser correctement
       If CType(Application("erreur"), Boolean) Then
           ' on redirige vers la page d'erreurs
           Dim erreurs As New ArrayList
           erreurs.Add("Application momentanément indisponible...")
           context.Items("erreurs") = erreurs
           context.Items("lien") = ""
           context.Items("href") = ""
           Server.Transfer("erreurs.aspx")
       End If
       ' on récupère l'action à faire
       Dim action As String
       If Request.QueryString("action") Is Nothing Then
           action = "init"
       Else
           action = Request.QueryString("action").ToString.ToLower
       End If
       ' on exécute l'action
       Select Case action
           Case "init"
               ' init application
               initAppli()
           Case "calcul"
               ' calcul impot
               calculImpot()
           Case "retour"
               ' retour au formulaire
               retourFormulaire()
           Case "effacer"
               ' init application
               initAppli()
           Case Else
               ' action inconnue = init
               initAppli()
       End Select
   End Sub

   Private Sub initAppli()
       ' on affiche le formulaire pré-rempli
       Context.Items("formulaire") = initForm()
       Context.Items("txtImpot") = ""
       Server.Transfer("formulaire.aspx", True)
   End Sub

   Private Function initForm() As NameValueCollection
       ' on initialise le formulaire
       Dim form As New NameValueCollection
       form.Set("rdMarie", "non")
       form.Set("txtEnfants", "")
       form.Set("txtSalaire", "")
       Return form
   End Function

   Private Sub calculImpot()
       ' on vérifie la validité des données saisies
       Dim erreurs As ArrayList = checkData()
       ' s'il y a des erreurs, on le signale
       If erreurs.Count <> 0 Then
           ' on sauvegarde les saisies
           Session.Item("formulaire") = Request.Form
           ' on prépare la page d'erreurs
           context.Items("href") = "main.aspx?action=retour"
           context.Items("lien") = "Retour au formulaire"
           context.Items("erreurs") = erreurs
           Server.Transfer("erreurs.aspx")
       End If
       ' ici pas d'erreurs - on calcule l'impôt
       Dim impot As Long = CType(Application("objImpot"), impot).calculer( _
       Request.Form("rdMarie") = "oui", _
       CType(Request.Form("txtEnfants"), Integer), _
       CType(Request.Form("txtSalaire"), Long))
       ' on affiche la page de résultat
       context.Items("txtImpot") = impot.ToString + " euro(s)"
       context.Items("formulaire") = Request.Form
       Server.Transfer("formulaire.aspx", True)
   End Sub

   Private Sub retourFormulaire()
       ' on affiche le formulaire avec des valeurs prises dans la session
       Context.Items("formulaire") = Session.Item("formulaire")
       Context.Items("txtImpot") = ""
       Server.Transfer("formulaire.aspx", True)
   End Sub

   Private Function checkData() As ArrayList
       ' au départ pas d'erreurs
       Dim erreurs As New ArrayList
       Dim erreur As Boolean = False
       ' bouton radio marié
       Try
           Dim rdMarie As String = Request.Form("rdMarie").ToString
           If rdMarie <> "oui" And rdMarie <> "non" Then
               Throw New Exception
           End If
       Catch
           erreurs.Add("Vous n'avez pas indiqué votre statut marital")
       End Try
       ' nbre d'enfants
       Try
           Dim txtEnfants As String = Request.Form("txtEnfants").ToString
           Dim nbEnfants As Integer = CType(txtEnfants, Integer)
           If nbEnfants < 0 Then Throw New Exception
       Catch
           erreurs.Add("Le nombre d'enfants est incorrect")
       End Try
       ' salaire
       Try
           Dim txtSalaire As String = Request.Form("txtSalaire").ToString
           Dim salaire As Integer = CType(txtSalaire, Long)
           If salaire < 0 Then Throw New Exception
       Catch
           erreurs.Add("Le salaire annuel est incorrect")
       End Try
       ' on rend la liste des erreurs
       Return erreurs
   End Function
End Class

Le contrôleur commence par vérifier que l'application s'est correctement initialisée :


       ' avant tout, on regarde si l'application a pu s'initialiser correctement
       If CType(Application("erreur"), Boolean) Then
           ' on redirige vers la page d'erreurs
           Dim erreurs As New ArrayList
           erreurs.Add("Application momentanément indisponible...")
           context.Items("erreurs") = erreurs
           context.Items("lien") = ""
           context.Items("href") = ""
           Server.Transfer("erreurs.aspx")
       End If

Si le contrôleur découvre que l'application n'a pas pu s'initialiser correctement (l'objet [impot] nécessaire au calcul n'a pu être créé), alors il fait afficher la page d'erreurs avec les paramètres adéquats. Ici, il n'y a pas lieu de placer le lien de retour sur le formulaire puisque la totalité de l'application s'avère indisponible. Un message d'erreur général est placé dans [Context.Items("erreurs")] de type [ArrayList].

Si le contrôleur découvre que l'application est opérationnelle, il analyse alors l'action qu'on lui demande d'exécuter via le paramètre [action]. Nous avons déjà rencontré souvent maintenant ce mode de fonctionnement. Le traitement de chaque type d'action est délégué à une fonction.

6.1.9.1. Les actions init, effacer

Ces deux actions doivent faire afficher le formulaire de saisie vide. On rappelle que celui-ci (cf vues) a deux paramètres :

  • Context.Items("formulaire") : dictionnaire de type [NameValueCollection] contenant les valeurs des champs HTML [rdmarie,txtEnfants,txtSalaire]
  • Context.Items("txtImpot") : valeur de l'impôt

La fonction [initAppli] initialise ces deux paramètres de façon à afficher un formulaire vide.

6.1.9.2. L'action calcul

Cette action doit calculer l'impôt à payer à partir des données saisies dans le formulaire et renvoyer celui-ci pré-rempli avec les valeurs saisies et avec de plus le montant de l'impôt calculé. La fonction [calculImpot] chargée de ce travail commence par vérifier que les données du formulaire sont bien correctes :

  • le champ [rdMarie] doit être présent et avoir la valeur [oui] ou [non]
  • le champ [txtEnfants] doit être présent et être un entier >=0
  • le champ [txtSalaire] doit être présent et être un entier >=0

Si les données saisies se révèlent invalides, le contrôleur fait afficher la vue [erreurs.aspx] en ayant auparavant mis dans le contexte les valeurs attendues par celle-ci :

  • les messages d'erreurs sont placés dans un objet [ArrayList], objet placé ensuite dans le contexte [Context.Items("erreurs")]
  • l'url du lien de retour et le texte de ce lien sont également placés dans le contexte.

Avant de passer la main à la page [erreurs.aspx] qui va envoyer la réponse au client, les valeurs saisies dans le formulaire (Request.Form) sont placées dans la session, associées à la clé "formulaire". Ceci permettra à une requête ultérieure de les récupérer.

On peut se demander ici s'il est utile de vérifier que les champs [rdMarie, txtEnfants, txtSalaire] sont présents dans la requête envoyée par le client. C'est inutile si on est sûr que notre client est un navigateur ayant reçu la vue [formulaire.aspx] qui contient ces champs. On ne peut jamais être sûr de cela. Nous montrerons un peu plus tard un exemple où le client est l'application [curl] déjà rencontrée. Nous interrogerons l'application sans envoyer les champs qu'elle attend et nous verrons comment elle réagira. C'est une règle déjà énoncée à plusieurs reprises et que nous rappelons ici : une application ne doit jamais faire d'hypothèses sur le type de client qui l'interroge. Par sécurité, elle doit considérer qu'elle peut être interrogée par une application programmée qui peut lui envoyer des chaînes de paramètres inattendues. Elle doit correctement se comporter dans tous les cas.

Dans notre cas, nous avons vérifié que les champs [rdMarie, txtEnfants, txtSalaire] étaient présents dans la requête mais pas que celle-ci pouvait en contenir d'autres. Dans cette application, ils seraient ignorés. Néanmoins, toujours par mesure de sécurité, il serait intéressant d'enregistrer ce type de demande dans un fichier de logs et de remonter une alerte à l'administrateur de l'application afin que celui-ci sache que l'application reçoit des demandes "bizarres". En analysant celles-ci dans le fichier de logs, il pourrait détecter une éventuelle attaque de l'application et prendre alors les mesures nécessaires à la protection de celle-ci.

Si les données attendues sont correctes, le contrôleur lance le calcul de l'impôt avec l'objet [impot] stocké dans l'application. Puis il stocke dans le contexte les deux informations attendues par la vue [formulaire.aspx] :

  • Context.Items("formulaire") : dictionnaire de type [NameValueCollection] contenant les valeurs des champs HTML [rdmarie,txtEnfants,txtSalaire], ici [Request.Form)], c.a.d. les valeurs saisies précédemment dans le formulaire
  • Context.Items("txtImpot") : valeur de l'impôt qui vient d'être obtenue

Le lecteur attentif s'est peut être posé une question à la lecture de ce qui précède : puisque l'objet [impot] créé au démarrage de l'application est partagé entre toutes les requêtes, ne peut-il y avoir de conflits d'accès entraînant une corruption des données de l'objet [impot]. Pour répondre à cette question, il nous faut revenir au code de la classe [impot]. Les requêtes font appel à la méthode [impot].calculerImpot pour obtenir l'impôt à payer. C'est donc ce code qu'il nous faut examiner :

        Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Long) As Long
            ' calcul du nombre de parts
            Dim nbParts As Decimal
            If marié Then
                nbParts = CDec(nbEnfants) / 2 + 2
            Else
                nbParts = CDec(nbEnfants) / 2 + 1
            End If
            If nbEnfants >= 3 Then
                nbParts += 0.5D
            End If
            ' calcul revenu imposable & Quotient familial
            Dim revenu As Decimal = 0.72D * salaire
            Dim QF As Decimal = revenu / nbParts
            ' calcul de l'impôt
            limites((limites.Length - 1)) = QF + 1
            Dim i As Integer = 0
            While QF > limites(i)
                i += 1
            End While
            Dim impot As Long = CLng(revenu * coeffR(i) - nbParts * coeffN(i))
            Return impot
        End Function

Supposons qu'un thread soit en train d'exécuter la méthode précédente et qu'il soit interrompu. Un autre thread exécute alors la méthode. Quels sont les risques ? Pour le savoir, nous avons rajouté le code suivant :


           Dim impot As Long = CLng(revenu * coeffR(i) - nbParts * coeffN(i))
           ' on patiente 10 secondes
           Thread.Sleep(10000)
           Return impot

Le thread 1, après avoir calculé la valeur [impot1] de la variable locale [impot] est interrompu. Le thread 2 s'exécute alors et calcule une nouvelle valeur [impot2] pour cette même variable [impot] avant d'être interrompu. Le thread 1 récupère la main. Que retrouve-t-il dans la variable locale [impot] ? Cette variable étant locale à une méthode est stockée dans une structure de la mémoire appelée pile. Cette pile fait partie du contexte du thread qui est sauvegardé lorsque celui-ci est interrompu. Lorsque le thread 2 s'installe, son contexte est mis en place avec une nouvelle pile et donc une nouvelle variable locale [impot]. Lorsque le thread 2 va être interrompu à son tour, son contexte sera à son tour sauvegardé. Lorsque le thread 1 est relancé, son contexte est restauré dont sa pile. Il retrouve alors sa variable locale [impot] et non celle du thread 2. On est donc dans une situation où il n'y a pas de conflits d'accès entre requêtes. Les tests faits avec la pause de 10 secondes ci-dessus ont confirmé que les requêtes simultanées obtenaient bien le résultat attendu.

6.1.9.3. L'action retour

Cette action correspond à l'activation du lien [Retour vers le formulaire] de la vue [erreurs.aspx] pour revenir à la vue [formulaire.aspx] pré-rempli avec les valeurs saisies précédemment et sauvegardées dans la session. La fonction [retourFormulaire] récupère cette information. Les deux paramètres attendus par la vue [formulaire.aspx] sont initialisés :

  • Context.Items("formulaire") avec les valeurs saisies précédemment et sauvegardées dans la session
  • Context.Items("txtImpot") avec la chaîne vide

6.1.10. Test de l'application web

L'ensemble des fichiers précédents sont placés dans un dossier <application-path>.

Image

Dans ce dossier, est créé un sous-dossier [bin] dans lequel est placé l'assemblage [impot.dll] issu de la compilation des fichiers des classes métier : [impots.vb, impotsData.vb, impotsArray.vb]. On rappelle ci-dessous, la commande de compilation nécessaire :

dos>vbc /t:library /out:impot.dll impotsData.vb impotsArray.vb impots.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4
dos>dir
05/04/2004  13:28                1 337 impots.vb
21/04/2004  08:23                1 311 impotsArray.vb
21/04/2004  08:26                1 634 impotsData.vb
21/04/2004  09:21                5 632 impot.dll

Le fichier [impot.dll] ci-dessus doit être placé dans <application-path>\bin afin que l'application web y ait accès. Le serveur Cassini est lancé avec les paramètres (<application-path>,/impots1). Avec un navigateur, nous demandons l'url [http://localhost/impots1/main.aspx] :

Image

Nous remplissons le formulaire :

Image

Puis nous lançons le calcul de l'impôt avec le bouton [Calculer]. Nous obtenons la réponse suivante :

Image

Puis nous faisons des saisies erronées :

Image

L'appui sur le bouton [Calculer] amène la réponse suivante :

Image

L'utilisation du lien [Retour au formulaire] nous ramène au formulaire dans l'état où il était lorsqu'il a été validé :

Image

Enfin, l'utilisation du bouton [Effacer] réinitialise la page :

Image

6.1.11. Utilisation du client [curl]

Il est important de tester les applications web avec d'autres clients que les navigateurs. Si on envoie à un navigateur un formulaire avec des paramètres à poster lorsqu'il sera validé, le navigateur renverra les valeurs de ces paramètres au serveur. Un autre client pourrait ne pas le faire et alors le serveur aurait une requête où il manquerait des paramètres. Il doit savoir quoi faire dans ce cas. Un autre exemple est celui des vérifications de saisie faites côté client. Si le formulaire contient des données à vérifier, cette vérification peut être faite côté client grâce à des scripts inclus dans le document contenant le formulaire. Le navigateur ne postera le formulaire que si toutes les données vérifiées côté client sont valides. On pourrait alors être tenté, côté serveur, de considérer qu'on va recevoir des données vérifiées et ne pas vouloir faire cette vérification une seconde fois. Ce serait une erreur. En effet, un client autre qu'un navigateur pourrait au serveur des données invalides et alors l'application web risque d'avoir un comportement inattendu. Nous allons illustrer ces points en utilisant le client [curl].

Tout d'abord, nous demandons l'url [http://localhost/impots1/main.aspx] :

dos>curl --include --url http://localhost/impots1/main.aspx

HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 15:18:10 GMT
Set-Cookie: ASP.NET_SessionId=ivthkl45tjdjrzznevqsf255; path=/
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 982
Connection: Close


<html>
    <head>
        <title>Impôt</title>
    </head>
    <body>
        <P>Calcul de votre impôt</P>
        <HR width="100%" SIZE="1">
        <form method="post" action="main.aspx?action=calcul">
            <TABLE border="0">
                <TR>
                    <TD>Etes-vous marié(e)</TD>
                    <TD>
                        <INPUT type="radio" value="oui" name="rdMarie" >Oui <INPUT type="radio"  value="non" name="rdMarie" checked>Non</TD>
                </TR>
                <TR>
                    <TD>Nombre d'enfants</TD>
                    <TD><INPUT type="text" size="3" maxLength="3" name="txtEnfants" value=""></TD>
                </TR>
                <TR>
                    <TD>Salaire annuel (euro)</TD>
                    <TD><INPUT type="text" maxLength="12" size="12" name="txtSalaire" value=""></TD>
                </TR>
                <TR>
                    <TD>Impôt à payer :
                    </TD>
                    <TD></TD>
                </TR>
            </TABLE>
            <hr>
            <P>
                <INPUT type="submit" value="Calculer">
            </P>
        </form>
        <form method="post" action="main.aspx?action=effacer">
                <INPUT type="submit" value="Effacer">
        </form>
    </body>
</html>

Le serveur nous a envoyé le code HTML du formulaire. Dans les entêtes HTTP nous avons le cookie de session. Nous allons l'utiliser dans les requêtes suivantes afin de maintenir la session. Demandons l'action [calcul] sans fournir de paramètres :

dos>curl --cookie ASP.NET_SessionId=ivthkl45tjdjrzznevqsf255 --include --url  http://localhost/impots1/main.aspx?action=calcul 

HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 15:22:42 GMT
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 380
Connection: Close


<HTML>
    <HEAD>
        <title>Impôt</title>
    </HEAD>
    <body>
        <P>Les erreurs suivantes se sont produites :</P>
        <HR>
        <ul>
            <li> Vous n'avez pas indiqué votre statut marital</li>
<li> Le nombre d'enfants est incorrect</li>
<li> Le salaire annuel est incorrect</li>
        </ul>
        <a href="main.aspx?action=retour">
            Retour au formulaire
        </a>
    </body>
</HTML>

Nous pouvons constater que l'application web a renvoyé la vue [erreurs] avec trois messages d'erreurs pour les trois paramètres manquants. Envoyons maintenant des paramètres erronés :

dos>curl --cookie ASP.NET_SessionId=ivthkl45tjdjrzznevqsf255 --include --data rdMarie=xx --data txtEnfants=xx --data txtSalaire=xx --url http://localhost/impots1/main.aspx?action=calcul 

HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 15:25:50 GMT
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 380
Connection: Close


<HTML>
    <HEAD>
        <title>Impôt</title>
    </HEAD>
    <body>
        <P>Les erreurs suivantes se sont produites :</P>
        <HR>
        <ul>
            <li> Vous n'avez pas indiqué votre statut marital</li>
<li> Le nombre d'enfants est incorrect</li>
<li> Le salaire annuel est incorrect</li>
        </ul>
        <a href="main.aspx?action=retour">
            Retour au formulaire
        </a>
    </body>
</HTML>

Les trois erreurs ont été correctement détectées. Maintenant envoyons des paramètres valides :

dos>curl --cookie ASP.NET_SessionId=ivthkl45tjdjrzznevqsf255 --include --data rdMarie=oui --data txtEnfants=2 --data txtSalaire=60000 --url http://localhost/impots1/main.aspx?action=calcul 

HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 15:28:24 GMT
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 1000
Connection: Close


<html>
    <head>
        <title>Impôt</title>
    </head>
    <body>
        <P>Calcul de votre impôt</P>
        <HR width="100%" SIZE="1">
        <form method="post" action="main.aspx?action=calcul">
            <TABLE border="0">
                <TR>
                    <TD>Etes-vous marié(e)</TD>
                    <TD>
                        <INPUT type="radio" value="oui" name="rdMarie" checked>Oui <INPUT type="radio"  value="non" name="rdMarie" >Non</TD>
                </TR>
                <TR>
                    <TD>Nombre d'enfants</TD>
                    <TD><INPUT type="text" size="3" maxLength="3" name="txtEnfants" value="2"></TD>
                </TR>
                <TR>
                    <TD>Salaire annuel (euro)</TD>
                    <TD><INPUT type="text" maxLength="12" size="12" name="txtSalaire" value="60000"></TD>
                </TR>
                <TR>
                    <TD>Impôt à payer :
                    </TD>
                    <TD>4300 euro(s)</TD>
                </TR>
            </TABLE>
            <hr>
            <P>
                <INPUT type="submit" value="Calculer">
            </P>
        </form>
        <form method="post" action="main.aspx?action=effacer">
                <INPUT type="submit" value="Effacer">
        </form>
    </body>
</html>

Nous avons bien récupéré l'impôt à payer : 4300 euros. Nous retiendrons de cette illustration qu'il ne faut se laisser illusionner par le fait qu'on écrit une application web à destination de clients qui sont des navigateurs. Une application web est un service tcp-ip et ce protocole réseau ne permet pas de dire la nature de l'application cliente d'un service. Donc, on ne peut pas savoir si le client d'une application web est un navigateur ou non. On suit alors deux règles :

  • à réception d'une requête d'un client, on ne fait aucune hypothèse sur le client et on vérifie que les paramètres attendus dans la requête sont bien présents et valides
  • on élabore une réponse à destination des navigateurs, donc en général des documents HTML

Une application web peut être construite pour servir simultanément des clients différents, par exemple des navigateurs et des téléphones mobiles. On peut alors inclure dans chaque requête un nouveau paramètre indiquant le type du client. Ainsi un navigateur demandera le calcul de l'impôt par une requête à l'url http://machine/impots/main.aspx?client=navigateur&action=calcul alors que le téléphone mobile lui fera une requête à l'url http://machine/impots/main.aspx?client=mobile&action=calcul. La structure MVC facilite l'écriture d'une telle application. Elle devient la suivante :

Image

Le bloc [Classes métier, Classes d'accès aux données] ne change pas. C'est en effet une partie indifférente au client. Le bloc [Contrôleur] change peu mais doit prendre un compte un nouveau paramètre dans la requête, le paramètre [client] indiquant à quel type de client il a affaire. Le bloc [vues] doit générer des vues pour chaque type de client. Il pourrait être intéressant de prendre en compte dès la conception de l'application la présence du paramètre [client] dans la requête, même si on n'a comme objectif à court ou moyen terme que les seuls navigateurs. Si l'application doit ultérieurement gérer un nouveau type de client, seules des vues adaptées à celui-ci sont à écrire.

6.2. Exemple 2

6.2.1. Le problème

Nous nous proposons ici de traiter le même problème que précédemment mais en modifiant la source des données de l'objet [impot] créé par l'application web. Dans la version précédente, celle utilisée délivrait les valeurs de tableaux écrits en "dur" dans le code. Cette fois-ci, la nouvelle source de données les prendra dans une source de données ODBC associée à une base MySQL.

6.2.2. La source de données ODBC

Les données se trouveront dans une table appelée [IMPOTS] d'une base MySQL appelée [dbimpots]. Le contenu de cette table sera le suivant :

Image

Le propiétaire de la base est l'utilisateur [admimpots] de mot de passe [mdpimpots]. Nous associons une source de données ODBC à cette base. Avant de le faire, rappelons d'abord les différents moyens d'accéder à une base de données avec la plate-forme .NET.

Il existe de nombreuses bases de données pour les plate-formes windows. Pour y accéder, les applications passent au travers de programmes appelés pilotes (drivers).

Image

Dans le schéma ci-dessus, le pilote présente deux interfaces :

  • l'interface I1 présentée à l'application
  • l'interface I2 vers la base de données

Afin d'éviter qu'une application écrite pour une base de données B1 doive être réécrite si on migre vers une base de données B2 différente, un effort de normalisation a été fait sur l'interface I1. Si on utilise des bases de données utilisant des pilotes "normalisés", la base B1 sera fournie avec un pilote P1, la base B2 avec un pilote P2, et l'interface I1 de ces deux pilotes sera identique. Aussi n'aura-t-on pas à réécrire l'application. On pourra ainsi, par exemple, migrer une base de données ACCESS vers une base de données MySQL sans changer l'application.

Il existe deux types de pilotes normalisés :

  • les pilotes ODBC (Open DataBase Connectivity)
  • les pilotes OLE DB (Object Linking and Embedding DataBase)

Les pilotes ODBC permettent l'accès à des bases de données. Les sources de données pour les pilotes OLE DB sont plus variées : bases de données, messageries, annuaires, ... Il n'y a pas de limite. Toute source de données peut faire l'objet d'un pilote Ole DB si un éditeur le décide. L'intérêt est évidemment grand : on a un accès uniforme à une grande variété de données.

La plate-forme .NET 1.1 est livrée avec trois types de classes d'accès aux données :

  1. les classes SQL Server.NET, pour accéder aux bases SQL Server de Microsoft
  2. les classes Ole Db.NET, pour accéder aux bases des SGBD offrant un pilote OLE DB
  3. les classes odbc.net, pour accéder aux bases des SGBD offrant un pilote ODBC

Le SGBD MySQL dispose depuis longtemps d'un pilote ODBC. C'est celui-ci que nous utilisons maintenant. Sous Windows, nous prenons l'option [Menu Démarrer/Panneau de configuration/Outils d'administration/Sources ODBC 32 bits]. Selon la version de windows, ce chemin peut légèrement varier. On obtient l'application suivante qui va nous permettre de créer notre source ODBC :

Image

Nous allons créer une source de données Système, c.a.d. une source de données que tout utilisateur de la machine pourra utiliser.Aussi, ci-dessus sélectionnons-nous l'onglet [Source de données système]. La page présentée a un bouton [Ajouter] que nous utilisons pour créer une nouvelle source de données ODBC :

Image

L'assistant demande de sélectionner le pilote ODBC à utiliser. Windows amène avec lui un certain nombre de pilotes ODBC pré-installés. Le pilote ODBC de MySQL ne fait pas partie du lot. Il faut donc auparavant l'installer. On le trouvera sur internet en tapant la chaîne clé "MySQL ODBC" ou encore "MyODBC" dans un moteur de recherche. Ici, nous avons installé le pilote [MySQL ODBC 3.51]. Nous le sélectionnons et faisons [Terminer] :

Image

Un certain nombre de renseignements doivent être fournis :


Data Source Name

le nom qui désignera la source de données ODBC. Toute application windows pourra avoir accès à la source via ce nom


Description

un texte arbitraire décrivant la source de données


Host Name

le nom de la machine hébergeant le SGBD MySQL. Ici c'est la machine locale. Ce pourrait être une machine distante. Cela permettrait à une application windows d'accéder à une base distante sans aucun codage particulier. C'est un grand intérêt de la source ODBC.


Database Name

un SGBD MySQL peut gérer plusieurs bases. Ici on précise laquelle on veut gérer : dbimpots


User

nom d'un utilisateur déclaré au sein du SGBD MySQL. C'est sous son nom que se feront les accès à la source de données. Ici : admimpots


Password

le mot de passe de cet utilisateur. Ici : mdpimpots


Port

port de travail du SGBD MySQL. Par défaut c'est le port 3306. Nous ne l'avons pas changé

Ceci fait, nous testons la validité de nos paramètres de connexion avec le bouton [Test Data Source] :

Image

Ceci fait, nous sommes sûrs de notre source de données ODBC. Nous pouvons désormais l'exploiter. Nous faisons autant de fois que nécessaire [OK] pour sortir de l'assistant ODBC.

Si le lecteur ne dispose pas du SGBD mySQL, il peut se le procurer librement à l'url [http://www.mysql.com]. Nous présentons ci-dessous la démarche pour créer une source ODBC avec Access. Les premières étapes sont identiques à ce qui a été décrit précédemment. On ajoute une nouvelle source de données système :

Image

Le pilote sélectionné sera [Microsoft Access Driver]. On fait [Terminer] pour passer à la définition de la source ODBC :

Image

Les renseignements à fournir sont les suivants :


Nom de la source de données

le nom qui désignera la source de données ODBC. Toute application windows pourra avoir accès à la source via ce nom


Description

un texte arbitraire décrivant la source de données


Base de données

le nom complet du fichier ACCESS à exploiter

6.2.3. Une nouvelle classe d'accès aux données

Revenons la structure MVC de notre application :

Image

Sur le schéma ci-dessus, la classe [impotsData] est chargée de récupérer les données. Elle devra le faire ici auprès de la base MySQL [dbimpots]. Nous savons depuis la version précédente de cette application, que [impotsData] est une classe abstraite qu'il faut dériver à chaque fois qu'on veut l'adapter à une nouvelle source de données. Rappelons la structure de cette classe abstraite :


Imports System.Collections

Namespace st.istia.univangers.fr
   Public MustInherit Class impotsData
       Protected limites() As Decimal
       Protected coeffr() As Decimal
       Protected coeffn() As Decimal
       Protected checked As Boolean
       Protected valide As Boolean

       ' méthode d'accès aux données
       Public MustOverride Function getData() As Object()

       ' méthode de vérification des données
       Protected Function checkData() As Integer
           ' vérifie les données acquises
...
       End Function

       ' vérifie la validité du contenu d'un tableau
       Protected Function check(ByRef tableau() As Decimal, ByVal n As Integer) As Boolean
       ...
       End Function
   End Class
End Namespace

La classe qui dérive [impotsData] doit implémenter deux méthodes :

  • un constructeur si le constructeur sans arguments de [impotsData] ne lui convient pas
  • la méthode [getData] qui rend les trois tableaux (limites,coeffr,coeffn)

Nous créons la classe [impotsODBC] qui va aller chercher les données (limites,coeffr,coeffn) dans une source ODBC dont on lui donnera le nom :


Imports System.Data.Odbc
Imports System.Data
Imports System.Collections
Imports System

Namespace st.istia.univangers.fr
   Public Class impotsODBC
       Inherits impotsData

       ' variables d'instance
       Protected DSNimpots As String

       ' constructeur
       Public Sub New(ByVal DSNimpots As String)
           ' on note les trois informations
           Me.DSNimpots = DSNimpots
       End Sub

       Public Overrides Function getdata() As Object()
           ' initialise les trois tableaux limites, coeffr, coeffn à partir
           ' du contenu de la table [impots] de la base ODBC DSNimpots
           ' limites, coeffr, coeffn sont les trois colonnes de cette table
           ' peut lancer diverses exceptions

           Dim connectString As String = "DSN=" + DSNimpots + ";"      ' chaîne de connexion à la base
           Dim impotsConn As OdbcConnection = Nothing      ' la connexion
           Dim sqlCommand As OdbcCommand = Nothing         ' la commande SQL
           ' la requête SELECT
           Dim selectCommand As String = "select limites,coeffr,coeffn from impots"
           ' tableaux pour récupérer les données
           Dim aLimites As New ArrayList
           Dim aCoeffR As New ArrayList
           Dim aCoeffN As New ArrayList
           Try
               ' on tente d'accéder à la base de données
               impotsConn = New OdbcConnection(connectString)
               impotsConn.Open()
               ' on crée un objet command
               sqlCommand = New OdbcCommand(selectCommand, impotsConn)
               ' on exécute la requête
               Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
               ' Exploitation de la table récupérée
               While myReader.Read()
                   ' les données de la ligne courante sont mis dans les tableaux
                   aLimites.Add(myReader("limites"))
                   aCoeffR.Add(myReader("coeffr"))
                   aCoeffN.Add(myReader("coeffn"))
               End While
               ' libération des ressources
               myReader.Close()
               impotsConn.Close()
           Catch e As Exception
               Throw New Exception("Erreur d'accès à la base de données (" + e.Message + ")")
           End Try
           ' les tableaux dynamiques sont mis dans des tableaux statiques
           Me.limites = New Decimal(aLimites.Count - 1) {}
           Me.coeffr = New Decimal(aLimites.Count - 1) {}
           Me.coeffn = New Decimal(aLimites.Count - 1) {}
           Dim i As Integer
           For i = 0 To aLimites.Count - 1
               limites(i) = Decimal.Parse(aLimites(i).ToString())
               coeffR(i) = Decimal.Parse(aCoeffR(i).ToString())
               coeffN(i) = Decimal.Parse(aCoeffN(i).ToString())
           Next i
           ' on vérifie les données acquises
           Dim erreur As Integer = checkData()
           ' si données pas valides, alors on lance une exception
           If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
           ' sinon on rend les trois tableaux
           Return New Object() {limites, coeffr, coeffn}
       End Function
   End Class
End Namespace

Intéressons-nous au constructeur :


       ' constructeur
       Public Sub New(ByVal DSNimpots As String)
           ' on note les trois informations
           Me.DSNimpots = DSNimpots
       End Sub

Il reçoit en paramètre, le nom de la source ODBC dans laquelle se trouvent les données à acquérir. Le constructeur se contente de mémoriser ce nom. La méthode [getData] est chargée de lire les données de la table [impots] et de les mettre dans trois tableaux (limites, coeffr, coeffn). Commentons son code :

  • les paramètres de la connexion à la source de données ODBC sont définis mais celle-ci n'est pas ouverte
                ' chaîne de connexion à la base
                Dim connectString As String = "DSN=" + DSNimpots + ";"
                ' on crée un objet connexion à la base de données - cette connexion n'est pas ouverte
                Dim impotsConn As OdbcConnection = New OdbcConnection(connectString)
    
  • on définit trois objets [ArrayList] pour récupérer les données de la table [impots] :
    
               ' tableaux pour récupérer les données
               Dim aLimites As New ArrayList
               Dim aCoeffR As New ArrayList
               Dim aCoeffN As New ArrayList
    
  • Toute le code d'accès à la base est entouré d'un try/catch pour gérer une éventuelle erreur d'accès. On ouvre la connexion avec la base :
    
                   ' on tente d'accéder à la base de données
                   impotsConn = New OdbcConnection(connectString)
                   impotsConn.Open()
    
  • on exécute la commande [select] sur la connexion ouverte. On obtient un objet [OdbcDataReader] qui va nous permettre de parcourir les lignes de la table résultat du select :
    
                   ' on crée un objet command
                   Dim sqlCommand As OdbcCommand = New OdbcCommand(selectCommand, impotsConn)
                   ' on exécute la requête
                   Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
    
  • on parcourt la table résultat, ligne par ligne. Pour cela on utilise la méthode [Read] de l'objet [OdbcDataReader] obtenu précédemment. Cette méthode fait deux choses :
    • elle avance d'une ligne dans la table. Au départ, on est positionné avant la 1ère ligne
    • elle rend le booléen [true] si elle a pu avancer, [false] sinon, ce dernier cas indiquant que toutes les lignes ont été exploitées.

Les colonnes de la ligne courante de l'objet [OdbcDataReader] sont obtenues par [OdbcDataReader](nomColonne). On obtient un objet représentant la valeur de la colonne. Nous parcourons la totalité de la table pour mettre son contenu dans les trois objets [ArrayList] :


               ' Exploitation de la table récupérée
               While myReader.Read()
                   ' les données de la ligne courante sont mis dans les tableaux
                   aLimites.Add(myReader("limites"))
                   aCoeffR.Add(myReader("coeffr"))
                   aCoeffN.Add(myReader("coeffn"))

    

  • ceci fait, nous libérons les ressources associées à la connexion :
                    ' libération des ressources
                    myReader.Close()
                    impotsConn.Close()
    
  • le contenu des trois objets [ArrayList] sont transférées dans trois tableaux classiques :
    
               ' les tableaux dynamiques sont mis dans des tableaux statiques
               limites = New Decimal(aLimites.Count - 1) {}
               coeffr = New Decimal(aLimites.Count - 1) {}
               coeffn = New Decimal(aLimites.Count - 1) {}
               Dim i As Integer
               For i = 0 To aLimites.Count - 1
                   limites(i) = CType(aLimites(i), Decimal)
                   coeffR(i) = CType(aCoeffR(i), Decimal)
                   coeffN(i) = CType(aCoeffN(i), Decimal)
               Next i
    
  • une fois les données de la table [impots] arrivées dans les trois tableaux, il ne reste plus qu'à vérifier le contenu de ceux-ci à l'aide de la méthode [checkData] de la classe de base [impotsData] :
                ' on vérifie les données acquises
                Dim erreur As Integer = checkData()
                ' si données pas valides, alors on lance une exception
                If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
                ' sinon on rend les trois tableaux
                Return New Object() {limites, coeffr, coeffn}
    

6.2.4. Tests de la classe d'accès aux données

Un programme de test pourrait être le suivant :

Option Explicit On 
Option Strict On

' espaces de noms
Imports System
Imports Microsoft.VisualBasic

Namespace st.istia.univangers.fr

    ' pg de test
    Module testimpots
        Sub Main(ByVal arguments() As String)
            ' programme interactif de calcul d'impôt
            ' l'utilisateur tape trois données au clavier : marié nbEnfants salaire
            ' le programme affiche alors l'impôt à payer
            Const syntaxe1 As String = "pg DSNimpots"
            Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"

            ' vérification des paramètres du programme
            If arguments.Length <> 1 Then
                ' msg d'erreur
                Console.Error.WriteLine(syntaxe1)
                ' fin
                Environment.Exit(1)
            End If
            ' on récupère les arguments
            Dim DSNimpots As String = arguments(0)

            ' création d'un objet impôt
            Dim objImpot As impot = Nothing
            Try
                objImpot = New impot(New impotsODBC(DSNimpots))
            Catch ex As Exception
                Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
                Environment.Exit(2)
            End Try

            ' boucle infinie
            While True
                ' au départ pas d'erreurs
                Dim erreur As Boolean = False

                ' on demande les paramètres du calcul de l'impôt
                Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
                Dim paramètres As String = Console.In.ReadLine().Trim()

                ' qq chose à faire ?
                If paramètres Is Nothing Or paramètres = "" Then
                    Exit While
                End If

                ' vérification du nombre d'arguments dans la ligne saisie
                Dim args As String() = paramètres.Split(Nothing)
                Dim nbParamètres As Integer = args.Length
                If nbParamètres <> 3 Then
                    Console.Error.WriteLine(syntaxe2)
                    erreur = True
                End If
                Dim marié As String
                Dim nbEnfants As Integer
                Dim salaire As Integer
                If Not erreur Then
                    ' vérification de la validité des paramètres
                    ' marié
                    marié = args(0).ToLower()
                    If marié <> "o" And marié <> "n" Then
                        Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
                        erreur = True
                    End If
                    ' nbEnfants
                    nbEnfants = 0
                    Try
                        nbEnfants = Integer.Parse(args(1))
                        If nbEnfants < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        Console.Error.WriteLine(syntaxe2 + "\nArgument nbEnfants incorrect : tapez un entier positif ou nul")
                        erreur = True
                    End Try
                    ' salaire
                    salaire = 0
                    Try
                        salaire = Integer.Parse(args(2))
                        If salaire < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        Console.Error.WriteLine(syntaxe2 + "\nArgument salaire incorrect : tapez un entier positif ou nul")
                        erreur = True
                    End Try
                End If
                If Not erreur Then
                    ' les paramètres sont corrects - on calcule l'impôt
                    Console.Out.WriteLine(("impôt=" & objImpot.calculer(marié = "o", nbEnfants, salaire).ToString + " euro(s)"))
                End If
            End While
        End Sub
    End Module
End Namespace

L'application est lancée avec un paramètre :

  • DSNimpots : nom de la source de données ODBC à exploiter

Le calcul de l'impôt est fait à l'aide d'un objet de type [impot] créé dès le lancement de l'application :


           ' création d'un objet impôt
           Dim objImpôt As impot = Nothing
           Try
               objImpot = New impot(New impotsODBC(DSNimpots))
           Catch ex As Exception
               Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
               Environment.Exit(1)
           End Try

Une fois initialisée, l'application demande de faon répétée à l'utilisateur de taper au clavier les trois informations dont on a besoin pour calculer son impôt :

  • son statut marital : o pour marié, n pour non marié
  • son nombre d'enfants
  • son salaire annuel

L'ensemble des classes est compilé :

dos>vbc /r:system.dll /r:system.data.dll /t:library /out:impot.dll impots.vb impotsArray.vb impotsData.vb impotsODBC.vb
dos>dir
01/04/2004  19:34             7 168 impot.dll
01/04/2004  19:31             1 360 impots.vb
21/04/2004  08:23             1 311 impotsArray.vb
21/04/2004  08:26             1 634 impotsData.vb
01/04/2004  19:34             2 735 impotsODBC.vb
01/04/2004  19:32             3 210 testimpots.vb

Le programme de test est compilé à son tour :

dos>vbc /r:impot.dll testimpots.vb
dir>dir
01/04/2004  19:34             7 168 impot.dll
01/04/2004  19:31             1 360 impots.vb
21/04/2004  08:23             1 311 impotsArray.vb
21/04/2004  08:26             1 634 impotsData.vb
01/04/2004  19:34             2 735 impotsODBC.vb
01/04/2004  19:34             6 144 testimpots.exe
01/04/2004  19:32             3 210 testimpots.vb

Le programme de test est exécuté d'abord avec la source de données ODBC MySQL :

dos>testimpots odbc-mysql-dbimpots
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 60000
impôt=4300 euro(s)

On change de source ODBC pour prendre une source Access :

dos>testimpots odbc-access-dbimpots
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 60000
impôt=4300 F

6.2.5. Les vues de l'application web

Ce sont celles de l'application précédente : formulaire.aspx et erreurs.aspx

6.2.6. Les contrôleurs d'application [global.asax, main.aspx]

Seul le contrôleur [global.asax] doit être modifié. Il est en effet en charge de créer l'objet [impot] lors du démarrage de l'application. Le constructeur de cet objet a pour unique paramètre l'objet de type [impotsData] chargé de récupérer les données. Ce paramètre change donc pour chaque nouveau type de sources de données. Le contrôleur [global.asax.vb] devient le suivant :


Imports System
Imports System.Web
Imports System.Web.SessionState
Imports st.istia.univangers.fr
Imports System.Configuration

Public Class Global
    Inherits System.Web.HttpApplication

   Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
       ' on crée un objet impot
       Dim objImpot As impot
       Try
           objImpot = New impot(New impotsODBC(ConfigurationSettings.AppSettings("DSNimpots")))
           ' on met l'objet dans l'application
           Application("objImpot") = objImpot
           ' pas d'erreur
           Application("erreur") = False
       Catch ex As Exception
           'il y a eu erreur, on le note dans l'application
           Application("erreur") = True
           Application("message") = ex.Message
       End Try
   End Sub
End Class

La source de données de l'objet [impot] est maintenant un objet [impotODBC]. Ce dernier a pour paramètre le nom DSN de la source de données ODBC à exploiter. Plutôt que d'écrire ce nom en dur dans le code, on le met dans le fichier de configuration [web.config] de l'application :


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <appSettings>
       <add key="DSNimpots" value="odbc-mysql-dbimpots" />
   </appSettings>
</configuration>

On sait que la valeur d'une clé C de la section <appSettings> du fichier [web.config] est obtenue dans le code de l'application par [ConfigurationSettings.AppSettings(C)].

Afin de connaître la cause de l'exception, on enregistre le message de celle-ci dans l'application afin qu'il reste disponible pour les requêtes. Le contrôelir [main.aspx.vb] incluera ce message dans sa liste d'erreurs :


   Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
       ' avant tout, on regarde si l'application a pu s'initialiser correctement
       If CType(Application("erreur"), Boolean) Then
           ' on redirige vers la page d'erreurs
           Dim erreurs As New ArrayList
           erreurs.Add("Application momentanément indisponible...(" + Application("message").ToString + ")")
           context.Items("erreurs") = erreurs
           context.Items("lien") = ""
           context.Items("href") = ""
           Server.Transfer("erreurs.aspx")
       End If
       ' on récupère l'action à faire
...

6.2.7. Bilan des modifications

L'application est prête à être testée. Listons les modifications amenées à la version précédente :

  1. une nouvelle classe d'accès aux données a été construite
  2. le contrôleur [global.asax.vb] a été modifié en deux endroits : construction de l'objet [impot] et enregistrement dans l'application du message lié à l'éventuelle exception
  3. le contrôleur [main.aspx.vb] a été modifié en un endroit pour afficher le message d'exception précédent
  4. un fichier [web.config] a été ajouté

Le travail de modification s'est fait essentiellement en 1, c.a.d. en-dehors de l'application web. Ceci a été rendu possible grâce à l'architecture MVC de l'application qui sépare le contrôleur des classes métier. C'est là tout l'intérêt de cette architecture. On pourrait montrer qu'avec un fichier de configuration [web.config] adéquat, on aurait pu éviter toute modification du contrôleur de l'application. Il est possible de mettre dans [web.config] le nom de la classe d'accès aux données à instancier dynamiquement ainsi que les divers paramètres nécessaires à cette instanciation. Avec ces informations, [global.asax] peut instancier l'objet d'accès aux données. Changer de source de données revient alors à :

  • créer la classe d'accès à cette source si elle n'existe pas encore
  • modifier le fichier [web.config] pour permettre la création dynamique d'une instance de cette classe dans [global.asax]

6.2.8. Test de l'application web

L'ensemble des fichiers précédents sont placés dans un dossier <application-path>.

Image

Dans ce dossier, est créé un sous-dossier [bin] dans lequel est placé l'assemblage [impot.dll] issu de la compilation des fichiers des classes métier : [impots.vb, impotsData.vb, impotsArray.vb, impotsODBC.vb]. On rappelle ci-dessous, la commande de compilation nécessaire :

dos>vbc /r:system.dll /r:system.data.dll /t:library /out:impot.dll impots.vb impotsArray.vb impotsData.vb impotsODBC.vb
dos>dir
01/04/2004  19:34             7 168 impot.dll
01/04/2004  19:31             1 360 impots.vb
21/04/2004  08:23             1 311 impotsArray.vb
21/04/2004  08:26             1 634 impotsData.vb
01/04/2004  19:34             2 735 impotsODBC.vb
01/04/2004  19:32             3 210 testimpots.vb

Le fichier [impot.dll] ci-dessus doit être placé dans <application-path>\bin afin que l'application web y ait accès. Le serveur Cassini est lancé avec les paramètres (<application-path>,/impots2). Les tests donnent les mêmes résultats que dans la version précédente, la présence de la base de données étant transparente pour l'utilisateur. Pour illustrer néanmoins cette présence, nous faisons en sorte que la source ODBC ne soit pas disponible en arrêtant le SGBD MySQL et nous demandons l'url [http://localhost/impots2/main.aspx]. Nous obtenons la réponse suivante :

Image

6.3. Exemple 3

6.3.1. Le problème

Nous nous proposons ici de traiter le même problème en modifiant de nouveau la source des données de l'objet [impot] créé par l'application web. Cette fois-ci, la nouvelle source de données sera une base ACCESS à laquelle on accèdera via un pilote OLEDB. Notre objectif est de montrer une autre façon d'accéder à une base de données.

6.3.2. La source de données OLEDB

Les données se trouveront dans une table appelée [IMPOTS] d'une base ACCESS. Le contenu de cette table sera le suivant :

Image

6.3.3. La classe d'accès aux données

Revenons la structure MVC de notre application :

Image

  • Sur le schéma ci-dessus, la classe [impotsData] est chargée de récupérer les données. Elle devra le faire cette fois-ci auprès d'une source OLEDB.

Nous créons la classe [impotsOLEDB] qui va aller chercher les données (limites,coeffr,coeffn) dans une source ODBC dont on lui donnera le nom :


Imports System.Data
Imports System.Collections
Imports System
Imports System.Xml
Imports System.Data.OleDb

Namespace st.istia.univangers.fr
   Public Class impotsOLEDB
       Inherits impotsData

       ' variables d'instance
       Protected chaineConnexion As String

       ' constructeur
       Public Sub New(ByVal chaineConnexion As String)
           ' on note les trois informations
           Me.chaineConnexion = chaineConnexion
       End Sub

       Public Overrides Function getData() As Object()
           ' initialise les trois tableaux limites, coeffr, coeffn à partir
           ' du contenu de la table [impots] de la base OLEDB [chaineConnexion]
           ' limites, coeffr, coeffn sont les trois colonnes de cette table
           ' peut lancer diverses exceptions

           ' on crée un objet DataAdapter pour lire les données de la source OLEDB
           Dim adaptateur As New OleDbDataAdapter("select limites,coeffr,coeffn from impots", chaineConnexion)
           ' on crée une image en mémoire du résultat du select
           Dim contenu As New DataTable("impots")
           Try
               adaptateur.Fill(contenu)
           Catch e As Exception
               Throw New Exception("Erreur d'accès à la base de données (" + e.Message + ")")
           End Try
           ' on récupère le contenu de la table impots
           Dim lignesImpots As DataRowCollection = contenu.Rows
           ' on dimensionne les tableaux de réception
           Me.limites = New Decimal(lignesImpots.Count - 1) {}
           Me.coeffr = New Decimal(lignesImpots.Count - 1) {}
           Me.coeffn = New Decimal(lignesImpots.Count - 1) {}
           ' on transfère le contenu de la table impots dans les tableaux
           Dim i As Integer
           Dim ligne As DataRow
           Try
               For i = 0 To lignesImpots.Count - 1
                   ' ligne i de la table
                   ligne = lignesImpots.Item(i)
                   ' on récupère le contenu de la ligne
                   limites(i) = CType(ligne.Item(0), Decimal)
                   coeffr(i) = CType(ligne.Item(1), Decimal)
                   coeffn(i) = CType(ligne.Item(2), Decimal)
               Next
           Catch
               Throw New Exception("Les données des tranches d'impôts n'ont pas le bon type")
           End Try
           ' on vérifie les données acquises
           Dim erreur As Integer = checkData()
           ' si données pas valides, alors on lance une exception
           If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
           ' sinon on rend les trois tableaux
           Return New Object() {limites, coeffr, coeffn}
       End Function
   End Class
End Namespace

Intéressons-nous au constructeur :


       ' constructeur
       Public Sub New(ByVal chaineConnexion As String)
           ' on note les trois informations
           Me.chaineConnexion = chaineConnexion
       End Sub

Il reçoit en paramètre, la chaîne de connexion de la source OLEDB dans laquelle se trouvent les données à acquérir. Le constructeur se contente de la mémoriser. Une chaîne de connexion contient tous les paramètres nécessaires au pilote OLEDB pour se connecter à la source OLEDB. Elle est en général assez complexe. Pour découvrir celle des bases ACCESS, on peut se faire aider par l'outil [WebMatrix]. On lance cet outil. Il offre une fenêtre permettant de se connecter à une source de données :

ImageImage ImageImage

Grâce à l'icone désignée par la flèche ci-dessus, il est possible de créer une connexion à deux types de bases de données Microsoft : SQL Server et ACCESS. Choisissons ACCESS :

Image

Nous avons utilisé le bouton [...] pour désigner la base ACCESS. Nous validons l'assistant. Dans l'onglet [Data], des icônes symbolisent la connexion :

Image

Maintenant, construisons un nouveau fichier .aspx par [Files/New File] :

Image

Nous obtenons une feuille vierge sur laquelle on peut dessiner notre interface web :

Image

Faisons glisser de l'onglet [Data] la table [impots] sur la feuille ci-dessus. Nous obtenons le résultat suivant :

Image

Cliquons droit sur l'objet [AccessDataSourceControl] ci-dessous pour avoir accès à ces propriétés :

Image

La chaîne de connexion OLEDB à la base ACCESS est donnée par la propriété [ConnectionString] ci-dessus :

Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=D:\data\serge\devel\aspnet\poly\chap5\impots\3\impots.mdb

On voit que cette chaîne est formée d'une partie fixe et d'une partie variable qui est tout simplement le nom du fichier ACCESS. Nous utiliserons ce fait pour générer la chaîne de connexion à notre source de données OLEDB.

Revenons maintenant à notre classe [impotsOLEDB]. La méthode [getData] est chargée de lire les données de la table [impots] et de les mettre dans trois tableaux (limites, coeffr, coeffn). Commentons son code :

  • on définit l'objet [DataAdapter] qui va nous permettre de transférer en mémoire le résultat d'une requête SQL select. Pour cela, nous définissons la requête [select] à exécuter et l'associons à l'objet [DataAdapter]. Le constructeur de celui-ci réclame également la chaîne de connexion qu'il devra utiliser pour se connecter à la source OLEDB
    
               ' on crée un objet DataAdapter pour lire les données de la source OLEDB
               Dim adaptateur As New OleDbDataAdapter("select limites,coeffr,coeffn from impots", chaineConnexion)
    
  • on exécute la commande [select] grâce à la méthode [Fill] de l'objet [DataAdapter]. Le résultat du [select] est injecté dans un objet [DataTable] créé pour l'occasion. Un objet [DataTable] est l'image en mémoire d'une table de base de données, c.a.d. un ensemble de lignes et de colonnes. Nous gérons une exception qui peut survenir si par exemple la chaîne de connexion est incorrecte.
    
               ' on crée une image en mémoire du résultat du select
               Dim contenu As New DataTable("impots")
               Try
                   adaptateur.Fill(contenu)
               Catch e As Exception
                   Throw New Exception("Erreur d'accès à la base de données (" + e.Message + ")")
               End Try
    
  • dans [contenu], nous avons la table [impots]ramenée par le [select]. Un objet [DataTable] est une table donc un ensemble de lignes. Celles-ci sont accessibles via la propriété [rows] de [datatable] :
    
               ' on récupère le contenu de la table impots
               Dim lignesImpots As DataRowCollection = contenu.Rows
    
  • chaque élément de la collection [lignesImpots] est un objet de type [DataRow] représentant une ligne de la table. Celle-ci a des colonnes accessibles via l'objet [DataRow] via sa propriété [Item]. [DataRow].[Item(i)] est la colonne n° i de la ligne [DataRow]. En parcourant la collection des lignes (la collection DataRows de lignesImpots), et la collections des colonnes de chaque ligne, on est capable d'obtenir la totalité de la table :
                ' on dimensionne les tableaux de réception
                Me.limites = New Decimal(lignesImpots.Count - 1) {}
                Me.coeffr = New Decimal(lignesImpots.Count - 1) {}
                Me.coeffn = New Decimal(lignesImpots.Count - 1) {}
                ' on transfère le contenu de la table impots dans les tableaux
                Dim i As Integer
                Dim ligne As DataRow
                Try
                    For i = 0 To lignesImpots.Count - 1
                        ' ligne i de la table
                        ligne = lignesImpots.Item(i)
                        ' on récupère le contenu de la ligne
                        limites(i) = CType(ligne.Item(0), Decimal)
                        coeffr(i) = CType(ligne.Item(1), Decimal)
                        coeffn(i) = CType(ligne.Item(2), Decimal)
                    Next
                Catch
                    Throw New Exception("Les données des tranches d'impôts n'ont pas le bon type")
                End Try
    

  • une fois les données de la table [impots] arrivées dans les trois tableaux, il ne reste plus qu'à vérifier le contenu de ceux-ci à l'aide de la méthode [checkData] de la classe de base [impotsData] :
                ' on vérifie les données acquises
                Dim erreur As Integer = checkData()
                ' si données pas valides, alors on lance une exception
                If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
                ' sinon on rend les trois tableaux
                Return New Object() {limites, coeffr, coeffn}
    

6.3.4. Tests de la classe d'accès aux données

Un programme de test pourait être le suivant :

Option Explicit On 
Option Strict On

' espaces de noms
Imports System
Imports Microsoft.VisualBasic

Namespace st.istia.univangers.fr

    ' pg de test
    Module testimpots
        Sub Main(ByVal arguments() As String)
            ' programme interactif de calcul d'impôt
            ' l'utilisateur tape trois données au clavier : marié nbEnfants salaire
            ' le programme affiche alors l'impôt à payer
            Const syntaxe1 As String = "pg bdACCESS"
            Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"

            ' vérification des paramètres du programme
            If arguments.Length <> 1 Then
                ' msg d'erreur
                Console.Error.WriteLine(syntaxe1)
                ' fin
                Environment.Exit(1)
            End If
            ' on récupère les arguments
            Dim chemin As String = arguments(0)
            ' on prépare la chaîne de connexion
            Dim chaineConnexion As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + chemin

            ' création d'un objet impôt
            Dim objImpot As impot = Nothing
            Try
                objImpot = New impot(New impotsOLEDB(chaineConnexion))
            Catch ex As Exception
                Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
                Environment.Exit(2)
            End Try

            ' boucle infinie
            While True
                ' au départ pas d'erreurs
                Dim erreur As Boolean = False

                ' on demande les paramètres du calcul de l'impôt
                Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
                Dim paramètres As String = Console.In.ReadLine().Trim()

                ' qq chose à faire ?
                If paramètres Is Nothing Or paramètres = "" Then
                    Exit While
                End If

                ' vérification du nombre d'arguments dans la ligne saisie
                Dim args As String() = paramètres.Split(Nothing)
                Dim nbParamètres As Integer = args.Length
                If nbParamètres <> 3 Then
                    Console.Error.WriteLine(syntaxe2)
                    erreur = True
                End If
                Dim marié As String
                Dim nbEnfants As Integer
                Dim salaire As Integer
                If Not erreur Then
                    ' vérification de la validité des paramètres
                    ' marié
                    marié = args(0).ToLower()
                    If marié <> "o" And marié <> "n" Then
                        Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
                        erreur = True
                    End If
                    ' nbEnfants
                    nbEnfants = 0
                    Try
                        nbEnfants = Integer.Parse(args(1))
                        If nbEnfants < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        Console.Error.WriteLine(syntaxe2 + "\nArgument nbEnfants incorrect : tapez un entier positif ou nul")
                        erreur = True
                    End Try
                    ' salaire
                    salaire = 0
                    Try
                        salaire = Integer.Parse(args(2))
                        If salaire < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        Console.Error.WriteLine(syntaxe2 + "\nArgument salaire incorrect : tapez un entier positif ou nul")
                        erreur = True
                    End Try
                End If
                If Not erreur Then
                    ' les paramètres sont corrects - on calcule l'impôt
                    Console.Out.WriteLine(("impôt=" & objImpot.calculer(marié = "o", nbEnfants, salaire).ToString + " euro(s)"))
                End If
            End While
        End Sub
    End Module
End Namespace

L'application est lancée avec un paramètre :

  • bdACCESS : nom du fichier ACCESS à exploiter

Le calcul de l'impôt est fait à l'aide d'un objet de type [impot] créé dès le lancement de l'application :


           ' on récupère les arguments
           Dim chemin As String = arguments(0)
           ' on prépare la chaîne de connexion
           Dim chaineConnexion As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + chemin

           ' création d'un objet impôt
           Dim objImpot As impot = Nothing
           Try
               objImpot = New impot(New impotsOLEDB(chaineConnexion))
           Catch ex As Exception
               Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
               Environment.Exit(2)
           End Try

La chaîne de connexion à la source OLEDB a été construite à partir des informations obtenues avec [WebMatrix].

Une fois initialisée, l'application demande de façon répétée à l'utilisateur de taper au clavier les trois informations dont on a besoin pour calculer son impôt :

  • son statut marital : o pour marié, n pour non marié
  • son nombre d'enfants
  • son salaire annuel

L'ensemble des classes est compilé :

dos>vbc /r:system.dll /r:system.data.dll /t:library /out:impot.dll impots.vb impotsArray.vb impotsData.vb impotsOLEDB.vb
dos>vbc /r:impot.dll testimpots.vb

Le fichier [impots.mdb] est placé dans le dossier de l'application de test et celle-ci est lancée de la façon suivante :

dos>testimpots impots.mdb
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 60000
impôt=4300 euro(s)

On peut lancer l'application avec un fichier ACCESS incorrect :

dos>testimpots xx
L'erreur suivante s'est produite : Erreur d'accès à la base de données (Ficher 'D:\data\serge\devel\aspnet\poly\chap5\impots\3\xx' introuvable.)

6.3.5. Les vues de l'application web

Ce sont celles de l'application précédente : formulaire.aspx et erreurs.aspx

6.3.6. Les contrôleurs d'application [global.asax, main.aspx]

Seul le contrôleur [global.asax] doit être modifié. Il est en effet en charge de créer l'objet [impot] lors du démarrage de l'application. Le constructeur de cet objet a pour unique paramètre l'objet de type [impotsData] chargé de récupérer les données. Ce paramètre change donc puisqu'on change de sources de données. Le contrôleur [global.asax.vb] devient le suivant :


Imports System
Imports System.Web
Imports System.Web.SessionState
Imports st.istia.univangers.fr
Imports System.Configuration

Public Class Global
    Inherits System.Web.HttpApplication

   Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
       ' on crée un objet impot
       Dim objImpot As impot
       Try
           objImpot = New impot(New impotsOLEDB(ConfigurationSettings.AppSettings("chaineConnexion")))
           ' on met l'objet dans l'application
           Application("objImpot") = objImpot
           ' pas d'erreur
           Application("erreur") = False
       Catch ex As Exception
           'il y a eu erreur, on le note dans l'application
           Application("erreur") = True
           Application("message") = ex.Message
       End Try
   End Sub
End Class

La source de données de l'objet [impot] est maintenant un objet [impotOLEDB]. Ce dernier a pour paramètre la chaîne de connexion de la source de données OLEDB à exploiter. Celle-ci est placée dans le fichier de configuration [web.config] de l'application :


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <appSettings>
       <add key="chaineConnexion" 
    value="Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=D:\data\serge\devel\aspnet\poly\chap5\impots2\impots.mdb" />
   </appSettings>
</configuration>

Le contrôleur [main.aspx] ne change pas.

6.3.7. Bilan des modifications

L'application est prête à être testée. Listons les modifications amenées à la version précédente :

  1. une nouvelle classe d'accès aux données a été construite
  2. le contrôleur [global.asax.vb] a été modifié en un endroit : construction de l'objet [impot]
  3. un fichier [web.config] a été ajouté

6.3.8. Test de l'application web

L'ensemble des fichiers précédents sont placés dans un dossier <application-path>.

Image

Dans ce dossier, est créé un sous-dossier [bin] dans lequel est placé l'assemblage [impot.dll] issu de la compilation des fichiers des classes métier : [impots.vb, impotsData.vb, impotsArray.vb, impotsOLEDB.vb]. On rappelle ci-dessous, la commande de compilation nécessaire :

dos>vbc /r:system.dll /r:system.data.dll /t:library /out:impot.dll impots.vb impotsArray.vb impotsData.vb impotsOLEDB.vb

Le fichier [impot.dll] produit par cette commande doit être placé dans <application-path>\bin afin que l'application web y ait accès. Le serveur Cassini est lancé avec les paramètres (<application-path>,/impots3). Les tests donnent les mêmes résultats que dans la version précédente.

6.4. Exemple 4

6.4.1. Le problème

Nous nous proposons maintenant de transformer notre application en application de simulation de calculs d'impôt. Un utilisateur pourra faire des calculs successifs d'impôts et ceux-ci lui seront présentés sur une nouvelle vue ressemblant à ceci :

Image

6.4.2. La structure MVC de l'application

La structure MVC de l'application devient la suivante :

Image

Apparaît une nouvelle vue [simulations.aspx] dont nous venons de donner une copie d'écran. La classe d'accès aux données sera la classe [impotsODBC] de l'exemple 2.

6.4.3. Les vues de l'application web

La vue [erreurs.aspx] ne change pas. La vue [formulaire.aspx] change légèrement. En effet, le montant de l'impôt n'apparaît désormais plus sur cette vue. Il est maintenant sur la vue [simulations.aspx]. Ainsi au démarrage, la page présentée à l'utilisateur est la suivante :

Image

Par ailleurs, la vue [formulaire] transporte avec elle un script javascript qui vérifie la validité des données saisies avant de les envoyer au serveur comme le montre l'exemple suivant :

Image

Le code de présentation est le suivant :


<%@ page src="formulaire.aspx.vb" inherits="formulaire" AutoEventWireup="false"%>
<html>
   <head>
       <title>Impôt</title>
       <script language="javascript">
       function calculer(){
       // vérification des paramètres avant de les envoyer au serveur
        with(document.frmImpots){
          //nbre d'enfants
          champs=/^\s*(\d+)\s*$/.exec(txtEnfants.value);
          if(champs==null){
            // le modéle n'est pas vérifié
            alert("Le nombre d'enfants n'a pas été donné ou est incorrect");
            txtEnfants.focus();
            return;
          }//if
          //salaire
          champs=/^\s*(\d+)\s*$/.exec(txtSalaire.value);
          if(champs==null){
            // le modéle n'est pas vérifié
            alert("Le salaire n'a pas été donné ou est incorrect");
            txtSalaire.focus();
            return;
          }//if
          // c'est bon - on envoie le formulaire au serveur
          submit();
        }//with
      }//calculer  
       </script>
   </head>
   <body>
       <P>Calcul de votre impôt</P>
       <HR width="100%" SIZE="1">
       <form name="frmImpots" method="post" action="main.aspx?action=calcul">
           <TABLE border="0">
               <TR>
                   <TD>Etes-vous marié(e)</TD>
                   <TD>
                       <INPUT type="radio" value="oui" name="rdMarie" <%=rdouichecked%>>Oui 
                   <INPUT type="radio"  value="non" name="rdMarie" <%=rdnonchecked%>>Non</TD>
               </TR>
               <TR>
                   <TD>Nombre d'enfants</TD>
                   <TD><INPUT type="text" size="3" maxLength="3" name="txtEnfants" value="<%=txtEnfants%>"></TD>
               </TR>
               <TR>
                   <TD>Salaire annuel (euro)</TD>
                   <TD><INPUT type="text" maxLength="12" size="12" name="txtSalaire" value="<%=txtSalaire%>"></TD>
               </TR>
           </TABLE>
           <hr>
           <P>
               <INPUT type="button" value="Calculer" onclick="calculer()">
           </P>
       </form>
       <form method="post" action="main.aspx?action=effacer">
           <INPUT type="submit" value="Effacer">
       </form>
   </body>
</html>

Les champs dynamiques de la page sont ceux des versions précédentes. Le champ dynamique du montant de l'impôt a disparu. Le bouton [Calculer] n'est plus un bouton de type [submit]. Il est de type [button] et lorsqu'il est cliqué, la fonction javascript [calculer()] est exécutée :


               <INPUT type="button" value="Calculer" onclick="calculer()">

On a donné au formulaire un nom [frmImpots] afin de pouvoir le référencer dans le script [calculer] :


       <form name="frmImpots" method="post" action="main.aspx?action=calcul">

La fonction javascript [calculer] utilise des expression régulières pour vérifier la validité des champs du formulaire [document.frmImpots.txtEnfants] et [document.frmImpots.txtSalaire]. Si les valeurs saisies sont correctes, elles sont envoyées au serveur par [document.frmImpots.submit()].

La page de présentation obtient ses champs dynamiques auprès de son contrôleur [formulaire.aspx.vb] suivant :


Imports System.Collections.Specialized

Public Class formulaire
   Inherits System.Web.UI.Page

   ' champs de la page
   Protected rdouichecked As String
   Protected rdnonchecked As String
   Protected txtEnfants As String
   Protected txtSalaire As String
   Protected txtImpot As String

   Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
       ' on récupère la précédente requête dans le contexte
       Dim form As NameValueCollection = Context.Items("formulaire")
       ' on prépare la page à afficher
       ' boutons radio
       rdouichecked = ""
       rdnonchecked = "checked"
       If form("rdMarie").ToString = "oui" Then
           rdouichecked = "checked"
           rdnonchecked = ""
       End If
       ' le reste
       txtEnfants = CType(form("txtEnfants"), String)
       txtSalaire = CType(form("txtSalaire"), String)
   End Sub
End Class

Le contrôleur [formulaire.aspx.vb] est identique aux versions précédentes si ce n'est qu'il n'a plus à récupérer le champ [txtImpot] dans le contexte, ce champ ayant disparu de la page.

La vue [simulations.aspx] se présente visuellement comme suit :

Image

et correspond au code de présentation suivant :


<%@ page src="simulations.aspx.vb" inherits="simulations" autoeventwireup="false" %>
<HTML>
   <HEAD>
       <title>simulations</title>
   </HEAD>
   <body>
       <P>Résultats des simulations</P>
       <HR width="100%" SIZE="1">
       <table>
           <tr>
               <th>
                   Marié</th>
               <th>
                   Enfants</th>
               <th>
                   Salaire annuel (euro)</th>
               <th>
                   Impôt à payer (euro)</th>
           </tr>
           <%=simulationsHTML%>
       </table>
       <p></p>
       <a href="<%=href%>">
           <%=lien%>
       </a>
   </body>
</HTML>

Ce code présente trois champs dynamiques :


simulationsHTML

code HTML d'une liste de simulations sous la forme de lignes de table HTML


href

url d'un lien


lien

texte du lien

Ils sont générés par la partie contrôleur [simulations.aspx.vb] :


Imports System.Collections
Imports Microsoft.VisualBasic

Public Class simulations
   Inherits System.Web.UI.Page

   Protected simulationsHTML As String = ""
   Protected href As String
   Protected lien As String

   Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
       'on récupère les simulations dans le contexte
       Dim simulations As ArrayList = CType(context.Items("simulations"), ArrayList)
       ' chaque simulation est un tableau de 4 éléments string
       Dim simulation() As String
       Dim i, j As Integer
       For i = 0 To simulations.Count - 1
           simulation = CType(simulations(i), String())
           simulationsHTML += "<tr>"
           For j = 0 To simulation.Length - 1
               simulationsHTML += "<td>" + simulation(j) + "</td>"
           Next
           simulationsHTML += "</tr>" + ControlChars.CrLf
       Next
       ' on récupère les autres éléments du contexte
       href = context.Items("href").ToString
       lien = context.Items("lien").ToString
   End Sub
End Class

Le contrôleur de la page récupère des informations placées par le contrôleur de l'application dans le contexte de la page :


Context.Items("simulations")

objet ArrayList contenant la liste des simulations à afficher. Chaque élément est un tableau de 4 chaînes de caractères représentant les informations (marié, enfants, salaire, impot) de la simulation.


Context.Items("href")

url d'un lien


Context.Items("lien")

texte du lien

6.4.4. Les contrôleurs [global.asax, main.aspx]

Rappelons le schéma MVC de notre application :

Image

Le contrôleur [main.aspx] a à traiter trois actions :

  • init : correspond à la première requête du client. Le contrôleur affiche la vue [formulaire.aspx]
  • calcul : correspond à la demande de calcul de l'impôt. Si les données du formulaire de saisie sont correctes, l'impôt est calculé grâce à la classe métier [impotsODBC]. Le contrôleur retourne au client la vue [simulations.aspx] avec le résultat de la simulation courante plus toutes les précédentes. Si les données du formulaire de saisie sont incorrectes, le contrôleur retourne la vue [erreurs.aspx] avec la liste des erreurs et un lien pour retourner au formulaire.
  • retour : correspond au retour au formulaire après une erreur. Le contrôleur affiche la vue [formulaire.aspx] telle qu'elle a été validée avant l'erreur.

Dans cette nouvelle version, seule l'action [calcul] a changé. En effet, si les données sont valides, elle doit aboutir à la vue [simulations.aspx] alors qu'auparavant elle aboutisait à la vue [formulaire.aspx]. Le contrôleur [main.aspx.vb] devient le suivant :


Imports System
...

Public Class main
   Inherits System.Web.UI.Page

   Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
       ' avant tout, on regarde si l'application a pu s'initialiser correctement
...
       ' on exécute l'action
       Select Case action
           Case "init"
               ' init application
               initAppli()
           Case "calcul"
               ' calcul impot
               calculImpot()
           Case "retour"
               ' retour au formulaire
               retourFormulaire()
           Case "effacer"
               ' init application
               initAppli()
           Case Else
               ' action inconnue = init
               initAppli()
       End Select
   End Sub

...
   Private Sub calculImpot()
       ' on sauvegarde les saisies
       Session.Item("formulaire") = Request.Form
       ' on vérifie la validité des données saisies
       Dim erreurs As ArrayList = checkData()
       ' s'il y a des erreurs, on le signale
       If erreurs.Count <> 0 Then
           ' on prépare la page d'erreurs
           context.Items("href") = "main.aspx?action=retour"
           context.Items("lien") = "Retour au formulaire"
           context.Items("erreurs") = erreurs
           Server.Transfer("erreurs.aspx")
       End If
       ' ici pas d'erreurs - on calcule l'impôt
       Dim impot As Long = CType(Application("objImpot"), impot).calculer( _
       Request.Form("rdMarie") = "oui", _
       CType(Request.Form("txtEnfants"), Integer), _
       CType(Request.Form("txtSalaire"), Long))
       ' on rajoute le résultat aux simulations existantes
       Dim simulations As ArrayList
       If Not Session.Item("simulations") Is Nothing Then
           simulations = CType(Session.Item("simulations"), ArrayList)
       Else
           simulations = New ArrayList
       End If
       ' ajout de la simulation courante
       Dim simulation() As String = New String() {Request.Form("rdMarie").ToString, _
       Request.Form("txtEnfants").ToString, Request.Form("txtSalaire").ToString, _
       impot.ToString}
       simulations.Add(simulation)
       ' on met les simulations dans la session et le contexte
       context.Items("simulations") = simulations
       Session.Item("simulations") = simulations
       ' on affiche la page de résultat
       context.Items("href") = "main.aspx?action=retour"
       context.Items("lien") = "Retour au formulaire"
       Server.Transfer("simulations.aspx", True)
   End Sub
...
End Class

Nous n'avons conservé ci-dessus que ce qui était nécessaire pour comprendre les modifications qui se trouvent uniquement dans la fonction [calculImpots] :

  • tout d'abord la fonction sauve le formulaire [Request.Form] dans la session, ceci afin de pouvoir régénérer le formulaire dans l'état où il a été validé. C'est à faire dans tous les cas, puisque que l'opération aboutisse sur la réponse [erreurs.aspx] ou sur la réponse [simulations.aspx], on revient au formulaire par le lien [Retour au formulaire]. Pour restituer correctement celui-ci, il faut avoir sauvegardé ses valeurs auparavant dans la session.
  • si les données saisies s'avèrent correctes, la fonction met la simulation courante (marié, enfants, salaire, impôt) dans la liste des simulations. Celle-ci est trouvée dans la session associée à la clé "simulations".
  • la liste des simulations est replacée dans la session pour un usage futur. Elle est également placée dans le contexte courant car c'est là que l'attend la vue [simulations.aspx]
  • la vue [simulations.aspx] est affichée une fois mises dans le contextes les autres informations qu'elle attend

6.4.5. Bilan des modifications

L'application est prête à être testée. Listons les modifications amenées aux versions précédentes :

  1. une nouvelle vue a été construite
  2. le contrôleur [main.aspx.vb] a été modifié en un endroit : traitement de l'action [calcul]

6.4.6. Test de l'application web

Le lecteur est invité à faire les tests. On rappelle la démarche. L'ensemble des fichiers de l'application sont placés dans un dossier <application-path>. Dans ce dossier, est créé un sous-dossier [bin] dans lequel est placé l'assemblage [impot.dll] issu de la compilation des fichiers des classes métier : [impots.vb, impotsData.vb, impotsArray.vb, impotsODBC.vb. Le fichier [impot.dll] produit par cette commande doit être placé dans <application-path>\bin afin que l'application web y ait accès. Le serveur Cassini est lancé avec les paramètres (<application-path>,/impots4).

6.5. Conclusion

Les exemples précédents ont montré sur un cas concret des mécanismes couramment utilisés dans le développement web. Nous avons utilisé systématiquement l'architecture MVC pour son intérêt pédagogique. On aurait pu traiter ces mêmes exemples différemment et peut-être plus simplement sans cette architecture. Mais celle-ci apporte de grands avantages dès que l'application devient un peu complexe avec de multiples pages.

Nous pourrions poursuivre nos exemples de différentes façons. En voici quelques unes :

  • l'utilisateur pourrait vouloir garder ses simulations au fil du temps. Il ferait des simulations un jour J et pourrait les retrouver le jour J+3 par exemple. Une solution possible à ce problème est l'utilisation de cookies. Nous savons que le jeton de session entre le serveur et un client est transporté par ce mécanisme. On pourrait utiliser également celui-ci pour transporter les simulations entre le client et le serveur.
    • en même temps que le serveur envoie la page résultat des simulations, il envoie dans ses entêtes HTTP un cookie contenant une chaîne de caractères représentant les simulations. Comme celles-ci sont dans un objet [ArrayList] il y a un travail de transformation de cet objet en [String] à opérer. Le serveur donnerait au cookie une durée de vie, par exemple 30 jours.
    • le navigateur client stocke les cookies reçus dans un fichier et les renvoie à chaque fois qu'il fait une requête à un serveurqui les lui a envoyés s'ils sont encore valides (durée de vie pas dépassée). Le serveur recevra pour les simulations une chaîne de caractères [String] qu'il devra transformer en objet [ArrayList].

Les cookies sont gérés par [Response.Cookies] lors de leur envoi vers le client et par [Request.Cookies] lors de leur réception sur le serveur.

  • le mécanisme précédent peut devenir assez lourd si il y a un nombre important de simulations. Par ailleurs, il est fréquent qu'un utilisateur nettoie périodiquement ses cookies en les supprimant tous même si par ailleurs il autorise son navigateur à les utiliser. Donc un jour ou l'autre on perdra le cookie des simulations. On peut vouloir alors stocker celles-ci sur le serveur plutôt que sur le client, dans une base de données par exemple. Pour lier des simulations à un utilisateur particulier, l'application pourrait démarrer par une phase d'identification réclamant un login et un mot de passe eux-mêmes stockés dans une base de données ou tout autre type de dépôt de données.
  • nous pourrions également vouloir sécuriser le fonctionnement de notre application. Celle-ci fait actuellement deux hypothèses :
    1. l'utilisateur passe toujours par le contrôleur [main.aspx]
    2. et dans ce cas, il utilise toujours les actions proposées dans la page qu'on lui a envoyée

Que se passe-t-il par exemple, si l'utilisateur demande directement l'url [http://localhost/impots4/formulaire.aspx] ? Ce cas est peu propable puisque l'utilisateur ne connaît pas l'existence de cette url. Cependant il doit être prévu. Il peut être géré par le contrôleur d'application [global.asax] qui voit passer toutes les requêtes faites à l'application. il peut ainsi vérifier que la ressource demandée est bien [main.aspx].

Un cas plus probable est qu'un utilisateur n'utilise pas les actions présentes sur la page que le serveur lui a envoyée. Par exemple, que se passe-t-il si l'utilisateur demande directement l'url [http://localhost/impots4/main.aspx?action=retour] sans passer auparavant par le remplissage du formulaire ? Essayons. Nous obtenons la réponse suivante :

Image

On a un plantage du serveur. C'est normal. Pour l'action [retour] le contrôleur s'attend à trouver dans la session un objet [NameValueCollection] représentant les valeurs du formulaire qu'il doit afficher. Il ne les trouve pas. Le mécanisme du contrôleur permet de donner une solution élégante à ce problème. Pour chaque requête, le contrôleur [main.aspx] peut vérifier que l'action demandée est bien l'une des actions de la page précédemment envoyée à l'utilisateur. On peut utiliser le mécanisme suivant :

  • le contrôleur avant d'envoyer sa réponse au client, stocke dans la session de celui-ci une information identifiant cette page
  • lorsqu'il reçoit une nouvelle demande du client, il vérifie que l'action demandée appartient bien à la dernière page envoyée à ce client
  • les informations liant pages et actions autorisées dans ces pages peuvent être introduites dans le fichier de configuration [web.config] de l'application.
  • l'expérience montre que les contrôleurs d'applications ont une large base commune et qu'il est possible de construire un contrôleur générique, la spécialisation de celui-ci pour une application donnée se faisant via un fichier de configuration. C'est la voie suivie par exemple, par l'outil [Struts] dans le domaine de la programmation web en Java.