Skip to content

1. Parte 1

Il PDF del documento è disponibile |QUI|.

Gli esempi nel documento sono disponibili |QUI|.

1.1. Introduzione

Obiettivi di questo articolo:

  • Scrivere un'applicazione web a tre livelli [interfaccia utente, logica di business, accesso ai dati]
  • configurare l'applicazione con Spring IOC
  • Scrivere diverse versioni modificando l'implementazione di uno o più dei tre livelli.

Strumenti utilizzati:

  • Visual Studio.NET per lo sviluppo — vedere Appendice, Sezione 3.1;
  • server web Cassini per l'esecuzione — vedi Appendice, Sezione 3.2;
  • Nunit per i test unitari — vedi Appendice, Sezione 3.4;
  • Spring per l'integrazione e la configurazione dei livelli dell'applicazione web — vedi Appendice, Sezione 3.3;

Su una scala che va da principiante a intermedio ad avanzato, questo documento rientra nella categoria [intermedio-avanzato]. Per comprenderlo sono necessari diversi prerequisiti. Alcuni di questi si trovano nei documenti che ho scritto. In tali casi, li cito. Va da sé che questo è solo un suggerimento e che il lettore è libero di utilizzare le risorse che preferisce.

Questo documento segue la stessa struttura di un documento scritto per Java [Architetture a 3 livelli e architetture MVC con Struts, Spring e Java]. Stiamo realizzando l'applicazione web MVC a tre livelli scritta in Java utilizzando VB.NET. Il punto che vogliamo sottolineare qui è che le piattaforme di sviluppo Java e .NET sono sufficientemente simili da consentire il riutilizzo delle competenze acquisite in uno di questi due ambiti nell'altro.

Non sembra esserci una soluzione di sviluppo ASP.NET MVC ampiamente riconosciuta. La soluzione seguente adotta il metodo introdotto nel documento [Sviluppo Web con ASP.NET 1.1]. Sebbene questo metodo abbia il pregio di utilizzare concetti comuni nello sviluppo J2EE, dovrebbe comunque essere considerato per quello che è: uno dei tanti metodi di sviluppo MVC. Non appena un metodo di sviluppo MVC in ASP.NET sarà ampiamente accettato, dovrebbe essere adottato. La versione .NET di Spring, attualmente in fase di sviluppo, potrebbe benissimo essere una prima soluzione.

1.2. L'applicazione webarticles

Qui presentiamo i componenti di un'applicazione web di e-commerce semplificata. Questa applicazione consentirà agli utenti web di:

  • visualizzare un elenco di articoli da un database
  • aggiungerne alcuni a un carrello online
  • confermare il carrello. Questa conferma aggiornerà semplicemente l'inventario degli articoli acquistati nel database.

Le diverse schermate che verranno visualizzate all'utente saranno le seguenti:

- la visualizzazione "LIST", che mostra un elenco di articoli in vendita
- la vista [INFO], che fornisce ulteriori informazioni su un prodotto:
  • le schede [CARRELLO] e [SVUOTA CARRELLO], che mostrano il contenuto del carrello del cliente
  • la vista [ERRORI], che segnala eventuali errori dell'applicazione

Image

1.3. Architettura generale dell'applicazione

Vogliamo realizzare un'applicazione con la seguente struttura a tre livelli:

  • I tre livelli sono resi indipendenti tramite l'uso di interfacce
  • L'integrazione dei diversi livelli è gestita da Spring
  • Ogni livello ha il proprio namespace: web (livello UI), domain (livello business) e dao (livello di accesso ai dati).

L'applicazione seguirà un'architettura MVC (Model-View-Controller). Se ci riferiamo al diagramma a livelli sopra riportato, l'architettura MVC si inserisce in esso come segue:

L'elaborazione di una richiesta del client segue questi passaggi:

  1. Il client invia una richiesta al controller. In questo caso, il controller è una pagina .aspx che svolge un ruolo specifico. Gestisce tutte le richieste del client. È il punto di ingresso dell'applicazione. È la C in MVC.
  1. Il controller elabora questa richiesta. Per farlo, potrebbe aver bisogno dell'assistenza del livello business, noto come la M nella struttura MVC.
  2. Il controller riceve una risposta dal livello di business. La richiesta del client è stata elaborata. Ciò può innescare diverse possibili risposte. Un classico esempio è
    • una pagina di errore se la richiesta non è stata elaborata correttamente
    • una pagina di conferma in caso contrario
  3. Il controller sceglie la risposta (= vista) da inviare al cliente. Si tratta molto spesso di una pagina contenente elementi dinamici. Il controller fornisce questi alla vista.
  4. La vista viene inviata al cliente. Questa è la V in MVC.

1.4. Il Modello

Qui esaminiamo la M in MVC. Il modello è costituito dai seguenti elementi:

  1. classi di business
  2. classi di accesso ai dati
  3. il database

1.4.1. Il database

Il database contiene una sola tabella denominata ARTICLES. Questa tabella è stata generata utilizzando i seguenti comandi SQL:

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
);
/* constraints */
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);
/* primary key */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
chiave primaria che identifica in modo univoco un articolo
nome
nome dell'elemento
prezzo
il suo prezzo
disponibilità attuale
scorte attuali
scorte minime
il livello di scorte al di sotto del quale è necessario effettuare un riordino

1.4.2. Gli spazi dei nomi del modello

Il modello M è qui fornito sotto forma di due spazi dei nomi:

  • istia.st.articles.dao: contiene le classi di accesso ai dati del livello [dao]
  • istia.st.articles.domain: contiene le classi di business del livello [domain]

Ciascuno di questi spazi dei nomi verrà generato all'interno del proprio file "assembly":

assembly
contenuto
ruolo
webarticles-dao
- [IArticlesDao]: l'interfaccia per accedere al livello [dao].
Questa è l'unica interfaccia visibile al livello [domain]. Non ne vede altre.
- [Article]: classe che definisce un articolo
- [ArticlesDaoArrayList]: classe di implementazione dell'
dell'interfaccia [IArticlesDao] con un [ArrayList]
livello di accesso ai dati
- si trova interamente all'interno del
livello [DAO] dell'architettura a 3 livelli dell'
dell'applicazione web
webarticles-domain
- [IArticlesDomain]: l'interfaccia per accedere al livello [domain]. Questa è l'unica interfaccia visibile al livello web. Non ne vede altre.
- [AchatsArticles]: una classe che implementa [IArticlesDomain]
- [Purchase]: una classe che rappresenta l'acquisto di un cliente
- [Cart]: una classe che rappresenta il totale degli acquisti di un cliente
rappresenta il modello degli acquisti web
web - risiede interamente nel
livello [domain] dell'
architettura a 3 livelli dell'applicazione web

1.4.3. Il livello [DAO]

Il livello [DAO] contiene i seguenti elementi:

  • [IArticlesDao]: l'interfaccia per accedere al livello [dao]

  • [Article]: classe che definisce un articolo

  • [ArticlesDaoArrayList]: classe di implementazione per l'interfaccia [IArticlesDao] che utilizza una classe [ArrayList]

La struttura del progetto [Visual Studio] per il livello [dao] è la seguente:

Image

Commenti:

  • Il progetto [dao] è di tipo [libreria di classi]
  • Le classi sono state inserite in una struttura ad albero a partire dalla cartella [istia]. Si trovano tutte nello spazio dei nomi [istia.st.articles.dao].

1.4.3.1. La classe [Article]

La classe che definisce un articolo è la seguente:

Imports System

Namespace istia.st.articles.dao

    Public Class Article

        ' private fields
        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

        ' item name
        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

        ' item price
        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

        ' current stock item
        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

        ' minimum stock item
        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

        ' default builder
        Public Sub New()
        End Sub

        ' builder with properties
        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

        ' article identification method
        Public Overrides Function ToString() As String
            Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
        End Function
    End Class
End Namespace

Questa classe fornisce:

  1. un costruttore per impostare le 5 informazioni relative a un articolo: [id, nome, prezzo, stockAttuale, stockMinimo]
  2. proprietà pubbliche per la lettura e la scrittura delle 5 informazioni.
  3. una convalida dei dati inseriti per l'articolo. Se i dati non sono validi, viene generata un'eccezione.
  4. un metodo toString che restituisce il valore di un elemento sotto forma di stringa. Ciò risulta spesso utile per il debug di un'applicazione.

1.4.3.2. L'interfaccia [IArticlesDao]

L'interfaccia [IArticlesDao] è definita come segue:

Imports System
Imports System.Collections

Namespace istia.st.articles.dao

    Public Interface IArticlesDao
        ' list of all items
        Function getAllArticles() As IList
        ' add an article
        Function ajouteArticle(ByVal unArticle As Article) As Integer
        ' deletes an article
        Function supprimeArticle(ByVal idArticle As Integer) As Integer
        ' modify an article
        Function modifieArticle(ByVal unArticle As Article) As Integer
        ' search for an article
        Function getArticleById(ByVal idArticle As Integer) As Article
        ' deletes all articles
        Sub clearAllArticles()
        ' changes the stock of an item
        Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
    End Interface
End Namespace

I ruoli dei vari metodi nell'interfaccia sono i seguenti:

getAllArticles
restituisce tutti gli elementi dall'origine dati
clearAllArticles
cancella l'origine dati
getArticleById
restituisce l'oggetto [Article] identificato dalla sua chiave primaria
addArticle
consente di aggiungere un articolo all'origine dati
modifyArticle
consente di modificare un articolo nell'origine dati
deleteItem
consente di eliminare un elemento dall'origine dati
updateItemStock
consente di modificare la quantità di un articolo nell'origine dati

L'interfaccia fornisce ai programmi client una serie di metodi definiti esclusivamente dalle loro firme. Non si occupa di come questi metodi saranno effettivamente implementati. Ciò garantisce flessibilità all'applicazione. Il programma client effettua chiamate a un'interfaccia piuttosto che a una specifica implementazione di tale interfaccia.

La scelta di una specifica implementazione verrà effettuata tramite un file di configurazione Spring. Per dimostrare che, ai fini del test dell'applicazione web, conta solo l'interfaccia di accesso ai dati — e non la sua classe di implementazione — implementeremo innanzitutto la fonte dati utilizzando un semplice oggetto [ArrayList]. Successivamente, presenteremo una soluzione basata su database.

1.4.3.3. La classe di implementazione [ArticlesDaoArrayList]

La classe di implementazione [ArticlesDaoArrayList] è definita come segue:

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

        ' default builder
        Public Sub New()
            ' we build a few items
            For i As Integer = 1 To nbArticles
                articles.Add(New Article(i, "article" + i.ToString, i * 10, i * 10, i * 10))
            Next
        End Sub

        ' list of all items
        Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
            ' returns the list of items
            SyncLock Me
                Return articles
            End SyncLock
        End Function

        ' delete all items
        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
            ' empty the list of items
            SyncLock Me
                articles.Clear()
            End SyncLock
        End Sub

        ' obtain an item identified by its key
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
            ' search for the item in the list
            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

        ' add an item to the list of items
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
            ' add the item to the list of items
            SyncLock Me
                ' we check that it doesn't already exist
                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
                ' we add the article
                articles.Add(unArticle)
                ' we return the result
                Return 1
            End SyncLock
        End Function

        ' modify an article
        Public Function modifieArticle(ByVal articleNouveau As Article) As Integer Implements IArticlesDao.modifieArticle
            ' modify an article
            SyncLock Me
                ' we check that
                Dim ipos As Integer = posArticle(articles, articleNouveau.id)
                ' if it doesn't exist
                If ipos = -1 Then Return 0
                ' it exists - we modify it
                articles(ipos) = articleNouveau
                ' we return the result
                Return 1
            End SyncLock
        End Function

        ' delete an item identified by its key
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
            ' article deletion
            SyncLock Me
                ' we check that
                Dim ipos As Integer = posArticle(articles, idArticle)
                ' if it doesn't exist
                If ipos = -1 Then Return 0
                ' it exists - we remove it
                articles.RemoveAt(ipos)
                ' we return the result
                Return 1
            End SyncLock
        End Function

        ' change the stock of an item identified by its key
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
            ' change the stock of an item
            SyncLock Me
                ' we check that
                Dim ipos As Integer = posArticle(articles, idArticle)
                ' if it doesn't exist
                If ipos = -1 Then Return 0
                ' it exists - you modify your stock if you can
                Dim unArticle As Article = CType(articles(ipos), Article)
                ' only change stock if it is sufficient
                If unArticle.stockactuel + mouvement >= 0 Then
                    unArticle.stockactuel += mouvement
                    Return 1
                Else
                    Return 0
                End If
            End SyncLock
        End Function

        ' search for an item identified by its key
        Private Function posArticle(ByVal listArticles As ArrayList, ByVal idArticle As Integer) As Integer
            ' returns the position of item [idArticle] in the list or -1 if not found
            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
            ' not found
            Return -1
        End Function
    End Class

End Namespace

Commenti:

  • L'origine dati è simulata dal campo privato [articles] di tipo [ArrayList]
  • Il costruttore della classe crea 4 elementi nell'origine dati per impostazione predefinita.
  • Tutti i metodi di accesso ai dati sono stati sincronizzati per evitare problemi di accesso simultaneo all'origine dati. In un dato momento, solo un thread ha accesso a un determinato metodo.
  • Il metodo [posArticle] restituisce la posizione [0..N] nell'origine dati [ArrayList] di un articolo identificato dal suo numero. Se l'articolo non esiste, il metodo restituisce la posizione -1. Questo metodo viene utilizzato ripetutamente dagli altri metodi.
  • Il metodo [addArticle] aggiunge un articolo all'elenco degli articoli. Restituisce il numero di articoli inseriti: 1. Se l'articolo esisteva già, viene generata un'eccezione.
  • Il metodo [modifyItem] consente di modificare un elemento esistente. Restituisce il numero di elementi modificati: 1 se l'elemento esisteva, 0 in caso contrario.
  • Il metodo [deleteArticle] elimina un articolo esistente. Restituisce il numero di articoli eliminati: 1 se l'articolo esisteva, 0 in caso contrario.
  • Il metodo [getAllArticles] restituisce un elenco di tutti gli articoli
  • Il metodo [getArticleById] recupera un articolo identificato dal suo ID. Restituisce il valore [nothing] se l'articolo non esiste.
  • Il codice non presenta alcuna difficoltà reale. Lasciamo al lettore il compito di esaminarlo e comprenderlo.

1.4.3.4. Generazione dell'assembly del livello [dao]

Il progetto Visual Studio è configurato per generare l'assembly [webarticles-dao.dll]. Questo viene generato nella cartella [bin] del progetto:

1.4.3.5. Test NUnit per il livello [dao]

In Java, le classi vengono testate utilizzando il framework [JUnit]. In .NET, il framework NUnit offre le stesse funzionalità di test unitari:

Image

La struttura del progetto di test di Visual Studio è la seguente:

Image

Commenti:

  • Il progetto [tests] è una [libreria di classi]
  • I test [NUnit] richiedono un riferimento all'assembly [nunit.framework.dll]
  • la classe di test [NUnit] recupera un'istanza dell'oggetto in fase di test tramite Spring. Pertanto,
    • nella cartella [bin], i file di classe Spring
    • in [Riferimenti], un riferimento all'assembly [Spring-Core.dll] dalla cartella [bin]
    • in [bin], un file di configurazione per Spring
  • La classe di test richiede l'assembly [webarticles-dao.dll] dal livello [dao]. Questo è stato collocato nella cartella [bin] e il suo riferimento è stato aggiunto ai riferimenti del progetto.

Una classe di test [NUnit] richiede l'accesso alle classi nello spazio dei nomi [NUnit.Framework]. Pertanto, è inclusa la seguente istruzione di importazione:

Imports NUnit.Framework

Lo spazio dei nomi [NUnit.Framework] si trova nell'assembly [nunit.framework.dll], che deve essere aggiunto ai riferimenti del progetto:

L'assembly [nunit.framework.dll] dovrebbe essere presente nell'elenco fornito se [NUnit] è stato installato. È sufficiente fare doppio clic sull'assembly per aggiungerlo al progetto:

Image

La classe di test [NUnit] per il livello [DAO] potrebbe apparire così:

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
        ' the test object
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
            ' retrieve an instance of the Spring object manufacturer
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
            ' request instantiation of articlesdao object
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
        End Sub

        <Test()> _
        Public Sub testGetAllArticles()
            ' visual check
            listArticles()
        End Sub

        <Test()> _
        Public Sub testClearAllArticles()
            ' delete all articles
            articlesDao.clearAllArticles()
            ' all articles are requested
            Dim articles As IList = articlesDao.getAllArticles
            ' verification: there must be 0
            Assert.AreEqual(0, articles.Count)
        End Sub

        <Test()> _
        Public Sub testAjouteArticle()
            ' delete all items
            articlesDao.clearAllArticles()
            ' check: the item table must be empty
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' we add two items
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' check: there must be two items
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' visual check
            listArticles()
        End Sub

        <Test()> _
        Public Sub testSupprimeArticle()
            ' delete all items
            articlesDao.clearAllArticles()
            ' check: the item table must be empty
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' we add two items
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' check: there must be 2 items
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' we delete article 4
            articlesDao.supprimeArticle(4)
            ' check: there must be 1 item left
            articles = articlesDao.getAllArticles
            Assert.AreEqual(1, articles.Count)
            ' visual check
            listArticles()
        End Sub

        <Test()> _
        Public Sub testModifieArticle()
            ' delete all items
            articlesDao.clearAllArticles()
            ' check
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' 2 items added
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' check
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' article 3 search
            Dim unArticle As Article = articlesDao.getArticleById(3)
            ' check
            Assert.AreEqual(unArticle.nom, "article3")
            ' research article 4
            unArticle = articlesDao.getArticleById(4)
            ' check
            Assert.AreEqual(unArticle.nom, "article4")
            ' modification article 4
            articlesDao.modifieArticle(New Article(4, "article4", 44, 44, 44))
            ' check
            unArticle = articlesDao.getArticleById(4)
            Assert.AreEqual(unArticle.prix, 44, 0.000001)
            ' visual check
            listArticles()
        End Sub

        <Test()> _
        Public Sub testGetArticleById()
            ' article deletion
            articlesDao.clearAllArticles()
            ' check
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' 2 items added
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' check
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' research article 3
            Dim unArticle As Article = articlesDao.getArticleById(3)
            ' check
            Assert.AreEqual(unArticle.nom, "article3")
            ' research article 4
            unArticle = articlesDao.getArticleById(4)
            ' check
            Assert.AreEqual(unArticle.nom, "article4")
        End Sub

        ' screen listing
        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()
            ' delete all items
            articlesDao.clearAllArticles()
            ' research article 1
            Dim article As article = articlesDao.getArticleById(1)
            ' check
            Assert.IsNull(article)
            ' modification of a non-existent item
            Dim i As Integer = articlesDao.modifieArticle(New article(1, "1", 1, 1, 1))
            ' had to modify no line
            Assert.AreEqual(i, 0)
            ' deletion of non-existent item
            i = articlesDao.supprimeArticle(1)
            ' had to delete no line
            Assert.AreEqual(0, i)
        End Sub

        <Test()> _
        Public Sub testChangerStockArticle()
            ' delete all items
            articlesDao.clearAllArticles()
            ' add an item
            Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
            Assert.AreEqual(nbArticles, 1)
            ' add an item
            nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            Assert.AreEqual(nbArticles, 1)
            ' creation of 100 threads
            Dim taches(99) As Thread
            For i As Integer = 0 To taches.Length - 1
                ' create thread i
                taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
                ' set the thread name
                taches(i).Name = "tache_" & i
                ' start execution of thread i
                taches(i).Start()
            Next
            ' wait for all threads to finish
            For i As Integer = 0 To taches.Length - 1
                taches(i).Join()
            Next
            ' checks - item 3 must have a stock of 1
            Dim unArticle As Article = articlesDao.getArticleById(3)
            Assert.AreEqual(unArticle.nom, "article3")
            Assert.AreEqual(1, unArticle.stockactuel)
            ' item 4 stock is decremented
            Dim erreur As Boolean = False
            Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
            ' check: its stock must not have changed
            Assert.AreEqual(0, nbLignes)
            ' visual check
            listArticles()
        End Sub

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

    End Class
End Namespace

Commenti:

  • Volevamo scrivere un programma di test per l'interfaccia [IArticlesDao] che fosse indipendente dalla sua classe di implementazione. Pertanto, abbiamo utilizzato Spring per nascondere il nome della classe di implementazione al programma di test.
  • Il metodo <Setup()> recupera da Spring un riferimento all'oggetto [articlesdao] da testare. Questo è definito nel seguente file [spring-config.xml]:
<?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>

Questo file specifica il nome della classe di implementazione [istia.st.articles.dao.ArticlesDaoArrayList] per l'interfaccia [IArticlesDao] e dove trovarla [webarticles-dao.dll]. Poiché l'istanziazione non richiede alcun parametro, qui non ne viene definito nessuno.

  • La maggior parte dei test è di facile comprensione. Si invita il lettore a leggere i commenti.
  • Il metodo [testChangerStockArticle] richiede alcune spiegazioni. Crea 100 thread incaricati di decrementare lo stock di un determinato articolo.
        <Test()> _
        Public Sub testChangerStockArticle()
            ' delete all items
            articlesDao.clearAllArticles()
            ' add an item
            Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
            Assert.AreEqual(nbArticles, 1)
            ' add an item
            nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            Assert.AreEqual(nbArticles, 1)
            ' creation of 100 threads
            Dim taches(99) As Thread
            For i As Integer = 0 To taches.Length - 1
                ' create thread i
                taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
                ' set the thread name
                taches(i).Name = "tache_" & i
                ' start execution of thread i
                taches(i).Start()
            Next
            ' wait for all threads to finish
            For i As Integer = 0 To taches.Length - 1
                taches(i).Join()
            Next
            ' checks - item 3 must have a stock of 1
            Dim unArticle As Article = articlesDao.getArticleById(3)
            Assert.AreEqual(unArticle.nom, "article3")
            Assert.AreEqual(1, unArticle.stockactuel)
            ' item 4 stock is decremented
            Dim erreur As Boolean = False
            Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
            ' check: its stock must not have changed
            Assert.AreEqual(0, nbLignes)
            ' visual check
            listArticles()
        End Sub

Questo serve a testare l'accesso simultaneo all'origine dati. Il metodo responsabile dell'aggiornamento delle scorte è il seguente:

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

Diminuisce di una unità lo stock dell'articolo n. 3. Se ci riferiamo al codice del metodo [testChangerStockArticle], vediamo che:

  • lo stock dell'articolo n. 3 è inizializzato a 101
  • i 100 thread decrementeranno ciascuno questa scorta di una unità
  • dovremmo quindi avere una quantità pari a 1 al termine dell'esecuzione di tutti i thread

Inoltre, sempre all'interno di questo metodo, proviamo a impostare lo stock dell'articolo n. 4 su un valore negativo. Questo dovrebbe fallire.

Per testare il livello [dao], generiamo la DLL [tests-webarticles-dao.dll] nella cartella [bin] del progetto [tests]:

Quindi, utilizzando l'applicazione [Nunit-Gui], carichiamo questa DLL ed eseguiamo i test:

Image

Nella finestra a sinistra è riportato l'elenco dei metodi sottoposti a test. Il colore del puntino che precede il nome di ciascun metodo indica se il test è stato superato (verde) o fallito (rosso). I lettori che visualizzano questo documento sullo schermo noteranno che tutti i test sono stati superati. Considereremo quindi il livello [dao] operativo.

1.4.4. Il livello [domain]

Il livello [domain] contiene i seguenti elementi:

  • [IArticlesDomain]: l'interfaccia per accedere al livello [domain]

  • [Purchase]: classe che definisce un acquisto

  • [ShoppingCart]: classe che definisce un carrello

  • [ProductPurchases]: la classe che implementa l'interfaccia [IArticlesDomain]

La struttura della soluzione [Visual Studio] per il livello [domain] è la seguente:

Image

Commenti:

  • Il progetto [domain] è di tipo [libreria di classi]
  • Le classi sono state inserite in una struttura ad albero a partire dalla cartella [istia]. Si trovano tutte nello spazio dei nomi [istia.st.articles.domain].
  • La DLL del livello [dao] è stata collocata nella cartella [bin] del nuovo progetto. Inoltre, questa DLL è stata aggiunta come riferimento al progetto.

1.4.4.1. L'interfaccia [IArticlesDomain]

L'interfaccia [IArticlesDomain] disaccoppia il livello [business] dal livello [web]. Quest'ultimo accede al livello [business/domain] tramite questa interfaccia senza preoccuparsi della classe che la implementa effettivamente. L'interfaccia definisce le seguenti azioni per accedere al livello business:

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

Namespace istia.st.articles.domain
    Public Interface IArticlesDomain
        ' methods
        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
Funzione getAllArticles() As IList
restituisce l'elenco degli oggetti [Article] dall'origine dati associata
Funzione getArticleById(ByVal idArticle As Integer) As Article
restituisce l'oggetto [Article] identificato da [idArticle]
buy(ByVal cart As Cart)
elabora il carrello del cliente sottraendo la quantità acquistata dalle scorte degli articoli acquistati - potrebbe fallire se le scorte sono insufficienti
Proprietà di sola lettura errors() As ArrayList
restituisce l'elenco degli errori verificatisi - vuoto se non ci sono errori

1.4.4.2. La classe [Purchase]

La classe [Purchase] rappresenta un acquisto effettuato da un cliente:

Imports istia.st.articles.dao

Namespace istia.st.articles.domain

    Public Class Achat

        ' private fields
        Private _article As article
        Private _qte As Integer

        ' default builder
        Public Sub New()
        End Sub

        ' builder with parameters
        Public Sub New(ByVal unArticle As article, ByVal qte As Integer)
            ' we go through the properties
            Me.article = unArticle
            Me.qte = qte
        End Sub

        ' item purchased
        Public Property article() As article
            Get
                Return _article
            End Get
            Set(ByVal Value As article)
                _article = Value
            End Set
        End Property

        ' qty purchased
        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 purchase
        Public ReadOnly Property totalAchat() As Double
            Get
                Return _qte * _article.prix
            End Get
        End Property

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

End Namespace

Commenti:

  • La classe [Purchase] presenta le seguenti proprietà e metodi:
Proprietà pubblica item() As item
l'articolo acquistato
Proprietà pubblica qty() As Integer
la quantità acquistata
Proprietà pubblica di sola lettura purchaseTotal() As Double
l'importo dell'acquisto
Pubblico override Funzione ToString() As String
rappresentazione stringa dell'oggetto
  • Dispone di un costruttore che inizializza le proprietà [item, qty] che definiscono un acquisto.

1.4.4.3. La classe [Cart]

La classe [Cart] rappresenta il totale degli acquisti del cliente:

Namespace istia.st.articles.domain

Public Class Panier

    ' private fields
    Private _achats As New ArrayList
        Private _totalPanier As Double = 0

    ' default builder
    Public Sub New()
    End Sub

    ' list of purchases
    Public ReadOnly Property achats() As ArrayList
        Get
            Return _achats
        End Get
    End Property

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

    ' methods
    Public Sub ajouter(ByVal unAchat As Achat)
        ' find out if the purchase already exists
        Dim iAchat As Integer = posAchat(unAchat.article.id)
        If iAchat <> -1 Then
            ' we found
                Dim achatCourant As Achat = CType(_achats(iAchat), Achat)
            achatCourant.qte += unAchat.qte
        Else
            ' we didn't find
            _achats.Add(unAchat)
        End If
        ' increment the basket total
        _totalPanier += unAchat.totalAchat
    End Sub

    ' remove a purchase
    Public Sub enlever(ByVal idAchat As Integer)
        ' we're looking to buy
            Dim iachat As Integer = posAchat(idAchat)
        ' if found, remove
        If iachat <> -1 Then
                Dim achatCourant As Achat = CType(_achats(iachat), Achat)
            ' remove from basket
            _achats.RemoveAt(iachat)
            ' decrement the basket total
            _totalPanier -= achatCourant.totalAchat
        End If
    End Sub

    Private Function posAchat(ByVal idArticle As Integer) As Integer
        ' search for a purchase in the purchase list
        ' returns its position in the list or -1 if not found
        Dim achatCourant As Achat
        Dim trouvé As Boolean = False
        Dim i As Integer = 0
            While Not trouvé AndAlso i < _achats.Count
                ' regular purchase
                achatCourant = CType(_achats(i), Achat)
                ' comparison with article searched
                If achatCourant.article.id = idArticle Then
                    Return i
                End If
                'next purchase
                i += 1
            End While
            ' not found
            Return -1
    End Function

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

End Namespace

Commenti:

  • La classe [ShoppingCart] presenta le seguenti proprietà e metodi:
Proprietà di sola lettura purchases() come ArrayList
la lista della spesa del cliente - un elenco di oggetti di tipo [Purchase]
add(ByVal aPurchase As Purchase)
aggiunge un acquisto all'elenco degli acquisti
remove(ByVal purchaseId As Integer)
rimuove l'acquisto con idAcquisto
Proprietà di sola lettura totalBasket() As Double
l'importo totale degli acquisti nel carrello
Funzione ToString() As String
restituisce la stringa che rappresenta il carrello
  • Il metodo [posAchat] è un metodo di utilità che restituisce la posizione nell'elenco degli acquisti di un acquisto identificato dal numero dell'articolo. L'elenco degli acquisti è gestito in modo tale che un articolo acquistato più volte occupi una sola posizione nell'elenco. Pertanto, un acquisto può essere identificato dal numero dell'articolo. Il metodo [posAchat] restituisce -1 se l'acquisto ricercato non esiste.
  • Il metodo [add] aggiunge un nuovo acquisto all'elenco degli acquisti. Questo aggiunge una nuova voce all'elenco degli acquisti se l'articolo acquistato non era già presente nell'elenco, oppure incrementa la quantità acquistata se era già presente.
  • Il metodo [remove] consente di rimuovere un acquisto identificato da un numero dall'elenco degli acquisti. Se l'acquisto non esiste, il metodo non esegue alcuna operazione.
  • L'importo totale dell'acquisto [totalPanier] viene aggiornato man mano che gli articoli vengono aggiunti e rimossi.

1.4.4.4. La classe [PurchaseItems]

L'interfaccia [IArticlesDomain] sarà implementata dalla seguente classe [PurchaseItems]:

Imports istia.st.articles.dao

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

        'private fields
        Private _articlesDao As IArticlesDao
        Private _erreurs As ArrayList

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

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

        ' list of items
        Public Function getAllArticles() As IList Implements IArticlesDomain.getAllArticles
            ' list of all items
            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

        ' get an item identified by its number
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDomain.getArticleById
            ' a special item
            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


        ' buy a basket
        Public Sub acheter(ByVal panier As Panier) Implements IArticlesDomain.acheter
            ' basket purchase - stocks of purchased items must be decremented
            _erreurs = New ArrayList
            Dim achat As achat
            Dim achats As ArrayList = panier.achats
            For i As Integer = achats.Count - 1 To 0 Step -1
                ' decrement stock item i
                achat = CType(achats(i), achat)
                Try
                    If _articlesDao.changerStockArticle(achat.article.id, -achat.qte) = 0 Then
                        ' we couldn't do the operation
                        _erreurs.Add("L'achat " + achat.ToString + " n'a pu se faire - Vérifiez les stocks")
                    Else
                        ' the transaction has been completed - the purchase is removed from the basket
                        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

Commenti:

  • Questa classe implementa i quattro metodi dell'interfaccia [IArticlesDomain]. Ha due campi privati:
_articlesDao As IArticlesDao
l'oggetto di accesso ai dati
_errors As ArrayList
l'elenco dei possibili errori. È accessibile tramite la proprietà pubblica [errors]
  • Per creare un'istanza della classe, è necessario fornire l'oggetto che consente l'accesso ai dati:
Sub New(ByVal articlesDao As IArticlesDao)
  • I metodi [getAllArticles] e [getArticleById] si basano sui metodi omonimi presenti nel livello [dao]
  • Il metodo [buy] convalida l'acquisto di un carrello. Questa convalida comporta semplicemente la diminuzione delle scorte degli articoli acquistati. Un articolo può essere acquistato solo se le scorte sono sufficienti. In caso contrario, l'acquisto viene rifiutato: l'articolo rimane nel carrello e viene segnalato un errore nell'elenco [errors]. Un acquisto convalidato viene rimosso dal carrello e le scorte dell'articolo corrispondente vengono diminuite della quantità acquistata.

1.4.4.5. Generazione dell'assembly del livello [domain]

Il progetto Visual Studio è configurato per generare l'assembly [webarticles-domain.dll]. Questo viene generato nella cartella [bin] del progetto:

1.4.4.6. Test NUnit per il livello [domain]

La struttura del progetto di test di Visual Studio è la seguente:

Image

Commenti:

  • Il progetto [tests] è di tipo [libreria di classi]
  • i test [NUnit] richiedono un riferimento all'assembly [nunit.framework.dll]
  • La classe di test [NUnit] recupera un'istanza dell'oggetto in fase di test tramite Spring. Pertanto,
  • nella cartella [bin], i file di classe Spring
  • in [Riferimenti], un riferimento all'assembly [Spring-Core.dll] dalla cartella [bin]
  • in [bin], un file di configurazione per Spring
  • La classe di test richiede l'assembly [webarticles-dao.dll] dal livello [dao] e l'assembly [webarticles-domain.dll] dal livello [domain]. Questi sono stati collocati nella cartella [bin] e i relativi riferimenti sono stati aggiunti ai riferimenti del progetto.

Una classe di test NUnit per il livello [domain] potrebbe apparire così:

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

        ' the test object
        Private articlesDomain As IArticlesDomain
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
            ' retrieve an instance of the Spring object manufacturer
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
            ' request instantiation of the articles dao object
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
            ' then the articlesdomain aobject
            articlesDomain = CType(factory.GetObject("articlesdomain"), IArticlesDomain)
        End Sub

        <Test()> _
        Public Sub getAllArticles()
            ' visual check
            listArticles()
        End Sub

        <Test()> _
        Public Sub getArticleById()
            ' article deletion
            articlesDao.clearAllArticles()
            ' check
            Dim articles As IList = articlesDomain.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' 2 items added
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' check
            articles = articlesDomain.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' research article 3
            Dim unArticle As Article = articlesDomain.getArticleById(3)
            ' check
            Assert.AreEqual(unArticle.nom, "article3")
            ' research article 4
            unArticle = articlesDao.getArticleById(4)
            ' check
            Assert.AreEqual(unArticle.nom, "article4")
        End Sub

        <Test()> _
        Public Sub acheterPanier()
            ' article deletion
            articlesDao.clearAllArticles()
            ' check
            Dim articles As IList = articlesDomain.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' 2 items added
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' check
            articles = articlesDomain.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' create a basket with two purchases
            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))
            ' checks
            Assert.AreEqual(700, panier.totalPanier, 0.000001)
            Assert.AreEqual(2, panier.achats.Count)
            ' shopping cart validation
            articlesDomain.acheter(panier)
            ' checks
            Assert.AreEqual(0, articlesDomain.erreurs.Count)
            Assert.AreEqual(0, panier.achats.Count)
            ' research article 3
            Dim unArticle As Article = articlesDomain.getArticleById(3)
            ' check
            Assert.AreEqual(unArticle.stockactuel, 20)
            ' research article 4
            unArticle = articlesDao.getArticleById(4)
            ' check
            Assert.AreEqual(unArticle.stockactuel, 30)
            ' new basket
            panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 100))
            ' shopping cart validation
            articlesDomain.acheter(panier)
            ' checks
            Assert.AreEqual(1, articlesDomain.erreurs.Count)
            ' research article 3
            unArticle = articlesDomain.getArticleById(3)
            ' check
            Assert.AreEqual(unArticle.stockactuel, 20)
        End Sub

        <Test()> _
        Public Sub testRetirerAchats()
            ' delete contents of ARTICLES
            articlesDao.clearAllArticles()
            ' reads the ARTICLES table
            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)
            ' reads the ARTICLES table
            articles = articlesDomain.getAllArticles()
            Assert.AreEqual(2, articles.Count)
            ' create a basket with two purchases
            Dim monPanier As New Panier
            monPanier.ajouter(New Achat(article3, 10))
            monPanier.ajouter(New Achat(article4, 10))
            ' checks
            Assert.AreEqual(700.0, monPanier.totalPanier, 0.000001)
            Assert.AreEqual(2, monPanier.achats.Count)
            ' add a previously purchased item
            monPanier.ajouter(New Achat(article3, 10))
            ' checks
            ' the total must be increased to 1000
            Assert.AreEqual(1000.0, monPanier.totalPanier, 0.000001)
            ' always 2 items in the basket
            Assert.AreEqual(2, monPanier.achats.Count)
            ' qty item 3 increased to 20
            Dim unAchat As Achat = CType(monPanier.achats(0), Achat)
            Assert.AreEqual(20, unAchat.qte)
            ' article 3 is removed from the basket
            monPanier.enlever(3)
            ' checks
            ' the total must be increased to 400
            Assert.AreEqual(400.0, monPanier.totalPanier, 0.000001)
            ' 1 item only in basket
            Assert.AreEqual(1, monPanier.achats.Count)
            ' this must be article no. 4
            Assert.AreEqual(4, CType(monPanier.achats(0), Achat).article.id)
        End Sub

        ' screen listing
        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

Commenti:

  • Volevamo scrivere un programma di test per l'interfaccia [IArticlesDomain] che fosse indipendente dalla sua classe di implementazione. Pertanto, abbiamo utilizzato Spring per nascondere il nome della classe di implementazione al programma di test.
  • Il metodo con attributo <Setup()> recupera da Spring un riferimento agli oggetti [articlesdomain] e [articlesdao] da testare. Questi sono definiti nel seguente file [spring-config.xml]:
<?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>

Questo file indica:

  • (continua)
    • per il singleton [articlesdao], il nome della classe di implementazione [istia.st.articles.dao.ArticlesDaoArrayList] e dove trovarla [webarticles-dao.dll]. Poiché l'istanziazione non richiede alcun parametro, qui non ne viene definito nessuno.
    • per il singleton [articlesdomain], il nome della classe di implementazione [istia.st.articles.domain.AchatsArticles] e dove trovarla [webarticles-domain.dll]. La classe [AchatsArticles] ha un costruttore con un parametro: il singleton che gestisce l'accesso al livello [dao]. Qui, questo è definito come il singleton [articlesdao] definito in precedenza.
  • La classe di test ottiene un'istanza della classe sotto test [articlesdomain] e un'istanza della classe di accesso ai dati [articlesdao]. Quest'ultimo punto è controverso. La classe di test, in teoria, non dovrebbe aver bisogno di accedere al livello [dao], di cui non dovrebbe nemmeno essere a conoscenza. Qui abbiamo ignorato questa "etica", che, se seguita, ci avrebbe richiesto di creare nuovi metodi nella nostra interfaccia [IArticlesDomain].

Per testare il livello [domain], generiamo la DLL [tests-webarticles-domain.dll] nella cartella [bin] del progetto [tests]:

Quindi, utilizzando l'applicazione [Nunit-Gui], carichiamo questa DLL ed eseguiamo i test:

Image

I lettori che visualizzano questo documento sullo schermo vedranno che tutti i test hanno avuto esito positivo. Considereremo ora il livello [domain] operativo.

1.4.5. Conclusione

Ricordiamo che vogliamo realizzare la seguente applicazione web a tre livelli:

Il modello M della nostra applicazione MVC è ora scritto e testato. Ci viene fornito in due DLL [webarticles-dao.dll, webarticles-domain.dll]. Possiamo passare all'ultimo livello, il livello [web], che contiene il controller C e le viste V. Prenderemo innanzitutto in considerazione un metodo presentato nel documento [Sviluppo Web con ASP.NET 1.1]

  • Il controller C è implementato da due file [global.asax, main.aspx]
  • Le viste V sono gestite da pagine aspx

1.5. Il livello [web]

L'architettura MVC dell'applicazione web sarà la seguente:

M = Modello
Le classi di business [dominio], le classi di accesso ai dati [DAO] e la fonte dati
V = Viste
le pagine ASPX
C = Controller
Tutte le richieste HTTP dei client passano attraverso i seguenti due controller:
global.asax: gestisce gli eventi relativi all'avvio iniziale dell'applicazione
main.aspx: elabora singolarmente ogni richiesta del client

1.5.1. Le viste

Le viste corrispondono a quelle presentate all'inizio di questo documento:

LISTA
list.aspx
Le viste si trovano nella cartella [views] dell'applicazione
INFO
info.aspx
CARRELLO
carrello.aspx
SVUOTA CARRELLO
empty-cart.aspx
ERRORI
errori.aspx

1.5.2. I controller

Come accennato, il controller sarà composto da due elementi:

  1. [global.asax, global.asax.vb]: utilizzati principalmente per inizializzare l'applicazione e impostare il contesto per tutti i dati da condividere tra i diversi client
  2. [main.aspx, main.aspx.vb]: il controller vero e proprio, che gestisce le richieste HTTP provenienti dai client.

Le varie richieste dei client saranno inviate al controller [main.aspx] e conterranno un parametro denominato [action] che specifica l'azione richiesta dal client:

richiesta
significato
azione del controller
risposte possibili
azione=elenco
il client desidera l'elenco degli
elementi
- richiede l'elenco degli elementi dal livello
professione
- [LIST]
- [ERRORI]
azione=info
Il cliente richiede
informazioni su uno degli
elementi visualizzati nella vista
[LIST]
- richiede l'elemento dal livello di business
- [INFO]
- [ERRORI]
azione=acquisto
Il cliente acquista un articolo
- richiede l'articolo dal livello aziendale
e lo aggiunge al carrello del cliente
- [INFO] in caso di errore nella quantità
- [LIST] se non ci sono errori
azione=rimuovi acquisto
il cliente desidera rimuovere un
articolo dal carrello
- recupera il carrello dalla sessione
e lo modifica
- [CARRELLO]
- [SVUOTA CARRELLO]
- [ERRORI]
azione=carrello
Il cliente desidera visualizzare il proprio
carrello
- recupera il carrello dalla sessione
- [CARRELLO]
- [CARRELLO VUOTO]
- [ERRORI]
azione=convalida-carrello
Il cliente ha terminato gli acquisti
e procede al checkout
- Aggiorna il database con i livelli di stock
degli articoli acquistati
- svuota il carrello del cliente dagli articoli
il cui acquisto è stato confermato
- [LISTA]
- [ERRORI]

1.5.3. Configurazione dell'applicazione

Cercheremo di configurare l'applicazione in modo da renderla il più flessibile possibile rispetto a modifiche quali:

  1. modifiche agli URL delle varie viste
  2. modifiche alle classi che implementano le interfacce [IArticlesDao] e [IArticlesDomain]
  3. modifiche al DBMS, al database o alla tabella degli articoli

1.5.3.1. Modifiche agli URL

I nomi degli URL delle viste saranno inseriti nel file di configurazione [web.config] dell'applicazione insieme ad alcuni altri parametri:

<?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. Modifica delle classi che implementano le interfacce

In linea con i principi delle architetture a tre livelli, i livelli devono essere isolati l'uno dall'altro. Tale isolamento si ottiene nel modo seguente:

  • i livelli comunicano tra loro tramite interfacce, non classi concrete
  • il codice di un livello non istanzia mai la classe di un altro livello per poterla utilizzare. Richiede semplicemente un'istanza dell'implementazione dell'interfaccia da uno strumento esterno — in questo caso, Spring — per il livello che desidera utilizzare. Per farlo, sappiamo che non ha bisogno di conoscere il nome della classe di implementazione, ma solo il nome del singleton Spring per il quale vuole un riferimento.

Nella nostra applicazione, Spring verrà configurato nel file [web.config] dell'applicazione web come segue:

<?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>

Per accedere al livello [business], una classe nel livello [web] può richiedere il singleton [articlesDomain]. Spring istanzierà quindi un oggetto di tipo [istia.st.articles.domain.AchatsArticles]. Per questa istanziazione, ha bisogno di un oggetto di tipo [articlesDao], ovvero un oggetto di tipo [istia.st.articles.dao.ArticlesDaoArrayList]. Spring istanzierà quindi tale oggetto. Al termine dell'operazione, il livello [web] che ha richiesto il singleton [articlesDomain] dispone dell'intera catena che lo collega alla fonte dati:

1.5.3.3. Modifiche relative al DBMS o al database

Questo punto verrà ignorato in questa sede poiché stiamo lavorando in un'applicazione di test senza un DBMS. Affronteremo l'implementazione di un livello [dao] basato su un DBMS in una sezione successiva.

1.5.4. La libreria di tag <asp:>

Consideriamo la vista [ERRORS], che visualizza un elenco di errori:

Image

La vista [ERRORS] è responsabile della visualizzazione di un elenco di errori che il controller [main.aspx] ha inserito nel contesto della richiesta con il nome [context.Items("errors")]. Esistono diversi modi per scrivere una pagina di questo tipo. In questa sede, ci interessa solo la parte relativa alla visualizzazione degli errori.

Ricordiamo che una pagina ASPX presenta un livello di presentazione HTML e un livello di codice .NET che prepara i dati che il livello di presentazione deve visualizzare. Queste due parti possono trovarsi nello stesso file [aspx] (soluzione WebMatrix) o in due file: [aspx] per la presentazione, [aspx.vb] per il codice. Quest'ultima soluzione è quella utilizzata da Visual Studio. A complicare le cose, il livello di presentazione HTML può contenere anche codice .NET, il che tende a rendere meno netta la separazione tra i livelli [controller] e [presentation] della vista. Questo approccio è generalmente fortemente sconsigliato. Rimuovere tutto il codice dalla sezione [presentation] richiedeva la creazione di librerie di tag. Queste "nascondono" il codice sotto le spoglie di tag simili a quelli HTML. Presentiamo due possibili soluzioni per la pagina [ERRORS].

La nostra prima soluzione utilizza codice .NET nella sezione [presentation] della pagina. La pagina ASPX recupera l'elenco degli errori presenti nella richiesta nella sua sezione controller [errors.aspx.vb]:

        Protected erreurs As ArrayList

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
            ' error recovery
            erreurs = CType(context.Items("erreurs"), ArrayList)
        End Sub

quindi visualizzarli nella sezione [presentazione, errors.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 seconda soluzione utilizza il tag <asp:repeater> della libreria di tag <asp:> di ASP.NET. Se si crea una pagina ASPX graficamente, questo tag è disponibile come controllo server che è possibile trascinare nel modulo di progettazione. Se si scrive il codice ASPX manualmente, è possibile fare riferimento alla libreria di tag.

Con la libreria di tag <asp:>, il codice ASPX per la precedente vista [ERRORS] diventa il seguente:

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

Il

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

Il tag viene utilizzato per ripetere un modello HTML nei vari elementi di un'origine dati. I suoi vari elementi sono i seguenti:

HeaderTemplate
il modello HTML da visualizzare prima degli elementi dell'origine dati
ItemTemplate
il modello HTML da ripetere per ogni elemento nell'origine dati. L'espressione [<%# Container.DataItem %>] viene utilizzata per visualizzare il valore dell'elemento corrente nell'origine dati
FooterTemplate
il modello HTML da visualizzare dopo che sono stati visualizzati gli elementi dell'origine dati

L'origine dati è associata al tag, solitamente nella sezione [controller] della pagina:

        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
..
            ' link errors to rptErreurs
            With rptErreurs
                .DataSource = context.Items("erreurs")
                .DataBind()
            End With
        End Sub

Questo binding può essere effettuato anche durante la progettazione della pagina se l'origine dati è già nota, come ad esempio un database esistente.

Nelle nostre viste useremo un altro tag: <asp:datagrid>, che ci permette di visualizzare un'origine dati sotto forma di tabella.

1.5.5. Struttura della soluzione Visual Studio per l'applicazione [webarticles]

Un'applicazione web è un puzzle composto da molti pezzi. Dotarla di un'architettura MVC generalmente aumenta il numero di questi pezzi. La struttura dell'applicazione [webarticles] in [Visual Studio] è la seguente:

  

Commenti:

  • Il progetto [web] è di tipo [Class Library] e non di tipo [ASP.NET Web Application], come ci si potrebbe logicamente aspettare. Il tipo [ASP.NET Web Application] richiede la presenza del server web IIS sul computer di sviluppo o su un computer remoto. Il server IIS non è incluso di default sui computer con Windows XP Home Edition. Tuttavia, molti PC vengono venduti con questa versione. Per consentire ai lettori che utilizzano Windows XP di implementare l'applicazione in esame, useremo il server web Cassini (vedi appendice), disponibile gratuitamente da Microsoft, e sostituiremo il progetto [Applicazione Web ASP.NET] con un progetto [Libreria di classi]. Ciò comporta alcuni inconvenienti, che sono spiegati nelle appendici.
  • Le DLL utilizzate dall'applicazione sono le seguenti:
webarticles-dao.dll
contiene le classi del livello di accesso ai dati
webarticles-domain.dll
contiene le classi del livello di business
Spring.Core.dll
contiene le classi Spring che ci consentono di integrare i livelli web, di dominio e DAO
log4net.dll
classi di logging — utilizzate da Spring

Queste DLL vengono collocate nella cartella [bin] e aggiunte ai riferimenti del progetto.

1.5.6. Le viste ASPX

Come raccomandato in precedenza, useremo la libreria di tag <asp:> nelle nostre viste ASPX.

1.5.6.1. Il componente utente [entete.ascx]

Per garantire la coerenza tra le diverse viste, queste condivideranno la stessa intestazione, che mostra il nome dell'applicazione insieme al menu:

Il menu è dinamico e viene impostato dal controller. Il controller include un attributo chiave "actions" nella richiesta inviata alla pagina ASPX, con un valore associato costituito da un array di elementi Hashtable(). Ogni elemento di questo array è un dizionario destinato a generare un'opzione del menu dell'intestazione. Ogni dizionario ha due chiavi:

  • href: l'URL associato all'opzione del menu
  • link: il testo del menu

Trasformeremo l'intestazione in un controllo utente. Un controllo utente incapsula una sezione di una pagina (layout e codice associato) in un componente che può poi essere riutilizzato in altre pagine. In questo caso, vogliamo riutilizzare il componente [entete] in altre viste dell'applicazione. Il codice di presentazione sarà in [entete.ascx] e il codice di controllo associato in [entete.ascx.vb]. Il codice di presentazione utilizzerà un componente <asp:repeater> per visualizzare la tabella delle opzioni del menu:

No.
tipo
nome
ruolo
1
ripetitore
rptMenu
fonte dati: un array di dizionari
con due chiavi: href, link
visualizza le opzioni del menu

Il codice di presentazione della pagina sarà il seguente:

<%@ 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>

Commenti:

  • Il componente [repeater] è definito alle righe 6–14
  • Ogni elemento nell'origine dati associata al repeater è un dizionario con due chiavi: href (riga 9) e link (riga 10)

Il codice di controllo associato sarà il seguente:

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())
                ' associate the action table with its component
                With rptMenu
                    .DataSource = Value
                    .DataBind()
                End With
            End Set
        End Property
    End Class
End Namespace

Commenti:

  • Il componente [EnteteWebArticles] ha una proprietà [actions] pubblica e di sola scrittura - riga 7
  • Questa proprietà consente al componente <asp:repeater> denominato [rptMenu] — riga 10 — di essere associato all'array di opzioni calcolato dal controller dell'applicazione — righe 11–12.

Le altre viste dell'applicazione utilizzeranno l'intestazione definita da [entete.ascx]. La pagina [erreurs.aspx], ad esempio, includerà l'intestazione utilizzando il codice seguente:

<%@ 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>    

Commenti:

  • La riga 1 specifica che il tag <WA:entete> deve essere associato al componente definito dal file [entete.ascx]. Gli attributi [TagPrefix] e [TagName] sono facoltativi.
  • Una volta fatto ciò, il componente viene inserito nel codice di presentazione della pagina utilizzando la riga 9. In fase di esecuzione, questo tag includerà il codice della pagina [entete.ascx] nella pagina ASPX che lo contiene. Il codice di controllo [erreurs.aspx.vb] gestirà l'inizializzazione di questo componente. Può farlo come segue:
    Public Class ErreursWebarticles
        Inherits System.Web.UI.Page

        ' page components
...
        Protected WithEvents entete As New EnteteWebArticles

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

Commenti:

  • La riga 6 crea un oggetto di tipo [EnteteWebArticles], che è il tipo del componente che si sta creando
  • La riga 11 inizializza la proprietà [actions] di questo oggetto

1.5.6.2. La vista [liste.aspx]

1.5.6.2.1. Introduzione

Questa schermata mostra l'elenco degli articoli disponibili per la vendita:

Viene visualizzata in seguito a una richiesta a /main?action=list o /main?action=cartvalidation. Gli elementi della richiesta al controller sono i seguenti:

azioni
Oggetto Hashtable() - l'array delle opzioni di menu
listarticles
ArrayList di oggetti di tipo [Item]
messaggio
Oggetto String - messaggio da visualizzare in fondo alla pagina

Ogni link [Info] nella tabella HTML degli articoli ha un URL nel formato [?action=info&id=ID], dove ID è il campo id dell'articolo visualizzato.

1.5.6.2.2. Componenti della pagina
No.
Tipo
nome
ruolo
1
componente utente
intestazione
intestazione di visualizzazione
2
DataGrid
DataGridArticoli
3 - colonna correlata: intestazione: Nome, campo: nome
4 - colonna correlata: intestazione: Prezzo, campo: prezzo
5 - colonna ipertestuale: testo: Info, campo URL: id,
formato URL: /webarticles/main.aspx?action=info&id={0}
visualizza articoli in vendita
6
etichetta
lblMessage
visualizza un messaggio

Vediamo come impostare queste proprietà:

  • In Visual Studio, selezionare [DataGrid] per accedere alla finestra delle proprietà:

Image

  • Utilizza il collegamento [AutoFormat] in alto per gestire il layout della griglia visualizzata
  • e il link [Generatore di proprietà] per gestirne il contenuto
1.5.6.2.3. Codice di presentazione [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>

Commenti:

  • La riga 9 definisce l'intestazione della pagina
  • Le righe 12–24 definiscono le proprietà di [DataGrid]
  • La riga 26 definisce l'etichetta [lblMessage]
1.5.6.2.4. Codice del controller [liste.aspx.vb]
Imports System
Imports System.Collections
Imports System.Data
Imports istia.st.articles.dao

Namespace istia.st.articles.web

    ' manages the item list display page
    Public Class ListeWebarticles
        Inherits System.Web.UI.Page

        ' page components
        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
            ' prepare the [list] view using context information
            ' link menu options to rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
            ' retrieve items from a datatable
            Dim articles As ArrayList = CType(context.Items("articles"), ArrayList)
            ' we link them to the [DataGrid] component of the page
            With DataGridArticles
                .DataSource = articles
                .DataBind()
            End With
            ' the message
            lblMessage.Text = context.Items("message").ToString
        End Sub
    End Class
End Namespace

Commenti:

  • I componenti della pagina compaiono alle righe 13–15. Si noti che è stato necessario creare un oggetto [EnteteWebArticles] utilizzando l'operatore [new], mentre ciò non era necessario con gli altri componenti. Senza questa creazione esplicita, si è verificato un errore di runtime che indicava che l'oggetto [entete] non faceva riferimento a nulla. Questo punto merita un'ulteriore indagine. Non è stato ancora approfondito.
  • La tabella delle opzioni del menu di intestazione viene recuperata dal contesto per inizializzare il componente [entete] della pagina — riga 20
  • L'elenco degli articoli viene prelevato dal contesto — riga 22
  • per inizializzare il componente [DataGridArticles] — righe 24–27
  • Il componente [lblMessage] viene inizializzato con un messaggio inserito nel contesto — riga 29

1.5.6.3. La vista [infos.aspx]

1.5.6.3.1. Introduzione

Questa vista mostra le informazioni relative a un articolo e ne consente l'acquisto:

Image

Viene visualizzata in seguito a una richiesta /main?action=infos&id=ID o a una richiesta /main?action=achat&id=ID quando la quantità acquistata non è corretta. I parametri di richiesta del controller sono i seguenti:

azioni
Oggetto Hashtable() - l'array delle opzioni di menu
item
oggetto di tipo [Articolo] - elemento da visualizzare
msg
Oggetto stringa - messaggio da visualizzare in caso di errore con la quantità
qty
Oggetto stringa - valore da visualizzare nel campo di immissione [Qty]

I campi [msg] e [qte] vengono utilizzati in caso di errore di immissione relativo alla quantità:

Image

Questa pagina contiene un modulo che viene inviato tramite il pulsante [Acquista]. L'URL di destinazione per la richiesta POST è [?action=purchase&id=ID], dove ID è l'ID dell'articolo acquistato.

1.5.6.3.2. Componenti della pagina
No.
Tipo
nome
ruolo
1
componente utente
intestazione
intestazione di visualizzazione
2
letterale
litID
visualizza numero articolo
da 3 a 6
DataGrid
DataGridArticolo
3 - colonna correlata: intestazione: Nome, campo: nome
4 - colonna correlata: intestazione: Prezzo, campo: prezzo
5 - colonna correlata: intestazione: Stock attuale, campo: currentStock
6 - colonna correlata: intestazione: Stock minimo, campo: stockMinimo
visualizza un articolo
7
Invia HTML
 
Invia il modulo
8
Input HTML
runat=server
txtQty
Inserisci la quantità acquistata
9
label
lblMsgQte
qualsiasi messaggio di errore
1.5.6.3.3. Il codice di presentazione [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>

Commenti:

  • l'intestazione è inclusa nella pagina - riga 9
  • Il letterale [litId] è definito alla riga 10
  • Il DataGrid [DataGridArticles] è definito alle righe 12–34
  • Il modulo è definito alle righe 36–46. È di tipo POST.
  • La destinazione POST è fornita da una variabile [strAction] - riga 36. Questa variabile deve essere definita dal controller.
  • Il campo di immissione per la quantità acquistata è definito alla riga 41. Si tratta di un componente HTML lato server (runat=server). Dal punto di vista del codice, vi si accede tramite un oggetto.
  • La riga 42 definisce l'etichetta [lblMsgQte], che conterrà eventuali messaggi di errore relativi alla quantità inserita
1.5.6.3.4. Il codice di controllo [infos.aspx.vb]
Imports istia.st.articles.dao
Imports System
Imports System.Collections

Namespace istia.st.articles.web

    ' manages an article information page
    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

        ' the URL where the form will be posted
        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

        ' page display
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
            ' retrieve query info
            Dim unArticle As Article = CType(Session.Item("article"), Article)
            ' link menu options to rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
            ' we link the article to [DataGrid]
            Dim articles As New ArrayList
            articles.Add(unArticle)
            With DataGridArticle
                .DataSource = articles
                .DataBind()
            End With
            ' the id label
            litId.Text = unArticle.id.ToString
            ' the error message
            lblMsgQte.Text = context.Items("msg").ToString
            ' the previous qty
            txtQte.Value = context.Items("qte").ToString
            ' the URL action
            strAction = "?action=achat&id=" + unArticle.id.ToString
        End Sub
End Namespace

Commenti:

  • I componenti della pagina sono definiti nelle righe 10–14
  • La classe definisce una proprietà pubblica [strAction] utilizzata per definire la destinazione POST del modulo - righe 17-25
  • L'articolo da visualizzare viene recuperato dal contesto dell'applicazione - riga 30
  • L'array delle opzioni del menu di intestazione viene recuperato dal contesto per inizializzare il componente [entete] della pagina — riga 32
  • righe 33–39: il componente [DataGridArticle] è associato a un'origine dati di tipo [ArrayList] contenente solo l'articolo recuperato alla riga 30
  • i componenti [lblMsgQte, txtQte] vengono inizializzati con le informazioni prese dal contesto - righe 42-45
  • anche la proprietà [straction] viene inizializzata con le informazioni prese dal contesto - riga 47. Questa variabile viene utilizzata per generare l'attributo [action] del modulo HTML presente nella pagina:
        <form method="post" action="<%=strAction%>">
....
        </form>

1.5.6.4. La vista [panier.aspx]

1.5.6.4.1. Introduzione

Questa vista mostra il contenuto del carrello:

Image

Viene visualizzata in risposta a una richiesta del tipo /main?action=cart o /main?action=cancelpurchase&id=ID. I parametri di richiesta del controller sono i seguenti:

azioni
object Hashtable() - l'array delle opzioni di menu
carrello
oggetto di tipo [Carrello] - il carrello da visualizzare

Ogni link [Rimuovi] nella tabella HTML degli articoli nel carrello ha un URL nella forma [?action=removeitem&id=ID], dove ID è il campo [id] dell'articolo da rimuovere dal carrello.

1.5.6.4.2. Componenti della pagina
No.
tipo
nome
ruolo
1
componente utente
intestazione
intestazione di visualizzazione
2
DataGrid
DataGridAcquisti
3 - colonna correlata - intestazione: Articolo, campo: nome
4 - colonna correlata - intestazione: Qtà, campo: qtà
5 - colonna correlata - intestazione: Prezzo, campo: prezzo
6 - colonna correlata - intestazione: Totale, campo: total, formattazione {0:C}
7 - colonna ipertestuale - Testo: Rimuovi, URL: id, formato URL: /webarticles/main.aspx?action=retirerachat&id={0}
visualizza l'elenco degli articoli acquistati
8
etichetta
lblTotal
visualizza l'importo dovuto
1.5.6.4.3. Il codice di presentazione [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>

Commenti

  • La riga 9 include l'intestazione
  • Le righe 12–27 definiscono il componente [DataGridAchats]
  • Riga 29: viene definito il componente [lblTotal]
1.5.6.4.4. Il codice di controllo [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
    ' manages the basket display page
    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
            ' link menu options to rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
            ' we pick up the basket
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            ' transfer purchases to a table of purchase lines
            Dim achats(unPanier.achats.Count - 1) As LigneAchat
            ' change the type of ArrayList elements
            For i As Integer = 0 To achats.Length - 1
                achats(i) = New LigneAchat(CType(unPanier.achats(i), Achat))
            Next
            ' link the data to the [DataGrid] components of the page
            With DataGridAchats
                .DataSource = achats
                .DataBind()
            End With
            ' total payable is displayed
            lblTotal.Text = unPanier.totalPanier.ToString
        End Sub

        ' purchase line built from a Purchase object
        Private Class LigneAchat
            Inherits Achat

            ' builder receives a purchase
            Public Sub New(ByVal unAchat As Achat)
                Me.article = unAchat.article
                Me.qte = unAchat.qte
            End Sub

            ' id: returns the id of the item purchased
            Public ReadOnly Property id() As Integer
                Get
                    Return article.id
                End Get
            End Property

            ' name: name of item purchased
            Public ReadOnly Property nom() As String
                Get
                    Return article.nom
                End Get
            End Property

            ' price of item purchased
            Public ReadOnly Property prix() As Double
                Get
                    Return article.prix
                End Get
            End Property

        End Class
    End Class
End Namespace

Commenti:

  • I componenti della pagina sono dichiarati alle righe 11–13
  • L'inizializzazione del componente [header] è identica a quella presente nelle pagine già studiate — riga 17
  • Il carrello da visualizzare viene recuperato dalla sessione — riga 19
  • La visualizzazione di questo carrello utilizzando il componente [DataGridAchats] pone dei problemi. La difficoltà deriva dall'inizializzazione del componente. Esaminiamo le sue colonne:
    • colonna [Article] associata al campo [name] nell'origine dati
    • Colonna [Qty] associata al campo [qty] nell'origine dati
    • Colonna [Price] associata al campo [price] nell'origine dati
    • Colonna [Total] associata al campo [total] nell'origine dati

L'origine dati di cui disponiamo è il carrello della spesa e la relativa lista della spesa. Quest'ultima fungerà da origine dati per il [DataGrid]. Tuttavia, gli oggetti [Purchase] che popoleranno le righe del [DataGrid] non dispongono delle proprietà [name, qty, price, total] previste dal [DataGrid]. Pertanto, creiamo qui, specificamente per il [DataGrid], un'origine dati i cui elementi abbiano le caratteristiche previste dal [DataGrid]. Questi elementi saranno di tipo [PurchaseLine], una classe creata a questo scopo e derivata dalla classe [Purchase] — righe 36–66

  • Una volta definita la classe [PurchaseLine], l'origine dati per [DataGridPurchases] viene costruita a partire dal carrello presente nella sessione — righe 20–30
  • L'importo totale dell'acquisto viene visualizzato utilizzando la proprietà [totalPanier] della classe [Panier] — riga 32

1.5.6.5. La vista [emptyCart.aspx]

1.5.6.5.1. Introduzione

Questa vista visualizza le informazioni che indicano che il carrello è vuoto:

Image

Viene visualizzata in seguito a una richiesta a /main?action=panier o /main?action=retirerachat&id=ID. I parametri della richiesta al controller sono i seguenti:

azioni
Oggetto Hashtable() - l'array delle opzioni del menu
1.5.6.5.2. Componenti della pagina
N.
Tipo
nome
ruolo
1
componente utente
intestazione
visualizza intestazione
1.5.6.5.3. Il codice di presentazione [emptycart.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>

Commenti:

  • L'intestazione è inclusa alla riga 9
1.5.6.5.4. Il codice di controllo [paniervide.aspx.vb]
Namespace istia.st.articles.web
    ' manages the empty basket display page
    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
            ' prepare the [emptybasket] view using context information
            ' link menu options to rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
        End Sub
    End Class
End Namespace

Commenti:

  • Inizializziamo semplicemente l'unico componente dinamico della pagina - riga 10

1.5.6.6. La vista [errors.aspx]

1.5.6.6.1. Introduzione

Questa vista viene visualizzata in caso di errori:

Image

Viene visualizzata a seguito di qualsiasi richiesta che generi un errore, ad eccezione dell'azione di acquisto con una quantità errata, che viene gestita dalla vista [INFOS]. Gli elementi della richiesta del controller sono i seguenti:

azioni
Oggetto Hashtable() - l'array delle opzioni di menu
errori
ArrayList di oggetti [String] che rappresentano i messaggi di errore da visualizzare
1.5.6.6.2. Componenti della pagina
N.
tipo
nome
ruolo
1
componente utente
intestazione
intestazione di visualizzazione
2
ripetitore
rptErrors
visualizza l'elenco degli errori
1.5.6.6.3. Il codice di presentazione [errors.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>

Commenti:

  • L'intestazione è definita alla riga 9
  • Il componente [rptErrors] è definito alle righe 13–19. Il suo contenuto proviene da una fonte dati di tipo [ArrayList] contenente oggetti [String].
1.5.6.6.4. Il codice di controllo [errors.aspx.vb]
Namespace istia.st.articles.web

    ' manages the error page
    Public Class ErreursWebarticles
        Inherits System.Web.UI.Page

        ' page components
        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
            ' prepare the [errors] view using context information
            ' link menu options to rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
            ' link errors to rptErreurs
            With rptErreurs
                .DataSource = context.Items("erreurs")
                .DataBind()
            End With
        End Sub
    End Class

End Namespace

Commenti:

  • Il componente [header] viene inizializzato come di consueto, righe 9 e 14
  • Il componente [rptErrors] viene inizializzato con l'elenco degli errori [ArrayList] presente nel contesto — righe 16–19

1.5.7. I controller global.asax e main.aspx

Dobbiamo ancora scrivere il cuore della nostra applicazione web: il controller. Il suo ruolo è quello di:

  • recuperare la richiesta del client,
  • elaborare l'azione richiesta dal client utilizzando le classi di business,
  • inviare la vista appropriata in risposta.

1.5.7.1. Il controller [global.asax.vb]

Quando l'applicazione riceve la sua primissima richiesta, viene eseguita la procedura [Application_Start] nel file [global.asax.vb]. Ciò accadrà una sola volta. Lo scopo della procedura [Application_Start] è inizializzare gli oggetti richiesti dall'applicazione web, che saranno condivisi in modalità di sola lettura da tutti i thread client. Questi oggetti condivisi possono essere collocati in due posizioni:

  • i campi privati del controller
  • il contesto di esecuzione dell'applicazione (Application)

Il metodo [Application_Start] nel file [global.asax.vb] eseguirà le seguenti azioni:

  • verificherà il file [web.config] alla ricerca dei parametri necessari per il corretto funzionamento dell'applicazione. Questi sono stati descritti nella sezione 1.5.3.
  • inserirà un elenco di eventuali errori nel contesto dell'applicazione sotto forma di un oggetto [ArrayList errors]. Questo elenco sarà vuoto se non ci sono errori, ma esisterà comunque.
  • Se ci sono stati errori, il metodo [Application_Start] si ferma lì. Altrimenti, richiede un riferimento a un singleton di tipo [IArticlesDomain], che sarà l'oggetto di business che il controller utilizzerà per le proprie esigenze. Come spiegato in 1.5.3.2, il controller richiederà questo singleton al framework Spring. Questa operazione di istanziazione può causare vari errori. In tal caso, questi errori verranno nuovamente memorizzati nell'oggetto [errors] del contesto dell'applicazione.

Il controller [global.asax.vb] dispone di una procedura [Session_Start] che viene eseguita ogni volta che arriva un nuovo client. In questa procedura, creeremo un carrello della spesa vuoto per il client. Questo carrello della spesa verrà mantenuto per tutta la durata delle richieste di questo particolare client. Il codice potrebbe essere il seguente:

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)

            ' local data
            Dim parameters() As String = {"urlMain", "urlErreurs", "urlInfos", "urlListe", "urlPanier", "urlPanierVide"}
            Dim erreurs As New ArrayList

            ' retrieve application initialization parameters
            Dim param As String
            For i As Integer = 0 To parameters.Length - 1
                ' read from conf file
                param = ConfigurationSettings.AppSettings(parameters(i))
                If param Is Nothing Then
                    ' we note the error
                    erreurs.Add("Paramètre [" + parameters(i) + "] absent dans le fichier [web.config]")
                Else
                    ' the parameter is stored in the application
                    Application.Item(parameters(i)) = param
                End If
            Next
            ' mistakes?
            If erreurs.Count = 0 Then
                ' create a IArticlesDomain business layer access object
                Dim contexte As IApplicationContext = CType(ConfigurationSettings.GetConfig("spring/context"), IApplicationContext)
                Dim articlesDomain As IArticlesDomain
                Try
                    articlesDomain = CType(contexte.GetObject("articlesDomain"), IArticlesDomain)
                    ' the object is stored in the application
                    Application.Item("articlesDomain") = articlesDomain
                Catch ex As Exception
                    ' we memorize the error
                    erreurs.Add("Erreur lors de la construction de l'objet d'accès à la couche métier [" + ex.ToString + "]")
                End Try
            End If
            ' errors are placed in the application
            Application.Item("erreurs") = erreurs
            ' it's over if there were mistakes
            If erreurs.Count <> 0 Then Return
            ' build an array of menu options
            Dim options As New Hashtable
            ' retrieve the URL from the controller
            Dim urlMain As String = CType(Application.Item("urlMain"), String)
            Dim uneOption As Hashtable
            ' list of items
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=liste")
            uneOption.Add("lien", "Liste des articles")
            options.Add("liste", uneOption)
            ' basket
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=panier")
            uneOption.Add("lien", "Voir le panier")
            options.Add("panier", uneOption)
            ' shopping cart validation
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=validationpanier")
            uneOption.Add("lien", "Valider le panier")
            options.Add("validationpanier", uneOption)
            ' set menu options in the application
            Application.Item("options") = options
            Return
        End Sub

        ' init session
        Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
            ' create a basket for the customer
            Session.Item("panier") = New Panier
        End Sub
    End Class
End Namespace

Commenti:

  • I parametri previsti in [web.config] sono definiti in un array - riga 18
  • Vengono cercati in [web.config]. Se presenti, vengono memorizzati nel contesto dell'applicazione; in caso contrario, viene registrato un errore nell'elenco degli errori [errors] - righe 21-33
  • Se non ci sono errori, viene richiesto a Spring un riferimento al singleton [articlesDomain], che gestisce l'accesso al livello [domain] dell'applicazione - righe 35-47. Eventuali errori vengono registrati in [errors].
  • Gli errori vengono registrati nel contesto dell'applicazione - riga 49
  • La procedura termina se si sono verificati errori - riga 51
  • Creiamo un array di tre dizionari. Ciascuno ha due chiavi: href e link. Questo array rappresenta le tre possibili opzioni di menu - righe 52–71
  • Questo array viene memorizzato nel contesto dell'applicazione - riga 73
  • Per ogni nuovo cliente, viene eseguita la procedura [Session_Start]. Viene creato un carrello vuoto nella sessione del cliente - righe 78-81

1.5.7.2. Il controller [main.aspx.vb]

Il controller [main.aspx.vb] gestisce tutte le richieste dei client. Queste richieste seguono tutte il formato [/webarticles/main.aspx?action=XX]. Una richiesta viene elaborata come segue:

  • Viene controllato l'oggetto [errors] nel contesto dell'applicazione. Se non è vuoto, significa che si sono verificati errori durante l'inizializzazione dell'applicazione e che l'applicazione non può essere eseguita. In risposta, viene inviata la vista [ERRORS].
  • Il parametro [action] della richiesta viene recuperato e controllato. Se non corrisponde a un'azione nota, viene inviata la vista [ERRORS] con un messaggio di errore appropriato.
  • Se il parametro [action] è valido, la richiesta del client viene passata a una procedura specifica per l'azione per l'elaborazione:
metodo
richiesta
elaborazione
possibili risposte
doList
GET /main?action=list
- richiedi l'elenco degli elementi
dalla classe di business
- visualizzarlo
[LIST] o [ERRORS]
doInfo
GET /main?action=info&id=ID
- recupera l'elemento con id=ID da
classe di business
- visualizzarlo
[INFO] o [ERRORI]
doPurchase
POST /main?action=purchase&id=ID
- La quantità acquistata è inclusa nei parametri inviati
- richiedi l'articolo con id=ID da
classe business
- aggiungilo al carrello nella
sessione del cliente
[LIST] o [INFO] o [ERRORS]
doRetirePurchase
GET /main?action=removePurchase&id=ID
- Rimuovi l'articolo con id=ID dalla
lista della spesa nella
sessione del cliente
[CART]
doCart
GET /main?action=cart
- Visualizza la
sessione del cliente
[CART] o [EMPTY_CART]
doCartValidation
GET /main?action=cartvalidation
- Ridurre le
livelli di stock di tutti gli articoli
nel
[LIST] o [ERRORS]
[LIST] o [ERRORS]

Il modello del controller [main.aspx.vb] potrebbe apparire così:

Imports System.Collections
Imports System
Imports System.Data

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

Namespace istia.st.articles.web

    ' web application controller class
    Public Class MainWebArticles
        Inherits System.Web.UI.Page

        ' private fields
        Private articlesDomain As IArticlesDomain
        Private options As Hashtable

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

        ' stock processing methods

        ' list of items
        Public Sub doListe()
...
        End Sub

        ' article info
        Public Sub doInfos()
...
        End Sub

        ' purchase an item
        Public Sub doAchat()
...
        End Sub

        ' delete a purchase
        Public Sub doRetirerAchat()
...
        End Sub

        ' view basket
        Public Sub doPanier()
...
        End Sub

        ' bUY BASKET
        Public Sub doValidationPanier()
...
        End Sub

    End Class

End Namespace

Commenti:

  • La classe ha due campi privati che saranno condivisi tra i metodi — righe 15–16:
    • articlesDomain: il singleton per accedere al livello [domain]
    • options: l'array di dizionari contenente le opzioni di menu
  • La procedura [Page_Load]:
    • inizializzerà i due campi privati della classe
    • recupera il parametro [action] dalla richiesta ed esegue il metodo che gestisce quell'azione.

1.5.7.3. Il metodo [Page_Load]

Questo evento è il primo a verificarsi sulla pagina. Il codice è il seguente:

        ' page loading
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
            ' check that the application has started correctly
            Dim erreurs As ArrayList = CType(Application.Item("erreurs"), ArrayList)
            ' if there are errors, we send the view [errors]
            If erreurs.Count <> 0 Then
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {}
                Server.Transfer(CType(Application("urlErreurs"), String))
            End If
            ' retrieve the business class access object
            articlesDomain = CType(Application.Item("articlesDomain"), IArticlesDomain)
            ' and menu options
            options = CType(Application.Item("options"), Hashtable)
            ' retrieve the action to be performed
            Dim action As String = Request.QueryString("action")
            If action Is Nothing Then
                action = "liste"
            End If
            ' execute the 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

Commenti:

  • Ogni volta che la pagina viene caricata, ci assicuriamo che l'inizializzazione dell'applicazione eseguita da [global.asax] sia andata a buon fine.
  • Per farlo, recuperiamo l'elenco degli errori inseriti da [global.asax] dal contesto dell'applicazione - riga 4
  • Se questo elenco non è vuoto, visualizziamo la vista [ERRORS] — righe 6–10
  • Recuperiamo il singleton [articlesDomain] inserito da [global.asax] nel contesto dell'applicazione e lo memorizziamo nel campo privato [articlesDomain] in modo che sia disponibile per i vari metodi della classe — riga 12
  • Eseguiamo un'operazione simile con l'array delle opzioni di menu - riga 14
  • recuperiamo il parametro [action] dalla richiesta - riga 16
  • Eseguiamo il metodo corrispondente all'azione richiesta. Un'azione non specificata viene trattata come l'azione [list] — righe 16-36

1.5.7.4. Gestione dell'azione [list]

Ciò comporta la visualizzazione dell'elenco degli elementi:

Image

Il codice è il seguente:

        ' list of items
        Public Sub doListe()
            ' error management
            Dim erreurs As New ArrayList
            ' the list of items is requested
            Dim articles As IList
            Try
                articles = articlesDomain.getAllArticles
            Catch ex As Exception
                ' we note the error
                erreurs.Add(ex.ToString)
            End Try
            ' mistakes?
            If erreurs.Count <> 0 Then
                ' display view [errors]
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
            End If
            ' display view [list]
            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

Commenti:

  • Eventuali errori vengono inseriti in un [ArrayList] - riga 4
  • L'elenco degli elementi viene richiesto al singleton [articlesDomain] - righe 5-12
  • Se sono presenti errori, viene visualizzata la vista [ERRORS] - righe 13-19
  • altrimenti, viene restituita la vista [LIST] - righe 20-24

1.5.7.5. Gestione dell'azione [info]

Il client ha richiesto informazioni su un elemento specifico:

Image

Il codice è il seguente:

        ' article info
        Public Sub doInfos()
            ' error management
            Dim erreurs As New ArrayList
            ' retrieve the id of the requested item
            Dim strId As String = Request.QueryString("id")
            ' do we have anything?
            If strId Is Nothing Then
                ' not normal - sends error page
                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
            ' do we have an integer?
            Dim id As Integer
            Try
                id = Integer.Parse(strId)
            Catch ex As Exception
                ' not normal - sends error page
                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
            ' the key item id is requested
            Dim unArticle As Article
            Try
                unArticle = articlesDomain.getArticleById(id)
            Catch ex As Exception
                ' data access issues
                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
            ' has an item been recovered?
            If unArticle Is Nothing Then
                ' article does not exist
                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
            ' we have the article - we put it in the current session
            Session.Item("article") = unArticle
            ' prepare your display
            context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
            context.Items("msg") = ""
            context.Items("qte") = ""
            Server.Transfer(CType(Application.Item("urlInfos"), String))
        End Sub

Commenti:

  • Eventuali errori vengono inseriti in un [ArrayList] - riga 4
  • L'ID dell'elemento richiesto viene recuperato dalla richiesta - riga 6
  • Questo ID viene convalidato. Deve esistere e deve essere un numero intero. In caso contrario, viene visualizzata la vista [ERRORS] con il messaggio di errore appropriato - righe 7-27
  • Una volta verificato l'ID, l'elemento viene richiesto al singleton [articlesDomain]. Se si verifica un'eccezione, viene inviata la vista [ERRORS] - righe 29-39
  • Se l'elemento non viene trovato, viene inviata la vista [ERRORS] — righe 41–48
  • Se l'articolo viene trovato, viene inserito nella sessione dell'utente e quindi visualizzato nella vista [INFO] - righe 50-56

1.5.7.6. Elaborazione dell'azione [purchase]

Il cliente ha acquistato l'elemento visualizzato nella vista [INFO].

Image

Il codice è il seguente:

        ' purchase an item
        Public Sub doAchat()
            ' purchase an item
            ' we retrieve the article that was in the session
            Dim unArticle As Article = CType(Session.Item("article"), Article)
            ' do we have anything?
            If unArticle Is Nothing Then
                ' not normal - item list is displayed
                doListe()
            End If
            ' we retrieve the posted qty
            Dim strQte As String = Request.Form("txtQte")
            ' do we have anything?
            If strQte Is Nothing Then
                ' not normal - we send the list of items
                doListe()
            End If
            ' do we have an integer?
            Dim qte As Integer
            Try
                qte = Integer.Parse(strQte)
                If (qte <= 0) Then Throw New Exception
            Catch ex As Exception
                ' not normal - send info page with error message
                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
            ' all's well - we put the purchase in the customer's shopping cart
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            unPanier.ajouter(New Achat(unArticle, qte))
            ' displays the list of items
            doListe()
        End Sub

Commenti:

  • Viene recuperato l'elemento inserito nella sessione - riga 5
  • Se non è presente (la sessione potrebbe essere scaduta), viene visualizzata la vista [LIST] - righe 7-10
  • La quantità acquistata viene recuperata dalla query - riga 12
  • Se non è valida, a seconda del caso, viene visualizzata la vista [LIST] - riga 16
  • se non è valida, a seconda dei casi, viene visualizzata la vista [LIST] - riga 16 oppure la vista [INFO] - righe 24-28
  • Se tutto è normale, l'acquisto viene aggiunto al carrello - righe 31-32
  • quindi viene inviata la vista [LIST] - riga 34

1.5.7.7. Elaborazione dell'azione [cart]

Il cliente ha effettuato diversi acquisti e desidera visualizzare il carrello:

Image

Il codice è il seguente:

        ' view basket
        Public Sub doPanier()
            'the basket is retrieved from the session
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            ' empty basket?
            If unPanier.achats.Count = 0 Then
                ' empty basket display
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlPanierVide"), String))
            End If
            ' display basket not empty
            context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable), CType(options("validationpanier"), Hashtable)}
            Server.Transfer(CType(Application.Item("urlPanier"), String))
        End Sub

Commenti:

  • Recuperiamo il carrello dalla sessione - riga 4. Qui non verifichiamo se effettivamente abbiamo recuperato qualcosa. Dovremmo farlo perché la sessione potrebbe essere scaduta.
  • Se il carrello è vuoto, inviamo la vista [EMPTY CART] - righe 6-10
  • Altrimenti, inviamo la vista [SHOPPINGCART] — righe 11–14

1.5.7.8. Elaborazione dell'azione [removepurchase]

Il cliente desidera rimuovere un acquisto dal proprio carrello:

Image

Il codice è il seguente:

        ' delete a purchase
        Public Sub doRetirerAchat()
            'the basket is retrieved from the session
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            ' we remove the purchase
            Try
                ' retrieve the id of the removed item
                Dim idArticle As Integer = Integer.Parse(Request.QueryString("id"))
                ' take it out of the basket
                unPanier.enlever(idArticle)
            Catch ex As Exception
                ' displays the list of items
                doListe()
            End Try
            ' the basket is displayed
            doPanier()
        End Sub

Commenti:

  • Recupera il carrello dalla sessione - riga 4. Qui non verifichiamo che il recupero sia effettivamente avvenuto. Questo dovrebbe essere fatto perché la sessione potrebbe essere scaduta.
  • Recupera l'ID [id] dell'articolo da rimuovere dalla query - riga 8.
  • L'acquisto corrispondente viene rimosso dal carrello - riga 10
  • Qui non è stata verificata la validità dell'ID dell'articolo acquistato. Se ha un tipo non valido, si verificherà un'eccezione che verrà gestita nelle righe 11-13. Se è valido ma non esiste, il metodo [cart.remove] (riga 10) non fa nulla.
  • Viene visualizzato il nuovo carrello - riga 16

1.5.7.9. Elaborazione dell'azione [validateCart]

Il cliente desidera confermare il proprio carrello:

Image

Il codice è il seguente:

        ' bUY BASKET
        Public Sub doValidationPanier()
            ' at the start, no mistakes
            Dim erreurs As ArrayList
            'the basket is retrieved from the session
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            ' we try to validate the basket
            Try
                articlesDomain.acheter(unPanier)
                ' note any purchasing errors
                erreurs = articlesDomain.erreurs
            Catch ex As Exception
                ' we note the error
                erreurs = New ArrayList
                erreurs.Add(String.Format("Erreur lors de la validation du panier [{0}]", ex.Message))
            End Try
            ' if errors then error page
            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
            ' all is well - the list of items is displayed with a success message
            context.Items("message") = "Votre panier a été validé"
            doListe()
        End Sub

Commenti:

  • Recuperiamo il carrello dalla sessione - riga 6. Qui non verifichiamo se abbiamo effettivamente recuperato qualcosa. Dovremmo farlo perché la sessione potrebbe essere scaduta.
  • Se la sessione è scaduta, avremo un puntatore [nothing] per il carrello e il metodo [buy] — riga 9 — genererà un'eccezione e verrà visualizzata la vista [ERRORS]. Tuttavia, il messaggio di errore non sarà chiaro per l'utente.
  • Righe 8–16: Tentiamo di convalidare il carrello recuperato dalla sessione. Alcuni acquisti potrebbero non andare a buon fine se la quantità richiesta supera la disponibilità dell'articolo richiesto. Questi casi vengono memorizzati dal metodo [buy] in un elenco di errori, che viene recuperato alla riga 11.
  • Se ci sono errori, viene inviata la vista [ERRORS] — righe 18–23
  • altrimenti, viene inviata la vista [LIST] — righe 25–26

1.6. Conclusione

Qui abbiamo sviluppato un'applicazione utilizzando il modello MVC. Ad aprile 2005 non sembrano esserci framework di sviluppo MVC professionali per ASP.NET paragonabili a quelli disponibili per Java (Struts, Spring, ecc.). Il progetto [Spring.net] dovrebbe rilasciarne uno a breve. Fino ad allora, il metodo sopra descritto fornisce un approccio di sviluppo MVC valido per applicazioni di medie dimensioni.