Skip to content

1. Parte 1

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

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

1.1. Introduction

Objetivos do artigo:

  • escrever uma aplicação web de 3 camadas [interface utilisateur, métier, accès aux données]
  • configurar a aplicação com o Spring IOC
  • escrever diferentes versões, alterando a implementação de uma ou outra das três camadas.

Ferramentas utilizadas:

  • Visual Studio.net para o desenvolvimento — ver o parágrafo 3.1 do anexo;
  • Servidor web Cassini para a execução — ver anexo, parágrafo 3.2;
  • Nunit para os testes unitários – ver anexo, parágrafo 3.4;
  • Spring para a integração e configuração das camadas da aplicação web — ver anexo, parágrafo 3.3;

Numa escala iniciante-intermédio-avançado, este documento situa-se na parte [intermédiaire-avancé]. A sua compreensão requer vários pré-requisitos. Alguns deles podem ser adquiridos em documentos que escrevi. Nesse caso, faço referência aos mesmos. É evidente que se trata apenas de uma sugestão e que o leitor pode utilizar os seus documentos preferidos.

Este documento segue a linha condutora de um documento escrito para Java [Arquiteturas de três camadas e arquiteturas MVC com Struts, Spring e Java (2005)]. Estamos a construir em VB.NET a aplicação web MVC de três camadas, escrita em Java. A ideia que pretendemos transmitir aqui é que as plataformas de desenvolvimento Java e .NET são suficientemente semelhantes entre si para que as competências adquiridas numa destas duas áreas possam ser reutilizadas na outra.

Não parece existir uma solução de desenvolvimento MVC ASP.NET amplamente reconhecida. A solução que se segue retoma o método apresentado no documento [Desenvolvimento Web com ASP.NET 1.1 (2004)]. Embora esta solução tenha o mérito de utilizar conceitos comuns no desenvolvimento J2EE, não deve, no entanto, ser considerada mais do que aquilo que é: c.a.d, um método entre outros de desenvolvimento MVC. Assim que um método de desenvolvimento MVC em ASP.NET for amplamente aceite, será então necessário adotar este último. 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 elementos de uma aplicação web simplificada de comércio eletrónico. Esta permitirá aos clientes da web:

  • consultar uma lista de artigos provenientes de uma base de dados
  • colocar alguns deles num cesto de compras eletrónico
  • confirmar o cesto. Esta confirmação terá como único efeito atualizar, na base de dados, os stocks dos artigos comprados.

As diferentes vistas apresentadas ao utilizador serão as seguintes:

- a vista «LISTE», que apresenta uma lista dos artigos à venda
- a vista [INFOS], que fornece informações adicionais sobre um produto:
  • as vistas [PANIER] e [PANIERVIDE], que apresentam o conteúdo do cesto de compras do cliente
  • a vista [ERREURS], que sinaliza qualquer erro da aplicação

Image

1.3. Arquitetura geral da aplicação

Pretende-se 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 é realizada pelo Spring
  • cada camada está sujeita a espaços de nomes separados: web (camada UI), domain (camada de negócio) e dao (camada de acesso aos dados).

A aplicação seguirá uma arquitetura MVC (Modelo - Vista - Controlador). Se retomarmos o esquema em camadas acima, a arquitetura MVC integra-se nele da seguinte forma:

O processamento de um pedido de um cliente decorre de acordo com as seguintes etapas:

  1. o cliente envia um pedido ao controlador. Este controlador será, neste caso, uma página .aspx à qual será atribuída uma função específica. Esta página recebe todos os pedidos dos clientes. É a porta de entrada da aplicação. É o C de MVC.
  1. o controlador processa essa solicitação. Para tal, pode necessitar da ajuda da camada de negócio, a que se chama modelo M na estrutura MVC.
  2. O controlador recebe uma resposta da camada de negócio. O pedido do cliente foi processado. Este pode dar origem a várias respostas possíveis. Um exemplo clássico é
    • uma página de erros, caso a solicitação não tenha podido ser processada corretamente
    • uma página de confirmação, caso contrário
  3. o controlador escolhe a resposta (= vista) a enviar ao cliente. Esta é, na maioria das vezes, uma página que contém elementos dinâmicos. O controlador fornece esses elementos à vista.
  4. A vista é enviada ao cliente. É o V de MVC.

1.4. O modelo

Analisamos aqui o M de MVC. O modelo é aqui constituído pelos seguintes elementos:

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

1.4.1. A base de dados

A base de dados contém apenas uma tabela denominada ARTICLES. Esta foi gerada com 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
);
/* restrições */
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);
/* chave primária */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
chave primária que identifica um artigo de forma única
nom
nome do artigo
prix
o seu preço
stockactuel
stock atual
stockminimum
o nível de stock abaixo do qual deve ser efetuada uma encomenda de reabastecimento

1.4.2. Os espaços de nomes do modelo

O modelo M é aqui fornecido sob a forma de dois espaços de nomes:

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

Cada um destes espaços de nomes será gerado num ficheiro «assembly» que lhe é próprio:

assembly
contenu
rôle
webarticles-dao
- [IArticlesDao]: a interface de acesso à camada [dao].
Esta é a única interface que a camada [domain] vê. Não vê nenhuma outra.
- [Article]: classe que define um artigo
- [ArticlesDaoArrayList]: classe de implementação da
da interface [IArticlesDao] com uma classe [ArrayList]
camada de acesso aos dados
- encontra-se inteiramente na camada
[dao] da arquitetura de três camadas da
aplicação web
webarticles-domain
- [IArticlesDomain]: a interface de acesso à camada [domain]. É a única interface que a camada web vê. Não vê nenhuma outra.
- [AchatsArticles]: uma classe que implementa [IArticlesDomain]
- [Achat]: classe que representa a compra de um cliente
- [Panier]: classe que representa o conjunto de compras de um cliente
representa o modelo das compras na
Web - encontra-se inteiramente na
camada [domain] da arquitetura
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 de acesso à camada [dao]

  • [Article]: classe que define um artigo

  • [ArticlesDaoArrayList]: classe de implementação da interface [IArticlesDao] com uma classe [ArrayList]

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

Image

Comentários:

  • o projeto [dao] é do tipo [bibliothèque de classes]
  • as classes foram colocadas numa árvore cuja raiz é a pasta [istia]. Estão todas no espaço de nomes [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

         ' campos privados
        Private _id As Integer
        Private _nom As String
        Private _prix As Double
        Private _stockactuel As Integer
        Private _stockminimum As Integer

         ' ID do artigo
        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

         ' nome do artigo
        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

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

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

         ' stock mínimo do artigo
        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

         ' fabricante por predefinição
        Public Sub New()
        End Sub

         ' fabricante com propriedades
        Public Sub New(ByVal id As Integer, ByVal nom As String, ByVal prix As Double, ByVal stockactuel As Integer, ByVal stockminimum As Integer)
            Me.id = id
            Me.nom = nom
            Me.prix = prix
            Me.stockactuel = stockactuel
            Me.stockminimum = stockminimum
        End Sub

         ' método de identificação do artigo
        Public Overrides Function ToString() As String
            Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
        End Function
    End Class
End Namespace

Esta classe oferece:

  1. um construtor que permite definir as 5 informações de um artigo: [id, nom, prix, stockactuel, stockminimum]
  2. propriedades públicas que permitem ler e escrever as 5 informações.
  3. uma verificação dos dados inseridos no artigo. Em caso de dados incorretos, é lançada uma exceção.
  4. um método toString que permite obter o valor de um artigo na forma de uma cadeia de caracteres. Isto é frequentemente útil para a depuração de uma aplicação.

1.4.3.2. A interface [IArticlesDao]

A interface [IArticlesDao] está definida da seguinte forma:

Imports System
Imports System.Collections

Namespace istia.st.articles.dao

    Public Interface IArticlesDao
         ' lista de todos os artigos
        Function getAllArticles() As IList
         ' adiciona um artigo
        Function ajouteArticle(ByVal unArticle As Article) As Integer
         ' elimina um artigo
        Function supprimeArticle(ByVal idArticle As Integer) As Integer
         ' altera um artigo
        Function modifieArticle(ByVal unArticle As Article) As Integer
         ' procura um artigo
        Function getArticleById(ByVal idArticle As Integer) As Article
         ' elimina todos os artigos
        Sub clearAllArticles()
         ' altera o stock de um artigo
        Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
    End Interface
End Namespace

A função dos diferentes métodos da interface é a seguinte:

getAllArticles
retorna todos os artigos da fonte de dados
clearAllArticles
esvazia a fonte de dados
getArticleById
retorna o objeto [Article] identificado pela sua chave primária
ajouteArticle
permite adicionar um artigo à fonte de dados
modifieArticle
permite modificar um artigo da fonte de dados
supprimerArticle
permite eliminar um artigo da fonte de dados
changerStockArticle
permite alterar o stock de um artigo da fonte de dados

A interface disponibiliza aos programas clientes um certo número de métodos definidos apenas pelas suas assinaturas. Não se ocupa da forma como esses métodos serão efetivamente implementados. Isto confere flexibilidade a uma aplicação. O programa cliente efetua as suas chamadas a uma interface e não a uma implementação específica da mesma.

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 é relevante e não a sua classe de implementação, vamos, em primeiro lugar, implementar a fonte de dados através de um objeto simples [ArrayList]. Posteriormente, apresentaremos uma solução baseada em SGBD.

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

         ' fabricante por predefinição
        Public Sub New()
             ' fabricamos alguns artigos
            For i As Integer = 1 To nbArticles
                articles.Add(New Article(i, "article" + i.ToString, i * 10, i * 10, i * 10))
            Next
        End Sub

         ' lista de todos os artigos
        Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
             ' retorna a lista de artigos
            SyncLock Me
                Return articles
            End SyncLock
        End Function

         ' eliminação de todos os artigos
        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
             ' esvazia a lista de artigos
            SyncLock Me
                articles.Clear()
            End SyncLock
        End Sub

         ' obter um artigo identificado pela sua chave
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
             ' procurar o artigo na lista
            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

         ' adicionar um artigo à lista de artigos
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' o artigo é adicionado à lista de artigos
            SyncLock Me
                 ' verifica-se se já não existe
                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
                 ' adiciona-se o artigo
                articles.Add(unArticle)
                 ' retorna o resultado
                Return 1
            End SyncLock
        End Function

         ' alterar um artigo
        Public Function modifieArticle(ByVal articleNouveau As Article) As Integer Implements IArticlesDao.modifieArticle
             ' modifica-se um artigo
            SyncLock Me
                 ' verifica-se se existe
                Dim ipos As Integer = posArticle(articles, articleNouveau.id)
                 ' no caso de não existir
                If ipos = -1 Then Return 0
                 ' existe - altera-se
                articles(ipos) = articleNouveau
                 ' retorna-se o resultado
                Return 1
            End SyncLock
        End Function

         ' eliminar um artigo identificado pela sua chave
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
             ' eliminação de um artigo
            SyncLock Me
                 ' verifica-se se existe
                Dim ipos As Integer = posArticle(articles, idArticle)
                 ' no caso de não existir
                If ipos = -1 Then Return 0
                 ' existe - elimina-se
                articles.RemoveAt(ipos)
                 ' retornar o resultado
                Return 1
            End SyncLock
        End Function

         ' alterar o stock de um artigo identificado pela sua chave
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
             ' alterar o stock de um artigo
            SyncLock Me
                 ' verifica-se se existe
                Dim ipos As Integer = posArticle(articles, idArticle)
                 ' no caso de não existir
                If ipos = -1 Then Return 0
                 ' existe - altera-se o seu stock, se for possível
                Dim unArticle As Article = CType(articles(ipos), Article)
                 ' só se altera o stock se for suficiente
                If unArticle.stockactuel + mouvement >= 0 Then
                    unArticle.stockactuel += mouvement
                    Return 1
                Else
                    Return 0
                End If
            End SyncLock
        End Function

         ' procurar um artigo identificado pela sua chave
        Private Function posArticle(ByVal listArticles As ArrayList, ByVal idArticle As Integer) As Integer
             ' retorna a posição do artigo [idArticle] na lista ou -1 se não for encontrado
            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
             ' não encontrado
            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, por predefinição, 4 itens na fonte de dados.
  • 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 thread tem acesso a um determinado método.
  • O método [posArticle] permite determinar a posição [0..N] na fonte [ArrayList] de um artigo identificado pelo seu n.º. 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 [ajouteArticle] permite adicionar 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 [modifieArticle] permite modificar um artigo existente. Devolve o número de artigos modificados: 1 se o artigo existisse, 0 caso contrário.
  • O método [supprimeArticle] permite eliminar um artigo existente. Retorna o número de artigos eliminados: 1 se o artigo existisse, 0 caso contrário.
  • O método [getAllArticles] fornece a lista de todos os artigos
  • O método [getArticleById] permite obter um artigo identificado pelo seu n.º. Obtém-se o valor [nothing] se o artigo não existir.
  • O código não apresenta grandes dificuldades. Deixamos que o leitor o analise e compreenda.

1.4.3.4. Geração do assembly 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 da camada [dao]

Em Java, as classes são testadas com o framework [Junit]. Em .NET, o framework Nunit oferece as mesmas possibilidades de testes unitários:

Image

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

Image

Comentários:

  • o projeto [tests] é do tipo [bibliothèque de classes]
  • os testes [NUnit] requerem uma referência ao assembly [nunit.framework.dll]
  • A classe de teste [NUnit] obtém uma instância do objeto a testar através do Spring. Por isso, encontramos
    • na pasta [bin], os ficheiros de classes do Spring
    • em [References], 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 necessita do assembly [webarticles-dao.dll] da camada [dao]. Este foi colocado na pasta [bin] e a sua referência foi adicionada às referências do projeto.

Uma classe de teste [NUnit] necessita de acesso às classes do espaço de nomes [NUnit.Framework]. Por isso, encontra-se aí a seguinte instrução de importação:

Imports NUnit.Framework

O espaço de nomes [NUnit.Framework] encontra-se no «assembly» [nunit.framework.dll], que deve ser adicionado às referências do projeto:

O assembly [nunit.framework.dll] deve constar da lista apresentada se a instalação do [Nunit] tiver sido efetuada. Basta clicar duas vezes no assembly para o adicionar ao projeto:

Image

A classe de teste [NUnit] da camada [dao] poderia ser a seguinte:

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
         ' o objeto a testar
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
             ' recupera-se uma instância do criador de objetos Spring
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
             ' solicita-se a instanciação do objeto articlesdao
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
        End Sub

        <Test()> _
        Public Sub testGetAllArticles()
             ' verificação visual
            listArticles()
        End Sub

        <Test()> _
        Public Sub testClearAllArticles()
             ' eliminam-se todos os artigos
            articlesDao.clearAllArticles()
             ' solicita-se todos os artigos
            Dim articles As IList = articlesDao.getAllArticles
             ' verificação: deve haver 0
            Assert.AreEqual(0, articles.Count)
        End Sub

        <Test()> _
        Public Sub testAjouteArticle()
             ' eliminação de todos os artigos
            articlesDao.clearAllArticles()
             ' verificação: a tabela de artigos deve estar vazia
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
             ' adicionam-se dois artigos
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
             ' verificação: deve haver dois artigos
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
             ' verificação visual
            listArticles()
        End Sub

        <Test()> _
        Public Sub testSupprimeArticle()
             ' eliminação de todos os artigos
            articlesDao.clearAllArticles()
             ' verificação: a tabela de artigos deve estar vazia
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
             ' adicionam-se dois artigos
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
             ' verificação: deve haver 2 artigos
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
             ' elimina-se o artigo 4
            articlesDao.supprimeArticle(4)
             ' verificação: deve restar 1 artigo
            articles = articlesDao.getAllArticles
            Assert.AreEqual(1, articles.Count)
             ' verificação visual
            listArticles()
        End Sub

        <Test()> _
        Public Sub testModifieArticle()
             ' eliminação de todos os artigos
            articlesDao.clearAllArticles()
             ' verificação
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
             ' adição de 2 artigos
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
             ' verificação
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
             ' pesquisa do artigo 3
            Dim unArticle As Article = articlesDao.getArticleById(3)
             ' verificação
            Assert.AreEqual(unArticle.nom, "article3")
             ' pesquisa do artigo 4
            unArticle = articlesDao.getArticleById(4)
             ' verificação
            Assert.AreEqual(unArticle.nom, "article4")
             ' alteração do artigo 4
            articlesDao.modifieArticle(New Article(4, "article4", 44, 44, 44))
             ' verificação
            unArticle = articlesDao.getArticleById(4)
            Assert.AreEqual(unArticle.prix, 44, 0.000001)
             ' verificação visual
            listArticles()
        End Sub

        <Test()> _
        Public Sub testGetArticleById()
             ' eliminação de artigos
            articlesDao.clearAllArticles()
             ' verificação
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
             ' adição de 2 artigos
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
             ' verificação
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
             ' pesquisa do artigo 3
            Dim unArticle As Article = articlesDao.getArticleById(3)
             ' verificação
            Assert.AreEqual(unArticle.nom, "article3")
             ' pesquisa do artigo 4
            unArticle = articlesDao.getArticleById(4)
             ' verificação
            Assert.AreEqual(unArticle.nom, "article4")
        End Sub

         ' lista no ecrã
        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()
             ' eliminação de todos os artigos
            articlesDao.clearAllArticles()
             ' pesquisa de artigo 1
            Dim article As article = articlesDao.getArticleById(1)
             ' verificação
            Assert.IsNull(article)
             ' alteração de um artigo inexistente
            Dim i As Integer = articlesDao.modifieArticle(New article(1, "1", 1, 1, 1))
             ' não foi necessário alterar nenhuma linha
            Assert.AreEqual(i, 0)
             ' eliminação de artigo inexistente
            i = articlesDao.supprimeArticle(1)
             ' não foi necessário eliminar nenhuma linha
            Assert.AreEqual(0, i)
        End Sub

        <Test()> _
        Public Sub testChangerStockArticle()
             ' eliminação de todos os artigos
            articlesDao.clearAllArticles()
             ' adição de um artigo
            Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
            Assert.AreEqual(nbArticles, 1)
             ' adição de um artigo
            nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            Assert.AreEqual(nbArticles, 1)
             ' criação de 100 threads
            Dim taches(99) As Thread
            For i As Integer = 0 To taches.Length - 1
                 ' cria-se o tópico i
                taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
                 ' define-se o nome do thread
                taches(i).Name = "tache_" & i
                 ' inicia-se a execução do thread i
                taches(i).Start()
            Next
             ' aguarda-se o fim de todos os threads
            For i As Integer = 0 To taches.Length - 1
                taches(i).Join()
            Next
             ' verificações — o artigo 3 deve ter um stock de 1
            Dim unArticle As Article = articlesDao.getArticleById(3)
            Assert.AreEqual(unArticle.nom, "article3")
            Assert.AreEqual(1, unArticle.stockactuel)
             ' diminui-se o stock do artigo 4
            Dim erreur As Boolean = False
            Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
             ' verificação: o seu stock não deve ter mudado
            Assert.AreEqual(0, nbLignes)
             ' verificação visual
            listArticles()
        End Sub

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

    End Class
End Namespace

Comentários:

  • pretendíamos escrever um programa de teste para a interface [IArticlesDao] que fosse independente da classe de implementação da mesma. Por isso, utilizámos o Spring para ocultar do programa de teste o nome da classe de implementação.
  • O método de atributo <Setup()> obtém do Spring uma referência ao objeto [articlesdao] a ser testado. Este 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 indica o nome da classe de implementação [istia.st.articles.dao.ArticlesDaoArrayList] da interface [IArticlesDao] e onde a encontrar: [ webarticles-dao.dll]. Como a instanciação não requer 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 algumas explicações. Cria 100 threads encarregadas de diminuir o stock de um determinado artigo.
        <Test()> _
        Public Sub testChangerStockArticle()
             ' eliminação de todos os artigos
            articlesDao.clearAllArticles()
             ' adição de um artigo
            Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
            Assert.AreEqual(nbArticles, 1)
             ' adição de um artigo
            nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            Assert.AreEqual(nbArticles, 1)
             ' criação de 100 threads
            Dim taches(99) As Thread
            For i As Integer = 0 To taches.Length - 1
                 ' cria-se o tópico i
                taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
                 ' define-se o nome do thread
                taches(i).Name = "tache_" & i
                 ' inicia-se a execução do thread i
                taches(i).Start()
            Next
             ' aguarda-se o fim de todos os threads
            For i As Integer = 0 To taches.Length - 1
                taches(i).Join()
            Next
             ' verificações — o artigo 3 deve ter um stock de 1
            Dim unArticle As Article = articlesDao.getArticleById(3)
            Assert.AreEqual(unArticle.nom, "article3")
            Assert.AreEqual(1, unArticle.stockactuel)
             ' diminui-se o stock do artigo 4
            Dim erreur As Boolean = False
            Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
             ' verificação: o seu stock não deve ter mudado
            Assert.AreEqual(0, nbLignes)
             ' verificação visual
            listArticles()
        End Sub

O objetivo aqui é testar os acessos simultâneos à fonte de dados. O método responsável pela atualização do stock é o seguinte:

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

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

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

Além disso, ainda neste método, tentamos fazer com que o stock do artigo n.º 4 assuma um valor negativo. A tentativa deve falhar.

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

Em seguida, utilizando a aplicação [Nunit-Gui], carregamos este ficheiro 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 bem-sucedido (verde) ou falhou (vermelho). O leitor que visualizar este documento no ecrã poderá ver que todos os testes foram bem-sucedidos. Posteriormente, consideraremos que temos uma camada [dao] operacional.

1.4.4. A camada [domain]

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

  • [IArticlesDomain]: a interface de acesso à camada [domain]

  • [Achat]: classe que define uma compra

  • [Panier]: classe que define um cesto de compras

  • [AchatsArticles]: classe de implementação da interface [IArticlesDomain]

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

Image

Comentários:

  • o projeto [domain] é do tipo [bibliothèque de classes]
  • as classes foram colocadas numa árvore cuja raiz é a pasta [istia]. Estão todas no espaço de nomes [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] separa a camada [métier] da camada [web]. Esta última acede à camada [métier/domain] através desta interface, sem se preocupar com a classe que a implementa efetivamente. A interface define as seguintes ações para o acesso à camada de negócio:

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

Namespace istia.st.articles.domain
    Public Interface IArticlesDomain
         ' métodos
        Sub acheter(ByVal panier As Panier)
        Function getAllArticles() As IList
        Function getArticleById(ByVal idArticle As Integer) As Article
        ReadOnly Property erreurs() As ArrayList
    End Interface
End Namespace
Function getAllArticles() As IList
retorna a lista de objetos [Article] da fonte de dados associada
Function getArticleById(ByVal idArticle As Integer) As Article
retorna o objeto [Article] identificado por [idArticle]
acheter(ByVal panier As Panier)
valida o carrinho do cliente, deduzindo dos stocks dos artigos comprados a quantidade adquirida — pode falhar se o stock for insuficiente
ReadOnly Property erreurs() As ArrayList
retorna a lista de erros que ocorreram — vazia se não houver erros

1.4.4.2. A classe [Achat]

A classe [Achat] representa uma compra do cliente:

Imports istia.st.articles.dao

Namespace istia.st.articles.domain

    Public Class Achat

         ' campos privados
        Private _article As article
        Private _qte As Integer

         ' construtor por predefinição
        Public Sub New()
        End Sub

         ' construtor com parâmetros
        Public Sub New(ByVal unArticle As article, ByVal qte As Integer)
             ' através das propriedades
            Me.article = unArticle
            Me.qte = qte
        End Sub

         ' artigo comprado
        Public Property article() As article
            Get
                Return _article
            End Get
            Set(ByVal Value As article)
                _article = Value
            End Set
        End Property

         ' quantidade comprada
        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 da compra
        Public ReadOnly Property totalAchat() As Double
            Get
                Return _qte * _article.prix
            End Get
        End Property

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

End Namespace

Comentários:

  • a classe [Achat] possui as seguintes propriedades e métodos:
Public Property article() As article
o artigo adquirido
Public Property qte() As Integer
a quantidade comprada
Public ReadOnly Property totalAchat() As Double
o valor da compra
Public Overrides Function ToString() As String
cadeia de identificação do objeto
  • possui um construtor que permite inicializar as propriedades [article, qte] que definem uma compra.

1.4.4.3. A classe [Panier]

A classe [Panier] representa o conjunto de compras do cliente:

Namespace istia.st.articles.domain

Public Class Panier

     ' campos privados
    Private _achats As New ArrayList
        Private _totalPanier As Double = 0

     ' fabricante predefinido
    Public Sub New()
    End Sub

     ' lista de compras
    Public ReadOnly Property achats() As ArrayList
        Get
            Return _achats
        End Get
    End Property

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

     ' métodos
    Public Sub ajouter(ByVal unAchat As Achat)
         ' verifica se a compra já existe
        Dim iAchat As Integer = posAchat(unAchat.article.id)
        If iAchat <> -1 Then
             ' foi encontrada
                Dim achatCourant As Achat = CType(_achats(iAchat), Achat)
            achatCourant.qte += unAchat.qte
        Else
             ' não foi encontrada
            _achats.Add(unAchat)
        End If
         ' incrementa-se o total do carrinho
        _totalPanier += unAchat.totalAchat
    End Sub

     ' remover um produto
    Public Sub enlever(ByVal idAchat As Integer)
         ' procura-se o produto
            Dim iachat As Integer = posAchat(idAchat)
         ' se tiver sido encontrado, remove-se
        If iachat <> -1 Then
                Dim achatCourant As Achat = CType(_achats(iachat), Achat)
             ' retira-se do cesto
            _achats.RemoveAt(iachat)
             ' diminuir o total do carrinho
            _totalPanier -= achatCourant.totalAchat
        End If
    End Sub

    Private Function posAchat(ByVal idArticle As Integer) As Integer
         ' procura uma compra na lista de compras
         ' retorna a sua posição na lista ou -1 se não for encontrado
        Dim achatCourant As Achat
        Dim trouvé As Boolean = False
        Dim i As Integer = 0
            While Not trouvé AndAlso i < _achats.Count
                 ' compra atual
                achatCourant = CType(_achats(i), Achat)
                 ' comparação com o artigo procurado
                If achatCourant.article.id = idArticle Then
                    Return i
                End If
                 'compra seguinte
                i += 1
            End While
             ' não encontrado
            Return -1
    End Function

     ' função de identidade
    Public Overrides Function ToString() As String
        Return _achats.ToString
    End Function
End Class

End Namespace

Comentários:

  • a classe [Panier] possui as seguintes propriedades e métodos:
ReadOnly Property achats() As ArrayList
lista de compras do cliente — lista de objetos do tipo [Achat]
ajouter(ByVal unAchat As Achat)
adiciona uma compra à lista de compras
enlever(ByVal idAchat As Integer)
retira a compra do artigo idAchat
ReadOnly Property totalPanier() As Double
o valor total das compras no cesto
Function ToString() As String
retorna a cadeia de identificação do carrinho
  • O método [posAchat] é um método utilitário que permite obter a posição na lista de compras de uma compra identificada pelo n.º do artigo adquirido. 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 comprado. O método [posAchat] devolve -1 se a compra procurada não existir.
  • O método [ajouter] adiciona uma nova compra à lista de compras. Isto equivale a adicionar uma nova entrada à lista de compras, caso o artigo adquirido ainda não existisse na lista, ou a aumentar a quantidade adquirida, caso já existisse.
  • O método [enlever] permite remover uma compra identificada por um número da lista de compras. Se a compra não existir, o método não emite qualquer mensagem e não realiza nenhuma ação.
  • O valor das compras [totalPanier] é mantido à medida que as compras são adicionadas e removidas.

1.4.4.4. A classe [AchatsArticles]

A interface [IArticlesDomain] será implementada pela seguinte classe [AchatsArticles]:

Imports istia.st.articles.dao

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

         'campos privados
        Private _articlesDao As IArticlesDao
        Private _erreurs As ArrayList

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

         ' lista de erros
        Public ReadOnly Property erreurs() As ArrayList Implements IArticlesDomain.erreurs
            Get
                Return _erreurs
            End Get
        End Property

         ' lista de artigos
        Public Function getAllArticles() As IList Implements IArticlesDomain.getAllArticles
             ' lista de todos os artigos
            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

         ' obter um artigo identificado pelo seu n.º
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDomain.getArticleById
             ' um artigo específico
            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


         ' comprar um cesto
        Public Sub acheter(ByVal panier As Panier) Implements IArticlesDomain.acheter
             ' compra de um cesto - os stocks dos artigos comprados devem ser reduzidos
            _erreurs = New ArrayList
            Dim achat As achat
            Dim achats As ArrayList = panier.achats
            For i As Integer = achats.Count - 1 To 0 Step -1
                 ' reduzir o stock do artigo i
                achat = CType(achats(i), achat)
                Try
                    If _articlesDao.changerStockArticle(achat.article.id, -achat.qte) = 0 Then
                         ' não foi possível realizar a operação
                        _erreurs.Add("L'achat " + achat.ToString + " n'a pu se faire - Vérifiez les stocks")
                    Else
                         ' a operação foi concluída — a compra é removida do cesto
                        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
_erreurs As ArrayList
a lista de eventuais erros. É acessível através da propriedade pública [erreurs]
  • Para criar uma instância da classe, é necessário fornecer o objeto que permite o acesso aos dados:
Sub New(ByVal articlesDao As IArticlesDao)
  • Os métodos [getAllArticles] e [getArticleById] baseiam-se nos métodos com o mesmo nome da camada [dao]
  • O método [acheter] valida a compra de um cesto de compras. Esta validação consiste simplesmente em reduzir os stocks dos artigos comprados. A compra de um artigo só é possível se o seu stock o permitir. Caso contrário, a compra é recusada: o artigo permanece no cesto e é sinalizado um erro na lista [erreurs]. Uma compra validada é removida do cesto e o stock do artigo correspondente é reduzido na 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 da camada [domain]

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

Image

Comentários:

  • o projeto [tests] é do tipo [bibliothèque de classes]
  • os testes [NUnit] requerem uma referência ao assembly [nunit.framework.dll]
  • A classe de teste [NUnit] obtém uma instância do objeto a testar através do Spring. Por isso, encontramos
  • na pasta [bin], os ficheiros de classes do Spring
  • em [References], 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 necessita do assembly [webarticles-dao.dll] da camada [dao] e do assembly [webarticles-domain.dll] da camada [domain]. Estes foram colocados na pasta [bin] e as respetivas referências foram adicionadas às referências do projeto.

Uma classe de teste NUnit da camada [domain] poderia ser a seguinte:

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

         ' o objeto a testar
        Private articlesDomain As IArticlesDomain
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
             ' recuperamos uma instância do criador de objetos Spring
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
             ' solicita-se a instanciação do objeto DAO «articles»
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
             ' e, em seguida, do objeto «articlesdomain»
            articlesDomain = CType(factory.GetObject("articlesdomain"), IArticlesDomain)
        End Sub

        <Test()> _
        Public Sub getAllArticles()
             ' verificação visual
            listArticles()
        End Sub

        <Test()> _
        Public Sub getArticleById()
             ' eliminação dos artigos
            articlesDao.clearAllArticles()
             ' verificação
            Dim articles As IList = articlesDomain.getAllArticles
            Assert.AreEqual(0, articles.Count)
             ' adição de 2 artigos
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
             ' verificação
            articles = articlesDomain.getAllArticles
            Assert.AreEqual(2, articles.Count)
             ' pesquisa do artigo 3
            Dim unArticle As Article = articlesDomain.getArticleById(3)
             ' verificação
            Assert.AreEqual(unArticle.nom, "article3")
             ' pesquisa do artigo 4
            unArticle = articlesDao.getArticleById(4)
             ' verificação
            Assert.AreEqual(unArticle.nom, "article4")
        End Sub

        <Test()> _
        Public Sub acheterPanier()
             ' eliminação de artigos
            articlesDao.clearAllArticles()
             ' verificação
            Dim articles As IList = articlesDomain.getAllArticles
            Assert.AreEqual(0, articles.Count)
             ' adição de 2 artigos
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
             ' verificação
            articles = articlesDomain.getAllArticles
            Assert.AreEqual(2, articles.Count)
             ' criação de um cesto de compras com duas compras
            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))
             ' verificações
            Assert.AreEqual(700, panier.totalPanier, 0.000001)
            Assert.AreEqual(2, panier.achats.Count)
             ' confirmação do cesto de compras
            articlesDomain.acheter(panier)
             ' verificações
            Assert.AreEqual(0, articlesDomain.erreurs.Count)
            Assert.AreEqual(0, panier.achats.Count)
             ' pesquisa do artigo 3
            Dim unArticle As Article = articlesDomain.getArticleById(3)
             ' verificação
            Assert.AreEqual(unArticle.stockactuel, 20)
             ' pesquisa de artigo 4
            unArticle = articlesDao.getArticleById(4)
             ' verificação
            Assert.AreEqual(unArticle.stockactuel, 30)
             ' novo cesto de compras
            panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 100))
             ' confirmação do cesto de compras
            articlesDomain.acheter(panier)
             ' verificações
            Assert.AreEqual(1, articlesDomain.erreurs.Count)
             ' pesquisa de artigo 3
            unArticle = articlesDomain.getArticleById(3)
             ' verificação
            Assert.AreEqual(unArticle.stockactuel, 20)
        End Sub

        <Test()> _
        Public Sub testRetirerAchats()
             ' eliminação do conteúdo de ARTICLES
            articlesDao.clearAllArticles()
             ' lê a tabela ARTICLES
            Dim articles As IList = articlesDao.getAllArticles()
            Assert.AreEqual(0, articles.Count)
             ' inserção
            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)
             ' lê a tabela ARTICLES
            articles = articlesDomain.getAllArticles()
            Assert.AreEqual(2, articles.Count)
             ' criação de um cesto com duas compras
            Dim monPanier As New Panier
            monPanier.ajouter(New Achat(article3, 10))
            monPanier.ajouter(New Achat(article4, 10))
             ' verificações
            Assert.AreEqual(700.0, monPanier.totalPanier, 0.000001)
            Assert.AreEqual(2, monPanier.achats.Count)
             ' adicionar um artigo já comprado
            monPanier.ajouter(New Achat(article3, 10))
             ' verificações
             ' o total deve ser alterado para 1000
            Assert.AreEqual(1000.0, monPanier.totalPanier, 0.000001)
             ' continuam a existir 2 artigos no cesto
            Assert.AreEqual(2, monPanier.achats.Count)
             ' a quantidade do artigo 3 deve ter passado para 20
            Dim unAchat As Achat = CType(monPanier.achats(0), Achat)
            Assert.AreEqual(20, unAchat.qte)
             ' retira-se o artigo 3 do cesto
            monPanier.enlever(3)
             ' verificações
             ' o total deve ter passado para 400
            Assert.AreEqual(400.0, monPanier.totalPanier, 0.000001)
             ' apenas 1 artigo no cesto
            Assert.AreEqual(1, monPanier.achats.Count)
             ' deve ser o artigo n.º 4
            Assert.AreEqual(4, CType(monPanier.achats(0), Achat).article.id)
        End Sub

         ' lista no ecrã
        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:

  • pretendíamos escrever um programa de teste para a interface [IArticlesDomain] que fosse independente da classe de implementação da mesma. Por isso, utilizámos o Spring para ocultar do programa de teste o nome da classe de implementação.
  • O método de atributo <Setup()> obtém do Spring uma referência aos objetos [articlesdomain] e [articlesdao] a testar. 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 a encontrar: [ webarticles-dao.dll]. Como a instanciação não requer parâmetros, nenhum é definido aqui.
    • para o singleton [articlesdomain], o nome da classe de implementação [istia.st.articles.domain.AchatsArticles] e onde a encontrar: [ webarticles-domain.dll]. A classe [AchatsArticles] tem um construtor com um parâmetro: o singleton que gere o acesso à camada [dao]. Aqui, este é definido como sendo o singleton [articlesdao] definido anteriormente.
  • A classe de teste obtém uma instância da classe a testar [articlesdomain], bem como uma instância da classe de acesso aos dados [articlesdao]. Este último ponto é controverso. Teoricamente, a classe de teste não deveria precisar de ter acesso à camada [dao], que nem sequer deveria conhecer. Aqui, ignorámos esta «ética» que, para ser respeitada, nos teria obrigado a criar novos métodos na nossa interface [IArticlesDomain].

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

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

Image

O leitor que visualizar este documento no ecrã poderá verificar que todos os testes foram bem-sucedidos. Posteriormente, consideraremos que dispomos de uma camada [domain] operacional.

1.4.5. Conclusão

Recorde-se que pretendemos 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 dois ficheiros: DLL e [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 considerar, em primeiro lugar, um método apresentado no documento [Développement WEB avec ASP.NET 1.1 ]

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

1.5. A camada [web]

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

M=modèle
as classes de negócio [domain], as classes de acesso aos dados [dao] e a fonte de dados
V=vues
as páginas ASPX
C=contrôleur
todas as solicitações dos clientes HTTP passam pelos dois controladores seguintes:
global.asax: gere os eventos relacionados com o arranque inicial da aplicação
main.aspx: processa individualmente a solicitação de cada cliente

1.5.1. As vistas

As vistas correspondem às que foram apresentadas no início do documento:

LISTE
liste.aspx
As vistas estão reunidas na pasta [vues] da aplicação
INFOS
infos.aspx
PANIER
panier.aspx
PANIERVIDE
paniervide.aspx
ERREURS
erreurs.aspx

1.5.2. Os controladores

Conforme referido, o controlador será composto por dois elementos:

  1. [global.asax,global.asax.vb]: utilizado principalmente para inicializar a aplicação e colocar no contexto da mesma todos os dados a partilhar entre os diferentes clientes
  2. [main.aspx, main.aspx.vb]: o verdadeiro controlador, responsável por processar as solicitações HTTP dos clientes.

As diferentes solicitações dos clientes serão encaminhadas para o controlador [main.aspx] e conterão um parâmetro denominado [action] que especifica a ação solicitada pelo cliente:

solicitação
significado
ação do controlador
respostas possíveis
action=liste
o cliente pretende a lista dos
artigos
- solicita a lista de artigos à camada
de negócio
- [LISTE]
- [ERREURS]
action=infos
O cliente solicita
informações sobre um dos
artigos apresentados na vista
[LISTE]
- solicita o artigo à camada de negócios
- [INFOS]
- [ERREURS]
action=achat
o cliente compra um artigo
- solicita o artigo à camada de negócio
e adiciona-o ao cesto de compras do cliente
- [INFOS] se houver erro na quantidade
- [LISTE] se não houver erro
action=retirerachat
o cliente pretende eliminar um
compra do seu carrinho
- recupera o carrinho da sessão
e altera-o
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=panier
o cliente pretende visualizar o seu
cesto
- recupera o carrinho da sessão
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=validationpanier
o cliente concluiu as suas compras
e passa para a fase de pagamento
- atualiza os stocks na base de dados
dos artigos comprados
- esvazia o cesto de compras do cliente dos artigos
cuja compra foi validada
- [LISTE]
- [ERREURS]

1.5.3. Configuração da aplicação

Procuraremos configurar a aplicação de forma a torná-la o mais flexível possível face a alterações como:

  1. a alteração dos URL das diferentes vistas
  2. a alteração das classes que implementam as interfaces [IArticlesDao] e [IArticlesDomain]
  3. a alteração do SGBD, da base de dados e da tabela de artigos

1.5.3.1. As alterações ao URL

Os nomes das vistas URL serão colocados no ficheiro de configuração da aplicação [web.config], 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. A alteração das classes de implementação das interfaces

De acordo com o princípio das arquiteturas de três camadas, as camadas devem ser independentes umas das outras. Esta independência é obtida 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 por si próprio a classe de outra camada para a utilizar. Limita-se a solicitar a uma ferramenta externa, neste caso o Spring, uma instância de implementação da interface da 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 de que 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 [métier], uma classe da camada [web] poderá solicitar o singleton [articlesDomain]. O Spring irá então instanciar um objeto do tipo [istia.st.articles.domain.AchatsArticles]. Para esta instanciação, necessita de um objeto do tipo [articlesDao], ou seja, de 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 com a base de dados

Este ponto será ignorado aqui, uma vez que nos encontramos numa aplicação de teste sem o SGBD. Abordaremos, numa segunda fase, a implementação de uma camada [dao] baseada num SGBD.

1.5.4. A biblioteca de tags <asp:>

Consideremos a vista [ERREURS] que apresenta uma lista de erros:

Image

A vista [ERREURS] é responsável por apresentar uma lista de erros que o controlador [main.aspx] colocou no contexto da solicitação com o nome [context.Items("erreurs")]. Existem várias formas de escrever uma página deste tipo. Aqui, estamos interessados apenas na parte relativa à exibição dos erros.

Recorde-se que uma página ASPX tem uma parte de apresentação HTML e uma parte de código .NET que prepara os dados que a parte de apresentação deve apresentar. Estas duas partes podem estar num único ficheiro [aspx] (solução WebMatrix) ou em dois ficheiros: [aspx] para a apresentação e [aspx.vb] para o código. Esta última solução é a utilizada pelo Visual Studio. Para complicar ainda mais as coisas, a parte de apresentação HTML também pode conter código .NET, o que tende a confundir a separação entre [contrôleur] e [présentation] na visualização. Esta solução é, em geral, fortemente desaconselhada. A remoção de todo o código da parte [présentation] exigiu a criação de bibliotecas de tags. Estas «ocultam» o código sob a forma de tags semelhantes às tags HTML. Apresentamos duas soluções possíveis para a página [ERREURS].

A nossa primeira solução utiliza código .NET na parte [présentation] da página. A página ASPX recupera a lista de erros presente no pedido na sua parte controladora [erreurs.aspx.vb]:

        Protected erreurs As ArrayList

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
             ' recuperam-se os erros
            erreurs = CType(context.Items("erreurs"), ArrayList)
        End Sub

e, em seguida, apresenta-o na secção [présentation, erreurs.aspx]:

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

A segunda solução utiliza a baliza <asp:repeater> da biblioteca de balizas <asp:> do ASP.NET. Se criarmos uma página ASPX graficamente, esta baliza está disponível sob a forma de um componente de servidor que se arrasta para o formulário de design. Se criarmos o código ASPX manualmente, podemos referir-nos a uma biblioteca de balizas.

Com a biblioteca de tags <asp:>, o código ASPX da vista [ERREURS] 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>

A baliza

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

serve para repetir um padrão HTML nos diferentes elementos de uma fonte de dados. Os seus diferentes elementos são os seguintes:

HeaderTemplate
o padrão HTML a apresentar antes de os elementos da fonte de dados serem apresentados
ItemTemplate
o padrão HTML a repetir para cada um dos elementos da fonte de dados. A expressão [<%# Container.DataItem %>] serve para apresentar o valor do elemento atual da fonte de dados
FooterTemplate
o padrão HTML a ser apresentado após a apresentação dos elementos da fonte de dados

A fonte de dados está ligada à baliza, geralmente na parte [contrôleur] 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
..
             ' associa-se os erros a rptErreurs
            With rptErreurs
                .DataSource = context.Items("erreurs")
                .DataBind()
            End With
        End Sub

Esta ligação também pode ser feita na fase de conceção da página, caso a fonte de dados já seja conhecida, por exemplo, uma base de dados existente.

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

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

Uma aplicação web é um puzzle com inúmeros elementos. Dotá-la de uma arquitetura MVC aumenta, geralmente, o número desses elementos. A estrutura da aplicação [webarticles] no âmbito de [Visual Studio] é a seguinte:

  

Comentários:

  • o projeto [web] é do tipo [bibliothèque de classes] e não do tipo [Application web ASP.NET], como seria lógico esperar. O tipo [Application 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 existe de série nas máquinas Windows XP Família. No entanto, muitos PC são vendidos com esta versão. Para permitir que os leitores que dispõem do Windows XP possam implementar a aplicação em estudo, utilizaremos o servidor Web Cassini (ver anexo), disponível gratuitamente na Microsoft, e substituiremos o projeto [Application web ASP.NET] por um projeto [bibliothèque de classes]. Isto acarreta alguns inconvenientes, que são explicados nos anexos.
  • Os ficheiros DLL utilizados pela aplicação são os seguintes:
webarticles-dao.dll
reúne as classes da camada de acesso aos dados
webarticles-domain.dll
agrupa as classes da camada de negócios
Spring.Core.dll
contém as classes Spring que nos permitem integrar as camadas web, domain e DAO
log4net.dll
classes de registos — utilizadas pelo Spring

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

1.5.6. As vistas ASPX

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

1.5.6.1. O componente de utilizador [entete.ascx]

Para conferir uma certa uniformidade às diferentes vistas, estas partilharão um mesmo cabeçalho, aquele que exibe o nome da aplicação juntamente com o menu:

O menu é dinâmico e definido pelo controlador. Este insere na solicitação enviada à página ASPX um atributo-chave «actions», cujo valor associado é uma matriz de elementos do tipo 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 parte da página (apresentação e código associado) num componente reutilizável noutras páginas. Neste caso, pretendemos reutilizar o componente [entete] nas outras 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:

type
nom
rôle
1
repetidor
rptMenu
fonte de dados: uma tabela de dicionários
com duas chaves: href, link
exibir as 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("ligação") %>
                            </a>
                        </td>
                    </ItemTemplate>
                </asp:Repeater>
            </tr>
        </table>
        <hr>

Comentários:

  • o componente [repeater] é definido nas linhas 6 a 14
  • cada elemento da 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())
                ' associa-se a tabela de ações ao seu componente
                With rptMenu
                    .DataSource = Value
                    .DataBind()
                End With
            End Set
        End Property
    End Class
End Namespace

Comentários:

  • o componente do tipo [EnteteWebArticles] tem uma propriedade pública [actions] apenas para escrita — linha 7
  • esta propriedade permite associar ao componente <asp:repeater> denominado [rptMenu] — linha 10 — a tabela de opções calculada pelo controlador da aplicação — linhas 11-12.

As outras vistas da aplicação utilizarão o cabeçalho definido por [entete.ascx]. A página [erreurs.aspx], por exemplo, incluirá o cabeçalho através do 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 declara que a baliza <WA:entete> deverá ser associada ao componente definido pelo ficheiro [entete.ascx]. Os atributos [TagPrefix] e [TagName] são livres.
  • Feito isto, a inserção do componente no código de apresentação da página é efetuada na linha 9. Durante a execução, esta baliza terá como efeito incluir no código da página ASPX, que a contém, o código da página [entete.ascx]. O código de controlo [erreurs.aspx.vb] encarregar-se-á de inicializar este componente. Pode fazê-lo da seguinte forma:
    Public Class ErreursWebarticles
        Inherits System.Web.UI.Page

         ' componentes da página
...
        Protected WithEvents entete As New EnteteWebArticles

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
             ' associamos as opções do menu ao 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 criado
  • a linha 11 inicializa a propriedade [actions] deste objeto

1.5.6.2. A vista [liste.aspx]

1.5.6.2.1. Introduction

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

É apresentada na sequência de uma consulta /main?action=liste ou /main?action=validationpanier. Os elementos da consulta do controlador são os seguintes:

actions
objeto Hashtable() — a tabela de opções do menu
listarticles
ArrayList de objetos do tipo [Article]
message
objeto String - mensagem a apresentar na parte inferior da página

Cada ligação [Infos] da tabela HTML dos artigos tem um URL com o formato [?action=infos&id=ID], em que ID é o campo id doartigo apresentado.

1.5.6.2.2. Componentes da página
type
nom
rôle
1
componente do utilizador
cabeçalho
exibir o cabeçalho
2
DataGrid
DataGridArticles
3 - coluna relacionada: cabeçalho: Nome, campo: nome
4 - coluna associada: cabeçalho: Preço, campo: preço
5 - coluna de hipertexto: texto: Informações, campo URL: id,
formato URL: /webarticles/main.aspx?action=infos&id={0}
exibir os artigos à venda
6
rótulo
lblMessage
Mostrar uma mensagem

Recorde-se como proceder para definir estas propriedades:

  • no Visual Studio, seleciona-se o [DataGrid] para aceder à sua folha de propriedades:

Image

  • utilize-se o link [Mise en forme automatique] acima para gerir o formato da tabela apresentada
  • e o link [Générateur de propriétés] 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 características 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

     ' gere a página de visualização da lista de artigos
    Public Class ListeWebarticles
        Inherits System.Web.UI.Page

         ' componentes da página
        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
             ' prepara-se a vista [liste] a partir das informações do contexto
             ' associa as opções do menu ao rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
             ' recuperam-se os artigos numa tabela de dados
            Dim articles As ArrayList = CType(context.Items("articles"), ArrayList)
             ' associam-se ao componente [DataGrid] da página
            With DataGridArticles
                .DataSource = articles
                .DataBind()
            End With
             ' exibe a mensagem
            lblMessage.Text = context.Items("message").ToString
        End Sub
    End Class
End Namespace

Comentários:

  • os componentes da página aparecem nas linhas 13-15. De notar que foi necessário criar um objeto [EnteteWebArticles] com um operador [new], ao passo que isso não foi necessário com os outros componentes. Sem esta criação explícita, ocorria um erro de execução indicando que o objeto [entete] não fazia referência a nada. Este ponto mereceria ser aprofundado. Não o foi.
  • a tabela de opções do menu do cabeçalho é obtida 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 inserida no contexto - linha 29

1.5.6.3. A vista [infos.aspx]

1.5.6.3.1. Introduction

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

Image

É apresentada na sequência de uma solicitação /main?action=infos&id=ID ou de uma solicitação /main?action=achat&id=ID quando a quantidade comprada está errada. Os elementos da solicitação do controlador são os seguintes:

actions
objeto Hashtable() — a tabela de opções do menu
article
objeto do tipo [Article] — artigo a apresentar
msg
objeto String - mensagem a apresentar em caso de erro na quantidade
qte
objeto String - valor a apresentar no campo de introdução de dados [Qte]

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

Image

Esta página contém um formulário que é enviado através do botão [Acheter]. O destino do URL do POST é o [?action=achat&id=ID], sendo que o ID é o ID do artigo adquirido.

1.5.6.3.2. Os componentes da página
type
nom
rôle
1
componente do utilizador
cabeçalho
exibir o cabeçalho
2
literal
litID
Mostrar o n.º do artigo
3 à 6
DataGrid
DataGridArticle
3 - coluna associada: cabeçalho: Nome, campo: nome
4 - coluna relacionada: título: Preço, campo: preço
5 - coluna associada: cabeçalho: Stock atual, campo: stockActuel
6 - coluna relacionada: título: Stock mínimo, campo: stockMinimum
exibir um artigo
7
HTML Enviar
 
Enviar o formulário
8
HTML Entrada
runat=server
txtQte
introduzir a quantidade comprada
9
rótulo
lblMsgQte
eventual 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] está definido na linha 10
  • o DataGrid e o [DataGridArticles] estão definidos nas linhas 12 a 34
  • o formulário é definido nas linhas 36-46. É do tipo POST.
  • O destino do POST é fornecido por uma variável [strAction] — linha 36. Esta variável deverá ser definida pelo controlador.
  • O campo de introdução da quantidade comprada está definido na linha 41. Trata-se de um componente HTML do servidor (runat=server). No código, acede-se a ele através de um objeto.
  • A linha 42 define o rótulo [lblMsgQte], que conterá uma eventual mensagem de erro relativa à 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

     ' gere a página de informações sobre um artigo
    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

         ' o URL, para onde será enviado o formulário
        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

         ' exibição da página
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
             ' recuperam-se as informações da consulta
            Dim unArticle As Article = CType(Session.Item("article"), Article)
             ' associa-se as opções do menu ao rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
             ' associa-se o artigo a [DataGrid]
            Dim articles As New ArrayList
            articles.Add(unArticle)
            With DataGridArticle
                .DataSource = articles
                .DataBind()
            End With
             ' o ID do rótulo
            litId.Text = unArticle.id.ToString
             ' a mensagem de erro
            lblMsgQte.Text = context.Items("msg").ToString
             ' a quantidade anterior
            txtQte.Value = context.Items("qte").ToString
             ' a ação URL
            strAction = "?action=achat&id=" + unArticle.id.ToString
        End Sub
End Namespace

Comentários:

  • os componentes da página são definidos nas linhas 10-14
  • A classe define uma propriedade pública [strAction] que serve para definir o destino do POST do formulário — linhas 17 a 25
  • o artigo a apresentar é recuperado do contexto da aplicação — linha 30
  • a tabela de opções do menu do cabeçalho é obtida do contexto para inicializar o componente [entete] da página — linha 32
  • linhas 33-39, o componente [DataGridArticle] é associado a uma fonte de dados do tipo [ArrayList] que contém apenas o artigo recuperado na linha 30
  • os componentes [lblMsgQte, txtQte] são inicializados com informações obtidas do contexto — linhas 42-45
  • a propriedade [straction] é igualmente inicializada com uma informação retirada do contexto — linha 47. Esta variável serve 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. Introduction

Esta vista apresenta o conteúdo do cesto:

Image

É apresentada na sequência de uma solicitação /main?action=panier ou /main?action=retirerachat&id=ID. Os elementos da solicitação do controlador são os seguintes:

actions
objeto Hashtable() — a tabela de opções do menu
panier
objeto do tipo [Panier] — o carrinho a apresentar

Cada ligação [Retirer] da tabela HTML das compras do carrinho tem um URL com o formato [?action=retirerachat&id=ID], em que ID é o campo [id] do artigo que se pretende remover do cesto.

1.5.6.4.2. Os componentes da página
type
nom
rôle
1
componente do utilizador
cabeçalho
exibir o cabeçalho
2
DataGrid
DataGridAchats
3 - coluna associada - cabeçalho: Artigo, campo: nome
4 - coluna associada - cabeçalho: Qtd., campo: qtd.
5 - coluna associada - título: Preço, campo: preço
6 - coluna associada - título: Total, campo: total, formatação {0:C}
7 - coluna de hipertexto - Texto: Retirar, URL: id, formato da URL: /webarticles/main.aspx?action=retirerachat&id={0}
exibir a lista de artigos comprados
8
rótulo
lblTotal
Mostrar o montante 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
  • nas linhas 12-27, o componente [DataGridAchats] está definido
  • na 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
     ' gere a página de visualização do carrinho
    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
             ' associa as opções do menu ao rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
             ' recupera o carrinho
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
             ' transfere as compras para uma tabela de linhas de compras
            Dim achats(unPanier.achats.Count - 1) As LigneAchat
             ' altera-se o tipo dos elementos do ArrayList
            For i As Integer = 0 To achats.Length - 1
                achats(i) = New LigneAchat(CType(unPanier.achats(i), Achat))
            Next
             ' associa-se os dados aos componentes [DataGrid] da página
            With DataGridAchats
                .DataSource = achats
                .DataBind()
            End With
             ' exibe-se o total a pagar
            lblTotal.Text = unPanier.totalPanier.ToString
        End Sub

         ' linha de compra criada a partir de um objeto de compra
        Private Class LigneAchat
            Inherits Achat

             ' o criador recebe uma compra
            Public Sub New(ByVal unAchat As Achat)
                Me.article = unAchat.article
                Me.qte = unAchat.qte
            End Sub

             ' id: devolve o id do artigo comprado
            Public ReadOnly Property id() As Integer
                Get
                    Return article.id
                End Get
            End Property

             ' nome: nome do artigo adquirido
            Public ReadOnly Property nom() As String
                Get
                    Return article.nom
                End Get
            End Property

             ' preço do artigo comprado
            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 [entete] é idêntica à encontrada nas páginas já analisadas — linha 17
  • o carrinho a apresentar é recuperado da sessão — linha 19
  • a exibição deste cesto de compras com o componente [DataGridAchats] apresenta problemas. A dificuldade reside na inicialização do componente. Recorde-se as colunas deste:
    • coluna [Article] associada ao campo [nom] da fonte de dados
    • coluna [Qté] associada ao campo [qte] da fonte de dados
    • coluna [Prix] associada ao campo [prix] da fonte de dados
    • coluna [Total] associada ao campo [total] da fonte de dados

A fonte de dados de que dispomos é o cesto de compras e a respetiva lista de compras. Esta última será a fonte de dados do [DataGrid]. Apenas os objetos [Achat] que irão alimentar as linhas do [DataGrid] não possuem as propriedades [nom, qte, prix, total] esperadas pelo [DataGrid]. Por isso, cria-se aqui, especificamente para o [DataGrid], uma fonte de dados cujos elementos possuem as características esperadas pelo [DataGrid]. Estes elementos serão do tipo [LigneAchat], uma classe criada para o efeito e derivada da classe [Achat] — linhas 36-66

  • uma vez definida a classe [LigneAchat), a fonte de dados de [DataGridAchats] é construída a partir do cesto de compras encontrado na sessão — linhas 20-30
  • o valor das compras é apresentado através da propriedade [totalPanier] da classe [Panier] — linha 32

1.5.6.5. A vista [paniervide.aspx]

1.5.6.5.1. Introduction

Esta vista apresenta a informação que indica que o carrinho está vazio:

Image

É apresentada na sequência de uma solicitação /main?action=cesto ou /main?action=retirarcompra&id=ID. Os elementos da solicitação do controlador são os seguintes:

actions
objeto Hashtable() — a tabela de opções do menu
1.5.6.5.2. Os componentes da página
type
nom
rôle
1
componente do utilizador
cabeçalho
exibir o cabeçalho
1.5.6.5.3. O código de apresentação [paniervide.aspx]
<%@ Register TagPrefix="WA" TagName="entete" Src="entete.ascx"%>
<%@ Page codebehind="paniervide.aspx.vb" inherits="istia.st.articles.web.PaniervideWebarticles" autoeventwireup="false" Language="vb" %>
<HTML>
    <HEAD>
        <TITLE>webarticles</TITLE>
        <META http-equiv="Content-Type" content="text/html; charset=windows-1252">
    </HEAD>
    <body>
        <WA:entete id="entete" runat="server"></WA:entete>
        <h2>Contenu de votre panier</h2>
        <P>Votre panier est vide</P>
    </body>
</HTML>

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
     ' gere a página de visualização de um carrinho vazio
    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
             ' prepara a vista [paniervide] a partir das informações do contexto
             ' associa as opções do menu ao rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
        End Sub
    End Class
End Namespace

Comentários:

  • limita-se a inicializar o único componente dinâmico da página — linha 10

1.5.6.6. A vista [erreurs.aspx]

1.5.6.6.1. Introduction

Esta página é apresentada em caso de erros:

Image

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

actions
objeto Hashtable() — a tabela de opções do menu
erreurs
ArrayList de objetos [String] que representam as mensagens de erro a apresentar
1.5.6.6.2. Os componentes da página
type
nom
rôle
1
componente do utilizador
cabeçalho
exibir o cabeçalho
2
repetidor
rptErreurs
Mostrar a lista de erros
1.5.6.6.3. O código de apresentação [erreurs.aspx]
<%@ Register TagPrefix="WA" TagName="entete" Src="entete.ascx" %>
<%@ Page codebehind="erreurs.aspx.vb" inherits="istia.st.articles.web.ErreursWebarticles" autoeventwireup="false" Language="vb" %>
<HTML>
    <HEAD>
        <TITLE>webarticles</TITLE>
        <META http-equiv="Content-Type" content="text/html; charset=windows-1252">
    </HEAD>
    <body>
        <WA:entete id="entete" runat="server"></WA:entete>
        <h3>Les erreurs suivantes se sont produites :
        </h3>
        <ul>
            <asp:Repeater id="rptErreurs" runat="server">
                <ItemTemplate>
                    <li>
                        <%# Container.DataItem %>
                    </li>
                </ItemTemplate>
            </asp:Repeater></ul>
    </body>
</HTML>

Comentários:

  • o cabeçalho está definido na linha 9
  • O componente [rptErreurs] está definido nas linhas 13-19. O seu conteúdo provém de uma fonte de dados que será do tipo [ArrayList] de objetos [String].
1.5.6.6.4. O código de controlo [erreurs.aspx.vb]
Namespace istia.st.articles.web

     ' gere a página de erros
    Public Class ErreursWebarticles
        Inherits System.Web.UI.Page

         ' componentes da página
        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
             ' prepara-se a vista [erreurs] a partir das informações do contexto
             ' associamos as opções do menu ao rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
             ' associamos os erros a rptErreurs
            With rptErreurs
                .DataSource = context.Items("erreurs")
                .DataBind()
            End With
        End Sub
    End Class

End Namespace

Comentários:

  • o componente [entete] é inicializado como habitualmente, nas linhas 9 e 14
  • o componente [rptErreurs] é inicializado com a lista de erros do tipo [ArrayList] encontrada no contexto — linhas 16-19

1.5.7. Os controladores global.asax, main.aspx

Resta escrever o núcleo da nossa aplicação web, o controlador. A sua função consiste em:

  • recuperar o pedido do cliente,
  • processar a ação solicitada pelo cliente com a ajuda das classes de negócio,
  • enviar, em resposta, a vista adequada.

1.5.7.1. O controlador [global.asax.vb]

Quando a aplicação recebe a sua primeira solicitação, é executado o procedimento [Application_Start] do ficheiro [global.asax.vb]. Esta será a única vez. O procedimento [Application_Start] tem como objetivo inicializar os objetos necessários à aplicação web e que serão partilhados em modo de leitura apenas por todos os threads de cliente. Estes objetos partilhados podem ser colocados em dois locais:

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

O método [Application_Start] da aplicação [global.asax.vb] realizará as seguintes ações:

  • verificará a presença, no ficheiro [web.config], dos parâmetros necessários ao bom funcionamento da aplicação. Estes foram descritos no parágrafo 1.5.3.
  • inserirá no contexto da aplicação a lista de eventuais erros sob a forma de um objeto [ArrayList erreurs]. Esta lista estará vazia se não houver erros, mas existirá na mesma.
  • Se tiverem ocorrido erros, o método [Application_Start] termina aqui. Caso contrário, solicita a referência de um singleton do tipo [IArticlesDomain], que será o objeto de negócio que o controlador utilizará para as suas necessidades. Tal como explicado em 1.5.3.2, o controlador solicitará esse singleton ao framework Spring. Esta operação de instanciação pode dar origem a vários erros. Se for esse o caso, estes serão, mais uma vez, armazenados no objeto [erreurs] do contexto da aplicação.

O controlador [global.asax.vb] dispõe de um procedimento [Session_Start] executado sempre que um novo cliente chega. Neste procedimento, será criado um cesto vazio para o cliente. Este cesto será mantido ao longo das solicitações desse cliente específico. O código poderá 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

         ' inicialização da aplicação
        Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)

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

             ' recuperam-se os parâmetros de inicialização da aplicação
            Dim param As String
            For i As Integer = 0 To parameters.Length - 1
                 ' leitura do ficheiro de configuração
                param = ConfigurationSettings.AppSettings(parameters(i))
                If param Is Nothing Then
                     ' regista-se o erro
                    erreurs.Add("Paramètre [" + parameters(i) + "] absent dans le fichier [web.config]")
                Else
                     ' o parâmetro é guardado na aplicação
                    Application.Item(parameters(i)) = param
                End If
            Next
             ' há erros?
            If erreurs.Count = 0 Then
                 ' cria-se um objeto IArticlesDomain para aceder à camada de negócios
                Dim contexte As IApplicationContext = CType(ConfigurationSettings.GetConfig("spring/context"), IApplicationContext)
                Dim articlesDomain As IArticlesDomain
                Try
                    articlesDomain = CType(contexte.GetObject("articlesDomain"), IArticlesDomain)
                     ' o objeto é guardado na aplicação
                    Application.Item("articlesDomain") = articlesDomain
                Catch ex As Exception
                     ' o erro é guardado
                    erreurs.Add("Erreur lors de la construction de l'objet d'accès à la couche métier [" + ex.ToString + "]")
                End Try
            End If
             ' os erros são colocados na aplicação
            Application.Item("erreurs") = erreurs
             ' o processo termina se tiverem ocorrido erros
            If erreurs.Count <> 0 Then Return
             ' constrói-se uma tabela de opções de menu
            Dim options As New Hashtable
             ' recuperamos o URL do controlador
            Dim urlMain As String = CType(Application.Item("urlMain"), String)
            Dim uneOption As Hashtable
             ' lista de artigos
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=liste")
            uneOption.Add("lien", "Liste des articles")
            options.Add("liste", uneOption)
             ' cesto de compras
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=panier")
            uneOption.Add("lien", "Voir le panier")
            options.Add("panier", uneOption)
             ' confirmação do carrinho
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=validationpanier")
            uneOption.Add("lien", "Valider le panier")
            options.Add("validationpanier", uneOption)
             ' definem-se as opções do menu na aplicação
            Application.Item("options") = options
            Return
        End Sub

         ' inicialização da sessão
        Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
             ' criamos um cesto de compras para o cliente
            Session.Item("panier") = New Panier
        End Sub
    End Class
End Namespace

Comentários:

  • os parâmetros esperados em [web.config] estão definidos numa tabela — linha 18
  • são pesquisados em [web.config]. Se estiverem presentes, são armazenados no contexto da aplicação; caso contrário, é registado um erro na lista de erros [erreurs] — linhas 21-33
  • Se não houver erros, solicita-se ao Spring uma referência ao singleton [articlesDomain], que gere o acesso à camada [domain] da aplicação — linhas 35-47. Os eventuais erros são registados em [erreurs].
  • Os erros são registados no contexto da aplicação — linha 49
  • a rotina é encerrada caso tenham ocorrido erros — linha 51
  • É criado um array com três dicionários. Cada um deles tem duas chaves: href e link. Este array representa as três opções de menu possíveis — linhas 52-71
  • esta matriz é armazenada no contexto da aplicação — linha 73
  • a cada novo cliente, é executado o procedimento [Session_Start]. Nele, é criado um cesto vazio na sessão do cliente — linhas 78-81

1.5.7.2. O controlador [main.aspx.vb]

O controlador [main.aspx.vb] recebe todas as solicitações dos clientes. Com efeito, todas elas têm o formato [/webarticles/main.aspx?action=XX]. Uma solicitação será processada da seguinte forma:

  • o objeto [erreurs] do contexto da aplicação será verificado. Se não estiver vazio, isso significa que ocorreram erros durante a inicialização da aplicação e que esta não pode funcionar. Nesse caso, será enviada, como resposta, a vista [ERREURS].
  • o parâmetro [action] da solicitação será recuperado e verificado. Se não corresponder a uma ação conhecida, a vista [ERREURS] é enviada com uma mensagem de erro adequada.
  • 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
pedido
processamento
respostas possíveis
doListe
GET /main?action=liste
- solicitar a lista de artigos
à classe de negócio
- exibi-lo
[LISTE] ou [ERREURS]
doInfos
GET /main?action=infos&id=ID
- solicitar o artigo com id=ID à
classe de negócio
- exibi-lo
[INFOS] ou [ERREURS]
doAchat
POST /main?action=compra&id=ID
- a quantidade comprada faz parte dos parâmetros enviados
- solicitar o artigo com id=ID à
classe de negócio
- incluí-lo no cesto de compras na
a sessão do cliente
[LISTE] ou [INFOS] ou [ERREURS]
doRetirerAchat
GET /main?action=retirerachat&id=ID
- retirar o artigo com id=ID da
lista de compras do carrinho de
sessão do cliente
[PANIER]
doPanier
GET /main?action=cesto
- exibir o cesto de compras da
sessão do cliente
[PANIER] ou [PANIERVIDE]
doValidationPanier
GET /main?action=validationpanier
- Reduzir na base de dados os
stocks de todos os artigos
presentes no carrinho de
sessão do cliente
[LISTE] ou [ERREURS]

A estrutura do controlador [main.aspx.vb] poderia ser a seguinte:

Imports System.Collections
Imports System
Imports System.Data

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

Namespace istia.st.articles.web

     ' classe controladora da aplicação web
    Public Class MainWebArticles
        Inherits System.Web.UI.Page

         ' campos privados
        Private articlesDomain As IArticlesDomain
        Private options As Hashtable

         ' carregamento da página
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
....
        End Sub

         ' métodos de processamento de ações

         ' lista de artigos
        Public Sub doListe()
...
        End Sub

         ' informações sobre um artigo
        Public Sub doInfos()
...
        End Sub

         ' compra de um artigo
        Public Sub doAchat()
...
        End Sub

         ' eliminação de uma compra
        Public Sub doRetirerAchat()
...
        End Sub

         ' visualização do carrinho
        Public Sub doPanier()
...
        End Sub

         ' compra do cesto
        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 de acesso à camada [domain]
    • opções: o array de dicionários das opções do menu
  • o procedimento [Page_Load]:
    • inicializará os dois campos privados da classe
    • recuperará o parâmetro [action] da solicitação e executará o método de processamento dessa 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:

         ' carregamento da página
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
             ' verifica-se se a aplicação arrancou corretamente
            Dim erreurs As ArrayList = CType(Application.Item("erreurs"), ArrayList)
             ' se houver erros, é apresentada a vista [erreurs]
            If erreurs.Count <> 0 Then
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {}
                Server.Transfer(CType(Application("urlErreurs"), String))
            End If
             ' recupera-se o objeto de acesso à classe de negócio
            articlesDomain = CType(Application.Item("articlesDomain"), IArticlesDomain)
             ' bem como as opções do menu
            options = CType(Application.Item("options"), Hashtable)
             ' recuperamos a ação a realizar
            Dim action As String = Request.QueryString("action")
            If action Is Nothing Then
                action = "liste"
            End If
             ' executa-se a ação
            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:

  • a cada carregamento da página, verifica-se se a inicialização da aplicação efetuada por [global.asax] decorreu corretamente.
  • Para tal, recupera-se, no contexto da aplicação, a lista de erros colocada ali por [global.asax] — linha 4
  • Se essa lista não estiver vazia, exibe-se a vista [ERREURS] — linhas 6-10
  • recuperamos o singleton [articlesDomain] inserido pelo [global.asax] no contexto da aplicação e armazenamo-lo no campo privado [articlesDomain] para que esteja disponível para os diferentes métodos da classe — linha 12
  • faz-se um procedimento semelhante com a matriz de opções do menu - linha 14
  • recupera-se o parâmetro [action] da solicitação - linha 16
  • executa-se o método correspondente à ação solicitada. Uma ação não prevista é considerada como a ação [liste] — linhas 16-36

1.5.7.4. Processamento da ação [liste]

Trata-se de apresentar a lista de artigos:

Image

O código é o seguinte:

         ' lista de artigos
        Public Sub doListe()
             ' gestão de erros
            Dim erreurs As New ArrayList
             ' solicita-se a lista de artigos
            Dim articles As IList
            Try
                articles = articlesDomain.getAllArticles
            Catch ex As Exception
                 ' regista-se o erro
                erreurs.Add(ex.ToString)
            End Try
             ' erros?
            If erreurs.Count <> 0 Then
                 ' exibição da vista [erreurs]
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
            End If
             ' visualização da vista [liste]
            context.Items("articles") = articles
            context.Items("options") = New Hashtable() {CType(options("panier"), Hashtable)}
            If context.Items("message") Is Nothing Then context.Items("message") = ""
            Server.Transfer(CType(Application.Item("urlListe"), String))
        End Sub

Comentários:

  • as eventuais erros são colocados num [ArrayList] - linha 4
  • a lista de artigos é solicitada ao singleton [articlesDomain] — linhas 5-12
  • Se tiverem ocorrido erros, envia-se a vista [ERREURS] - linhas 13-19
  • caso contrário, envia-se a vista [LISTE] - linhas 20-24

1.5.7.5. Processamento da ação [infos]

O cliente solicitou informações sobre um determinado artigo:

Image

O código é o seguinte:

         ' informações sobre um artigo
        Public Sub doInfos()
             ' gestão de erros
            Dim erreurs As New ArrayList
             ' recuperar o ID do artigo solicitado
            Dim strId As String = Request.QueryString("id")
             ' há alguma coisa?
            If strId Is Nothing Then
                 ' anormal – enviamos a página de erros
                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
             ' Temos um número inteiro?
            Dim id As Integer
            Try
                id = Integer.Parse(strId)
            Catch ex As Exception
                 ' anormal — enviamos a página de erros
                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
             ' está a ser solicitado o artigo com o ID de chave
            Dim unArticle As Article
            Try
                unArticle = articlesDomain.getArticleById(id)
            Catch ex As Exception
                 ' problema de acesso aos dados
                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
             ' foi recuperado algum artigo?
            If unArticle Is Nothing Then
                 ' o artigo não existe
                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
             ' Temos o artigo — colocamo-lo na sessão atual
            Session.Item("article") = unArticle
             ' prepara-se a sua visualização
            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:

  • as eventuais erros são colocados num [ArrayList] - linha 4
  • o identificador do artigo solicitado é recuperado na consulta - linha 6
  • esse identificador é verificado. Deve estar presente e deve ser um número inteiro. Se não for esse o caso, a vista [ERREURS] é enviada com a mensagem de erro apropriada — linhas 7-27
  • assim que o identificador for verificado, o artigo é solicitado ao singleton [articlesDomain]. Se ocorrer uma exceção, é enviada a vista [ERREURS] - linhas 29-39
  • Se o artigo não tiver sido encontrado, é enviada a vista [ERREURS] — linhas 41-48
  • Se o artigo tiver sido encontrado, é colocado na sessão do utilizador e, em seguida, apresentado na vista [INFOS] — linhas 50-56

1.5.7.6. Processamento da ação [achat]

O cliente comprou o artigo apresentado na vista [INFOS].

Image

O código é o seguinte:

         ' compra de um artigo
        Public Sub doAchat()
             ' compra de um artigo
             ' recuperamos o artigo que estava na sessão
            Dim unArticle As Article = CType(Session.Item("article"), Article)
             ' Temos alguma coisa?
            If unArticle Is Nothing Then
                 ' não é normal — exibe-se a lista de artigos
                doListe()
            End If
             ' recuperamos a quantidade registada
            Dim strQte As String = Request.Form("txtQte")
             ' há alguma coisa?
            If strQte Is Nothing Then
                 ' Não é normal — está a enviar a lista de artigos
                doListe()
            End If
             ' temos um número inteiro?
            Dim qte As Integer
            Try
                qte = Integer.Parse(strQte)
                If (qte <= 0) Then Throw New Exception
            Catch ex As Exception
                 ' não é normal — enviamos a página de informações com a mensagem de erro
                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
             ' está tudo bem - colocamos a compra no cesto do cliente
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            unPanier.ajouter(New Achat(unArticle, qte))
             ' exibe-se a lista de artigos
            doListe()
        End Sub

Comentários:

  • o artigo adicionado à sessão é recuperado — linha 5
  • se não estiver presente (a sessão pode ter expirado), é apresentada a vista [LISTE] — linhas 7-10
  • a quantidade comprada é recuperada na consulta — linha 12
  • a sua validade é verificada — linhas 13-29
  • em caso de invalidade, consoante o caso, é enviada a vista [LISTE] — linha 16 ou a vista [INFOS] — linhas 24-28
  • se tudo estiver normal, a compra é registada no cesto - linhas 31-32
  • em seguida, é enviada a vista [LISTE] — linha 34

1.5.7.7. Processamento da ação [panier]

O cliente efetuou várias compras e solicita ver o cesto de compras:

Image

O código é o seguinte:

         ' visualização do carrinho
        Public Sub doPanier()
             'recupera-se o carrinho da sessão
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
             ' cesto vazio?
            If unPanier.achats.Count = 0 Then
                 ' exibição do carrinho vazio
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlPanierVide"), String))
            End If
             ' visualização do cesto não vazio
            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. Aqui não verificamos se recuperámos efetivamente algo. Seria necessário fazê-lo, pois a sessão pode ter expirado.
  • Se o carrinho estiver vazio, envia-se a vista [PANIERVIDE] — linhas 6-10
  • caso contrário, enviamos a vista [PANIER] — linhas 11-14

1.5.7.8. Processamento da ação [retirerachat]

O cliente pretende retirar um artigo do seu cesto:

Image

O código é o seguinte:

         ' eliminação de uma compra
        Public Sub doRetirerAchat()
             'recuperar o carrinho da sessão
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
             ' remoção do produto
            Try
                 ' recuperar o ID do artigo removido
                Dim idArticle As Integer = Integer.Parse(Request.QueryString("id"))
                 ' remoção do cesto
                unPanier.enlever(idArticle)
            Catch ex As Exception
                 ' exibe-se a lista de artigos
                doListe()
            End Try
             ' exibe o carrinho
            doPanier()
        End Sub

Comentários:

  • recuperamos o carrinho da sessão — linha 4. Aqui não verificamos se recuperámos efetivamente algo. Seria necessário fazê-lo, pois a sessão pode ter expirado.
  • Recupera-se na requisição o identificador [id] do artigo a remover — linha 8.
  • A compra correspondente é removida do carrinho - linha 10
  • A validade do identificador do artigo comprado não foi verificada aqui. Se este tiver um tipo inválido, ocorrerá uma exceção que será tratada nas linhas 11-13. Se for válido mas inexistente, o método [panier.enlever] — linha 10 — não faz nada.
  • Apresenta-se o novo cesto de compras - linha 16

1.5.7.9. Processamento da ação [validerpanier]

O cliente pretende confirmar o seu cesto de compras:

Image

O código é o seguinte:

         ' compra do cesto
        Public Sub doValidationPanier()
             ' inicialmente, sem erros
            Dim erreurs As ArrayList
             'recupera-se o carrinho da sessão
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
             ' tenta-se validar o carrinho
            Try
                articlesDomain.acheter(unPanier)
                 ' registam-se eventuais erros nas compras
                erreurs = articlesDomain.erreurs
            Catch ex As Exception
                 ' regista-se o erro
                erreurs = New ArrayList
                erreurs.Add(String.Format("Erreur lors de la validation du panier [{0}]", ex.Message))
            End Try
             ' se houver erros, então página de erros
            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
             ' tudo correu bem — é apresentada a lista de artigos com uma mensagem de sucesso
            context.Items("message") = "Votre panier a été validé"
            doListe()
        End Sub

Comentários:

  • recuperamos o carrinho da sessão — linha 6. Aqui não verificamos se recuperámos efetivamente algo. Seria necessário fazê-lo, pois a sessão pode ter expirado.
  • Caso a sessão tenha expirado, teremos um ponteiro [nothing] para o carrinho e o método [acheter] — linha 9 — lançará uma exceção e a vista [ERREURS] será enviada. No entanto, a mensagem de erro será pouco clara para o utilizador.
  • Nas linhas 8-16, tenta-se validar o carrinho recuperado da sessão. Algumas compras podem não ser validadas se a quantidade solicitada exceder o stock do artigo em questão. Estes casos são registados pelo método [acheter] numa lista de erros que é recuperada na linha 11.
  • Se houver erros, é enviada a vista [ERREURS] — linhas 18-23
  • caso contrário, é enviada a vista [LISTE] — linhas 25-26

1.6. Conclusion

Desenvolvemos aqui uma aplicação de acordo com um modelo MVC. Não parece existir (abril de 2005) nenhum «framework» profissional de desenvolvimento MVC em ASP.NET, tal como existe em Java (Struts, Spring, ...). O projeto [Spring.net] deverá propor um em breve. Enquanto se aguarda essa novidade, o método anterior permite um desenvolvimento MVC viável para aplicações de média dimensão.