Skip to content

1. Partie 1

Le PDF du document est disponible |ICI|.

Les exemples du document sont disponibles |ICI|.

1.1. Introduction

Objectifs de l'article :

  • écrire une application web à 3 couches [interface utilisateur, métier, accès aux données]
  • configurer l'application avec Spring IOC
  • écrire différentes versions en changeant l'implémentation de l'une ou l'autre des trois couches.

Outils utilisés :

  • Visual Studio.net pour le développement - voir en annexe le paragraphe 3.1 ;
  • Serveur web Cassini pour l'exécution - voir annexe paragraphe 3.2 ;
  • Nunit pour les tests unitaires – voir annexe paragraphe 3.4 ;
  • Spring pour l'intégration et la configuration des couches de l'application web - voir annexe paragraphe 3.3 ;

Dans une échelle débutant-intermédiaire-avancé, ce document est dans la partie [intermédiaire-avancé]. Sa compréhension nécessite divers pré-requis. Certains d'entre-eux peuvent être acquis dans des documents que j'ai écrits. Dans ce cas, je les cite. Il est bien évident que ce n'est qu'une suggestion et que le lecteur peut utiliser ses documents favoris.

Ce document reprend le fil conducteur d'un document écrit pour Java [Architectures à 3 couches et architectures MVC avec Struts, Spring et Java ]. Nous construisons en VB.NET l'application web MVC à trois couches écrite en Java. L'idée que l'on veut faire passer ici est que les plate-formes de développement Java et .NET sont suffisamment proches l'une de l'autre pour que les compétences acquises dans l'un de ces deux domaines puissent être réutilisées dans l'autre.

Il ne semble pas exister de solution de développement MVC ASP.NET largement reconnue. La solution qui suit reprend la méthode introduite dans le document [Développement WEB avec ASP.NET 1.1]. Si celle-ci a le mérite d'utiliser des concepts répandus dans le développement J2EE, il ne faut cependant la prendre que pour ce qu'elle est, c.a.d. une méthode parmi d'autres de développement MVC. Dès qu'une méthode de développement MVC en ASP.NET sera largement acceptée, il faudra alors adopter cette dernière. La version .NET de Spring, en cours de développement, pourrait bien être une première solution.

1.2. L'application webarticles

Nous présentons ici les éléments d'une application web simplifiée de commerce électronique. Celle-ci permettra à des clients du web :

  • de consulter une liste d'articles provenant d'une base de données
  • d'en mettre certains dans un panier électronique
  • de valider celui-ci. Cette validation aura pour seul effet de mettre à jour, dans la base, les stocks des articles achetés.

Les différentes vues présentées à l'utilisateur seront les suivantes :

- la vue "LISTE" qui présente une liste des articles en vente
- la vue [INFOS] qui donne des informations supplémentaires sur un produit :
  • les vues [PANIER] et [PANIERVIDE] qui donnent le contenu du panier du client
  • la vue [ERREURS] qui signale toute erreur de l'application

Image

1.3. Architecture générale de l'application

On souhaite construire une application ayant la structure à trois couches suivante :

  • les trois couches sont rendues indépendantes grâce à l'utilisation d'interfaces
  • l'intégration des différentes couches est réalisée par Spring
  • chaque couche fait l'objet d'espaces de noms séparés : web (couche UI), domain (couche métier) et dao (couche d'accès aux données).

L'application respectera une architecture MVC (Modèle - Vue - Contrôleur). Si nous reprenons le schéma en couches ci-dessus, l'architecture MVC s'y intègre de la façon suivante :

Le traitement d'une demande d'un client se déroule selon les étapes suivantes :

  1. le client fait une demande au contrôleur. Ce contrôleur sera ici une page .aspx à laquelle on fera jouer un rôle particulier. Elle voit passer toutes les demandes des clients. C'est la porte d'entrée de l'application. C'est le C de MVC.
  1. le contrôleur traite cette demande. Pour ce faire, il peut avoir besoin de l'aide de la couche métier, ce qu'on appelle le modèle M dans la structure MVC.
  2. le contrôleur reçoit une réponse de la couche métier. La demande du client a été traitée. Celle-ci peut appeler plusieurs réponses possibles. Un exemple classique est
    • une page d'erreurs si la demande n'a pu être traitée correctement
    • une page de confirmation sinon
  3. le contrôleur choisit la réponse (= vue) à envoyer au client. Celle-ci est le plus souvent une page contenant des éléments dynamiques. Le contrôleur fournit ceux-ci à la vue.
  4. la vue est envoyée au client. C'est le V de MVC.

1.4. Le modèle

Nous étudions ici le M de MVC. Le modèle est ici constitué des éléments suivants :

  1. les classes métier
  2. les classes d'accès aux données
  3. la base de données

1.4.1. La base de données

La base de données ne contient qu'une table appelée ARTICLES. Celle-ci a été générée avec les commandes SQL suivantes :

CREATE TABLE ARTICLES (
    ID            INTEGER NOT NULL,
    NOM           VARCHAR(20) NOT NULL,
    PRIX          NUMERIC(15,2) NOT NULL,
    STOCKACTUEL   INTEGER NOT NULL,
    STOCKMINIMUM  INTEGER NOT NULL
);
/* contraintes */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);
/* clé primaire */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
clé primaire identifiant un article de façon unique
nom
nom de l'article
prix
son prix
stockactuel
son stock actuel
stockminimum
le stock au-dessous duquel une commande de réapprovisionnement doit être faite

1.4.2. Les espaces de noms du modèle

Le modèle M est ici fourni sous la forme de deux espaces de noms :

  • istia.st.articles.dao : contient les classes d'accès aux données de la couche [dao]
  • istia.st.articles.domain : contient les classes métier de la couche [domain]

Chacun de ces espaces de noms sera généré au sein d'un fichier "assembly" qui lui sera propre :

assembly
contenu
rôle
webarticles-dao
- [IArticlesDao]: l'interface d'accès à la couche [dao].
C'est la seule interface que voit la couche [domain]. Elle n'en voit pas d'autre.
- [Article] : classe définissant un article
- [ArticlesDaoArrayList] : classe d'implémentation de
l'interface [IArticlesDao] avec une classe [ArrayList]
couche d'accès aux données
- se trouve entièrement dans la couche
[dao] de l'architecture 3-tier de
l'application web
webarticles-domain
- [IArticlesDomain]: l'interface d'accès à la couche [domain]. C'est la seule interface que voit la couche web. Elle n'en voit pas d'autre.
- [AchatsArticles] : une classe implémentant [IArticlesDomain]
- [Achat] : classe représentant l'achat d'un client
- [Panier] : classe représentant l'ensemble des achats d'un client
représente le modèle des achats sur le
web - se trouve entièrement dans la
couche [domain] de l'architecture
3-tier de l'application web

1.4.3. La couche [dao]

La couche [dao] contient les éléments suivants :

  • [IArticlesDao]: l'interface d'accès à la couche [dao]

  • [Article] : classe définissant un article

  • [ArticlesDaoArrayList] : classe d'implémentation de l'interface [IArticlesDao] avec une classe [ArrayList]

La structure du projet [Visual Studio] de la couche [dao] est la suivante :

Image

Commentaires :

  • la projet [dao] est de type [bibliothèque de classes]
  • les classes ont été mises dans une arborescence de racine le dossier [istia]. Elles sont toutes dans l'espace de noms [istia.st.articles.dao].

1.4.3.1. La classe [Article]

La classe définissant un article est la suivante :

Imports System

Namespace istia.st.articles.dao

    Public Class Article

        ' champs privés
        Private _id As Integer
        Private _nom As String
        Private _prix As Double
        Private _stockactuel As Integer
        Private _stockminimum As Integer

        ' id article
        Public Property id() As Integer
            Get
                Return _id
            End Get
            Set(ByVal Value As Integer)
                If Value <= 0 Then
                    Throw New Exception("Le champ id [" + Value.ToString + "] est invalide")
                End If
                Me._id = Value
            End Set
        End Property

        ' nom article
        Public Property nom() As String
            Get
                Return _nom
            End Get
            Set(ByVal Value As String)
                If Value Is Nothing OrElse Value.Trim.Equals("") Then
                    Throw New Exception("Le champ nom [" + Value + "] est invalide")
                End If
                Me._nom = Value
            End Set
        End Property

        ' prix article
        Public Property prix() As Double
            Get
                Return _prix
            End Get
            Set(ByVal Value As Double)
                If Value < 0 Then
                    Throw New Exception("Le champ prix [" + Value.ToString + "] est invalide")
                End If
                Me._prix = Value
            End Set
        End Property

        ' stock actuel article
        Public Property stockactuel() As Integer
            Get
                Return _stockactuel
            End Get
            Set(ByVal Value As Integer)
                If Value < 0 Then
                    Throw New Exception("Le champ stockActuel [" + Value.ToString + "] est invalide")
                End If
                Me._stockactuel = Value
            End Set
        End Property

        ' stock minimum article
        Public Property stockminimum() As Integer
            Get
                Return _stockminimum
            End Get
            Set(ByVal Value As Integer)
                If Value < 0 Then
                    Throw New Exception("Le champ stockMinimum [" + Value.ToString + "] est invalide")
                End If
                Me._stockminimum = Value
            End Set
        End Property

        ' constructeur par défaut
        Public Sub New()
        End Sub

        ' constructeur avec propriétés
        Public Sub New(ByVal id As Integer, ByVal nom As String, ByVal prix As Double, ByVal stockactuel As Integer, ByVal stockminimum As Integer)
            Me.id = id
            Me.nom = nom
            Me.prix = prix
            Me.stockactuel = stockactuel
            Me.stockminimum = stockminimum
        End Sub

        ' méthode d'identification de l'article
        Public Overrides Function ToString() As String
            Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
        End Function
    End Class
End Namespace

Cette classe offre :

  1. un constructeur permettant de fixer les 5 informations d'un article : [id, nom, prix, stockactuel, stockminimum]
  2. des propriétés publiques permettant de lire et écrire les 5 informations.
  3. une vérification des données insérées dans l'article. En cas de données erronées, une exception est lancée.
  4. une méthode toString qui permet d'obtenir la valeur d'un article sous forme de chaîne de caractères. C'est souvent utile pour le débogage d'une application.

1.4.3.2. L'interface [IArticlesDao]

L'interface [IArticlesDao] est définie comme suit :

Imports System
Imports System.Collections

Namespace istia.st.articles.dao

    Public Interface IArticlesDao
        ' liste de tous les articles
        Function getAllArticles() As IList
        ' ajoute un article
        Function ajouteArticle(ByVal unArticle As Article) As Integer
        ' supprime un article
        Function supprimeArticle(ByVal idArticle As Integer) As Integer
        ' modifie un article
        Function modifieArticle(ByVal unArticle As Article) As Integer
        ' recherche un article
        Function getArticleById(ByVal idArticle As Integer) As Article
        ' supprime tous les articles
        Sub clearAllArticles()
        ' change le stock d'u article
        Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
    End Interface
End Namespace

Le rôle des différentes méthodes de l'interface est le suivant :

getAllArticles
rend tous les articles de la source de données
clearAllArticles
vide la source de données
getArticleById
rend l'objet [Article] identifié par sa clé primaire
ajouteArticle
permet d'ajouter un article à la source de données
modifieArticle
permet de modidier un article de la source de données
supprimerArticle
permet de supprimer un article de la source de données
changerStockArticle
permet de modifier le stock d'un article de la source de données

L'interface met à disposition des programmes clients un certain nombre de méthodes définies uniquement par leurs signatures. Elle ne s'occupe pas de la façon dont ces méthodes seront réellement implémentées. Cela amène de la souplesse dans une application. Le programme client fait ses appels sur une interface et non pas sur une implémentation précise de celle-ci.

Le choix d'une implémentation précise se fera au moyen d'un fichier de configuration Spring. Afin de montrer que pour tester l'application web, seule l'interface d'accès aux données compte et non sa classe d'implémentation, nous allons tout d'abord implémenter la source de données par un simple objet [ArrayList]. Ultérieurement, nous présenterons une solution à base de SGBD.

1.4.3.3. La classe d'implémentation [ArticlesDaoArrayList]

La classe d'implémentation [ArticlesDaoArrayList] est définie comme suit :

Imports System
Imports System.Collections

Namespace istia.st.articles.dao

    Public Class ArticlesDaoArrayList
        Implements istia.st.articles.dao.IArticlesDao

        Private articles As New ArrayList
        Private Const nbArticles As Integer = 4

        ' constructeur par défaut
        Public Sub New()
            ' on construit qqs articles
            For i As Integer = 1 To nbArticles
                articles.Add(New Article(i, "article" + i.ToString, i * 10, i * 10, i * 10))
            Next
        End Sub

        ' liste de tous les articles
        Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
            ' on retourne la liste des articles
            SyncLock Me
                Return articles
            End SyncLock
        End Function

        ' suppression de tous les articles
        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
            ' on vide la liste des articles
            SyncLock Me
                articles.Clear()
            End SyncLock
        End Sub

        ' obtenir un article identifié par sa clé
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
            ' on recherche l'article dans la liste
            SyncLock Me
                Dim ipos As Integer = posArticle(articles, idArticle)
                If ipos <> -1 Then
                    Return CType(articles(ipos), Article)
                Else
                    Return Nothing
                End If
            End SyncLock
        End Function

        ' ajouter un article à la liste des articles
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
            ' on ajoute l'article à la liste des articles
            SyncLock Me
                ' on vérifie qu'il n'existe pas déjà
                Dim ipos As Integer = posArticle(articles, unArticle.id)
                If ipos <> -1 Then
                    Throw New Exception("L'article d'id [" + unArticle.id.ToString + "] existe déjà")
                End If
                ' on ajoute l'article
                articles.Add(unArticle)
                ' on rend le résultat
                Return 1
            End SyncLock
        End Function

        ' modifier un article
        Public Function modifieArticle(ByVal articleNouveau As Article) As Integer Implements IArticlesDao.modifieArticle
            ' on modifie un article
            SyncLock Me
                ' on vérifie qu'il existe
                Dim ipos As Integer = posArticle(articles, articleNouveau.id)
                ' le cas où il n'existe pas
                If ipos = -1 Then Return 0
                ' il existe - on le modifie
                articles(ipos) = articleNouveau
                ' on rend le résultat
                Return 1
            End SyncLock
        End Function

        ' supprimer un article identifié par sa clé
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
            ' suppression d'un article
            SyncLock Me
                ' on vérifie qu'il existe
                Dim ipos As Integer = posArticle(articles, idArticle)
                ' le cas où il n'existe pas
                If ipos = -1 Then Return 0
                ' il existe - on le supprime
                articles.RemoveAt(ipos)
                ' on rend le résultat
                Return 1
            End SyncLock
        End Function

        ' changer le stock d'un article identifié par sa clé
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
            ' changer le stock d'un article
            SyncLock Me
                ' on vérifie qu'il existe
                Dim ipos As Integer = posArticle(articles, idArticle)
                ' le cas où il n'existe pas
                If ipos = -1 Then Return 0
                ' il existe - on modifie son stock si on peut
                Dim unArticle As Article = CType(articles(ipos), Article)
                ' on ne change le stock que s'il est suffisant
                If unArticle.stockactuel + mouvement >= 0 Then
                    unArticle.stockactuel += mouvement
                    Return 1
                Else
                    Return 0
                End If
            End SyncLock
        End Function

        ' chercher un article identifié par sa clé
        Private Function posArticle(ByVal listArticles As ArrayList, ByVal idArticle As Integer) As Integer
            ' rend la position de l'article [idArticle] dans la liste ou -1 si pas trouvé
            Dim unArticle As Article
            For i As Integer = 0 To listArticles.Count - 1
                unArticle = CType(listArticles(i), Article)
                If unArticle.id = idArticle Then
                    Return i
                End If
            Next
            ' pas trouvé
            Return -1
        End Function
    End Class

End Namespace

Commentaires :

  • la source de données est simulée par le champ privé [articles] de type [ArrayList]
  • le constructeur de la classe crée par défaut 4 articles dans la source de données.
  • toutes les méthodes d'accès aux données ont été synchronisées afin d'éviter les problèmes d'accès concurrents à la source de données. A un moment donné, un seul thread a accès à une méthode donnée.
  • la méthode [posArticle] permet de connaître la position [0..N] dans la source [ArrayList] d'un article identifié par son n°. Si l'article n'existe pas, la méthode rend la position -1. Cette méthode est utillisée de façon répétée par les autres méthodes.
  • la méthode [ajouteArticle] permet d'ajouter un article dans la liste des articles. Elle rend le nombre d'articles insérés : 1. Si l'article existait déjà, une exception est lancée.
  • la méthode [modifieArticle] permet de modifier un article existant. Elle rend le nombre d'articles modifiés : 1 si l'article existait, 0 sinon.
  • la méthode [supprimeArticle] permet de supprimer un article existant. Elle rend le nombre d'articles supprimés : 1 si l'article existait, 0 sinon.
  • la méthode [getAllArticles] donne la liste de tous les articles
  • la méthode [getArticleById] permet d'obtenir un article identifié par son n°. On obtient la valeur [nothing] si l'article n'xiste pas.
  • le code ne présente pas de réelle difficulté. Nous laissons le lecteur le parcourir et le comprendre.

1.4.3.4. Génération de l'assembly de la couche [dao]

Le projet Visual Studio est configuré pour générer l'assembly [webarticles-dao.dll]. Celui-ci est généré dans le dossier [bin] du projet :

1.4.3.5. Tests Nunit de la couche [dao]

En Java, les classes sont testées avec le framework [Junit]. En .NET, le framework Nunit offre les mêmes possibilités de tests unitaires :

Image

La structure du projet Visual Studio de test est la suivante :

Image

Commentaires :

  • le projet [tests] est de type [bibliothèque de classes]
  • les tests [NUnit] nécessitent une référence sur l'assembly [nunit.framework.dll]
  • la classe de test [NUnit] récupère une instance de l'objet à tester via Spring. Aussi trouve-t-on
    • dans le dossier [bin], les fichiers de classes de Spring
    • dans [References], une référence à l'assembly [Spring-Core.dll] du dossier [bin]
    • dans [bin], un fichier de configuration pour Spring
  • la classe de test a besoin de l'assembly [webarticles-dao.dll] de la couche [dao]. Celui-ci a été placé dans le dossier [bin] et sa référence ajoutée aux références du projet.

Une classe de test [NUnit] nécessite l'accès aux classes de l'espace de noms [NUnit.Framework]. On y trouve donc l'instruction d'import suivante :

Imports NUnit.Framework

L'espace de noms [NUnit.Framework] se trouve dans " l'assembly " [nunit.framework.dll] qu'on doit ajouter aux références du projet :

L'assembly [nunit.framework.dll] doit être dans la liste proposée si l'installation de [Nunit] a été faite. Il suffit de double-cliquer sur l'assembly pour l'ajouter au projet :

Image

La classe de test [NUnit] de la couche [dao] pourrait être la suivante :

Imports System
Imports System.Collections
Imports NUnit.Framework
Imports istia.st.articles.dao
Imports System.Threading
Imports Spring.Objects.Factory.Xml
Imports System.IO

Namespace istia.st.articles.tests

    <TestFixture()> _
    Public Class NunitTestArticlesArrayList
        ' l'objet à tester
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
            ' on récupère une instance du fabricant d'objets Spring
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
            ' on demande l'instanciation de l'objet articlesdao
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
        End Sub

        <Test()> _
        Public Sub testGetAllArticles()
            ' vérification visuelle
            listArticles()
        End Sub

        <Test()> _
        Public Sub testClearAllArticles()
            ' on supprime tous les articles
            articlesDao.clearAllArticles()
            ' on demande tous les articles
            Dim articles As IList = articlesDao.getAllArticles
            ' vérification : il doit y en avoir 0
            Assert.AreEqual(0, articles.Count)
        End Sub

        <Test()> _
        Public Sub testAjouteArticle()
            ' suppression de tous les articles
            articlesDao.clearAllArticles()
            ' vérification : la table des articles doit être vide
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' on ajoute deux articles
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' vérification : il doit y avoir deux articles
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' vérification visuelle
            listArticles()
        End Sub

        <Test()> _
        Public Sub testSupprimeArticle()
            ' suppression de tous les articles
            articlesDao.clearAllArticles()
            ' vérification : la table des articles doit être vide
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' on ajoute deux articles
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' vérification : il doit y avoir 2 articles
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' on supprime l'article 4
            articlesDao.supprimeArticle(4)
            ' vérification : il doit rester 1 article
            articles = articlesDao.getAllArticles
            Assert.AreEqual(1, articles.Count)
            ' vérification visuelle
            listArticles()
        End Sub

        <Test()> _
        Public Sub testModifieArticle()
            ' suppression de tous les articles
            articlesDao.clearAllArticles()
            ' vérification
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' ajout de 2 articles
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' vérification
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' recherche de l'article 3
            Dim unArticle As Article = articlesDao.getArticleById(3)
            ' vérification
            Assert.AreEqual(unArticle.nom, "article3")
            ' recherche article 4
            unArticle = articlesDao.getArticleById(4)
            ' vérification
            Assert.AreEqual(unArticle.nom, "article4")
            ' modification article 4
            articlesDao.modifieArticle(New Article(4, "article4", 44, 44, 44))
            ' vérification
            unArticle = articlesDao.getArticleById(4)
            Assert.AreEqual(unArticle.prix, 44, 0.000001)
            ' vérification visuelle
            listArticles()
        End Sub

        <Test()> _
        Public Sub testGetArticleById()
            ' suppression des articles
            articlesDao.clearAllArticles()
            ' vérification
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' ajout de 2 articles
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' vérification
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' recherche article 3
            Dim unArticle As Article = articlesDao.getArticleById(3)
            ' vérification
            Assert.AreEqual(unArticle.nom, "article3")
            ' recherche article 4
            unArticle = articlesDao.getArticleById(4)
            ' vérification
            Assert.AreEqual(unArticle.nom, "article4")
        End Sub

        ' listing écran
        Private Sub listArticles()
            Dim articles As IList = articlesDao.getAllArticles
            For i As Integer = 0 To articles.Count - 1
                Console.WriteLine(CType(articles(i), Article).ToString)
            Next
        End Sub

        <Test()> _
        Public Sub testArticleAbsent()
            ' suppression de tous les articles
            articlesDao.clearAllArticles()
            ' recherche article 1
            Dim article As article = articlesDao.getArticleById(1)
            ' vérification
            Assert.IsNull(article)
            ' modification d'un article inexistant
            Dim i As Integer = articlesDao.modifieArticle(New article(1, "1", 1, 1, 1))
            ' a du modifier aucune ligne
            Assert.AreEqual(i, 0)
            ' suppression article inexistant
            i = articlesDao.supprimeArticle(1)
            ' a du supprimer aucune ligne
            Assert.AreEqual(0, i)
        End Sub

        <Test()> _
        Public Sub testChangerStockArticle()
            ' suppression tous les articles
            articlesDao.clearAllArticles()
            ' ajout d'un article
            Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
            Assert.AreEqual(nbArticles, 1)
            ' ajout d'un article
            nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            Assert.AreEqual(nbArticles, 1)
            ' création de 100 threads
            Dim taches(99) As Thread
            For i As Integer = 0 To taches.Length - 1
                ' on crée le thread i
                taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
                ' on fixe le nom du thread
                taches(i).Name = "tache_" & i
                ' on lance l'exécution du thread i
                taches(i).Start()
            Next
            ' on attend la fin de tous les threads
            For i As Integer = 0 To taches.Length - 1
                taches(i).Join()
            Next
            ' vérifications - article 3 doit avoir un stock de 1
            Dim unArticle As Article = articlesDao.getArticleById(3)
            Assert.AreEqual(unArticle.nom, "article3")
            Assert.AreEqual(1, unArticle.stockactuel)
            ' on décrémente le stock de l'article 4
            Dim erreur As Boolean = False
            Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
            ' vérification : son stock n'a pas du changer
            Assert.AreEqual(0, nbLignes)
            ' vérification visuelle
            listArticles()
        End Sub

        Public Sub décrémente()
            ' thread lancé
            System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
            ' thread décrémente le stock
            articlesDao.changerStockArticle(3, -1)
            ' thread terminé
            System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
        End Sub

    End Class
End Namespace

Commentaires :

  • on a voulu écrire un programme de test de l'interface [IArticlesDao] qui soit indépendant de la classe d'implémentation de celle-ci. Aussi avons-nous utilisé Spring pour cacher au programme de test le nom de la classe d'implémentation.
  • la méthode d'attribut <Setup()> récupère auprès de Spring une référence sur l'objet [articlesdao] à tester. Celui-ci est défini dans le fichier [spring-config.xml] suivant :
<?xml version="1.0" encoding="iso-8859-1" ?>
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">

<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoArrayList, webarticles-dao"/>
</objects>

Ce fichier indique le nom de la classe d'implémentation [istia.st.articles.dao.ArticlesDaoArrayList] de l'interface [IArticlesDao] et où la trouver [ webarticles-dao.dll]. L'instanciation ne nécessitant pas de paramètres, aucun n'est défini ici.

  • la plupart des tests sont simples à comprendre. Le lecteur est invité à lire les commentaires.
  • La méthode [testChangerStockArticle] nécessite quelques explications. Elle crée 100 threads chargés de décrémenter le stock d'un article donné.
        <Test()> _
        Public Sub testChangerStockArticle()
            ' suppression tous les articles
            articlesDao.clearAllArticles()
            ' ajout d'un article
            Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
            Assert.AreEqual(nbArticles, 1)
            ' ajout d'un article
            nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            Assert.AreEqual(nbArticles, 1)
            ' création de 100 threads
            Dim taches(99) As Thread
            For i As Integer = 0 To taches.Length - 1
                ' on crée le thread i
                taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
                ' on fixe le nom du thread
                taches(i).Name = "tache_" & i
                ' on lance l'exécution du thread i
                taches(i).Start()
            Next
            ' on attend la fin de tous les threads
            For i As Integer = 0 To taches.Length - 1
                taches(i).Join()
            Next
            ' vérifications - article 3 doit avoir un stock de 1
            Dim unArticle As Article = articlesDao.getArticleById(3)
            Assert.AreEqual(unArticle.nom, "article3")
            Assert.AreEqual(1, unArticle.stockactuel)
            ' on décrémente le stock de l'article 4
            Dim erreur As Boolean = False
            Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
            ' vérification : son stock n'a pas du changer
            Assert.AreEqual(0, nbLignes)
            ' vérification visuelle
            listArticles()
        End Sub

Il s'agit ici de tester les accès concurrents à la source de données. La méthode chargée de mettre à jour le stock est la suivante :

        Public Sub décrémente()
            ' thread lancé
            System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
            ' thread décrémente le stock
            articlesDao.changerStockArticle(3, -1)
            ' thread terminé
            System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
        End Sub

Elle décrémente d'une unité le stock de l'article n° 3. Si on se réfère au code de la méthode [testChangerStockArticle], on voit que :

  • le stock de l'article n° 3 est initialisé à 101
  • les 100 threads vont décrémenter ce stock d'une unité chacun
  • on doit donc avoir un stock de 1 à la fin de l'exécution de tous les threads

Par ailleurs, toujours dans cette méthode, on essaie de faire passer le stock de l'article n° 4 a une valeur négative. On doit échouer.

Pour tester la couche [dao], nous générons la DLL [tests-webarticles-dao.dll] dans le dossier [bin] du projet [tests] :

Puis, à l'aide de l'application [Nunit-Gui], nous chargeons cette DLL et exécutons les tests :

Image

Dans la fenêtre de gauche, on voit la liste des méthodes testées. La couleur du point qui précède le nom de chaque méthode indique la réussite (vert) ou l'échec (rouge) de la méthode. Le lecteur qui visualise ce document sur écran pourra voir que tous les tests ont été réussis. Par la suite, nous considèrerons que nous avons une couche [dao] opérationnelle.

1.4.4. La couche [domain]

La couche [domain] contient les éléments suivants :

  • [IArticlesDomain]: l'interface d'accès à la couche [domain]

  • [Achat] : classe définissant un achat

  • [Panier] : classe définissant un panier d'achats

  • [AchatsArticles] : classe d'implémentation de l'interface [IArticlesDomain]

La structure de la solution [Visual Studio] de la couche [domain] est la suivante :

Image

Commentaires :

  • le projet [domain] est de type [bibliothèque de classes]
  • les classes ont été mises dans une arborescence de racine le dossier [istia]. Elles sont toutes dans l'espace de noms [istia.st.articles.domain].
  • la DLL de la couche [dao] a été placée dans le dossier [bin] du nouveau projet. Par ailleurs, cette DLL a été ajoutée comme référence au projet.

1.4.4.1. L'interface [IArticlesDomain]

L'interface [IArticlesDomain] découple la couche [métier] de la couche [web]. Cette dernière accède à la couche [métier/domain] via cette interface sans se préoccuper de la classe qui l'implémente réellement. L'interface définit les actions suivantes pour l'accès à la couche métier :

Imports Article = istia.st.articles.dao.Article

Namespace istia.st.articles.domain
    Public Interface IArticlesDomain
        ' méthodes
        Sub acheter(ByVal panier As Panier)
        Function getAllArticles() As IList
        Function getArticleById(ByVal idArticle As Integer) As Article
        ReadOnly Property erreurs() As ArrayList
    End Interface
End Namespace
Function getAllArticles() As IList
rend la liste d'objets [Article] de la source de données associée
Function getArticleById(ByVal idArticle As Integer) As Article
rend l'objet [Article] identifié par [idArticle]
acheter(ByVal panier As Panier)
valide le panier du client en décrémentant les stocks des articles achetés de la quantité achetée - peut échouer si le stock est insuffisant
ReadOnly Property erreurs() As ArrayList
rend la liste des erreurs qui se sont produites - vide si pas d'erreurs

1.4.4.2. La classe [Achat]

La classe [Achat] représente un achat du client :

Imports istia.st.articles.dao

Namespace istia.st.articles.domain

    Public Class Achat

        ' champs privés
        Private _article As article
        Private _qte As Integer

        ' constructeur par défaut
        Public Sub New()
        End Sub

        ' constructeur avec paramètres
        Public Sub New(ByVal unArticle As article, ByVal qte As Integer)
            ' on passe par les propriétés
            Me.article = unArticle
            Me.qte = qte
        End Sub

        ' article acheté
        Public Property article() As article
            Get
                Return _article
            End Get
            Set(ByVal Value As article)
                _article = Value
            End Set
        End Property

        ' qte achetée
        Public Property qte() As Integer
            Get
                Return _qte
            End Get
            Set(ByVal Value As Integer)
                If Value < 0 Then
                    Throw New Exception("Quantité [" + Value.ToString + "] invalide")
                End If
                _qte = Value
            End Set
        End Property

        ' total achat
        Public ReadOnly Property totalAchat() As Double
            Get
                Return _qte * _article.prix
            End Get
        End Property

        ' identité
        Public Overrides Function ToString() As String
            Return "[" + _article.ToString + "," + _qte.ToString + "]"
        End Function
    End Class

End Namespace

Commentaires :

  • la classe [Achat] a les propriétés et méthodes suivantes :
Public Property article() As article
l'article acheté
Public Property qte() As Integer
la quantité achetée
Public ReadOnly Property totalAchat() As Double
le montant de l'achat
Public Overrides Function ToString() As String
chaîne d'identité de l'objet
  • elle a un constructeur permettant d'initialiser les propriétés [article, qte] qui définissent un achat.

1.4.4.3. La classe [Panier]

La classe [Panier] représente l'ensemble des achats du client :

Namespace istia.st.articles.domain

Public Class Panier

    ' champs privés
    Private _achats As New ArrayList
        Private _totalPanier As Double = 0

    ' constructeur par défaut
    Public Sub New()
    End Sub

    ' liste des achats
    Public ReadOnly Property achats() As ArrayList
        Get
            Return _achats
        End Get
    End Property

    ' total des achats
    Public ReadOnly Property totalPanier() As Double
        Get
            Return _totalPanier
        End Get
    End Property

    ' méthodes
    Public Sub ajouter(ByVal unAchat As Achat)
        ' on cherche si l'achat existe déjà
        Dim iAchat As Integer = posAchat(unAchat.article.id)
        If iAchat <> -1 Then
            ' on a trouvé
                Dim achatCourant As Achat = CType(_achats(iAchat), Achat)
            achatCourant.qte += unAchat.qte
        Else
            ' on n'a pas trouvé
            _achats.Add(unAchat)
        End If
        ' on incrémente le total du panier
        _totalPanier += unAchat.totalAchat
    End Sub

    ' enlever un achat
    Public Sub enlever(ByVal idAchat As Integer)
        ' on cherche l'achat
            Dim iachat As Integer = posAchat(idAchat)
        ' si on a trouvé, on enlève
        If iachat <> -1 Then
                Dim achatCourant As Achat = CType(_achats(iachat), Achat)
            ' on enlève du panier
            _achats.RemoveAt(iachat)
            ' on décrémente le total du panier
            _totalPanier -= achatCourant.totalAchat
        End If
    End Sub

    Private Function posAchat(ByVal idArticle As Integer) As Integer
        ' recherche un achat dans la liste des achats
        ' rend sa position dans la liste ou -1 si pas trouvé
        Dim achatCourant As Achat
        Dim trouvé As Boolean = False
        Dim i As Integer = 0
            While Not trouvé AndAlso i < _achats.Count
                ' achat courant
                achatCourant = CType(_achats(i), Achat)
                ' comparaison avec l'article cherché
                If achatCourant.article.id = idArticle Then
                    Return i
                End If
                'achat suivant
                i += 1
            End While
            ' pas trouvé
            Return -1
    End Function

    ' function identité
    Public Overrides Function ToString() As String
        Return _achats.ToString
    End Function
End Class

End Namespace

Commentaires :

  • la classe [Panier] a les propriétés et méthodes suivantes :
ReadOnly Property achats() As ArrayList
la liste des achats du client - liste d'objets de type [Achat]
ajouter(ByVal unAchat As Achat)
ajoute un achat à la liste des achats
enlever(ByVal idAchat As Integer)
enlève l'achat de l'article idAchat
ReadOnly Property totalPanier() As Double
le montant total des achats du panier
Function ToString() As String
rend la chaîne d'identité du panier
  • la méthode [posAchat] est une méthode utilitaire qui permet d'obtenir la position dans la liste des achats, d'un achat identifié par le n° de l'article acheté. La liste des achats est gérée de telle façon qu'un article acheté plusieurs fois n'occupe qu'une position dans la liste. Ainsi un achat peut-il être identifié par le n° de l'article acheté. La méthode [posAchat] rend -1 si l'achat recherché n'existe pas.
  • la méthode [ajouter] ajoute un nouvel achat à la liste des achats. Cela revient soit à ajouter une nouvelle entrée à la liste des achats si l'article acheté n'existait pas déjà dans la liste, soit à incrémenter la quantité achetée s'il existait déjà.
  • la méthode [enlever] permet d'enlever un achat identifié par un n° de la liste des achats. Si l'achat n'existe pas, la méthode est silencieuse et ne fait rien.
  • le montant des achats [totalPanier] est maintenu au fil des ajouts et retraits d'achats.

1.4.4.4. La classe [AchatsArticles]

L'interface [IArticlesDomain] sera implémentée par la classe [AchatsArticles] suivante :

Imports istia.st.articles.dao

Namespace istia.st.articles.domain
    Public Class AchatsArticles
        Implements IArticlesDomain

        'champs privés
        Private _articlesDao As IArticlesDao
        Private _erreurs As ArrayList

        ' constructeur
        Public Sub New(ByVal articlesDao As IArticlesDao)
            _articlesDao = articlesDao
        End Sub

        ' liste des erreurs
        Public ReadOnly Property erreurs() As ArrayList Implements IArticlesDomain.erreurs
            Get
                Return _erreurs
            End Get
        End Property

        ' liste des articles
        Public Function getAllArticles() As IList Implements IArticlesDomain.getAllArticles
            ' liste de tous les articles
            Try
                Return _articlesDao.getAllArticles
            Catch ex As Exception
                _erreurs = New ArrayList
                _erreurs.Add("Erreur d'accès aux données : " + ex.Message)
            End Try
        End Function

        ' obtenir un article identifié par son n°
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDomain.getArticleById
            ' un article particulier
            Try
                Return _articlesDao.getArticleById(idArticle)
            Catch ex As Exception
                _erreurs = New ArrayList
                _erreurs.Add("Erreur d'accès aux données : " + ex.Message)
            End Try
        End Function


        ' acheter un panier
        Public Sub acheter(ByVal panier As Panier) Implements IArticlesDomain.acheter
            ' achat d'un panier - les stocks des articles achetés doivent être décrémentés
            _erreurs = New ArrayList
            Dim achat As achat
            Dim achats As ArrayList = panier.achats
            For i As Integer = achats.Count - 1 To 0 Step -1
                ' décrémenter stock article i
                achat = CType(achats(i), achat)
                Try
                    If _articlesDao.changerStockArticle(achat.article.id, -achat.qte) = 0 Then
                        ' on n'a pas pu faire l'opération
                        _erreurs.Add("L'achat " + achat.ToString + " n'a pu se faire - Vérifiez les stocks")
                    Else
                        ' l'opération s'est faite - on enlève l'achat du panier
                        panier.enlever(achat.article.id)
                    End If
                Catch ex As Exception
                    _erreurs = New ArrayList
                    _erreurs.Add("Erreur d'accès aux données : " + ex.Message)
                End Try
            Next
        End Sub
    End Class

End Namespace

Commentaires :

  • cette classe implémente les quatre méthodes de l'interface [IArticlesDomain]. Elle a deux champs privés :
_articlesDao As IArticlesDao
l'objet d'accès aux données
_erreurs As ArrayList
la liste des erreurs éventuelles. Elle est accessible via la propriété publique [erreurs]
  • pour construire une instance de la classe, il faut fournir l'objet permettant l'accès aux données :
Sub New(ByVal articlesDao As IArticlesDao)
  • les méthodes [getAllArticles] et [getArticleById] s'appuient sur les méthodes de même nom de la couche [dao]
  • la méthode [acheter] valide l'achat d'un panier. Cette validation consiste simplement à décrémenter les stocks des articles achetés. L'achat d'un article n'est possible que si son stock le permet. Si ce n'est pas le cas, l'achat est refusé : il reste dans le panier et une erreur est signalée dans la liste [erreurs]. Un achat validé est retiré du panier et le stock de l'article correspondant décrémenté de la quantité achetée.

1.4.4.5. Génération de l'assembly de la couche [domain]

Le projet Visual Studio est configuré pour générer l'assembly [webarticles-domain.dll]. Celui-ci est généré dans le dossier [bin] du projet :

1.4.4.6. Tests NUnit de la couche [domain]

La structure du projet Visual Studio de test est la suivante :

Image

Commentaires :

  • le projet [tests] est de type [bibliothèque de classes]
  • les tests [NUnit] nécessitent une référence sur l'assembly [nunit.framework.dll]
  • la classe de test [NUnit] récupère une instance de l'objet à tester via Spring. Aussi trouve-t-on
  • dans le dossier [bin], les fichiers de classes de Spring
  • dans [References], une référence à l'assembly [Spring-Core.dll] du dossier [bin]
  • dans [bin], un fichier de configuration pour Spring
  • la classe de test a besoin de l'assembly [webarticles-dao.dll] de la couche [dao] et de l'assembly [webarticles-domain.dll] de la couche [domain]. Ceux-ci ont été placés dans le dossier [bin] et leurs références ajoutées aux références du projet.

Une classe de test NUnit de la couche [domain] pourrait être la suivante :

Imports NUnit.Framework
Imports istia.st.articles.dao
Imports istia.st.articles.domain
Imports Spring.Objects.Factory.Xml
Imports System.IO

Namespace istia.st.articles.tests

    <TestFixture()> _
    Public Class NunitTestArticlesDomain

        ' l'objet à tester
        Private articlesDomain As IArticlesDomain
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
            ' on récupère une instance du fabricant d'objets Spring
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
            ' on demande l'instanciation de l'objet articles dao
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
            ' puis celui de l'aobjet articlesdomain
            articlesDomain = CType(factory.GetObject("articlesdomain"), IArticlesDomain)
        End Sub

        <Test()> _
        Public Sub getAllArticles()
            ' vérification visuelle
            listArticles()
        End Sub

        <Test()> _
        Public Sub getArticleById()
            ' suppression des articles
            articlesDao.clearAllArticles()
            ' vérification
            Dim articles As IList = articlesDomain.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' ajout de 2 articles
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' vérification
            articles = articlesDomain.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' recherche article 3
            Dim unArticle As Article = articlesDomain.getArticleById(3)
            ' vérification
            Assert.AreEqual(unArticle.nom, "article3")
            ' recherche article 4
            unArticle = articlesDao.getArticleById(4)
            ' vérification
            Assert.AreEqual(unArticle.nom, "article4")
        End Sub

        <Test()> _
        Public Sub acheterPanier()
            ' suppression des articles
            articlesDao.clearAllArticles()
            ' vérification
            Dim articles As IList = articlesDomain.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' ajout de 2 articles
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' vérification
            articles = articlesDomain.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' création d'un panier avec deux achats
            Dim panier As New panier
            panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 10))
            panier.ajouter(New Achat(New Article(4, "article4", 40, 40, 4), 10))
            ' vérifications
            Assert.AreEqual(700, panier.totalPanier, 0.000001)
            Assert.AreEqual(2, panier.achats.Count)
            ' validation panier
            articlesDomain.acheter(panier)
            ' vérifications
            Assert.AreEqual(0, articlesDomain.erreurs.Count)
            Assert.AreEqual(0, panier.achats.Count)
            ' recherche article 3
            Dim unArticle As Article = articlesDomain.getArticleById(3)
            ' vérification
            Assert.AreEqual(unArticle.stockactuel, 20)
            ' recherche article 4
            unArticle = articlesDao.getArticleById(4)
            ' vérification
            Assert.AreEqual(unArticle.stockactuel, 30)
            ' nouveau panier
            panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 100))
            ' validation panier
            articlesDomain.acheter(panier)
            ' vérifications
            Assert.AreEqual(1, articlesDomain.erreurs.Count)
            ' recherche article 3
            unArticle = articlesDomain.getArticleById(3)
            ' vérification
            Assert.AreEqual(unArticle.stockactuel, 20)
        End Sub

        <Test()> _
        Public Sub testRetirerAchats()
            ' suppression du contenu de ARTICLES
            articlesDao.clearAllArticles()
            ' lit la table ARTICLES
            Dim articles As IList = articlesDao.getAllArticles()
            Assert.AreEqual(0, articles.Count)
            ' insertion
            Dim article3 As New Article(3, "article3", 30, 30, 3)
            articlesDao.ajouteArticle(article3)
            Dim article4 As New Article(4, "article4", 40, 40, 4)
            articlesDao.ajouteArticle(article4)
            ' lit la table ARTICLES
            articles = articlesDomain.getAllArticles()
            Assert.AreEqual(2, articles.Count)
            ' création d'un panier avec deux achats
            Dim monPanier As New Panier
            monPanier.ajouter(New Achat(article3, 10))
            monPanier.ajouter(New Achat(article4, 10))
            ' vérifications
            Assert.AreEqual(700.0, monPanier.totalPanier, 0.000001)
            Assert.AreEqual(2, monPanier.achats.Count)
            ' ajouter un article déjà acheté
            monPanier.ajouter(New Achat(article3, 10))
            ' vérifications
            ' le total doit être passé à 1000
            Assert.AreEqual(1000.0, monPanier.totalPanier, 0.000001)
            ' toujours 2 articles dans le panier
            Assert.AreEqual(2, monPanier.achats.Count)
            ' qté article 3 a du passer à 20
            Dim unAchat As Achat = CType(monPanier.achats(0), Achat)
            Assert.AreEqual(20, unAchat.qte)
            ' on retire l'article 3 du panier
            monPanier.enlever(3)
            ' vérifications
            ' le total doit être passé à 400
            Assert.AreEqual(400.0, monPanier.totalPanier, 0.000001)
            ' 1 seul article dans le panier
            Assert.AreEqual(1, monPanier.achats.Count)
            ' ce doit être l'article n° 4
            Assert.AreEqual(4, CType(monPanier.achats(0), Achat).article.id)
        End Sub

        ' listing écran
        Private Sub listArticles()
            Dim articles As IList = articlesDomain.getAllArticles
            For i As Integer = 0 To articles.Count - 1
                Console.WriteLine(CType(articles(i), Article).ToString)
            Next
        End Sub

    End Class


End Namespace

Commentaires :

  • on a voulu écrire un programme de test de l'interface [IArticlesDomain] qui soit indépendant de la classe d'implémentation de celle-ci. Aussi avons-nous utilisé Spring pour cacher au programme de test le nom de la classe d'implémentation.
  • la méthode d'attribut <Setup()> récupère auprès de Spring une référence sur les objets [articlesdomain] et [articlesdao] à tester. Ceux-ci sont définis dans le fichier [spring-config.xml] suivant :
<?xml version="1.0" encoding="iso-8859-1" ?>
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
<objects>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoArrayList, webarticles-dao" />
    <object id="articlesdomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
        <constructor-arg index="0">
            <ref object="articlesdao" />
        </constructor-arg>
    </object>
</objects>

Ce fichier indique :

  • (suite)
    • pour le singleton [articlesdao], le nom de la classe d'implémentation [istia.st.articles.dao.ArticlesDaoArrayList] et où la trouver [ webarticles-dao.dll]. L'instanciation ne nécessitant pas de paramètres, auncun n'est défini ici.
    • pour le singleton [articlesdomain], le nom de la classe d'implémentation [istia.st.articles.domain.AchatsArticles] et où la trouver [ webarticles-domain.dll]. La classe [AchatsArticles] a un constructeur à un paramètre : le singleton gérant l'accès à la couche [dao]. Ici, celui-ci est défini comme étant le singleton [articlesdao] défini précédemment.
  • la classe de test obtient une instance de la classe à tester [articlesdomain] ainsi qu'une instance de la classe d'accès aux données [articlesdao]. Ce dernier point est litigieux. La classe de test ne devrait théoriquement pas avoir besoin d'avoir accès à la couche [dao] qu'elle n'est même pas censée connaître. Ici, nous sommes passés outre cette " éthique " qui, pour être respectée, nous aurait obligés à créer de nouvelles méthodes dans notre interface [IArticlesDomain].

Pour tester la couche [domain], nous générons la DLL [tests-webarticles-domain.dll] dans le dossier [bin] du projet [tests] :

Puis, à l'aide de l'application [Nunit-Gui], nous chargeons cette DLL et exécutons les tests :

Image

Le lecteur qui visualise ce document sur écran pourra voir que tous les tests ont été réussis. Par la suite, nous considèrerons que nous avons une couche [domain] opérationnelle.

1.4.5. Conclusion

Rappelons que nous voulons construire l'application web à trois couches suivante :

Le modèle M de notre application MVC est désormais écrit et testé. Il nous est fourni dans deux DLL [webarticles-dao.dll, webarticles-domain.dll]. Nous pouvons passer à la dernière couche, la couche [web] qui contient le contrôleur C et les vues V. Nous considèrerons tout d'abord, une méthode présentée dans le document [Développement WEB avec ASP.NET 1.1 ]

  • le contrôleur C est assuré par deux fichiers [global.asax, main.aspx]
  • les vues V sont assurées par des pages aspx

1.5. La couche [web]

L'architecture MVC de l'application web sera la suivante :

M=modèle
les classes métier [domain], les classes d'accès aux données [dao] et la source de données
V=vues
les pages ASPX
C=contrôleur
toutes les requêtes des clients HTTP transitent par les deux contrôleurs suivants :
global.asax : gère les évts liés au lancement initial de l'application
main.aspx : traite individuellement la requête de chaque client

1.5.1. Les vues

Les vues correspondent à celles qui ont été présentées en début de document :

LISTE
liste.aspx
Les vues sont rassemblées dans le dossier [vues] de l'application
INFOS
infos.aspx
PANIER
panier.aspx
PANIERVIDE
paniervide.aspx
ERREURS
erreurs.aspx

1.5.2. Les contrôleurs

Comme il a été indiqué, le contrôleur sera formé de deux éléments:

  1. [global.asax,global.asax.vb] : utilisé principalement pour initialiser l'application et mettre dans le contexte de celle-ci toutes les données à partager entre les différents clients
  2. [main.aspx, main.aspx.vb] : le véritable contrôleur, celui qui traite les requêtes HTTP des clients.

Les différentes requêtes des clients seront adressées au contrôleur [main.aspx] et contiendront un paramètre appelé [action] précisant l'action demandée par le client :

requête
signification
action du contrôleur
réponses possibles
action=liste
le client veut la liste des
articles
- demande la liste des articles à la couche
métier
- [LISTE]
- [ERREURS]
action=infos
le client demande des
informations sur l'un des
articles affichés dans la vue
[LISTE]
- demande l'article à la couche métier
- [INFOS]
- [ERREURS]
action=achat
le client achète un article
- demande l'article à la couche métier
et l'intègre dans le panier du client
- [INFOS] si erreur de qté
- [LISTE] si pas d'erreur
action=retirerachat
le client veut supprimer un
achat de son panier
- récupère le panier dans la session
et le modifie
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=panier
le client veut visualiser son
panier
- récupère le panier dans la session
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=validationpanier
le client a terminé ses achats
et passe à la phase paiement
- met à jour dans la base les stocks
des articles achetés
- vide le panier du client des articles
dont l'achat a été validé
- [LISTE]
- [ERREURS]

1.5.3. Configuration de l'application

Nous chercherons à configurer l'application de façon à la rendre la plus souple possible vis à vis de changements tels que :

  1. le changement des URL des différentes vues
  2. le changement des classes implémentant les interfaces [IArticlesDao] et [IArticlesDomain]
  3. le changement du SGBD, de la base, de la table des articles

1.5.3.1. Les changements d'URL

Les noms des URL des vues seront placés dans le fichier [web.config] de configuration de l'application avec quelques autres paramètres :

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
..
    <appSettings>
        <add key="urlMain" value="/webarticles/main.aspx"/>
        <add key="urlInfos" value="vues/infos.aspx"/>
        <add key="urlErreurs" value="vues/erreurs.aspx"/>
        <add key="urlListe" value="vues/liste.aspx"/>
        <add key="urlPanier" value="vues/panier.aspx"/>
        <add key="urlPanierVide" value="vues/paniervide.aspx"/>
    </appSettings>
</configuration>

1.5.3.2. Le changement des classes d'implémentation des interfaces

Dans l'esprit des architectures à trois couches, les couches doivent être étanches les unes par rapport aux autres. Cette étanchéité est obtenue de la façon suivante :

  • les couches communiquent entre-elles par des interfaces et non par des classes concrètes
  • le code d'une couche n'instancie jamais elle-même la classe d'une autre couche afin de l'utiliser. Elle demande simplement à un outil externe, ici Spring, une instance d'implémentation de l'interface de la couche qu'elle veut utiliser. Pour cela, nous savons qu'elle n'a pas besoin de connaître le nom de la classe d'implémentation mais seulement le nom du singleton Spring dont elle veut une référence.

Dans notre application, Spring sera configuré dans le fichier [web.config] de l'application web de la façon suivante :

<?xml version="1.0" encoding="iso-8859-1" ?>
<configuration>
    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>
    <spring>
        <context type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
            <resource uri="config://spring/objects" />
        </context>
        <objects>
    <object id="articlesDao" type="istia.st.articles.dao.ArticlesDaoArrayList, webarticles-dao" />
    <object id="articlesDomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
        <constructor-arg index="0">
            <ref object="articlesDao" />
        </constructor-arg>
    </object>
        </objects>
    </spring>
        <appSettings>
        <add key="urlMain" value="/webarticles/main.aspx"/>
        <add key="urlInfos" value="vues/infos.aspx"/>
        <add key="urlErreurs" value="vues/erreurs.aspx"/>
        <add key="urlListe" value="vues/liste.aspx"/>
        <add key="urlPanier" value="vues/panier.aspx"/>
        <add key="urlPanierVide" value="vues/paniervide.aspx"/>
    </appSettings>
</configuration>

Pour avoir accès à la couche [métier], une classe de la couche [web] pourra demander le singleton [articlesDomain]. Spring instanciera alors un objet de type [istia.st.articles.domain.AchatsArticles]. Pour cette instanciation, il a besoin d'un objet de type [articlesDao], c'est à dire d'un objet de type [istia.st.articles.dao.ArticlesDaoArrayList]. Spring instanciera alors un tel objet. A la fin de l'opération, la couche [web] qui a demandé le singleton [articlesDomain] a toute la chaîne qui la relie à la source de données :

1.5.3.3. Les changements liés au SGBD ou à la base des données

Ce point sera ignoré ici puisque nous nous situons dans une application de test sans SGBD. Nous aborderons dans un deuxième temps l'implémentation d'une couche [dao] s'appuyant sur un SGBD.

1.5.4. La bibliothèque de balises <asp:>

Considérons la vue [ERREURS] qui affiche une liste d'erreurs :

Image

La vue [ERREURS] est chargée d'afficher une liste d'erreurs que le contrôleur [main.aspx] a placée dans le contexte de la requête sous le nom [context.Items("erreurs")]. Il y a plusieurs façons d'écrire une telle page. Nous ne nous intéressons ici qu'à la partie affichage des erreurs.

Rappelons qu'une page ASPX a une partie présentation HTML et une partie code .NET qui prépare les données que la partie présentation doit afficher. Ces deux parties peuvent être dans un même fichier [aspx] (solution WebMatrix) ou dans deux fichiers : [aspx] pour la présentation, [aspx.vb] pour le code. Cette dernière solution est celle de Visual Studio. Pour compliquer les choses, la partie présentation HTML peut comporter, elle aussi, du code .NET, ce qui tend à brouiller la séparation [contrôleur] et [présentation] de la vue. Cette solution est généralement vivement déconseillée. Retirer tout code de la partie [présentation] a nécessité la création de bibliothèques de balises. Celles-ci "cachent" le code sous l'apparence de balises analogues aux balises HTML. Nous présentons deux solutions possibles pour la page [ERREURS].

Notre première solution utilise du code .NET dans la partie [présentation] de la page. La page ASPX récupère la liste des erreurs présente dans la requête dans sa partie contrôleur [erreurs.aspx.vb] :

        Protected erreurs As ArrayList

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
            ' on récupère les erreurs
            erreurs = CType(context.Items("erreurs"), ArrayList)
        End Sub

puis l'affiche dans la partie [présentation, erreurs.aspx] :

                <h2>Les erreurs suivantes se sont produites :</h2>
                <ul>
                <%
                    for i as integer=0 to erreurs.count-1
                        response.write("<li>" & erreurs(i).ToString & "</li>")
                    next
                %>
                </ul>

La seconde solution utilise la balise <asp:repeater> de la bibliothèque de balises <asp:> d'ASP.NET. Si on construit une page ASPX graphiquement, cette balise est disponible sous la forme d'un composant serveur qu'on dépose sur le formulaire de conception. Si on construit le code ASPX à la main, on peut parler de bibliothèque de balises.

Avec la bibliothèque de balises <asp:>, le code ASPX de la vue [ERREURS] précédente devient le suivant :

        <asp:Repeater id="rptErreurs" runat="server">
            <HeaderTemplate>
                <h3>Les erreurs suivantes se sont produites :
                </h3>
                <ul>
            </HeaderTemplate>
            <ItemTemplate>
                <li>
                    <%# Container.DataItem %>
                </li>
            </ItemTemplate>
            <FooterTemplate>
                </ul>
            </FooterTemplate>
        </asp:Repeater>

La balise

        <asp:Repeater id="rptErreurs" runat="server">

sert à répéter un motif HTML sur les différents éléments d'une source de données. Ses différents éléments sont les suivants :

HeaderTemplate
le motif HTML à afficher avant que les éléments de la source de données ne soient affichés
ItemTemplate
le motif HTML à répéter pour chacun des éléments de la source de données. L'expression [<%# Container.DataItem %>] sert à afficher la valeur de l'élément courant de la source de données
FooterTemplate
le motif HTML à afficher après que les éléments de la source de données ont été affichés

La source de données est liée à la balise généralement dans la partie [contrôleur] de la page :

        Protected WithEvents rptErreurs As System.Web.UI.WebControls.Repeater

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
..
            ' on lie les erreurs à rptErreurs
            With rptErreurs
                .DataSource = context.Items("erreurs")
                .DataBind()
            End With
        End Sub

Cette liaison peut se faire également au moment de la conception de la page si la source de données est déjà connue, une base de données existante par exemple.

Nous utiliserons dans nos vues une autre balise : <asp:datagrid> qui permet d'afficher une source de données sous forme d'un tableau.

1.5.5. Structure de la solution Visual Studio de l'application [webarticles]

Une application web est un puzzle avec de nombreux éléments. Lui donner une architecture MVC augmente en général le nombre de ceux-ci. La structure de l'application [webarticles] sous [Visual Studio] est la suivante :

  

Commentaires :

  • le projet [web] est de type [bibliothèque de classes] et non de type [Application web ASP.NET] comme on pourrait logiquement s'y attendre. Le type [Application web ASP.NET] exige la présence du serveur web IIS sur la machine de développement ou sur une machine distante. Le serveur IIS n'existe pas en standard sur les machines Windows XP Familial. Or beaucoup de PC sont vendus avec cette version. Afin de permettre aux lecteurs disposant de Windows XP de mettre en oeuvre l'application étudiée, nous utiliserons le serveur Web Cassini (voir annexe) disponible gratuitement chez Microsoft et nous remplacerons le projet [Application web ASP.NET] par un projet [bibliothèque de classes]. Cela entraîne quelques inconvénients qui sont expliqués en annexes.
  • les DLL utilisées par l'application sont les suivantes :
webarticles-dao.dll
rassemble les classes de la couche d'accès aux données
webarticles-domain.dll
rassemble les classes de la couche métier
Spring.Core.dll
contient les classes Spring qui nous permettent d'intégrer les couches web, domain et dao
log4net.dll
classes de logs - utilisées par Spring

Ces DLL sont placées dans le dossier [bin] et ajoutées aux références du projet.

1.5.6. Les vues ASPX

Comme il a été conseillé précédemment, nous utiliserons la bibliothèque de balises <asp:> dans nos vues ASPX.

1.5.6.1. Le composant utilisateur [entete.ascx]

Afin de donner une certaine homogénéité aux différentes vues, celles-ci partageront un même entête, celui qui affiche le nom de l'application avec le menu :

Le menu est dynamique et fixé par le contrôleur. Celui-ci met dans la requête transmise à la page ASPX, un attribut de clé "actions" ayant pour valeur associée, un tableau d'éléments de type Hashtable(). Chaque élément de ce tableau est un dictionnaire destiné à générer une option du menu de l'entête. Chaque dictionnaire a deux clés :

  • href : l'URL associée à l'option de menu
  • lien : le texte du menu

On fera de l'entête un contrôle utilisateur. Un contrôle utilisateur encapsule un morceau de page (présentation et code associé) dans un composant réutilisable ensuite dans d'autres pages. Ici, nous voulons réutiliser le composant [entete] dans les autres vues de l'application. Le code de présentation sera dans [entete.ascx] et le code de contrôle associé dans [entete.ascx.vb]. Le code de présentation utilisera un composant <asp:repeater> pour afficher le tableau des options du menu :

type
nom
rôle
1
repeater
rptMenu
source de données : un tableau de dictionnaires
à deux clés : href, lien
afficher les options de menu

Le code de présentation de la page sera le suivant :

<%@ Control codebehind="entete.ascx.vb" Language="vb" autoeventwireup="false" inherits="istia.st.articles.web.EnteteWebArticles" %>
        <table>
            <tr>
                <td>
                    <h2>Magasin virtuel</h2></td>
                <asp:Repeater id="rptMenu" runat="server">
                    <ItemTemplate>
                        <td>
                            |<a href='<%# Container.DataItem("href") %>'>
                                <%# Container.DataItem("lien") %>
                            </a>
                        </td>
                    </ItemTemplate>
                </asp:Repeater>
            </tr>
        </table>
        <hr>

Commentaires :

  • le composant [repeater] est défini lignes 6-14
  • chaque élément de la source de données associée au répéteur est un dictionnaire à deux clés : href - ligne 9 et lien - ligne 10

Le code de contrôle associé sera le suivant :

Namespace istia.st.articles.web
    Public Class EnteteWebArticles
        Inherits System.Web.UI.UserControl

        Protected WithEvents rptMenu As System.Web.UI.WebControls.Repeater

        Public WriteOnly Property actions() As Hashtable()
            Set(ByVal Value As Hashtable())
                ' on associe le tableau des actions à son composant
                With rptMenu
                    .DataSource = Value
                    .DataBind()
                End With
            End Set
        End Property
    End Class
End Namespace

Commentaires :

  • le composant de type [EnteteWebArticles] a une propriété publique [actions] en écriture seule - ligne 7
  • cette propriété permet d'associer au composant <asp:repeater> appelé [rptMenu] - ligne 10 - le tableau des options calculé par le contrôleur de l'application - lignes 11-12.

Les autres vues de l'application utiliseront l'entête défini par [entete.ascx]. La page [erreurs.aspx] par exemple, incluera l'entête à l'aide du code suivant :

<%@ Register TagPrefix="WA" TagName="entete" Src="entete.ascx" %>
<%@ Page inherits="istia.st.articles.web.ErreursWebarticles" autoeventwireup="false" Language="vb" %>
<HTML>
    <HEAD>
        <TITLE>webarticles</TITLE>
        <META http-equiv="Content-Type" content="text/html; charset=windows-1252">
    </HEAD>
    <body>
        <WA:entete id="entete" runat="server"></WA:entete>
        <h3>Les erreurs suivantes se sont produites :
        </h3>    

Commentaires :

  • La ligne 1 déclare que la balise <WA:entete> devra être associée au composant défini par le fichier [entete.ascx]. Les attributs [TagPrefix] et [TagName] sont libres.
  • Ceci fait, l'insertion du composant dans le code de présentation de la page se fait avec la ligne 9. A l'exécution, cette balise aura pour effet d'inclure dans le code de la page ASPX qui la contient, celui de la page [entete.ascx]. Le code de contrôle [erreurs.aspx.vb] prendra soin d'initialiser ce composant. Il peut le faire de la façon suivante :
    Public Class ErreursWebarticles
        Inherits System.Web.UI.Page

        ' composants de la page
...
        Protected WithEvents entete As New EnteteWebArticles

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
            ' on lie les options de menu à rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
...
        End Sub
    End Class

Commentaires :

  • la ligne 6 crée un objet de type [EnteteWebArticles] qui est le type du composant créé
  • la ligne 11 initialise la propriété [actions] de cet objet

1.5.6.2. La vue [liste.aspx]

1.5.6.2.1. Introduction

Cette vue affiche la liste des articles disponibles à la vente :

Elle est affichée à la suite d'une requête /main?action=liste ou /main?action=validationpanier. Les éléments de la requête du contrôleur sont les suivants :

actions
objet Hashtable() - le tableau des options du menu
listarticles
ArrayList d'objets de type [Article]
message
objet String - message à afficher en bas de page

Chaque lien [Infos] du tableau HTML des articles a une URL de la forme [?action=infos&id=ID] où ID est le champ id de l'article affiché.

1.5.6.2.2. Composants de la page
type
nom
rôle
1
composant utilisateur
entete
afficher l'entête
2
DataGrid
DataGridArticles
3 - colonne connexe : entête : Nom, champ : nom
4 - colonne connexe : entête : Prix, champ : prix
5 - colonne hypertexte : texte : Infos, champ Url : id,
format URL : /webarticles/main.aspx?action=infos&id={0}
afficher les articles en vente
6
label
lblMessage
afficher un message

Rappelons comment procéder pour définir ces propriétés :

  • dans Visual Studio, on sélectionne le [DataGrid] pour avoir accès à sa feuille de propriétés :

Image

  • on utilise le lien [Mise en forme automatique] ci-dessus pour gérer la forme du tableau affiché
  • et le lien [Générateur de propriétés] pour gérer son contenu
1.5.6.2.3. Code de présentation [liste.aspx]
<%@ Page codebehind="liste.aspx.vb" inherits="istia.st.articles.web.ListeWebarticles" autoeventwireup="false" Language="vb" %>
<%@ Register TagPrefix="WA" TagName="entete" Src="entete.ascx"%>
<HTML>
    <HEAD>
        <TITLE>webarticles</TITLE>
        <META http-equiv="Content-Type" content="text/html; charset=windows-1252">
    </HEAD>
    <body>
        <WA:entete id="entete" runat="server"></WA:entete>
        <h2>Liste des articles</h2>
        <P>
            <asp:DataGrid id="DataGridArticles" runat="server" ForeColor="Black" BackColor="LightGoldenrodYellow"
                BorderColor="Tan" CellPadding="2" BorderWidth="1px" GridLines="None" AutoGenerateColumns="False">
                <SelectedItemStyle ForeColor="GhostWhite" BackColor="DarkSlateBlue"></SelectedItemStyle>
                <AlternatingItemStyle BackColor="PaleGoldenrod"></AlternatingItemStyle>
                <HeaderStyle Font-Bold="True" BackColor="Tan"></HeaderStyle>
                <FooterStyle BackColor="Tan"></FooterStyle>
                <Columns>
                    <asp:BoundColumn DataField="nom" HeaderText="Nom"></asp:BoundColumn>
                    <asp:BoundColumn DataField="prix" HeaderText="Prix" DataFormatString="{0:C}"></asp:BoundColumn>
                    <asp:HyperLinkColumn Text="Infos" DataNavigateUrlField="id" DataNavigateUrlFormatString="/webarticles/main.aspx?action=infos&amp;id={0}"></asp:HyperLinkColumn>
                </Columns>
                <PagerStyle HorizontalAlign="Center" ForeColor="DarkSlateBlue" BackColor="PaleGoldenrod"></PagerStyle>
            </asp:DataGrid></P>
        <P>
            <asp:Label id="lblMessage" runat="server" BackColor="#FFC080"></asp:Label></P>
    </body>
</HTML>

Commentaires :

  • la ligne 9 définit l'entête de la page
  • les lignes 12-24 définissent les caractéristiques du [DataGrid]
  • la ligne 26 définit le label [lblMessage]
1.5.6.2.4. Code du contrôleur [liste.aspx.vb]
Imports System
Imports System.Collections
Imports System.Data
Imports istia.st.articles.dao

Namespace istia.st.articles.web

    ' gère la page d'affichage de la liste d'articles
    Public Class ListeWebarticles
        Inherits System.Web.UI.Page

        ' composants de la page
        Protected WithEvents lblMessage As System.Web.UI.WebControls.Label
        Protected WithEvents DataGridArticles As System.Web.UI.WebControls.DataGrid
        Protected WithEvents entete As New EnteteWebArticles

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
            ' on prépare la vue [liste] à partir des informations du contexte
            ' on lie les options de menu à rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
            ' on récupère les articles dans un datatable
            Dim articles As ArrayList = CType(context.Items("articles"), ArrayList)
            ' on les lie au composant [DataGrid] de la page
            With DataGridArticles
                .DataSource = articles
                .DataBind()
            End With
            ' on affiche le message
            lblMessage.Text = context.Items("message").ToString
        End Sub
    End Class
End Namespace

Commentaires :

  • les composants de la page apparaissent lignes 13-15. A noter qu'on a eu besoin de créer un objet [EnteteWebArticles] avec un opérateur [new] alors que cela a été inutile avec les autres composants. Sans cette création explicite, on avait une erreur d'exécution indiquant que l'objet [entete] ne référençait rien. Ce point mériterait d'être creusé. Il ne l'a pas été.
  • le tableau des options du menu de l'entête est pris dans le contexte pour initialiser le composant [entete] de la page - ligne 20
  • la liste des articles est prise dans le contexte - ligne 22
  • pour initialiser le composant [DataGridArticles] - lignes 24-27
  • le composant [lblMessage] est initialisé avec un message placé dans le contexte - ligne 29

1.5.6.3. La vue [infos.aspx]

1.5.6.3.1. Introduction

Cette vue affiche des informations sur un article et permet également son achat :

Image

Elle est affichée à la suite d'une requête /main?action=infos&id=ID ou d'une requête /main?action=achat&id=ID lorsque la quantité achetée est erronée. Les éléments de la requête du contrôleur sont les suivants :

actions
objet Hashtable() - le tableau des options du menu
article
objet de type [Article] - article à afficher
msg
objet String - message à afficher en cas d'erreur sur la quantité
qte
objet String - valeur à afficher dans le champ de saisie [Qte]

Les champs [msg] et [qte] sont utilisés en cas d'erreur de saisie sur la quantité :

Image

Cette page contient un formulaire qui est posté par le bouton [Acheter]. L'URL cible du POST est [?action=achat&id=ID] où ID est l'id de l'article acheté.

1.5.6.3.2. Les composants de la page
type
nom
rôle
1
composant utilisateur
entete
afficher l'entête
2
literal
litID
afficher le n° de l'article
3 à 6
DataGrid
DataGridArticle
3 - colonne connexe : entête : Nom, champ : nom
4 - colonne connexe : entête : Prix, champ : prix
5 - colonne connexe : entête : Stock actuel, champ : stockActuel
6 - colonne connexe : entête : Stock minimum, champ : stockMinimum
afficher un article
7
HTML Submit
 
poster le formulaire
8
HTML Input
runat=server
txtQte
saisir la quantité achetée
9
label
lblMsgQte
message d'erreur éventuel
1.5.6.3.3. Le code de présentation [infos.aspx]
<%@ Register TagPrefix="WA" TagName="entete" Src="entete.ascx" %>
<%@ Page codebehind="infos.aspx.vb" inherits="istia.st.articles.web.InfosWebarticles" autoeventwireup="false" Language="vb" %>
<HTML>
    <HEAD>
        <TITLE>webarticles</TITLE>
        <META http-equiv="Content-Type" content="text/html; charset=windows-1252">
    </HEAD>
    <body>
        <WA:entete id="entete" runat="server"></WA:entete>
        <h2>Article d'id [<asp:Literal id="litId" runat="server"></asp:Literal>]</h2>
        <P>
            <asp:DataGrid id="DataGridArticle" runat="server" BackColor="White" BorderColor="#E7E7FF" CellPadding="3"
                BorderWidth="1px" BorderStyle="None" GridLines="Horizontal" AutoGenerateColumns="False">
                <SelectedItemStyle Font-Bold="True" ForeColor="#F7F7F7" BackColor="#738A9C"></SelectedItemStyle>
                <AlternatingItemStyle BackColor="#F7F7F7"></AlternatingItemStyle>
                <ItemStyle HorizontalAlign="Center" ForeColor="#4A3C8C" BackColor="#E7E7FF"></ItemStyle>
                <HeaderStyle Font-Bold="True" HorizontalAlign="Center" ForeColor="#F7F7F7" BackColor="#4A3C8C"></HeaderStyle>
                <FooterStyle ForeColor="#4A3C8C" BackColor="#B5C7DE"></FooterStyle>
                <Columns>
                    <asp:BoundColumn DataField="nom" HeaderText="Nom">
                        <HeaderStyle HorizontalAlign="Center"></HeaderStyle>
                    </asp:BoundColumn>
                    <asp:BoundColumn DataField="prix" HeaderText="Prix" DataFormatString="{0:C}">
                        <HeaderStyle HorizontalAlign="Center"></HeaderStyle>
                    </asp:BoundColumn>
                    <asp:BoundColumn DataField="stockactuel" HeaderText="Stock actuel">
                        <HeaderStyle HorizontalAlign="Center"></HeaderStyle>
                    </asp:BoundColumn>
                    <asp:BoundColumn DataField="stockminimum" HeaderText="Stock minimum">
                        <HeaderStyle HorizontalAlign="Center"></HeaderStyle>
                    </asp:BoundColumn>
                </Columns>
                <PagerStyle HorizontalAlign="Right" ForeColor="#4A3C8C" BackColor="#E7E7FF" Mode="NumericPages"></PagerStyle>
            </asp:DataGrid></P>
        <HR width="100%" SIZE="1">
        <form method="post" action="<%=strAction%>">
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qté</td>
                    <td><INPUT type="text" maxLength="3" size="3" id="txtQte" runat="server"></td>
                    <td><asp:Label id="lblMsgQte" runat="server" />
                    </td>
                </tr>
            </table>
        </form>
    </body>
</HTML>

Commentaires :

  • l'entête est inclus dans la page - ligne 9
  • le litéral [litId] est défini ligne 10
  • le DataGrid [DataGridArticles] est défini lignes 12-34
  • le formulaire est défini lignes 36-46. Il a de type POST.
  • la cible du POST est fourni par une variable [strAction] - ligne 36. Cette variable devra être définie par le contrôleur.
  • le champ de saisie de la quantité achetée est définie ligne 41. C'est un composant HTML serveur (runat=server). Côté code, on y a accès via un objet.
  • la ligne 42 définit le label [lblMsgQte] qui contiendra un éventuel message d'erreur sur la quantité saisie
1.5.6.3.4. Le code de contrôle [infos.aspx.vb]
Imports istia.st.articles.dao
Imports System
Imports System.Collections

Namespace istia.st.articles.web

    ' gère la page d'informations sur un article
    Public Class InfosWebarticles
        Inherits System.Web.UI.Page
        Protected WithEvents lblMsgQte As System.Web.UI.WebControls.Label
        Protected WithEvents litId As System.Web.UI.WebControls.Literal
        Protected WithEvents txtQte As System.Web.UI.HtmlControls.HtmlInputText
        Protected WithEvents DataGridArticle As System.Web.UI.WebControls.DataGrid
        Protected WithEvents entete As New EnteteWebArticles

        ' l'URL où sera postée le formulaire
        Private _strAction As String
        Public Property strAction() As String
            Get
                Return _strAction
            End Get
            Set(ByVal Value As String)
                _strAction = Value
            End Set
        End Property

        ' affichage page
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
            ' on récupère les infos de la requête
            Dim unArticle As Article = CType(Session.Item("article"), Article)
            ' on lie les options de menu à rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
            ' on lie l'article aux [DataGrid]
            Dim articles As New ArrayList
            articles.Add(unArticle)
            With DataGridArticle
                .DataSource = articles
                .DataBind()
            End With
            ' le label id
            litId.Text = unArticle.id.ToString
            ' le message d'erreur
            lblMsgQte.Text = context.Items("msg").ToString
            ' la qté précédente
            txtQte.Value = context.Items("qte").ToString
            ' l'URL action
            strAction = "?action=achat&id=" + unArticle.id.ToString
        End Sub
End Namespace

Commentaires :

  • les composants de la page sont définis lignes 10-14
  • la classe définit une propriété publique [strAction] qui sert à définir la cible du POST du formulaire - lignes 17-25
  • l'article à afficher est récupéré dans le contexte de l'application - ligne 30
  • le tableau des options du menu de l'entête est pris dans le contexte pour initialiser le composant [entete] de la page - ligne 32
  • lignes 33-39, le composant [DataGridArticle] est liée à une source de données de type [ArrayList] ne contenant que l'article récupéré ligne 30
  • les composants [lblMsgQte, txtQte] sont initialisés avec des informations prises dans le contexte - lignes 42-45
  • la propriété [straction] est également initialisée avec une information prise dans le contexte - ligne 47. Cette variable sert à générer l'attribut [action] du formulaire HTML présent dans la page :
        <form method="post" action="<%=strAction%>">
....
        </form>

1.5.6.4. La vue [panier.aspx]

1.5.6.4.1. Introduction

Cette vue affiche le contenu du panier :

Image

Elle est affichée à la suite d'une requête /main?action=panier ou /main?action=retirerachat&id=ID. Les éléments de la requête du contrôleur sont les suivants :

actions
objet Hashtable() - le tableau des options du menu
panier
objet de type [Panier] - le panier à afficher

Chaque lien [Retirer] du tableau HTML des achats du panier a une URL de la forme [?action=retirerachat&id=ID] où ID est le champ [id] de l'article qu'on veut retirer du panier.

1.5.6.4.2. Les composants de la page
type
nom
rôle
1
composant utilisateur
entete
afficher l'entête
2
DataGrid
DataGridAchats
3- colonne connexe - entête : Article, champ : nom
4 - colonne connexe - entête : Qté, champ : qte
5 - colonne connexe - entête : Prix, champ : prix
6 - colonne connexe - entête : Total, champ : total, mise en forme {0:C}
7 - colonne hypertexte - Texte : Retirer, p Url : id, format Url : /webarticles/main.aspx?action=retirerachat&id={0}
afficher la liste des articles achetés
8
label
lblTotal
afficher le montant à payer
1.5.6.4.3. Le code de présentation [panier.aspx]
<%@ Page codebehind="panier.aspx.vb" inherits="istia.st.articles.web.PanierWebarticles" autoeventwireup="false" Language="vb" %>
<%@ Register TagPrefix="WA" TagName="entete" Src="entete.ascx" %>
<HTML>
    <HEAD>
        <TITLE>webarticles</TITLE>
        <META http-equiv="Content-Type" content="text/html; charset=windows-1252">
    </HEAD>
    <body>
        <WA:entete id="entete" runat="server"></WA:entete>
        <h2>Contenu de votre panier</h2>
        <P>
            <asp:DataGrid id="DataGridAchats" runat="server" BorderWidth="1px" GridLines="Vertical" CellPadding="4"
                BackColor="White" BorderStyle="None" BorderColor="#DEDFDE" ForeColor="Black" AutoGenerateColumns="False">
                <SelectedItemStyle Font-Bold="True" ForeColor="White" BackColor="#CE5D5A"></SelectedItemStyle>
                <AlternatingItemStyle BackColor="White"></AlternatingItemStyle>
                <ItemStyle BackColor="#F7F7DE"></ItemStyle>
                <HeaderStyle Font-Bold="True" ForeColor="White" BackColor="#6B696B"></HeaderStyle>
                <FooterStyle BackColor="#CCCC99"></FooterStyle>
                <Columns>
                    <asp:BoundColumn DataField="nom" HeaderText="Article"></asp:BoundColumn>
                    <asp:BoundColumn DataField="qte" HeaderText="Qt&#233;"></asp:BoundColumn>
                    <asp:BoundColumn DataField="prix" HeaderText="Prix"></asp:BoundColumn>
                    <asp:BoundColumn DataField="totalAchat" HeaderText="Total" DataFormatString="{0:C}"></asp:BoundColumn>
                    <asp:HyperLinkColumn Text="Retirer" DataNavigateUrlField="id" DataNavigateUrlFormatString="/webarticles/main.aspx?action=retirerachat&amp;id={0}"></asp:HyperLinkColumn>
                </Columns>
                <PagerStyle HorizontalAlign="Right" ForeColor="Black" BackColor="#F7F7DE" Mode="NumericPages"></PagerStyle>
            </asp:DataGrid></P>
        <P>Total de la commande :
            <asp:Label id="lblTotal" runat="server"></asp:Label>&nbsp;euros</P>
    </body>
</HTML>

Commentaires

  • la ligne 9 inclut l'entête
  • lignes 12-27, le composant [DataGridAchats] est défini
  • ligne 29, le composant [lblTotal] est défini
1.5.6.4.4. Le code de contrôle [panier.aspx.vb]
Imports System
Imports System.Collections
Imports System.Data
Imports istia.st.articles.dao
Imports istia.st.articles.domain

Namespace istia.st.articles.web
    ' gère la page d'affichage du panier
    Public Class PanierWebarticles
        Inherits System.Web.UI.Page
        Protected WithEvents DataGridAchats As System.Web.UI.WebControls.DataGrid
        Protected WithEvents lblTotal As System.Web.UI.WebControls.Label
        Protected WithEvents entete As New EnteteWebArticles

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
            ' on lie les options de menu à rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
            ' on récupère le panier
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            ' on transfère les achats dans un tableau de lignes d'achats
            Dim achats(unPanier.achats.Count - 1) As LigneAchat
            ' on change le type des éléments du ArrayList
            For i As Integer = 0 To achats.Length - 1
                achats(i) = New LigneAchat(CType(unPanier.achats(i), Achat))
            Next
            ' on lie les données aux composants [DataGrid] de la page
            With DataGridAchats
                .DataSource = achats
                .DataBind()
            End With
            ' on affiche le total à payer
            lblTotal.Text = unPanier.totalPanier.ToString
        End Sub

        ' ligne d'achat construite à partir d'un objet Achat
        Private Class LigneAchat
            Inherits Achat

            ' constructeur reçoit un achat
            Public Sub New(ByVal unAchat As Achat)
                Me.article = unAchat.article
                Me.qte = unAchat.qte
            End Sub

            ' id : rend l'id de l'article acheté
            Public ReadOnly Property id() As Integer
                Get
                    Return article.id
                End Get
            End Property

            ' nom : nom de l'article acheté
            Public ReadOnly Property nom() As String
                Get
                    Return article.nom
                End Get
            End Property

            ' prix de l'article acheté
            Public ReadOnly Property prix() As Double
                Get
                    Return article.prix
                End Get
            End Property

        End Class
    End Class
End Namespace

Commentaires :

  • les composants de la page sont déclarés lignes 11-13
  • l'initialisation du composant [entete] est identique à celle trouvée dans les pages déjà étudiées - ligne 17
  • le panier à afficher est récupéré dans la session - ligne 19
  • l'affichage de ce panier à l'aide du composant [DataGridAchats] pose des problèmes. La difficulté vient de l'initialisation du composant. Rappelons les colonnes de celui-ci :
    • colonne [Article] associée au un champ [nom] de la source de données
    • colonne [Qté] associée au un champ [qte] de la source de données
    • colonne [Prix] associée au un champ [prix] de la source de données
    • colonne [Total] associée au un champ [total] de la source de données

La source de données dont nous disposons est le panier et sa liste d'achats. Cette dernière sera la source de données du [DataGrid]. Seulement les objets [Achat] qui vont alimenter les lignes du [DataGrid] n'ont pas les propriétés [nom, qte, prix, total] attendues par le [DataGrid]. Aussi crée-t-on ici, spécialement pour le [DataGrid] une source de données dont les éléments ont les caractéristiques attendues par le [DataGrid]. Ces éléments seront de type [LigneAchat] une classe créée pour l'occasion et dérivée de la classe [Achat] - lignes 36-66

  • la classe [LigneAchat) définie, la source de données de [DataGridAchats] est construite à partir du panier trouvé dans la session - lignes 20-30
  • le montant des achats est affiché grâce à la propriété [totalPanier] de la classe [Panier] - ligne 32

1.5.6.5. La vue [paniervide.aspx]

1.5.6.5.1. Introduction

Cette vue affiche l'information indiquant que le panier est vide :

Image

Elle est affichée à la suite d'une requête /main?action=panier ou /main?action=retirerachat&id=ID. Les éléments de la requête du contrôleur sont les suivants :

actions
objet Hashtable() - le tableau des options du menu
1.5.6.5.2. Les composants de la page
type
nom
rôle
1
composant utilisateur
entete
afficher l'entête
1.5.6.5.3. Le code de présentation [paniervide.aspx]
<%@ Register TagPrefix="WA" TagName="entete" Src="entete.ascx"%>
<%@ Page codebehind="paniervide.aspx.vb" inherits="istia.st.articles.web.PaniervideWebarticles" autoeventwireup="false" Language="vb" %>
<HTML>
    <HEAD>
        <TITLE>webarticles</TITLE>
        <META http-equiv="Content-Type" content="text/html; charset=windows-1252">
    </HEAD>
    <body>
        <WA:entete id="entete" runat="server"></WA:entete>
        <h2>Contenu de votre panier</h2>
        <P>Votre panier est vide</P>
    </body>
</HTML>

Commentaires :

  • l'entête est inclus ligne 9
1.5.6.5.4. Le code de contrôle [paniervide.aspx.vb]
Namespace istia.st.articles.web
    ' gère la page d'affichage d'un panier vide
    Public Class PaniervideWebarticles
        Inherits System.Web.UI.Page
        Protected WithEvents entete As New EnteteWebArticles

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
            ' on prépare la vue [paniervide] à partir des informations du contexte
            ' on lie les options de menu à rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
        End Sub
    End Class
End Namespace

Commentaires :

  • on se contente d'initialiser le seul composant dynamique de la page - ligne 10

1.5.6.6. La vue [erreurs.aspx]

1.5.6.6.1. Introduction

Cette vue est affichée en cas d'erreurs :

Image

Elle est affichée à la suite de toute requête menant à une erreur sauf pour l'action d'achat avec une quantité erronée, qui elle, est traitée par la vue [INFOS]. Les éléments de la requête du contrôleur sont les suivants :

actions
objet Hashtable() - le tableau des options du menu
erreurs
ArrayList d'objets [String] représentant les messages d'erreurs à afficher
1.5.6.6.2. Les composants de la page
type
nom
rôle
1
composant utilisateur
entete
afficher l'entête
2
repeater
rptErreurs
afficher la liste des erreurs
1.5.6.6.3. La code de présentation [erreurs.aspx]
<%@ Register TagPrefix="WA" TagName="entete" Src="entete.ascx" %>
<%@ Page codebehind="erreurs.aspx.vb" inherits="istia.st.articles.web.ErreursWebarticles" autoeventwireup="false" Language="vb" %>
<HTML>
    <HEAD>
        <TITLE>webarticles</TITLE>
        <META http-equiv="Content-Type" content="text/html; charset=windows-1252">
    </HEAD>
    <body>
        <WA:entete id="entete" runat="server"></WA:entete>
        <h3>Les erreurs suivantes se sont produites :
        </h3>
        <ul>
            <asp:Repeater id="rptErreurs" runat="server">
                <ItemTemplate>
                    <li>
                        <%# Container.DataItem %>
                    </li>
                </ItemTemplate>
            </asp:Repeater></ul>
    </body>
</HTML>

Commentaires :

  • l'entête est défini ligne 9
  • le composant [rptErreurs] est défini lignes 13-19. Son contenu provient d'une source de données qui sera de type [ArrayList] d'objets [String].
1.5.6.6.4. La code de contrôle [erreurs.aspx.vb]
Namespace istia.st.articles.web

    ' gère la page d'erreurs
    Public Class ErreursWebarticles
        Inherits System.Web.UI.Page

        ' composants de la page
        Protected WithEvents rptErreurs As System.Web.UI.WebControls.Repeater
        Protected WithEvents entete As New EnteteWebArticles

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
            ' on prépare la vue [erreurs] à partir des informations du contexte
            ' on lie les options de menu à rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
            ' on lie les erreurs à rptErreurs
            With rptErreurs
                .DataSource = context.Items("erreurs")
                .DataBind()
            End With
        End Sub
    End Class

End Namespace

Commentaires :

  • le composant [entete] est initialisé comme à l'habitude, lignes 9 et 14
  • le composant [rptErreurs] est initialisé avec la liste d'erreurs de type [ArrayList] trouvée dans le contexte - lignes 16-19

1.5.7. Les contrôleurs global.asax, main.aspx

Il reste à écrire le coeur de notre application web, le contrôleur. Son rôle consiste à :

  • récupérer la requête du client,
  • traiter l'action demandée par celui-ci à l'aide des classes métier,
  • envoyer en réponse la vue appropriée.

1.5.7.1. Le contrôleur [global.asax.vb]

Lorsque l'application reçoit sa toute première requête, la procédure [Application_Start] du fichier [global.asax.vb] est exécutée. Ce sera la seule fois. La procédure [Application_Start] a pour but d'initialiser les objets nécessaires à l'application web et qui seront partagés en lecture seule par tous les threads clients. Ces objets partagés peuvent être placés en deux endroits :

  • les champs privés du contrôleur
  • le contexte d'exécution de l'application (Application)

La méthode [Application_Start] de l'application [global.asax.vb] fera les actions suivantes :

  • vérifiera la présence, dans le fichier [web.config], des paramètres nécessaires au bon fonctionnement de l'application. Ceux-ci ont été décrits au paragraphe 1.5.3.
  • mettra dans le contexte de l'application la liste des erreurs éventuelles sous la forme d'un objet [ArrayList erreurs]. Cette liste sera vide s'il n'y a pas d'erreurs mais existera néanmoins.
  • s'il y a eu des erreurs, la méthode [Application_Start] s'arrête là. Sinon, elle demande la référence d'un singleton de type [IArticlesDomain] qui sera l'objet métier que le contrôleur utilisera pour ses besoins. Comme il a été expliqué en 1.5.3.2, le contrôleur demandera au framework Spring ce singleton. Cette opération d'instanciation peut amener différentes erreurs. Si tel est le cas, elles seront là encore, mémorisées dans l'objet [erreurs] du contexte de l'application.

Le contrôleur [global.asax.vb] dispose d'une procédure [Session_Start] exécutée à chaque fois qu'un nouveau client arrive. Dans cette procédure, on créera un panier vide pour le client. Ce panier sera maintenu au fil des requêtes de ce client particulier. Le code pourrait être le suivant :

Imports System
Imports System.Web
Imports System.Web.SessionState
Imports System.Configuration
Imports istia.st.articles.domain
Imports System.Collections
Imports Spring.Context

Namespace istia.st.articles.web

    Public Class GlobalWebArticles
        Inherits System.Web.HttpApplication

        ' init application
        Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)

            ' données locales
            Dim parameters() As String = {"urlMain", "urlErreurs", "urlInfos", "urlListe", "urlPanier", "urlPanierVide"}
            Dim erreurs As New ArrayList

            ' on récupère les paramètres d'initialisation de l'application
            Dim param As String
            For i As Integer = 0 To parameters.Length - 1
                ' lecture dans fichier de conf
                param = ConfigurationSettings.AppSettings(parameters(i))
                If param Is Nothing Then
                    ' on note l'erreur
                    erreurs.Add("Paramètre [" + parameters(i) + "] absent dans le fichier [web.config]")
                Else
                    ' on mémorise le paramètre dans l'application
                    Application.Item(parameters(i)) = param
                End If
            Next
            ' des erreurs ?
            If erreurs.Count = 0 Then
                ' on crée un objet IArticlesDomain d'accès à la couche métier
                Dim contexte As IApplicationContext = CType(ConfigurationSettings.GetConfig("spring/context"), IApplicationContext)
                Dim articlesDomain As IArticlesDomain
                Try
                    articlesDomain = CType(contexte.GetObject("articlesDomain"), IArticlesDomain)
                    ' on mémorise l'objet dans l'application
                    Application.Item("articlesDomain") = articlesDomain
                Catch ex As Exception
                    ' on mémorise l'erreur
                    erreurs.Add("Erreur lors de la construction de l'objet d'accès à la couche métier [" + ex.ToString + "]")
                End Try
            End If
            ' les erreurs sont placées dans l'application
            Application.Item("erreurs") = erreurs
            ' c'est fini s'il y a eu des erreurs
            If erreurs.Count <> 0 Then Return
            ' on construit un tableau d'options de menu
            Dim options As New Hashtable
            ' on récupère l'URL du contrôleur
            Dim urlMain As String = CType(Application.Item("urlMain"), String)
            Dim uneOption As Hashtable
            ' liste des articles
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=liste")
            uneOption.Add("lien", "Liste des articles")
            options.Add("liste", uneOption)
            ' panier
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=panier")
            uneOption.Add("lien", "Voir le panier")
            options.Add("panier", uneOption)
            ' validation panier
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=validationpanier")
            uneOption.Add("lien", "Valider le panier")
            options.Add("validationpanier", uneOption)
            ' on met les options du menu dans l'application
            Application.Item("options") = options
            Return
        End Sub

        ' init session
        Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
            ' on crée un panier pour le client
            Session.Item("panier") = New Panier
        End Sub
    End Class
End Namespace

Commentaires :

  • les paramètres attendus dans [web.config] sont définis dans un tableau - ligne 18
  • ils sont recherchés dans [web.config]. S'ils sont présents, ils sont mémorisés dans le contexte de l'application, sinon une erreur est enregistrée dans la liste des erreurs [erreurs] - lignes 21-33
  • s'il n'y a pas d'erreurs, on demande à Spring une référence du singleton [articlesDomain] qui gère l'accès à la couche [domain] de l'application - lignes 35-47. Les erreurs éventuelles sont enregistrées dans [erreurs].
  • les erreurs sont enregistrées dans le contexte de l'application - ligne 49
  • on quitte la procédure s'il y a eu des erreurs - ligne 51
  • on crée un tableau de trois dictionnaires. Chacun d'eux a deux clés : href et lien. Ce tableau représente les trois options de menu possibles - lignes 52-71
  • ce tableau est mémorisé dans le contexte de l'application - ligne 73
  • à chaque nouveau client, la procédure [Session_Start] est exécutée. On y crée un panier vide dans la session du client - lignes 78-81

1.5.7.2. Le contrôleur [main.aspx.vb]

Le contrôleur [main.aspx.vb] voit passer toutes les requêtes des clients. En effet, celles-ci sont toutes de la forme [/webarticles/main.aspx?action=XX]. Une requête sera traitée de la façon suivante :

  • l'objet [erreurs] du contexte de l'application sera vérifié. S'il est non vide, cela signifie qu'il y a eu des erreurs lors de l'initialisation de l'application et que celle-ci ne peut pas fonctionner. On enverra alors, en réponse, la vue [ERREURS].
  • le paramètre [action] de la requête sera récupéré et vérifié. S'il ne correspond pas à une action connue, la vue [ERREURS] est envoyée avec un message d'erreur approprié.
  • si le paramètre [action] est valide, la requête du client est passée à une procédure spécifique à l'action pour traitement :
méthode
demande
traitement
réponses possibles
doListe
GET /main?action=liste
- demander la liste des articles
à la classe métier
- l'afficher
[LISTE] ou [ERREURS]
doInfos
GET /main?action=infos&id=ID
- demander l'article d'id=ID à
la classe métier
- l'afficher
[INFOS] ou [ERREURS]
doAchat
POST /main?action=achat&id=ID
- la qté achetée fait partie des paramètres postés
- demander l'article d'id=ID à
la classe métier
- l'inclure dans le panier dans
la session client
[LISTE] ou [INFOS] ou [ERREURS]
doRetirerAchat
GET /main?action=retirerachat&id=ID
- retirer l'article d'id=ID de la
liste des achats du panier de
la session client
[PANIER]
doPanier
GET /main?action=panier
- faire afficher le panier de la
session client
[PANIER] ou [PANIERVIDE]
doValidationPanier
GET /main?action=validationpanier
- décrémenter dans la base les
stocks de tous les articles
présents dans le panier de
session du client
[LISTE] ou [ERREURS]

Le squelette du contrôleur [main.aspx.vb] pourrait être le suivant :

Imports System.Collections
Imports System
Imports System.Data

Imports istia.st.articles.dao
Imports istia.st.articles.domain

Namespace istia.st.articles.web

    ' classe contrôleur de l'application web
    Public Class MainWebArticles
        Inherits System.Web.UI.Page

        ' champs privés
        Private articlesDomain As IArticlesDomain
        Private options As Hashtable

        ' chargement de la page
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
....
        End Sub

        ' méthodes de traitement des actions

        ' liste des articles
        Public Sub doListe()
...
        End Sub

        ' infos sur un article
        Public Sub doInfos()
...
        End Sub

        ' achat d'un article
        Public Sub doAchat()
...
        End Sub

        ' suppression d'un achat
        Public Sub doRetirerAchat()
...
        End Sub

        ' visualisation panier
        Public Sub doPanier()
...
        End Sub

        ' achat panier
        Public Sub doValidationPanier()
...
        End Sub

    End Class

End Namespace

Commentaires :

  • la classe a deux champs privés qui seront partagés entre les méthodes - lignes 15-16 :
    • articlesDomain : le singleton d'accès à la couche [domain]
    • options : le tableau des dictionnaires des options de menu
  • la procédure [Page_Load] :
    • initialisera les deux champs privés de la classe
    • récupèrera le paramètre [action] de la requête et fera exécuter la méthode de traitement de cette action.

1.5.7.3. La méthode [Page_Load]

Cet événement est le premier à se produire sur la page. Le code est le suivant :

        ' chargement de la page
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
            ' on vérifie si l'application a démarré correctement
            Dim erreurs As ArrayList = CType(Application.Item("erreurs"), ArrayList)
            ' s'il y a des erreurs, on envoie la vue [erreurs]
            If erreurs.Count <> 0 Then
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {}
                Server.Transfer(CType(Application("urlErreurs"), String))
            End If
            ' on récupère l'objet d'accès à la classe métier
            articlesDomain = CType(Application.Item("articlesDomain"), IArticlesDomain)
            ' ainsi que les options de menu
            options = CType(Application.Item("options"), Hashtable)
            ' on récupère l'action à faire
            Dim action As String = Request.QueryString("action")
            If action Is Nothing Then
                action = "liste"
            End If
            ' on exécute l'action
            Select Case action
                Case "liste"
                    doListe()
                Case "infos"
                    doInfos()
                Case "achat"
                    doAchat()
                Case "panier"
                    doPanier()
                Case "retirerachat"
                    doRetirerAchat()
                Case "validationpanier"
                    doValidationPanier()
                Case Else
                    doListe()
            End Select
        End Sub

Commentaires :

  • à chaque chargement de page, on s'assure que l'initialisation de l'application faite par [global.asax] s'est bien déroulée.
  • pour cela, on récupère dans le contexte de l'application, la liste des erreurs placée là par [global.asax] - ligne 4
  • si cette liste est non vide, on fait afficher la vue [ERREURS] - lignes 6-10
  • on récupère le singleton [articlesDomain] placé par [global.asax] dans le contexte de l'application et on le mémorise dans le champ privé [articlesDomain] afin qu'il soit disponible aux différentes méthodes de la classe - ligne 12
  • on fait un travail analogue avec le tableau des options de menu - ligne 14
  • on récupère le paramètre [action] de la requête - ligne 16
  • on fait exécuter la méthode correspondant à l'action demandée. Une action non prévue est considérée comme l'action [liste] - lignes 16-36

1.5.7.4. Traitement de l'action [liste]

Il s'agit d'afficher la liste des articles :

Image

Le code est le suivant :

        ' liste des articles
        Public Sub doListe()
            ' gestion des erreurs
            Dim erreurs As New ArrayList
            ' on demande la liste des articles
            Dim articles As IList
            Try
                articles = articlesDomain.getAllArticles
            Catch ex As Exception
                ' on note l'erreur
                erreurs.Add(ex.ToString)
            End Try
            ' des erreurs ?
            If erreurs.Count <> 0 Then
                ' affichage vue [erreurs]
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
            End If
            ' affichage vue [liste]
            context.Items("articles") = articles
            context.Items("options") = New Hashtable() {CType(options("panier"), Hashtable)}
            If context.Items("message") Is Nothing Then context.Items("message") = ""
            Server.Transfer(CType(Application.Item("urlListe"), String))
        End Sub

Commentaires :

  • on met les erreurs éventuelles dans un [ArrayList] - ligne 4
  • la liste des articles est demandée au singleton [articlesDomain] - lignes 5-12
  • s'il y a eu des erreurs, on envoie la vue [ERREURS] - lignes 13-19
  • sinon on envoie la vue [LISTE] - lignes 20-24

1.5.7.5. Traitement de l'action [infos]

Le client a demandé de l'information sur un article donné :

Image

Le code est le suivant :

        ' infos sur un article
        Public Sub doInfos()
            ' gestion des erreurs
            Dim erreurs As New ArrayList
            ' on récupère l'id de l'article demandé
            Dim strId As String = Request.QueryString("id")
            ' a-t-on qq chose ?
            If strId Is Nothing Then
                ' pas normal - on envoie la page d'erreurs
                erreurs.Add("action incorrecte (action=infos, id=rien)")
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
                Exit Sub
            End If
            ' a-t-on un entier ?
            Dim id As Integer
            Try
                id = Integer.Parse(strId)
            Catch ex As Exception
                ' pas normal - on envoie la page d'erreurs
                erreurs.Add("action incorrecte (action=infos, id[" + strId + "] invalide)")
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
                Exit Sub
            End Try
            ' on demande l'article de clé id
            Dim unArticle As Article
            Try
                unArticle = articlesDomain.getArticleById(id)
            Catch ex As Exception
                ' pb d'accès aux données
                erreurs.Add("Problème d'accès aux données (" + ex.ToString + ")")
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
                Exit Sub
            End Try
            ' a-t-on récupéré un article ?
            If unArticle Is Nothing Then
                ' l'article n'existe pas
                erreurs.Add("L'article d'id=" + id.ToString + " n'existe pas")
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
                Exit Sub
            End If
            ' on a l'article - on le met dans la session courante
            Session.Item("article") = unArticle
            ' on prépare son affichage
            context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
            context.Items("msg") = ""
            context.Items("qte") = ""
            Server.Transfer(CType(Application.Item("urlInfos"), String))
        End Sub

Commentaires :

  • on met les erreurs éventuelles dans un [ArrayList] - ligne 4
  • l'identifiant de l'article demandé est récupéré dans la requête - ligne 6
  • cet identifiant est vérifié. Il doit être présent et ce doit être un entier. Si ce n'est pas le cas, la vue [ERREURS] est envoyée avec le message d'erreur approprié - lignes 7-27
  • une fois l'identifiant vérifié, l'article est demandé au singleton [articlesDomain]. S'il se produit une exception, la vue [ERREURS] est envoyée - lignes 29-39
  • si l'article n'a pas été trouvé, la vue [ERREURS] est envoyée - lignes 41-48
  • si l'article a été trouvé, il est placé dans la session de l'utilisateur puis affiché dans la vue [INFOS] - lignes 50-56

1.5.7.6. Traitement de l'action [achat]

Le client a acheté l'article affiché dans la vue [INFOS].

Image

Le code est le suivant :

        ' achat d'un article
        Public Sub doAchat()
            ' achat d'un article
            ' on récupère l'article qui était dans la session
            Dim unArticle As Article = CType(Session.Item("article"), Article)
            ' a-t-on qq chose ?
            If unArticle Is Nothing Then
                ' pas normal - on affiche la liste des articles
                doListe()
            End If
            ' on récupère la qté postée
            Dim strQte As String = Request.Form("txtQte")
            ' a-t-on qq chose ?
            If strQte Is Nothing Then
                ' pas normal - on envoie la liste des articles
                doListe()
            End If
            ' a-t-on un entier ?
            Dim qte As Integer
            Try
                qte = Integer.Parse(strQte)
                If (qte <= 0) Then Throw New Exception
            Catch ex As Exception
                ' pas normal - on envoie la page d'infos avec le msg d'erreur
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                context.Items("msg") = "Quantité incorrecte"
                context.Items("qte") = strQte
                Server.Transfer(CType(Application.Item("urlInfos"), String))
            End Try
            ' tout va bien - on met l'achat dans le panier du client
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            unPanier.ajouter(New Achat(unArticle, qte))
            ' on affiche la liste des articles
            doListe()
        End Sub

Commentaires :

  • l'article mis dans la session est récupéré - ligne 5
  • s'il n'est pas là (la session a pu expirer), on affiche la vue [LISTE] - lignes 7-10
  • la quantité achetée est récupérée dans la requête - ligne 12
  • sa validité est vérifiée - lignes 13-29
  • en cas d'invalidité, selon les cas, on envoie la vue [LISTE] - ligne 16 ou la vue [INFOS] - lignes 24-28
  • si tout est normal, l'achat est enregistré dans le panier - lignes 31-32
  • puis la vue [LISTE] est envoyée - ligne 34

1.5.7.7. Traitement de l'action [panier]

Le client a fait différents achats et il demande à voir le panier :

Image

Le code est le suivant :

        ' visualisation panier
        Public Sub doPanier()
            'on récupère le panier dans la session
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            ' panier vide ?
            If unPanier.achats.Count = 0 Then
                ' affichage panier vide
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlPanierVide"), String))
            End If
            ' affichage panier non vide
            context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable), CType(options("validationpanier"), Hashtable)}
            Server.Transfer(CType(Application.Item("urlPanier"), String))
        End Sub

Commentaires :

  • on récupère le panier dans la session - ligne 4. On ne vérifie pas ici qu'on récupère bien quelque chose. Il faudrait le faire car la session a pu expirer.
  • si le panier est vide, on envoie la vue [PANIERVIDE] - lignes 6-10
  • sinon on envoie la vue [PANIER] - lignes 11-14

1.5.7.8. Traitement de l'action [retirerachat]

Le client veut retirer un achat de son panier :

Image

Le code est le suivant :

        ' suppression d'un achat
        Public Sub doRetirerAchat()
            'on récupère le panier dans la session
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            ' on enlève l'achat
            Try
                ' on récupère l'id de l'article retiré
                Dim idArticle As Integer = Integer.Parse(Request.QueryString("id"))
                ' on l'enlève du panier
                unPanier.enlever(idArticle)
            Catch ex As Exception
                ' on affiche la liste des articles
                doListe()
            End Try
            ' on affiche le panier
            doPanier()
        End Sub

Commentaires :

  • on récupère le panier dans la session - ligne 4. On ne vérifie pas ici qu'on récupère bien quelque chose. Il faudrait le faire car la session a pu expirer.
  • on récupère dans la requête l'identifiant [id] de l'article à enlever - ligne 8.
  • l'achat correspondant est enlevé du panier - ligne 10
  • la validité de l'identifiant de l'article acheté n'a pas été vérifiée ici. Si celui-ci a un type invalide, une exception aura lieu et sera traitée lignes 11-13. S'il est valide mais inexistant, la méthode [panier.enlever] - ligne 10 - ne fait rien.
  • on fait afficher le nouveau panier - ligne 16

1.5.7.9. Traitement de l'action [validerpanier]

Le client veut valider son panier :

Image

Le code est le suivant :

        ' achat panier
        Public Sub doValidationPanier()
            ' au départ, pas d'erreurs
            Dim erreurs As ArrayList
            'on récupère le panier dans la session
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            ' on tente de valider le panier
            Try
                articlesDomain.acheter(unPanier)
                ' on note les éventuelles erreurs d'achats
                erreurs = articlesDomain.erreurs
            Catch ex As Exception
                ' on note l'erreur
                erreurs = New ArrayList
                erreurs.Add(String.Format("Erreur lors de la validation du panier [{0}]", ex.Message))
            End Try
            ' si erreurs alors page d'erreurs
            If erreurs.Count <> 0 Then
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable), CType(options("panier"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
                Exit Sub
            End If
            ' tout va bien - on affiche la liste des articles avec un message de réussite
            context.Items("message") = "Votre panier a été validé"
            doListe()
        End Sub

Commentaires :

  • on récupère le panier dans la session - ligne 6. On ne vérifie pas ici qu'on récupère bien quelque chose. Il faudrait le faire car la session a pu expirer.
  • dans le cas où la session a expiré, on aura un pointeur [nothing] pour le panier et la méthode [acheter] - ligne 9 - lancera une exception et la vue [ERREURS] sera envoyée. Seulement le message d'erreur sera peu explicite pour l'utilisateur.
  • lignes 8-16, on essaie de valider le panier récupéré dans la session. Certains achats peuvent être non validés si la quantité demandée excède le stock de l'article demandé. Ces cas sont mémorisés par la méthode [acheter] dans une liste d'erreurs qui est récupérée ligne 11.
  • si on a des erreurs, la vue [ERREURS] est envoyée - lignes 18-23
  • sinon c'est la vue [LISTE] qui est envoyée - lignes 25-26

1.6. Conclusion

Nous avons ici développé une application selon un modèle MVC. Il ne semble pas exister (avril 2005) de "frameworks" professionnels de développement MVC en ASP.NET comme il en existe en Java (Struts, Spring, ...). Le projet [Spring.net] devrait en proposer un rapidement. En attendant cet avènement, la méthode précédente permet un développement MVC viable pour des applications de taille moyenne.