Skip to content

1. Parte 1

O PDF do documento está disponível |AQUI|.

Os exemplos no documento estão disponíveis |AQUI|.

1.1. Introdução

Objetivos deste artigo:

  • Escrever uma aplicação web de três camadas [interface do utilizador, lógica de negócio, acesso a dados]
  • Configurar a aplicação com Spring IOC
  • Escrever diferentes versões alterando a implementação de uma ou mais das três camadas.

Ferramentas utilizadas:

  • Visual Studio.NET para desenvolvimento — ver Apêndice, Secção 3.1;
  • Servidor web Cassini para execução — ver Apêndice, Secção 3.2;
  • Nunit para testes unitários — ver Apêndice, Secção 3.4;
  • Spring para a integração e configuração das camadas da aplicação web — ver Apêndice, Secção 3.3;

Numa escala iniciante-intermédio-avançado, este documento enquadra-se na categoria [intermédio-avançado]. A sua compreensão requer vários pré-requisitos. Alguns destes podem ser encontrados em documentos que escrevi. Nesses casos, faço referência aos mesmos. Escusado será dizer que isto é meramente uma sugestão e que o leitor é livre de utilizar os recursos da sua preferência.

Este documento segue a mesma estrutura de um documento escrito para Java [Arquiteturas de 3 camadas e arquiteturas MVC com Struts, Spring e Java]. Estamos a construir a aplicação Web MVC de três camadas escrita em Java utilizando VB.NET. O que pretendemos demonstrar aqui é que as plataformas de desenvolvimento Java e .NET são suficientemente semelhantes para que as competências adquiridas num destes dois domínios possam ser reutilizadas no outro.

Não parece existir uma solução de desenvolvimento ASP.NET MVC amplamente reconhecida. A solução a seguir adota o método apresentado no documento [Desenvolvimento Web com ASP.NET 1.1]. Embora este método tenha o mérito de utilizar conceitos comuns no desenvolvimento J2EE, deve, no entanto, ser considerado pelo que é: um dos muitos métodos de desenvolvimento MVC. Assim que um método de desenvolvimento MVC em ASP.NET se tornar amplamente aceite, deve ser adotado. A versão .NET do Spring, atualmente em desenvolvimento, poderá muito bem ser uma primeira solução.

1.2. A aplicação webarticles

Apresentamos aqui os componentes de uma aplicação web de comércio eletrónico simplificada. Esta aplicação permitirá aos utilizadores da web:

  • visualizar uma lista de artigos a partir de uma base de dados
  • adicionar alguns deles a um carrinho de compras online
  • confirmar o carrinho. Esta confirmação atualizará simplesmente o inventário dos itens adquiridos na base de dados.

As diferentes visualizações apresentadas ao utilizador serão as seguintes:

- a visualização «LISTA», que apresenta uma lista de artigos à venda
- a vista [INFO], que fornece informações adicionais sobre um produto:
  • as vistas [CARRINHO] e [CARRINHO VAZIO], que mostram o conteúdo do carrinho do cliente
  • a vista [ERROS], que reporta quaisquer erros da aplicação

Image

1.3. Arquitetura geral da aplicação

Pretendemos construir uma aplicação com a seguinte estrutura de três camadas:

  • As três camadas são tornadas independentes através da utilização de interfaces
  • A integração das diferentes camadas é gerida pelo Spring
  • Cada camada tem o seu próprio namespace: web (camada de interface do utilizador), domain (camada de negócios) e dao (camada de acesso a dados).

A aplicação seguirá uma arquitetura MVC (Model-View-Controller). Se nos referirmos ao diagrama em camadas acima, a arquitetura MVC encaixa-se nele da seguinte forma:

O processamento de um pedido do cliente segue estes passos:

  1. O cliente faz uma solicitação ao controlador. Neste caso, o controlador é uma página .aspx que desempenha uma função específica. Ele lida com todas as solicitações do cliente. É o ponto de entrada da aplicação. É o C em MVC.
  1. O controlador processa esta solicitação. Para tal, pode necessitar da assistência da camada de negócios, conhecida como o M na estrutura MVC.
  2. O controlador recebe uma resposta da camada de negócios. A solicitação do cliente foi processada. Isso pode desencadear várias respostas possíveis. Um exemplo clássico é
    • uma página de erro, caso a solicitação não tenha sido processada corretamente
    • uma página de confirmação, caso contrário
  3. O controlador escolhe a resposta (= vista) a enviar ao cliente. Trata-se, na maioria das vezes, de uma página que contém elementos dinâmicos. O controlador fornece estes à vista.
  4. A vista é enviada ao cliente. Esta é o V em MVC.

1.4. O Modelo

Aqui, examinamos o M em MVC. O modelo é composto pelos seguintes elementos:

  1. classes de negócio
  2. classes de acesso a dados
  3. a base de dados

1.4.1. A base de dados

A base de dados contém apenas uma tabela chamada ARTICLES. Esta tabela foi gerada utilizando os seguintes comandos 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
chave primária que identifica um item de forma única
nome
nome do item
preço
o seu preço
stock atual
stock atual
stock mínimo
o nível de stock abaixo do qual deve ser efetuada uma nova encomenda

1.4.2. Os namespaces do modelo

O Modelo M é aqui apresentado sob a forma de dois espaços de nomes:

  • istia.st.articles.dao: contém as classes de acesso a dados da camada [dao]
  • istia.st.articles.domain: contém as classes de negócio da camada [domain]

Cada um destes namespaces será gerado no seu próprio ficheiro «assembly»:

assembly
conteúdo
função
webarticles-dao
- [IArticlesDao]: a interface para aceder à camada [dao].
Esta é a única interface visível para a camada [domain]. Não vê outras.
- [Article]: classe que define um artigo
- [ArticlesDaoArrayList]: classe de implementação da
da interface [IArticlesDao] com um [ArrayList]
camada de acesso a dados
- está inteiramente dentro da
camada [DAO] da arquitetura de três camadas da
aplicação web
webarticles-domain
- [IArticlesDomain]: a interface para aceder à camada [domain]. Esta é a única interface visível para a camada web. Não vê nenhuma outra.
- [AchatsArticles]: uma classe que implementa [IArticlesDomain]
- [Purchase]: uma classe que representa a compra de um cliente
- [Cart]: uma classe que representa o total de compras de um cliente
representa o modelo de compras na web
web - reside inteiramente na
camada [domain] da
arquitetura de 3 camadas da aplicação web

1.4.3. A camada [DAO]

A camada [DAO] contém os seguintes elementos:

  • [IArticlesDao]: a interface para aceder à camada [dao]

  • [Article]: classe que define um artigo

  • [ArticlesDaoArrayList]: classe de implementação para a interface [IArticlesDao] utilizando uma classe [ArrayList]

A estrutura do projeto [Visual Studio] para a camada [dao] é a seguinte:

Image

Comentários:

  • O projeto [dao] é do tipo [biblioteca de classes]
  • As classes foram colocadas numa estrutura em árvore a partir da pasta [istia]. Todas elas estão no namespace [istia.st.articles.dao].

1.4.3.1. A classe [Article]

A classe que define um artigo é a seguinte:

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

Esta classe fornece:

  1. um construtor para definir as 5 informações de um item: [id, nome, preço, stockAtual, stockMínimo]
  2. propriedades públicas para ler e gravar as 5 informações.
  3. uma validação dos dados introduzidos para o item. Se os dados forem inválidos, é lançada uma exceção.
  4. Um método toString que devolve o valor de um item como uma cadeia de caracteres. Isto é frequentemente útil para depurar uma aplicação.

1.4.3.2. A interface [IArticlesDao]

A interface [IArticlesDao] é definida da seguinte forma:

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

As funções dos vários métodos na interface são as seguintes:

getAllArticles
retorna todos os itens da fonte de dados
clearAllArticles
limpa a fonte de dados
getArticleById
retorna o objeto [Article] identificado pela sua chave primária
addArticle
permite adicionar um artigo à fonte de dados
modifyArticle
permite modificar um artigo na fonte de dados
deleteItem
permite eliminar um item da fonte de dados
updateItemStock
permite modificar o stock de um item na fonte de dados

A interface fornece aos programas clientes uma série de métodos definidos exclusivamente pelas suas assinaturas. Não se preocupa com a forma como esses métodos serão efetivamente implementados. Isto confere flexibilidade a uma aplicação. O programa cliente efetua chamadas a uma interface, em vez de a uma implementação específica dessa interface.

A escolha de uma implementação específica será feita através de um ficheiro de configuração do Spring. Para demonstrar que, para testar a aplicação web, apenas a interface de acesso aos dados importa — e não a sua classe de implementação —, iremos primeiro implementar a fonte de dados utilizando um objeto [ArrayList] simples. Posteriormente, apresentaremos uma solução baseada em base de dados.

1.4.3.3. A classe de implementação [ArticlesDaoArrayList]

A classe de implementação [ArticlesDaoArrayList] é definida da seguinte forma:

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

Comentários:

  • A fonte de dados é simulada pelo campo privado [articles] do tipo [ArrayList]
  • O construtor da classe cria 4 itens na fonte de dados por predefinição.
  • Todos os métodos de acesso aos dados foram sincronizados para evitar problemas de acesso simultâneo à fonte de dados. Em qualquer momento, apenas um segmento tem acesso a um determinado método.
  • O método [posArticle] devolve a posição [0..N] na fonte de dados [ArrayList] de um artigo identificado pelo seu número. Se o artigo não existir, o método devolve a posição -1. Este método é utilizado repetidamente pelos outros métodos.
  • O método [addArticle] adiciona um artigo à lista de artigos. Retorna o número de artigos inseridos: 1. Se o artigo já existisse, é lançada uma exceção.
  • O método [modifyItem] permite modificar um item existente. Retorna o número de itens modificados: 1 se o item existisse, 0 caso contrário.
  • O método [deleteArticle] elimina um artigo existente. Devolve o número de artigos eliminados: 1 se o artigo existisse, 0 caso contrário.
  • O método [getAllArticles] devolve uma lista de todos os artigos
  • O método [getArticleById] recupera um artigo identificado pelo seu ID. Retorna o valor [nothing] se o artigo não existir.
  • O código não apresenta qualquer dificuldade real. Deixamos ao leitor a tarefa de o rever e compreender.

1.4.3.4. Gerar a compilação da camada [dao]

O projeto do Visual Studio está configurado para gerar o assembly [webarticles-dao.dll]. Este é gerado na pasta [bin] do projeto:

1.4.3.5. Testes NUnit para a camada [dao]

Em Java, as classes são testadas utilizando a estrutura [JUnit]. No .NET, a estrutura NUnit oferece as mesmas capacidades de testes unitários:

Image

A estrutura do projeto de teste do Visual Studio é a seguinte:

Image

Comentários:

  • O projeto [tests] é uma [biblioteca de classes]
  • Os testes [NUnit] requerem uma referência ao assembly [nunit.framework.dll]
  • a classe de teste [NUnit] recupera uma instância do objeto em teste através do Spring. Portanto,
    • na pasta [bin], os ficheiros de classe do Spring
    • em [Referências], uma referência ao assembly [Spring-Core.dll] da pasta [bin]
    • em [bin], um ficheiro de configuração para o Spring
  • A classe de teste requer o assembly [webarticles-dao.dll] da camada [dao]. Este foi colocado na pasta [bin] e a sua referência adicionada às referências do projeto.

Uma classe de teste [NUnit] requer acesso às classes no namespace [NUnit.Framework]. Por conseguinte, é incluída a seguinte instrução de importação:

Imports NUnit.Framework

O namespace [NUnit.Framework] está localizado no assembly [nunit.framework.dll], que deve ser adicionado às referências do projeto:

O assembly [nunit.framework.dll] deverá constar na lista apresentada se o [NUnit] tiver sido instalado. Basta clicar duas vezes no assembly para o adicionar ao projeto:

Image

A classe de teste [NUnit] para a camada [DAO] poderia ter o seguinte aspeto:

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

Comentários:

  • Queríamos escrever um programa de teste para a interface [IArticlesDao] que fosse independente da sua classe de implementação. Por isso, utilizámos o Spring para ocultar o nome da classe de implementação do programa de teste.
  • O método <Setup()> recupera uma referência ao objeto [articlesdao] a ser testado a partir do Spring. Isto está definido no seguinte ficheiro [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>

Este ficheiro especifica o nome da classe de implementação [istia.st.articles.dao.ArticlesDaoArrayList] para a interface [IArticlesDao] e onde a encontrar [webarticles-dao.dll]. Uma vez que a instanciação não requer quaisquer parâmetros, nenhum é definido aqui.

  • A maioria dos testes é fácil de compreender. Recomenda-se ao leitor que leia os comentários.
  • O método [testChangerStockArticle] requer alguma explicação. Cria 100 threads responsáveis por diminuir o stock de um determinado artigo.
        <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

Isto serve para testar o acesso simultâneo à fonte de dados. O método responsável pela atualização do stock é o seguinte:

        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

Diminui o stock do artigo n.º 3 em uma unidade. Se consultarmos o código do método [testChangerStockArticle], vemos que:

  • o stock do artigo n.º 3 é inicializado para 101
  • as 100 threads irão, cada uma, diminuir este stock em um
  • devemos, portanto, ter um stock de 1 no final da execução de todas as threads

Além disso, ainda dentro deste método, tentamos definir o stock do item #4 para um valor negativo. Isto deve falhar.

Para testar a camada [dao], geramos a DLL [tests-webarticles-dao.dll] na pasta [bin] do projeto [tests]:

Em seguida, utilizando a aplicação [Nunit-Gui], carregamos esta DLL e executamos os testes:

Image

Na janela da esquerda, vemos a lista dos métodos testados. A cor do ponto que precede o nome de cada método indica se o método foi aprovado (verde) ou reprovado (vermelho). Os leitores que estiverem a visualizar este documento no ecrã verão que todos os testes foram aprovados. Consideraremos, portanto, que a camada [dao] está operacional.

1.4.4. A camada [domain]

A camada [domain] contém os seguintes elementos:

  • [IArticlesDomain]: a interface para aceder à camada [domain]

  • [Purchase]: classe que define uma compra

  • [ShoppingCart]: classe que define um carrinho de compras

  • [ProductPurchases]: a classe que implementa a interface [IArticlesDomain]

A estrutura da solução [Visual Studio] para a camada [domain] é a seguinte:

Image

Comentários:

  • O projeto [domain] é do tipo [biblioteca de classes]
  • As classes foram colocadas numa estrutura em árvore a partir da pasta [istia]. Todas elas estão no namespace [istia.st.articles.domain].
  • A DLL da camada [dao] foi colocada na pasta [bin] do novo projeto. Além disso, esta DLL foi adicionada como referência ao projeto.

1.4.4.1. A interface [IArticlesDomain]

A interface [IArticlesDomain] desacopla a camada [business] da camada [web]. Esta última acede à camada [business/domain] através desta interface sem se preocupar com a classe que a implementa efetivamente. A interface define as seguintes ações para aceder à camada de negócios:

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
Função getAllArticles() As IList
retorna a lista de objetos [Article] da fonte de dados associada
Função getArticleById(ByVal idArticle As Integer) As Article
retorna o objeto [Article] identificado por [idArticle]
buy(ByVal cart As Cart)
processa o carrinho do cliente, diminuindo o stock dos artigos comprados pela quantidade adquirida - pode falhar se o stock for insuficiente
Propriedade ReadOnly errors() As ArrayList
retorna a lista de erros que ocorreram - vazia se não houver erros

1.4.4.2. A classe [Purchase]

A classe [Purchase] representa uma compra do 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

Comentários:

  • A classe [Purchase] tem as seguintes propriedades e métodos:
Propriedade pública item() As item
o item adquirido
Propriedade Pública qty() Como Inteiro
a quantidade adquirida
Propriedade Pública ReadOnly totalDaCompra() As Double
o valor da compra
Função pública Overrides ToString() As String
representação em cadeia de caracteres do objeto
  • Possui um construtor que inicializa as propriedades [item, qty] que definem uma compra.

1.4.4.3. A classe [Cart]

A classe [Cart] representa o total das compras do 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

Comentários:

  • A classe [ShoppingCart] possui as seguintes propriedades e métodos:
Propriedade ReadOnly purchases() como ArrayList
a lista de compras do cliente - uma lista de objetos do tipo [Purchase]
add(ByVal aPurchase As Purchase)
adiciona uma compra à lista de compras
remove(ByVal purchaseId As Integer)
remove a compra com idPurchase
Propriedade ReadOnly totalBasket() As Double
o valor total das compras no carrinho
Função ToString() As String
retorna a string que representa o carrinho de compras
  • O método [posAchat] é um método utilitário que devolve a posição na lista de compras de uma compra identificada pelo número do artigo. A lista de compras é gerida de forma a que um artigo comprado várias vezes ocupe apenas uma posição na lista. Assim, uma compra pode ser identificada pelo número do artigo. O método [posAchat] devolve -1 se a compra procurada não existir.
  • O método [add] adiciona uma nova compra à lista de compras. Isto adiciona uma nova entrada à lista de compras se o artigo comprado ainda não existisse na lista, ou aumenta a quantidade comprada se já existisse.
  • O método [remove] permite remover uma compra identificada por um número da lista de compras. Se a compra não existir, o método não executa nenhuma ação.
  • O valor total da compra [totalPanier] é atualizado à medida que os itens são adicionados e removidos.

1.4.4.4. A classe [PurchaseItems]

A interface [IArticlesDomain] será implementada pela seguinte 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

Comentários:

  • Esta classe implementa os quatro métodos da interface [IArticlesDomain]. Possui dois campos privados:
_articlesDao As IArticlesDao
o objeto de acesso aos dados
_errors As ArrayList
a lista de possíveis erros. Pode ser acedida através da propriedade pública [errors]
  • Para criar uma instância da classe, deve fornecer o objeto que permite o acesso aos dados:
Sub New(ByVal articlesDao As IArticlesDao)
  • Os métodos [getAllArticles] e [getArticleById] dependem dos métodos com o mesmo nome na camada [dao]
  • O método [buy] valida a compra de um carrinho de compras. Esta validação envolve simplesmente a redução do stock dos artigos comprados. Um artigo só pode ser comprado se houver stock suficiente. Se não for esse o caso, a compra é rejeitada: o artigo permanece no carrinho de compras e é reportado um erro na lista [errors]. Uma compra validada é removida do carrinho e o stock do artigo correspondente é reduzido pela quantidade comprada.

1.4.4.5. Geração do assembly da camada [domain]

O projeto do Visual Studio está configurado para gerar o assembly [webarticles-domain.dll]. Este é gerado na pasta [bin] do projeto:

1.4.4.6. Testes NUnit para a camada [domain]

A estrutura do projeto de teste do Visual Studio é a seguinte:

Image

Comentários:

  • O projeto [tests] é do tipo [biblioteca de classes]
  • os testes [NUnit] requerem uma referência ao assembly [nunit.framework.dll]
  • A classe de teste [NUnit] recupera uma instância do objeto em teste através do Spring. Portanto,
  • na pasta [bin], os ficheiros de classe Spring
  • em [Referências], uma referência ao assembly [Spring-Core.dll] da pasta [bin]
  • em [bin], um ficheiro de configuração para o Spring
  • A classe de teste requer o assembly [webarticles-dao.dll] da camada [dao] e o assembly [webarticles-domain.dll] da camada [domain]. Estes foram colocados na pasta [bin] e as suas referências adicionadas às referências do projeto.

Uma classe de teste NUnit para a camada [domain] pode ter o seguinte aspeto:

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

Comentários:

  • Queríamos escrever um programa de teste para a interface [IArticlesDomain] que fosse independente da sua classe de implementação. Por isso, utilizámos o Spring para ocultar o nome da classe de implementação do programa de teste.
  • O método de atributo <Setup()> recupera do Spring uma referência aos objetos [articlesdomain] e [articlesdao] a serem testados. Estes estão definidos no seguinte ficheiro [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>

Este ficheiro indica:

  • (continuação)
    • para o singleton [articlesdao], o nome da classe de implementação [istia.st.articles.dao.ArticlesDaoArrayList] e onde encontrá-la [webarticles-dao.dll]. Uma vez que a instanciação não requer quaisquer parâmetros, nenhum é definido aqui.
    • para o singleton [articlesdomain], o nome da classe de implementação [istia.st.articles.domain.AchatsArticles] e onde encontrá-la [webarticles-domain.dll]. A classe [AchatsArticles] possui um construtor com um parâmetro: o singleton que gere o acesso à camada [dao]. Aqui, este é definido como o singleton [articlesdao] definido anteriormente.
  • A classe de teste obtém uma instância da classe em teste [articlesdomain], bem como uma instância da classe de acesso aos dados [articlesdao]. Este último ponto é controverso. A classe de teste, teoricamente, não deveria precisar de aceder à camada [dao], da qual nem sequer deveria ter conhecimento. Aqui, ignorámos esta «ética», que, se seguida, nos teria obrigado a criar novos métodos na nossa interface [IArticlesDomain].

Para testar a camada [domain], geramos a DLL [tests-webarticles-domain.dll] na pasta [bin] do projeto [tests]:

Em seguida, utilizando a aplicação [Nunit-Gui], carregamos esta DLL e executamos os testes:

Image

Os leitores que estiverem a visualizar este documento no ecrã verão que todos os testes foram bem-sucedidos. Consideraremos agora a camada [domain] como operacional.

1.4.5. Conclusão

Lembre-se de que queremos construir a seguinte aplicação web de três camadas:

O modelo M da nossa aplicação MVC já está escrito e testado. É-nos fornecido em duas DLLs [webarticles-dao.dll, webarticles-domain.dll]. Podemos passar para a última camada, a camada [web], que contém o controlador C e as vistas V. Vamos primeiro considerar um método apresentado no documento [Web Development with ASP.NET 1.1]

  • O controlador C é implementado por dois ficheiros [global.asax, main.aspx]
  • As vistas V são tratadas por páginas aspx

1.5. A camada [web]

A arquitetura MVC da aplicação web será a seguinte:

M = Modelo
As classes de negócio [domínio], as classes de acesso aos dados [DAO] e a fonte de dados
V = Visualizações
as páginas ASPX
C = Controlador
Todas as solicitações de clientes HTTP passam pelos dois controladores a seguir:
global.asax: trata de eventos relacionados com o arranque inicial da aplicação
main.aspx: processa cada pedido do cliente individualmente

1.5.1. As vistas

As vistas correspondem às apresentadas no início deste documento:

LISTA
list.aspx
As visualizações estão localizadas na pasta [views] da aplicação
INFORMAÇÃO
info.aspx
CARRINHO
cart.aspx
CARRINHO VAZIO
empty-cart.aspx
ERROS
errors.aspx

1.5.2. Os controladores

Como mencionado, o controlador será composto por dois elementos:

  1. [global.asax, global.asax.vb]: utilizados principalmente para inicializar a aplicação e configurar o contexto para todos os dados a serem partilhados entre diferentes clientes
  2. [main.aspx, main.aspx.vb]: o controlador propriamente dito, que lida com os pedidos HTTP dos clientes.

As várias solicitações dos clientes serão enviadas para o controlador [main.aspx] e conterão um parâmetro chamado [action] que especifica a ação solicitada pelo cliente:

request
significado
ação do controlador
respostas possíveis
ação=lista
o cliente deseja a lista de
itens
- solicita a lista de itens da camada
profissão
- [LISTA]
- [ERROS]
ação=informação
O cliente solicita
informações sobre um dos
itens apresentados na vista
[LIST]
- solicita o item à camada de negócios
- [INFO]
- [ERROS]
ação=compra
O cliente compra um artigo
- solicita o artigo à camada de negócios
e adiciona-o ao carrinho do cliente
- [INFO] se houver erro na quantidade
- [LIST] se não houver erro
ação=remover compra
o cliente deseja remover um
artigo do seu carrinho
- recuperar o carrinho da sessão
e modifica-o
- [CARRINHO]
- [CARRINHO VAZIO]
- [ERROS]
ação=carrinho
O cliente deseja visualizar o seu
carrinho
- recupera o carrinho da sessão
- [CARRINHO DE COMPRAS]
- [CARRINHO VAZIO]
- [ERROS]
action=validate-cart
O cliente terminou as compras
e avança para o checkout
- Atualiza a base de dados com os níveis de stock
dos artigos comprados
- esvazia o carrinho do cliente dos artigos
cuja compra foi confirmada
- [LISTA]
- [ERROS]

1.5.3. Configuração da aplicação

O nosso objetivo será configurar a aplicação de forma a torná-la o mais flexível possível no que diz respeito a alterações como:

  1. alterações aos URLs das várias vistas
  2. alterações nas classes que implementam as interfaces [IArticlesDao] e [IArticlesDomain]
  3. alterações no SGBD, na base de dados ou na tabela de artigos

1.5.3.1. Alterações de URL

Os nomes dos URLs das vistas serão colocados no ficheiro de configuração [web.config] da aplicação, juntamente com alguns outros parâmetros:

<?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. Alterar as classes que implementam as interfaces

De acordo com o princípio das arquiteturas de três camadas, as camadas devem estar isoladas umas das outras. Este isolamento é alcançado da seguinte forma:

  • as camadas comunicam entre si através de interfaces, e não de classes concretas
  • o código de uma camada nunca instancia a própria classe de outra camada para a utilizar. Simplesmente solicita uma instância da implementação da interface a uma ferramenta externa — neste caso, o Spring — para a camada que pretende utilizar. Para tal, sabemos que não precisa de conhecer o nome da classe de implementação, mas apenas o nome do singleton do Spring para o qual pretende uma referência.

Na nossa aplicação, o Spring será configurado no ficheiro [web.config] da aplicação web da seguinte forma:

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

Para aceder à camada [business], uma classe na camada [web] pode solicitar o singleton [articlesDomain]. O Spring irá então instanciar um objeto do tipo [istia.st.articles.domain.AchatsArticles]. Para esta instanciação, é necessário um objeto do tipo [articlesDao], ou seja, um objeto do tipo [istia.st.articles.dao.ArticlesDaoArrayList]. O Spring irá então instanciar esse objeto. No final da operação, a camada [web] que solicitou o singleton [articlesDomain] dispõe de toda a cadeia que a liga à fonte de dados:

1.5.3.3. Alterações relacionadas com o SGBD ou a base de dados

Este ponto será ignorado aqui, uma vez que estamos a trabalhar numa aplicação de teste sem um DBMS. Abordaremos a implementação de uma camada [dao] baseada num DBMS numa secção posterior.

1.5.4. A biblioteca de tags <asp:>

Considere a vista [ERRORS], que apresenta uma lista de erros:

Image

A vista [ERRORS] é responsável por exibir uma lista de erros que o controlador [main.aspx] colocou no contexto da solicitação sob o nome [context.Items("errors")]. Existem várias maneiras de escrever uma página deste tipo. Aqui, estamos interessados apenas na parte de exibição de erros.

Lembre-se de que uma página ASPX possui uma camada de apresentação HTML e uma camada de código .NET que prepara os dados que a camada de apresentação deve exibir. Estas duas partes podem estar no mesmo ficheiro [aspx] (solução WebMatrix) ou em dois ficheiros: [aspx] para a apresentação, [aspx.vb] para o código. A última solução é a utilizada pelo Visual Studio. Para complicar as coisas, a camada de apresentação HTML também pode conter código .NET, o que tende a esbater a separação entre as camadas [controlador] e [apresentação] da vista. Esta abordagem é geralmente fortemente desaconselhada. Remover todo o código da secção [apresentação] exigia a criação de bibliotecas de tags. Estas «escondem» o código sob o disfarce de tags semelhantes às tags HTML. Apresentamos duas soluções possíveis para a página [ERRORS].

A nossa primeira solução utiliza código .NET na secção [apresentação] da página. A página ASPX recupera a lista de erros presentes no pedido na sua secção de controlador [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

e, em seguida, exibi-los na secção [presentation, 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>

A segunda solução utiliza a tag <asp:repeater> da biblioteca de tags <asp:> do ASP.NET. Se criar uma página ASPX graficamente, esta tag está disponível como um controlo de servidor que pode arrastar para o formulário de design. Se escrever o código ASPX manualmente, pode consultar a biblioteca de tags.

Com a biblioteca de tags <asp:>, o código ASPX para a vista [ERRORS] anterior passa a ser o seguinte:

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

O

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

A tag é utilizada para repetir um modelo HTML nos vários itens de uma fonte de dados. Os seus vários elementos são os seguintes:

HeaderTemplate
o modelo HTML a apresentar antes dos elementos da fonte de dados serem apresentados
ItemTemplate
o modelo HTML a repetir para cada item na fonte de dados. A expressão [<%# Container.DataItem %>] é utilizada para apresentar o valor do item atual na fonte de dados
FooterTemplate
o modelo HTML a apresentar após a exibição dos itens da fonte de dados

A fonte de dados está associada à tag, normalmente na secção [controller] da página:

        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

Esta ligação também pode ser feita durante o design da página se a fonte de dados já for conhecida, como um banco de dados existente.

Nas nossas vistas, utilizaremos outra tag: <asp:datagrid>, que nos permite apresentar uma fonte de dados sob a forma de tabela.

1.5.5. Estrutura da solução do Visual Studio para a aplicação [webarticles]

Uma aplicação web é um puzzle com muitas peças. Atribuir-lhe uma arquitetura MVC geralmente aumenta o número dessas peças. A estrutura da aplicação [webarticles] no [Visual Studio] é a seguinte:

  

Comentários:

  • O projeto [web] é do tipo [Biblioteca de Classes] e não do tipo [Aplicação Web ASP.NET], como seria de esperar. O tipo [Aplicação Web ASP.NET] requer a presença do servidor web IIS na máquina de desenvolvimento ou numa máquina remota. O servidor IIS não está incluído por predefinição nas máquinas com Windows XP Home Edition. No entanto, muitos computadores são vendidos com esta versão. Para permitir que os leitores que utilizam o Windows XP implementem a aplicação em estudo, utilizaremos o servidor Web Cassini (ver apêndice), disponível gratuitamente na Microsoft, e substituiremos o projeto [Aplicação Web ASP.NET] por um projeto [Biblioteca de Classes]. Isto acarreta algumas desvantagens, que são explicadas nos apêndices.
  • As DLLs utilizadas pela aplicação são as seguintes:
webarticles-dao.dll
contém as classes da camada de acesso aos dados
webarticles-domain.dll
contém as classes da camada de negócios
Spring.Core.dll
contém as classes Spring que nos permitem integrar as camadas web, domínio e DAO
log4net.dll
classes de registo — utilizadas pelo Spring

Estas DLLs são colocadas na pasta [bin] e adicionadas às referências do projeto.

1.5.6. As vistas ASPX

Conforme recomendado anteriormente, utilizaremos a biblioteca de tags <asp:> nas nossas vistas ASPX.

1.5.6.1. O componente de utilizador [entete.ascx]

Para garantir a consistência entre as diferentes vistas, estas partilharão o mesmo cabeçalho, que exibe o nome da aplicação juntamente com o menu:

O menu é dinâmico e definido pelo controlador. O controlador inclui um atributo-chave «actions» no pedido enviado para a página ASPX, com um valor associado que consiste numa matriz de elementos Hashtable(). Cada elemento desta matriz é um dicionário destinado a gerar uma opção do menu do cabeçalho. Cada dicionário tem duas chaves:

  • href: o URL associado à opção do menu
  • link: o texto do menu

Vamos transformar o cabeçalho num controlo de utilizador. Um controlo de utilizador encapsula uma secção de uma página (layout e código associado) num componente que pode depois ser reutilizado noutras páginas. Aqui, queremos reutilizar o componente [entete] noutras vistas da aplicação. O código de apresentação estará em [entete.ascx] e o código de controlo associado em [entete.ascx.vb]. O código de apresentação utilizará um componente <asp:repeater> para apresentar a tabela de opções do menu:

N.º
tipo
nome
função
1
repetidor
rptMenu
fonte de dados: uma matriz de dicionários
com duas chaves: href, link
exibir opções do menu

O código de apresentação da página será o seguinte:

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

Comentários:

  • O componente [repeater] é definido nas linhas 6–14
  • Cada elemento na fonte de dados associada ao repetidor é um dicionário com duas chaves: href (linha 9) e link (linha 10)

O código de controlo associado será o seguinte:

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

Comentários:

  • O componente [EnteteWebArticles] possui uma propriedade [actions] pública e de gravação exclusiva - linha 7
  • Esta propriedade permite que o componente <asp:repeater> denominado [rptMenu] — linha 10 — seja vinculado à matriz de opções calculada pelo controlador da aplicação — linhas 11–12.

As outras vistas na aplicação utilizarão o cabeçalho definido por [entete.ascx]. A página [erreurs.aspx], por exemplo, incluirá o cabeçalho utilizando o seguinte código:

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

Comentários:

  • A linha 1 especifica que a tag <WA:entete> deve estar associada ao componente definido pelo ficheiro [entete.ascx]. Os atributos [TagPrefix] e [TagName] são opcionais.
  • Depois de feito isto, o componente é inserido no código de apresentação da página através da linha 9. Em tempo de execução, esta tag irá incluir o código da página [entete.ascx] na página ASPX que a contém. O código de controlo [erreurs.aspx.vb] irá tratar da inicialização deste componente. Pode fazê-lo da seguinte forma:
    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

Comentários:

  • A linha 6 cria um objeto do tipo [EnteteWebArticles], que é o tipo do componente que está a ser criado
  • A linha 11 inicializa a propriedade [actions] deste objeto

1.5.6.2. A vista [liste.aspx]

1.5.6.2.1. Introdução

Esta vista apresenta a lista de artigos disponíveis para venda:

É apresentada na sequência de um pedido para /main?action=list ou /main?action=cartvalidation. Os elementos do pedido do controlador são os seguintes:

ações
Objeto Hashtable() - a matriz de opções do menu
listarticles
ArrayList de objetos do tipo [Item]
mensagem
Objeto String - mensagem a exibir na parte inferior da página

Cada link [Info] na tabela HTML de artigos tem um URL no formato [?action=info&id=ID], em que ID é o campo id do artigo exibido.

1.5.6.2.2. Componentes da página
N.º
tipo
nome
função
1
componente de utilizador
cabeçalho
cabeçalho de exibição
2
DataGrid
DataGridArtigos
3 - coluna relacionada: cabeçalho: Nome, campo: nome
4 - coluna relacionada: cabeçalho: Preço, campo: preço
5 - coluna de hipertexto: texto: Informações, campo URL: id,
formato de URL: /webarticles/main.aspx?action=info&id={0}
exibir itens à venda
6
rótulo
lblMessage
exibir uma mensagem

Vamos rever como definir estas propriedades:

  • No Visual Studio, selecione o [DataGrid] para aceder à sua folha de propriedades:

Image

  • Utilize o link [AutoFormat] acima para gerir o layout da grelha apresentada
  • e o link [Property Generator] para gerir o seu conteúdo
1.5.6.2.3. Código de apresentação [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>

Comentários:

  • A linha 9 define o cabeçalho da página
  • As linhas 12–24 definem as propriedades do [DataGrid]
  • A linha 26 define o rótulo [lblMessage]
1.5.6.2.4. Código do controlador [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

Comentários:

  • Os componentes da página aparecem nas linhas 13–15. Note-se que foi necessário criar um objeto [EnteteWebArticles] utilizando o operador [new], ao passo que isso não foi necessário com os outros componentes. Sem esta criação explícita, ocorreu um erro de tempo de execução indicando que o objeto [entete] não referenciava nada. Este ponto merece uma investigação mais aprofundada. Ainda não foi investigado.
  • A tabela de opções do menu de cabeçalho é recuperada do contexto para inicializar o componente [entete] da página — linha 20
  • A lista de artigos é obtida do contexto — linha 22
  • para inicializar o componente [DataGridArticles] — linhas 24–27
  • O componente [lblMessage] é inicializado com uma mensagem colocada no contexto — linha 29

1.5.6.3. A vista [infos.aspx]

1.5.6.3.1. Introdução

Esta vista apresenta informações sobre um item e permite também a sua compra:

Image

É apresentada na sequência de um pedido /main?action=infos&id=ID ou de um pedido /main?action=achat&id=ID quando a quantidade comprada está incorreta. Os parâmetros de pedido do controlador são os seguintes:

ações
Objeto Hashtable() - a matriz de opções do menu
item
objeto do tipo [Artigo] - item a exibir
msg
Objeto String - mensagem a exibir em caso de erro com a quantidade
qty
Objeto String - valor a exibir no campo de entrada [Qty]

Os campos [msg] e [qte] são utilizados em caso de erro de introdução de dados relativo à quantidade:

Image

Esta página contém um formulário que é enviado através do botão [Comprar]. O URL de destino para o pedido POST é [?action=purchase&id=ID], em que ID é o ID do artigo adquirido.

1.5.6.3.2. Componentes da página
N.º
tipo
nome
função
1
componente de utilizador
cabeçalho
cabeçalho de exibição
2
literal
litID
exibir número do item
3 a 6
DataGrid
DataGridArticle
3 - coluna relacionada: cabeçalho: Nome, campo: nome
4 - coluna relacionada: cabeçalho: Preço, campo: preço
5 - coluna relacionada: cabeçalho: Stock atual, campo: stockAtual
6 - coluna relacionada: cabeçalho: Stock mínimo, campo: stockMinimum
exibir um item
7
Enviar HTML
 
Enviar o formulário
8
Entrada HTML
runat=server
txtQty
Introduza a quantidade comprada
9
label
lblMsgQte
qualquer mensagem de erro
1.5.6.3.3. O código de apresentação [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>

Comentários:

  • o cabeçalho está incluído na página - linha 9
  • O literal [litId] é definido na linha 10
  • O DataGrid [DataGridArticles] é definido nas linhas 12–34
  • O formulário é definido nas linhas 36–46. É do tipo POST.
  • O destino POST é fornecido por uma variável [strAction] - linha 36. Esta variável deve ser definida pelo controlador.
  • O campo de entrada para a quantidade comprada é definido na linha 41. É um componente HTML do lado do servidor (runat=server). No lado do código, é acedido através de um objeto.
  • A linha 42 define o rótulo [lblMsgQte], que conterá quaisquer mensagens de erro relativas à quantidade introduzida
1.5.6.3.4. O código de controlo [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

Comentários:

  • Os componentes da página estão definidos nas linhas 10–14
  • A classe define uma propriedade pública [strAction] utilizada para definir o destino POST do formulário - linhas 17-25
  • O artigo a ser exibido é recuperado do contexto da aplicação — linha 30
  • A matriz de opções do menu de cabeçalho é recuperada do contexto para inicializar o componente [entete] da página — linha 32
  • linhas 33–39: o componente [DataGridArticle] é vinculado a uma fonte de dados do tipo [ArrayList] contendo apenas o artigo recuperado na linha 30
  • os componentes [lblMsgQte, txtQte] são inicializados com informações retiradas do contexto - linhas 42-45
  • a propriedade [straction] também é inicializada com informações retiradas do contexto — linha 47. Esta variável é utilizada para gerar o atributo [action] do formulário HTML presente na página:
        <form method="post" action="<%=strAction%>">
....
        </form>

1.5.6.4. A vista [panier.aspx]

1.5.6.4.1. Introdução

Esta vista apresenta o conteúdo do carrinho de compras:

Image

É apresentada em resposta a um pedido como /main?action=cart ou /main?action=cancelpurchase&id=ID. Os parâmetros de pedido do controlador são os seguintes:

ações
object Hashtable() - o conjunto de opções do menu
cart
objeto do tipo [Cart] - o carrinho a apresentar

Cada link [Remover] na tabela HTML dos itens do carrinho de compras tem um URL no formato [?action=removeitem&id=ID], em que ID é o campo [id] do item a ser removido do carrinho.

1.5.6.4.2. Componentes da página
N.º
tipo
nome
função
1
componente de utilizador
cabeçalho
cabeçalho de exibição
2
DataGrid
DataGridCompras
3 - coluna relacionada - cabeçalho: Item, campo: nome
4 - coluna relacionada - cabeçalho: Qtd., campo: qtd.
5 - coluna relacionada - cabeçalho: Preço, campo: preço
6 - coluna relacionada - cabeçalho: Total, campo: total, formatação {0:C}
7 - coluna de hipertexto - Texto: Remover, URL: id, formato da URL: /webarticles/main.aspx?action=retirerachat&id={0}
exibir a lista de itens comprados
8
rótulo
lblTotal
exibir o valor a pagar
1.5.6.4.3. O código de apresentação [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>

Comentários

  • A linha 9 inclui o cabeçalho
  • As linhas 12–27 definem o componente [DataGridAchats]
  • Linha 29: o componente [lblTotal] é definido
1.5.6.4.4. O código de controlo [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

Comentários:

  • Os componentes da página são declarados nas linhas 11–13
  • A inicialização do componente [header] é idêntica à encontrada nas páginas já estudadas — linha 17
  • O carrinho de compras a ser exibido é recuperado da sessão — linha 19
  • A exibição deste carrinho de compras utilizando o componente [DataGridAchats] coloca problemas. A dificuldade decorre da inicialização do componente. Vamos rever as suas colunas:
    • Coluna [Article] associada ao campo [name] na fonte de dados
    • Coluna [Qty] associada ao campo [qty] na fonte de dados
    • Coluna [Price] associada ao campo [price] na fonte de dados
    • Coluna [Total] associada ao campo [total] na fonte de dados

A fonte de dados que temos é o carrinho de compras e a sua lista de compras. Esta última servirá como fonte de dados para o [DataGrid]. No entanto, os objetos [Purchase] que irão preencher as linhas do [DataGrid] não possuem as propriedades [name, qty, price, total] esperadas pelo [DataGrid]. Por isso, criamos aqui, especificamente para o [DataGrid], uma fonte de dados cujos elementos têm as características esperadas pelo [DataGrid]. Estes elementos serão do tipo [PurchaseLine], uma classe criada para este fim e derivada da classe [Purchase] — linhas 36–66

  • Uma vez definida a classe [PurchaseLine], a fonte de dados para [DataGridPurchases] é construída a partir do carrinho de compras encontrado na sessão — linhas 20–30
  • O valor total da compra é exibido utilizando a propriedade [totalPanier] da classe [Panier] — linha 32

1.5.6.5. A vista [emptyCart.aspx]

1.5.6.5.1. Introdução

Esta vista apresenta informações que indicam que o carrinho de compras está vazio:

Image

É apresentado após um pedido para /main?action=panier ou /main?action=retirerachat&id=ID. Os parâmetros do pedido do controlador são os seguintes:

ações
Objeto Hashtable() - a matriz de opções do menu
1.5.6.5.2. Componentes da página
N.º
tipo
nome
função
1
componente de utilizador
cabeçalho
cabeçalho de exibição
1.5.6.5.3. O código de apresentação [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>

Comentários:

  • O cabeçalho está incluído na linha 9
1.5.6.5.4. O código de controlo [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

Comentários:

  • Inicializamos simplesmente o único componente dinâmico da página - linha 10

1.5.6.6. A vista [errors.aspx]

1.5.6.6.1. Introdução

Esta vista é apresentada em caso de erros:

Image

É apresentada após qualquer pedido que resulte num erro, exceto no caso da ação de compra com uma quantidade incorreta, que é tratada pela vista [INFOS]. Os elementos do pedido do controlador são os seguintes:

ações
Objeto Hashtable() - a matriz de opções do menu
erros
ArrayList de objetos [String] que representam as mensagens de erro a exibir
1.5.6.6.2. Componentes da página
N.º
tipo
nome
função
1
componente de utilizador
cabeçalho
cabeçalho de exibição
2
repetidor
rptErrors
exibir a lista de erros
1.5.6.6.3. O código de apresentação [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>

Comentários:

  • O cabeçalho é definido na linha 9
  • O componente [rptErrors] é definido nas linhas 13–19. O seu conteúdo provém de uma fonte de dados do tipo [ArrayList] que contém objetos [String].
1.5.6.6.4. O código de controlo [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

Comentários:

  • O componente [header] é inicializado como habitualmente, nas linhas 9 e 14
  • O componente [rptErrors] é inicializado com a lista de erros [ArrayList] encontrada no contexto — linhas 16–19

1.5.7. Os controladores global.asax e main.aspx

Ainda precisamos de escrever o núcleo da nossa aplicação web: o controlador. A sua função é:

  • recuperar o pedido do cliente,
  • processar a ação solicitada pelo cliente utilizando as classes de negócio,
  • enviar a vista apropriada em resposta.

1.5.7.1. O controlador [global.asax.vb]

Quando a aplicação recebe a sua primeira solicitação, o procedimento [Application_Start] no ficheiro [global.asax.vb] é executado. Isto acontecerá apenas uma vez. O objetivo do procedimento [Application_Start] é inicializar os objetos necessários à aplicação web, que serão partilhados em modo de leitura apenas por todas as threads do cliente. Estes objetos partilhados podem ser colocados em dois locais:

  • os campos privados do controlador
  • o contexto de execução da aplicação (Application)

O método [Application_Start] no ficheiro [global.asax.vb] irá realizar as seguintes ações:

  • verificar o ficheiro [web.config] para obter os parâmetros necessários para que a aplicação funcione corretamente. Estes foram descritos na secção 1.5.3.
  • colocará uma lista de quaisquer erros no contexto da aplicação na forma de um objeto [ArrayList errors]. Esta lista estará vazia se não houver erros, mas existirá de qualquer forma.
  • Se houver erros, o método [Application_Start] pára aí. Caso contrário, solicita uma referência a um singleton do tipo [IArticlesDomain], que será o objeto de negócio que o controlador utilizará para as suas necessidades. Conforme explicado em 1.5.3.2, o controlador solicitará este singleton à estrutura Spring. Esta operação de instanciação pode resultar em vários erros. Se for esse o caso, estes erros serão novamente armazenados no objeto [errors] do contexto da aplicação.

O controlador [global.asax.vb] possui um procedimento [Session_Start] que é executado sempre que um novo cliente chega. Neste procedimento, iremos criar um carrinho de compras vazio para o cliente. Este carrinho de compras será mantido ao longo de todas as solicitações deste cliente específico. O código poderia ser o seguinte:

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

Comentários:

  • Os parâmetros esperados no [web.config] são definidos numa matriz - linha 18
  • Estes são procurados no [web.config]. Se estiverem presentes, são armazenados no contexto da aplicação; caso contrário, é registado um erro na lista de erros [errors] — linhas 21-33
  • Se não houver erros, é solicitada ao Spring uma referência ao singleton [articlesDomain], que gere o acesso à camada [domain] da aplicação - linhas 35-47. Quaisquer erros são registados em [errors].
  • Os erros são registados no contexto da aplicação - linha 49
  • O procedimento é encerrado se houver algum erro - linha 51
  • Criamos uma matriz de três dicionários. Cada um tem duas chaves: href e link. Esta matriz representa as três opções de menu possíveis — linhas 52–71
  • Esta matriz é armazenada no contexto da aplicação — linha 73
  • Para cada novo cliente, o procedimento [Session_Start] é executado. É criado um carrinho de compras vazio na sessão do cliente — linhas 78–81

1.5.7.2. O controlador [main.aspx.vb]

O controlador [main.aspx.vb] lida com todos os pedidos do cliente. Todos estes pedidos seguem o formato [/webarticles/main.aspx?action=XX]. Um pedido é processado da seguinte forma:

  • O objeto [errors] no contexto da aplicação é verificado. Se não estiver vazio, isso significa que ocorreram erros durante a inicialização da aplicação e que a aplicação não pode ser executada. Em resposta, a vista [ERRORS] é enviada.
  • O parâmetro [action] da solicitação será recuperado e verificado. Se não corresponder a uma ação conhecida, a visualização [ERRORS] é enviada com uma mensagem de erro apropriada.
  • Se o parâmetro [action] for válido, a solicitação do cliente é encaminhada para um procedimento específico da ação para processamento:
método
solicitação
processamento
respostas possíveis
doList
GET /main?action=list
- solicitar a lista de itens
da classe de negócios
- exibi-la
[LIST] ou [ERRORS]
doInfo
GET /main?action=info&id=ID
- recuperar o item com id=ID da
da classe de negócios
- exibi-lo
[INFO] ou [ERRORS]
doPurchase
POST /main?action=purchase&id=ID
- A quantidade comprada está incluída nos parâmetros enviados
- solicitar o item com id=ID da
classe de negócios
- adicione-o ao carrinho na
sessão do cliente
[LIST] ou [INFO] ou [ERRORS]
doRetirePurchase
GET /main?action=removePurchase&id=ID
- Remover o item com id=ID da
lista de compras na
sessão do cliente
[CART]
doCart
GET /main?action=cart
- Exibir a
sessão do cliente
[CART] ou [EMPTY_CART]
doCartValidation
GET /main?action=cartvalidation
- Reduzir os
níveis de stock de todos os artigos
na
[LIST] ou [ERRORS]
[LIST] ou [ERRORS]

O modelo do controlador [main.aspx.vb] poderia ter o seguinte aspeto:

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

Comentários:

  • A classe tem dois campos privados que serão partilhados entre os métodos — linhas 15–16:
    • articlesDomain: o singleton para aceder à camada [domain]
    • options: o array de dicionários que contém as opções do menu
  • O procedimento [Page_Load]:
    • inicializará os dois campos privados da classe
    • recupera o parâmetro [action] da solicitação e executa o método que lida com essa ação.

1.5.7.3. O método [Page_Load]

Este evento é o primeiro a ocorrer na página. O código é o seguinte:

        ' 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

Comentários:

  • Sempre que a página é carregada, garantimos que a inicialização da aplicação realizada pelo [global.asax] foi bem-sucedida.
  • Para tal, recuperamos a lista de erros colocada pelo [global.asax] a partir do contexto da aplicação — linha 4
  • Se esta lista não estiver vazia, exibimos a vista [ERRORS] — linhas 6–10
  • Recuperamos o singleton [articlesDomain] colocado por [global.asax] no contexto da aplicação e armazenamo-lo no campo privado [articlesDomain] para que esteja disponível para os vários métodos da classe — linha 12
  • Executamos uma operação semelhante com a matriz de opções de menu — linha 14
  • recuperamos o parâmetro [action] da solicitação — linha 16
  • Executamos o método correspondente à ação solicitada. Uma ação não especificada é tratada como a ação [list] — linhas 16–36

1.5.7.4. Tratamento da ação [list]

Isto envolve a exibição da lista de itens:

Image

O código é o seguinte:

        ' 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

Comentários:

  • Quaisquer erros são colocados numa [ArrayList] - linha 4
  • A lista de itens é solicitada ao singleton [articlesDomain] - linhas 5-12
  • Se houver erros, a vista [ERRORS] é renderizada - linhas 13-19
  • caso contrário, a vista [LIST] é devolvida - linhas 20-24

1.5.7.5. Tratamento da ação [info]

O cliente solicitou informações sobre um item específico:

Image

O código é o seguinte:

        ' 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

Comentários:

  • Quaisquer erros são colocados numa [ArrayList] - linha 4
  • O ID do item solicitado é recuperado da solicitação - linha 6
  • Este ID é validado. Deve existir e deve ser um número inteiro. Caso contrário, a vista [ERRORS] é apresentada com a mensagem de erro apropriada - linhas 7-27
  • Assim que o ID é verificado, o item é solicitado ao singleton [articlesDomain]. Se ocorrer uma exceção, a vista [ERRORS] é enviada - linhas 29-39
  • Se o item não for encontrado, a vista [ERRORS] é enviada — linhas 41–48
  • Se o item for encontrado, é colocado na sessão do utilizador e, em seguida, apresentado na vista [INFO] — linhas 50-56

1.5.7.6. Processamento da ação [purchase]

O cliente comprou o item exibido na vista [INFO].

Image

O código é o seguinte:

        ' 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

Comentários:

  • O item colocado na sessão é recuperado - linha 5
  • Se não estiver lá (a sessão pode ter expirado), a vista [LIST] é exibida - linhas 7-10
  • A quantidade comprada é recuperada da consulta - linha 12
  • A sua validade é verificada - linhas 13-29
  • se inválida, dependendo do caso, é apresentada a vista [LIST] - linha 16 ou a vista [INFO] - linhas 24-28
  • Se tudo estiver normal, a compra é adicionada ao carrinho - linhas 31-32
  • depois a vista [LIST] é enviada - linha 34

1.5.7.7. Processamento da ação [cart]

O cliente efetuou várias compras e pretende visualizar o carrinho de compras:

Image

O código é o seguinte:

        ' 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

Comentários:

  • Recuperamos o carrinho da sessão - linha 4. Não verificamos aqui se realmente recuperamos algo. Devemos fazê-lo porque a sessão pode ter expirado.
  • Se o carrinho estiver vazio, enviamos a vista [EMPTY CART] — linhas 6-10
  • Caso contrário, enviamos a vista [SHOPPINGCART] — linhas 11–14

1.5.7.8. Processamento da ação [removepurchase]

O cliente pretende remover uma compra do seu carrinho:

Image

O código é o seguinte:

        ' 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

Comentários:

  • Recuperar o carrinho de compras da sessão - linha 4. Não verificamos aqui se algo foi realmente recuperado. Isto deve ser feito porque a sessão pode ter expirado.
  • Recuperar o ID [id] do item a ser removido da consulta - linha 8.
  • A compra correspondente é removida do carrinho de compras - linha 10
  • A validade do ID do item comprado não foi verificada aqui. Se tiver um tipo inválido, ocorrerá uma exceção que será tratada nas linhas 11–13. Se for válido mas não existir, o método [cart.remove] — linha 10 — não faz nada.
  • O novo carrinho é exibido - linha 16

1.5.7.9. Processamento da ação [validateCart]

O cliente deseja confirmar o seu carrinho:

Image

O código é o seguinte:

        ' 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

Comentários:

  • Recuperamos o carrinho da sessão - linha 6. Não verificamos aqui se realmente recuperamos algo. Devemos fazer isso porque a sessão pode ter expirado.
  • Se a sessão tiver expirado, teremos um ponteiro [nothing] para o carrinho, e o método [buy] — linha 9 — lançará uma exceção, e a vista [ERRORS] será exibida. No entanto, a mensagem de erro não será clara para o utilizador.
  • Linhas 8–16: Tentamos validar o carrinho de compras recuperado da sessão. Algumas compras podem falhar se a quantidade solicitada exceder o stock do artigo solicitado. Estes casos são armazenados pelo método [buy] numa lista de erros, que é recuperada na linha 11.
  • Se houver erros, a vista [ERRORS] é enviada — linhas 18–23
  • caso contrário, a vista [LIST] é enviada — linhas 25–26

1.6. Conclusão

Aqui, desenvolvemos uma aplicação utilizando o padrão MVC. Em abril de 2005, não parecem existir quaisquer frameworks profissionais de desenvolvimento MVC para ASP.NET comparáveis aos disponíveis para Java (Struts, Spring, etc.). Espera-se que o projeto [Spring.net] lance um em breve. Até lá, o método descrito acima fornece uma abordagem viável de desenvolvimento MVC para aplicações de média dimensão.