1. Parte 1
O PDF do documento está disponível |AQUI|.
Os exemplos no documento estão disponíveis |AQUI|.
1.1. Introdução
Objetivos deste artigo:
- Escrever uma aplicação web de três camadas [interface do utilizador, lógica de negócio, acesso a dados]
- Configurar a aplicação com Spring IOC
- Escrever diferentes versões alterando a implementação de uma ou mais das três camadas.
Ferramentas utilizadas:
- Visual Studio.NET para desenvolvimento — ver Apêndice, Secção 3.1;
- Servidor web Cassini para execução — ver Apêndice, Secção 3.2;
- Nunit para testes unitários — ver Apêndice, Secção 3.4;
- Spring para a integração e configuração das camadas da aplicação web — ver Apêndice, Secção 3.3;
Numa escala iniciante-intermédio-avançado, este documento enquadra-se na categoria [intermédio-avançado]. A sua compreensão requer vários pré-requisitos. Alguns destes podem ser encontrados em documentos que escrevi. Nesses casos, faço referência aos mesmos. Escusado será dizer que isto é meramente uma sugestão e que o leitor é livre de utilizar os recursos da sua preferência.
- Linguagem VB.NET: [Introdução ao VB.NET através de exemplos];
- Programação Web em VB.NET: [Desenvolvimento Web com ASP.NET 1.1];
- Utilização do aspeto IoC do Spring: [Spring IoC para .NET];
- Documentação do Spring.NET: [Spring.NET | Página inicial]
Este documento segue a mesma estrutura de um documento escrito para Java [Arquiteturas de 3 camadas e arquiteturas MVC com Struts, Spring e Java]. Estamos a construir a aplicação Web MVC de três camadas escrita em Java utilizando VB.NET. O que pretendemos demonstrar aqui é que as plataformas de desenvolvimento Java e .NET são suficientemente semelhantes para que as competências adquiridas num destes dois domínios possam ser reutilizadas no outro.
Não parece existir uma solução de desenvolvimento ASP.NET MVC amplamente reconhecida. A solução a seguir adota o método apresentado no documento [Desenvolvimento Web com ASP.NET 1.1]. Embora este método tenha o mérito de utilizar conceitos comuns no desenvolvimento J2EE, deve, no entanto, ser considerado pelo que é: um dos muitos métodos de desenvolvimento MVC. Assim que um método de desenvolvimento MVC em ASP.NET se tornar amplamente aceite, deve ser adotado. A versão .NET do Spring, atualmente em desenvolvimento, poderá muito bem ser uma primeira solução.
1.2. A aplicação webarticles
Apresentamos aqui os componentes de uma aplicação web de comércio eletrónico simplificada. Esta aplicação permitirá aos utilizadores da web:
- visualizar uma lista de artigos a partir de uma base de dados
- adicionar alguns deles a um carrinho de compras online
- confirmar o carrinho. Esta confirmação atualizará simplesmente o inventário dos itens adquiridos na base de dados.
As diferentes visualizações apresentadas ao utilizador serão as seguintes:
- a visualização «LISTA», que apresenta uma lista de artigos à venda ![]() | - a vista [INFO], que fornece informações adicionais sobre um produto: ![]() |
- as vistas [CARRINHO] e [CARRINHO VAZIO], que mostram o conteúdo do carrinho do cliente
![]() | ![]() |
- a vista [ERROS], que reporta quaisquer erros da aplicação

1.3. Arquitetura geral da aplicação
Pretendemos construir uma aplicação com a seguinte estrutura de três camadas:
![]() |
- As três camadas são tornadas independentes através da utilização de interfaces
- A integração das diferentes camadas é gerida pelo Spring
- Cada camada tem o seu próprio namespace: web (camada de interface do utilizador), domain (camada de negócios) e dao (camada de acesso a dados).
A aplicação seguirá uma arquitetura MVC (Model-View-Controller). Se nos referirmos ao diagrama em camadas acima, a arquitetura MVC encaixa-se nele da seguinte forma:
![]() |
O processamento de um pedido do cliente segue estes passos:
- O cliente faz uma solicitação ao controlador. Neste caso, o controlador é uma página .aspx que desempenha uma função específica. Ele lida com todas as solicitações do cliente. É o ponto de entrada da aplicação. É o C em MVC.
- O controlador processa esta solicitação. Para tal, pode necessitar da assistência da camada de negócios, conhecida como o M na estrutura MVC.
- O controlador recebe uma resposta da camada de negócios. A solicitação do cliente foi processada. Isso pode desencadear várias respostas possíveis. Um exemplo clássico é
- uma página de erro, caso a solicitação não tenha sido processada corretamente
- uma página de confirmação, caso contrário
- O controlador escolhe a resposta (= vista) a enviar ao cliente. Trata-se, na maioria das vezes, de uma página que contém elementos dinâmicos. O controlador fornece estes à vista.
- A vista é enviada ao cliente. Esta é o V em MVC.
1.4. O Modelo
Aqui, examinamos o M em MVC. O modelo é composto pelos seguintes elementos:
1.4.1. A base de dados
A base de dados contém apenas uma tabela chamada ARTICLES. Esta tabela foi gerada utilizando os seguintes comandos SQL:
CREATE TABLE ARTICLES (
ID INTEGER NOT NULL,
NOM VARCHAR(20) NOT NULL,
PRIX NUMERIC(15,2) NOT NULL,
STOCKACTUEL INTEGER NOT NULL,
STOCKMINIMUM INTEGER NOT NULL
);
/* constraints */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);
/* primary key */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
chave primária que identifica um item de forma única | |
nome do item | |
o seu preço | |
stock atual | |
o nível de stock abaixo do qual deve ser efetuada uma nova encomenda |
1.4.2. Os namespaces do modelo
O Modelo M é aqui apresentado sob a forma de dois espaços de nomes:
- istia.st.articles.dao: contém as classes de acesso a dados da camada [dao]
- istia.st.articles.domain: contém as classes de negócio da camada [domain]
Cada um destes namespaces será gerado no seu próprio ficheiro «assembly»:
conteúdo | função | |
- [IArticlesDao]: a interface para aceder à camada [dao]. Esta é a única interface visível para a camada [domain]. Não vê outras. - [Article]: classe que define um artigo - [ArticlesDaoArrayList]: classe de implementação da da interface [IArticlesDao] com um [ArrayList] | camada de acesso a dados - está inteiramente dentro da camada [DAO] da arquitetura de três camadas da aplicação web | |
- [IArticlesDomain]: a interface para aceder à camada [domain]. Esta é a única interface visível para a camada web. Não vê nenhuma outra. - [AchatsArticles]: uma classe que implementa [IArticlesDomain] - [Purchase]: uma classe que representa a compra de um cliente - [Cart]: uma classe que representa o total de compras de um cliente | representa o modelo de compras na web web - reside inteiramente na camada [domain] da arquitetura de 3 camadas da aplicação web |
1.4.3. A camada [DAO]
A camada [DAO] contém os seguintes elementos:
-
[IArticlesDao]: a interface para aceder à camada [dao]
-
[Article]: classe que define um artigo
-
[ArticlesDaoArrayList]: classe de implementação para a interface [IArticlesDao] utilizando uma classe [ArrayList]
A estrutura do projeto [Visual Studio] para a camada [dao] é a seguinte:

Comentários:
- O projeto [dao] é do tipo [biblioteca de classes]
- As classes foram colocadas numa estrutura em árvore a partir da pasta [istia]. Todas elas estão no namespace [istia.st.articles.dao].
1.4.3.1. A classe [Article]
A classe que define um artigo é a seguinte:
Imports System
Namespace istia.st.articles.dao
Public Class Article
' private fields
Private _id As Integer
Private _nom As String
Private _prix As Double
Private _stockactuel As Integer
Private _stockminimum As Integer
' id article
Public Property id() As Integer
Get
Return _id
End Get
Set(ByVal Value As Integer)
If Value <= 0 Then
Throw New Exception("Le champ id [" + Value.ToString + "] est invalide")
End If
Me._id = Value
End Set
End Property
' item name
Public Property nom() As String
Get
Return _nom
End Get
Set(ByVal Value As String)
If Value Is Nothing OrElse Value.Trim.Equals("") Then
Throw New Exception("Le champ nom [" + Value + "] est invalide")
End If
Me._nom = Value
End Set
End Property
' item price
Public Property prix() As Double
Get
Return _prix
End Get
Set(ByVal Value As Double)
If Value < 0 Then
Throw New Exception("Le champ prix [" + Value.ToString + "] est invalide")
End If
Me._prix = Value
End Set
End Property
' current stock item
Public Property stockactuel() As Integer
Get
Return _stockactuel
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New Exception("Le champ stockActuel [" + Value.ToString + "] est invalide")
End If
Me._stockactuel = Value
End Set
End Property
' minimum stock item
Public Property stockminimum() As Integer
Get
Return _stockminimum
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New Exception("Le champ stockMinimum [" + Value.ToString + "] est invalide")
End If
Me._stockminimum = Value
End Set
End Property
' default builder
Public Sub New()
End Sub
' builder with properties
Public Sub New(ByVal id As Integer, ByVal nom As String, ByVal prix As Double, ByVal stockactuel As Integer, ByVal stockminimum As Integer)
Me.id = id
Me.nom = nom
Me.prix = prix
Me.stockactuel = stockactuel
Me.stockminimum = stockminimum
End Sub
' article identification method
Public Overrides Function ToString() As String
Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
End Function
End Class
End Namespace
Esta classe fornece:
- um construtor para definir as 5 informações de um item: [id, nome, preço, stockAtual, stockMínimo]
- propriedades públicas para ler e gravar as 5 informações.
- uma validação dos dados introduzidos para o item. Se os dados forem inválidos, é lançada uma exceção.
- Um método toString que devolve o valor de um item como uma cadeia de caracteres. Isto é frequentemente útil para depurar uma aplicação.
1.4.3.2. A interface [IArticlesDao]
A interface [IArticlesDao] é definida da seguinte forma:
Imports System
Imports System.Collections
Namespace istia.st.articles.dao
Public Interface IArticlesDao
' list of all items
Function getAllArticles() As IList
' add an article
Function ajouteArticle(ByVal unArticle As Article) As Integer
' deletes an article
Function supprimeArticle(ByVal idArticle As Integer) As Integer
' modify an article
Function modifieArticle(ByVal unArticle As Article) As Integer
' search for an article
Function getArticleById(ByVal idArticle As Integer) As Article
' deletes all articles
Sub clearAllArticles()
' changes the stock of an item
Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
End Interface
End Namespace
As funções dos vários métodos na interface são as seguintes:
retorna todos os itens da fonte de dados | |
limpa a fonte de dados | |
retorna o objeto [Article] identificado pela sua chave primária | |
permite adicionar um artigo à fonte de dados | |
permite modificar um artigo na fonte de dados | |
permite eliminar um item da fonte de dados | |
permite modificar o stock de um item na fonte de dados |
A interface fornece aos programas clientes uma série de métodos definidos exclusivamente pelas suas assinaturas. Não se preocupa com a forma como esses métodos serão efetivamente implementados. Isto confere flexibilidade a uma aplicação. O programa cliente efetua chamadas a uma interface, em vez de a uma implementação específica dessa interface.
![]() |
A escolha de uma implementação específica será feita através de um ficheiro de configuração do Spring. Para demonstrar que, para testar a aplicação web, apenas a interface de acesso aos dados importa — e não a sua classe de implementação —, iremos primeiro implementar a fonte de dados utilizando um objeto [ArrayList] simples. Posteriormente, apresentaremos uma solução baseada em base de dados.
1.4.3.3. A classe de implementação [ArticlesDaoArrayList]
A classe de implementação [ArticlesDaoArrayList] é definida da seguinte forma:
Imports System
Imports System.Collections
Namespace istia.st.articles.dao
Public Class ArticlesDaoArrayList
Implements istia.st.articles.dao.IArticlesDao
Private articles As New ArrayList
Private Const nbArticles As Integer = 4
' default builder
Public Sub New()
' we build a few items
For i As Integer = 1 To nbArticles
articles.Add(New Article(i, "article" + i.ToString, i * 10, i * 10, i * 10))
Next
End Sub
' list of all items
Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
' returns the list of items
SyncLock Me
Return articles
End SyncLock
End Function
' delete all items
Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
' empty the list of items
SyncLock Me
articles.Clear()
End SyncLock
End Sub
' obtain an item identified by its key
Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
' search for the item in the list
SyncLock Me
Dim ipos As Integer = posArticle(articles, idArticle)
If ipos <> -1 Then
Return CType(articles(ipos), Article)
Else
Return Nothing
End If
End SyncLock
End Function
' add an item to the list of items
Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
' add the item to the list of items
SyncLock Me
' we check that it doesn't already exist
Dim ipos As Integer = posArticle(articles, unArticle.id)
If ipos <> -1 Then
Throw New Exception("L'article d'id [" + unArticle.id.ToString + "] existe déjà")
End If
' we add the article
articles.Add(unArticle)
' we return the result
Return 1
End SyncLock
End Function
' modify an article
Public Function modifieArticle(ByVal articleNouveau As Article) As Integer Implements IArticlesDao.modifieArticle
' modify an article
SyncLock Me
' we check that
Dim ipos As Integer = posArticle(articles, articleNouveau.id)
' if it doesn't exist
If ipos = -1 Then Return 0
' it exists - we modify it
articles(ipos) = articleNouveau
' we return the result
Return 1
End SyncLock
End Function
' delete an item identified by its key
Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
' article deletion
SyncLock Me
' we check that
Dim ipos As Integer = posArticle(articles, idArticle)
' if it doesn't exist
If ipos = -1 Then Return 0
' it exists - we remove it
articles.RemoveAt(ipos)
' we return the result
Return 1
End SyncLock
End Function
' change the stock of an item identified by its key
Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
' change the stock of an item
SyncLock Me
' we check that
Dim ipos As Integer = posArticle(articles, idArticle)
' if it doesn't exist
If ipos = -1 Then Return 0
' it exists - you modify your stock if you can
Dim unArticle As Article = CType(articles(ipos), Article)
' only change stock if it is sufficient
If unArticle.stockactuel + mouvement >= 0 Then
unArticle.stockactuel += mouvement
Return 1
Else
Return 0
End If
End SyncLock
End Function
' search for an item identified by its key
Private Function posArticle(ByVal listArticles As ArrayList, ByVal idArticle As Integer) As Integer
' returns the position of item [idArticle] in the list or -1 if not found
Dim unArticle As Article
For i As Integer = 0 To listArticles.Count - 1
unArticle = CType(listArticles(i), Article)
If unArticle.id = idArticle Then
Return i
End If
Next
' not found
Return -1
End Function
End Class
End Namespace
Comentários:
- A fonte de dados é simulada pelo campo privado [articles] do tipo [ArrayList]
- O construtor da classe cria 4 itens na fonte de dados por predefinição.
- Todos os métodos de acesso aos dados foram sincronizados para evitar problemas de acesso simultâneo à fonte de dados. Em qualquer momento, apenas um segmento tem acesso a um determinado método.
- O método [posArticle] devolve a posição [0..N] na fonte de dados [ArrayList] de um artigo identificado pelo seu número. Se o artigo não existir, o método devolve a posição -1. Este método é utilizado repetidamente pelos outros métodos.
- O método [addArticle] adiciona um artigo à lista de artigos. Retorna o número de artigos inseridos: 1. Se o artigo já existisse, é lançada uma exceção.
- O método [modifyItem] permite modificar um item existente. Retorna o número de itens modificados: 1 se o item existisse, 0 caso contrário.
- O método [deleteArticle] elimina um artigo existente. Devolve o número de artigos eliminados: 1 se o artigo existisse, 0 caso contrário.
- O método [getAllArticles] devolve uma lista de todos os artigos
- O método [getArticleById] recupera um artigo identificado pelo seu ID. Retorna o valor [nothing] se o artigo não existir.
- O código não apresenta qualquer dificuldade real. Deixamos ao leitor a tarefa de o rever e compreender.
1.4.3.4. Gerar a compilação da camada [dao]
O projeto do Visual Studio está configurado para gerar o assembly [webarticles-dao.dll]. Este é gerado na pasta [bin] do projeto:
![]() | ![]() |
1.4.3.5. Testes NUnit para a camada [dao]
Em Java, as classes são testadas utilizando a estrutura [JUnit]. No .NET, a estrutura NUnit oferece as mesmas capacidades de testes unitários:

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

Comentários:
- O projeto [tests] é uma [biblioteca de classes]
- Os testes [NUnit] requerem uma referência ao assembly [nunit.framework.dll]
- a classe de teste [NUnit] recupera uma instância do objeto em teste através do Spring. Portanto,
- na pasta [bin], os ficheiros de classe do Spring
- em [Referências], uma referência ao assembly [Spring-Core.dll] da pasta [bin]
- em [bin], um ficheiro de configuração para o Spring
- A classe de teste requer o assembly [webarticles-dao.dll] da camada [dao]. Este foi colocado na pasta [bin] e a sua referência adicionada às referências do projeto.
Uma classe de teste [NUnit] requer acesso às classes no namespace [NUnit.Framework]. Por conseguinte, é incluída a seguinte instrução de importação:
O namespace [NUnit.Framework] está localizado no assembly [nunit.framework.dll], que deve ser adicionado às referências do projeto:
![]() | ![]() |
O assembly [nunit.framework.dll] deverá constar na lista apresentada se o [NUnit] tiver sido instalado. Basta clicar duas vezes no assembly para o adicionar ao projeto:

A classe de teste [NUnit] para a camada [DAO] poderia ter o seguinte aspeto:
Imports System
Imports System.Collections
Imports NUnit.Framework
Imports istia.st.articles.dao
Imports System.Threading
Imports Spring.Objects.Factory.Xml
Imports System.IO
Namespace istia.st.articles.tests
<TestFixture()> _
Public Class NunitTestArticlesArrayList
' the test object
Private articlesDao As IArticlesDao
<SetUp()> _
Public Sub init()
' retrieve an instance of the Spring object manufacturer
Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
' request instantiation of articlesdao object
articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
End Sub
<Test()> _
Public Sub testGetAllArticles()
' visual check
listArticles()
End Sub
<Test()> _
Public Sub testClearAllArticles()
' delete all articles
articlesDao.clearAllArticles()
' all articles are requested
Dim articles As IList = articlesDao.getAllArticles
' verification: there must be 0
Assert.AreEqual(0, articles.Count)
End Sub
<Test()> _
Public Sub testAjouteArticle()
' delete all items
articlesDao.clearAllArticles()
' check: the item table must be empty
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' we add two items
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check: there must be two items
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' visual check
listArticles()
End Sub
<Test()> _
Public Sub testSupprimeArticle()
' delete all items
articlesDao.clearAllArticles()
' check: the item table must be empty
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' we add two items
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check: there must be 2 items
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' we delete article 4
articlesDao.supprimeArticle(4)
' check: there must be 1 item left
articles = articlesDao.getAllArticles
Assert.AreEqual(1, articles.Count)
' visual check
listArticles()
End Sub
<Test()> _
Public Sub testModifieArticle()
' delete all items
articlesDao.clearAllArticles()
' check
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' 2 items added
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' article 3 search
Dim unArticle As Article = articlesDao.getArticleById(3)
' check
Assert.AreEqual(unArticle.nom, "article3")
' research article 4
unArticle = articlesDao.getArticleById(4)
' check
Assert.AreEqual(unArticle.nom, "article4")
' modification article 4
articlesDao.modifieArticle(New Article(4, "article4", 44, 44, 44))
' check
unArticle = articlesDao.getArticleById(4)
Assert.AreEqual(unArticle.prix, 44, 0.000001)
' visual check
listArticles()
End Sub
<Test()> _
Public Sub testGetArticleById()
' article deletion
articlesDao.clearAllArticles()
' check
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' 2 items added
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' research article 3
Dim unArticle As Article = articlesDao.getArticleById(3)
' check
Assert.AreEqual(unArticle.nom, "article3")
' research article 4
unArticle = articlesDao.getArticleById(4)
' check
Assert.AreEqual(unArticle.nom, "article4")
End Sub
' screen listing
Private Sub listArticles()
Dim articles As IList = articlesDao.getAllArticles
For i As Integer = 0 To articles.Count - 1
Console.WriteLine(CType(articles(i), Article).ToString)
Next
End Sub
<Test()> _
Public Sub testArticleAbsent()
' delete all items
articlesDao.clearAllArticles()
' research article 1
Dim article As article = articlesDao.getArticleById(1)
' check
Assert.IsNull(article)
' modification of a non-existent item
Dim i As Integer = articlesDao.modifieArticle(New article(1, "1", 1, 1, 1))
' had to modify no line
Assert.AreEqual(i, 0)
' deletion of non-existent item
i = articlesDao.supprimeArticle(1)
' had to delete no line
Assert.AreEqual(0, i)
End Sub
<Test()> _
Public Sub testChangerStockArticle()
' delete all items
articlesDao.clearAllArticles()
' add an item
Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
Assert.AreEqual(nbArticles, 1)
' add an item
nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
Assert.AreEqual(nbArticles, 1)
' creation of 100 threads
Dim taches(99) As Thread
For i As Integer = 0 To taches.Length - 1
' create thread i
taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
' set the thread name
taches(i).Name = "tache_" & i
' start execution of thread i
taches(i).Start()
Next
' wait for all threads to finish
For i As Integer = 0 To taches.Length - 1
taches(i).Join()
Next
' checks - item 3 must have a stock of 1
Dim unArticle As Article = articlesDao.getArticleById(3)
Assert.AreEqual(unArticle.nom, "article3")
Assert.AreEqual(1, unArticle.stockactuel)
' item 4 stock is decremented
Dim erreur As Boolean = False
Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
' check: its stock must not have changed
Assert.AreEqual(0, nbLignes)
' visual check
listArticles()
End Sub
Public Sub décrémente()
' thread launched
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
' thread decrements stock
articlesDao.changerStockArticle(3, -1)
' thread terminated
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
End Sub
End Class
End Namespace
Comentários:
- Queríamos escrever um programa de teste para a interface [IArticlesDao] que fosse independente da sua classe de implementação. Por isso, utilizámos o Spring para ocultar o nome da classe de implementação do programa de teste.
- O método <Setup()> recupera uma referência ao objeto [articlesdao] a ser testado a partir do Spring. Isto está definido no seguinte ficheiro [spring-config.xml]:
<?xml version="1.0" encoding="iso-8859-1" ?>
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
<objects>
<object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoArrayList, webarticles-dao"/>
</objects>
Este ficheiro especifica o nome da classe de implementação [istia.st.articles.dao.ArticlesDaoArrayList] para a interface [IArticlesDao] e onde a encontrar [webarticles-dao.dll]. Uma vez que a instanciação não requer quaisquer parâmetros, nenhum é definido aqui.
- A maioria dos testes é fácil de compreender. Recomenda-se ao leitor que leia os comentários.
- O método [testChangerStockArticle] requer alguma explicação. Cria 100 threads responsáveis por diminuir o stock de um determinado artigo.
<Test()> _
Public Sub testChangerStockArticle()
' delete all items
articlesDao.clearAllArticles()
' add an item
Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
Assert.AreEqual(nbArticles, 1)
' add an item
nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
Assert.AreEqual(nbArticles, 1)
' creation of 100 threads
Dim taches(99) As Thread
For i As Integer = 0 To taches.Length - 1
' create thread i
taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
' set the thread name
taches(i).Name = "tache_" & i
' start execution of thread i
taches(i).Start()
Next
' wait for all threads to finish
For i As Integer = 0 To taches.Length - 1
taches(i).Join()
Next
' checks - item 3 must have a stock of 1
Dim unArticle As Article = articlesDao.getArticleById(3)
Assert.AreEqual(unArticle.nom, "article3")
Assert.AreEqual(1, unArticle.stockactuel)
' item 4 stock is decremented
Dim erreur As Boolean = False
Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
' check: its stock must not have changed
Assert.AreEqual(0, nbLignes)
' visual check
listArticles()
End Sub
Isto serve para testar o acesso simultâneo à fonte de dados. O método responsável pela atualização do stock é o seguinte:
Public Sub décrémente()
' thread launched
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
' thread decrements stock
articlesDao.changerStockArticle(3, -1)
' thread terminated
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
End Sub
Diminui o stock do artigo n.º 3 em uma unidade. Se consultarmos o código do método [testChangerStockArticle], vemos que:
- o stock do artigo n.º 3 é inicializado para 101
- as 100 threads irão, cada uma, diminuir este stock em um
- devemos, portanto, ter um stock de 1 no final da execução de todas as threads
Além disso, ainda dentro deste método, tentamos definir o stock do item #4 para um valor negativo. Isto deve falhar.
Para testar a camada [dao], geramos a DLL [tests-webarticles-dao.dll] na pasta [bin] do projeto [tests]:
![]() | ![]() |
Em seguida, utilizando a aplicação [Nunit-Gui], carregamos esta DLL e executamos os testes:

Na janela da esquerda, vemos a lista dos métodos testados. A cor do ponto que precede o nome de cada método indica se o método foi aprovado (verde) ou reprovado (vermelho). Os leitores que estiverem a visualizar este documento no ecrã verão que todos os testes foram aprovados. Consideraremos, portanto, que a camada [dao] está operacional.
1.4.4. A camada [domain]
A camada [domain] contém os seguintes elementos:
-
[IArticlesDomain]: a interface para aceder à camada [domain]
-
[Purchase]: classe que define uma compra
-
[ShoppingCart]: classe que define um carrinho de compras
-
[ProductPurchases]: a classe que implementa a interface [IArticlesDomain]
A estrutura da solução [Visual Studio] para a camada [domain] é a seguinte:

Comentários:
- O projeto [domain] é do tipo [biblioteca de classes]
- As classes foram colocadas numa estrutura em árvore a partir da pasta [istia]. Todas elas estão no namespace [istia.st.articles.domain].
- A DLL da camada [dao] foi colocada na pasta [bin] do novo projeto. Além disso, esta DLL foi adicionada como referência ao projeto.
1.4.4.1. A interface [IArticlesDomain]
A interface [IArticlesDomain] desacopla a camada [business] da camada [web]. Esta última acede à camada [business/domain] através desta interface sem se preocupar com a classe que a implementa efetivamente. A interface define as seguintes ações para aceder à camada de negócios:
Imports Article = istia.st.articles.dao.Article
Namespace istia.st.articles.domain
Public Interface IArticlesDomain
' methods
Sub acheter(ByVal panier As Panier)
Function getAllArticles() As IList
Function getArticleById(ByVal idArticle As Integer) As Article
ReadOnly Property erreurs() As ArrayList
End Interface
End Namespace
retorna a lista de objetos [Article] da fonte de dados associada | |
retorna o objeto [Article] identificado por [idArticle] | |
processa o carrinho do cliente, diminuindo o stock dos artigos comprados pela quantidade adquirida - pode falhar se o stock for insuficiente | |
retorna a lista de erros que ocorreram - vazia se não houver erros |
1.4.4.2. A classe [Purchase]
A classe [Purchase] representa uma compra do cliente:
Imports istia.st.articles.dao
Namespace istia.st.articles.domain
Public Class Achat
' private fields
Private _article As article
Private _qte As Integer
' default builder
Public Sub New()
End Sub
' builder with parameters
Public Sub New(ByVal unArticle As article, ByVal qte As Integer)
' we go through the properties
Me.article = unArticle
Me.qte = qte
End Sub
' item purchased
Public Property article() As article
Get
Return _article
End Get
Set(ByVal Value As article)
_article = Value
End Set
End Property
' qty purchased
Public Property qte() As Integer
Get
Return _qte
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New Exception("Quantité [" + Value.ToString + "] invalide")
End If
_qte = Value
End Set
End Property
' total purchase
Public ReadOnly Property totalAchat() As Double
Get
Return _qte * _article.prix
End Get
End Property
' identity
Public Overrides Function ToString() As String
Return "[" + _article.ToString + "," + _qte.ToString + "]"
End Function
End Class
End Namespace
Comentários:
- A classe [Purchase] tem as seguintes propriedades e métodos:
o item adquirido | |
a quantidade adquirida | |
o valor da compra | |
representação em cadeia de caracteres do objeto |
- Possui um construtor que inicializa as propriedades [item, qty] que definem uma compra.
1.4.4.3. A classe [Cart]
A classe [Cart] representa o total das compras do cliente:
Namespace istia.st.articles.domain
Public Class Panier
' private fields
Private _achats As New ArrayList
Private _totalPanier As Double = 0
' default builder
Public Sub New()
End Sub
' list of purchases
Public ReadOnly Property achats() As ArrayList
Get
Return _achats
End Get
End Property
' total purchases
Public ReadOnly Property totalPanier() As Double
Get
Return _totalPanier
End Get
End Property
' methods
Public Sub ajouter(ByVal unAchat As Achat)
' find out if the purchase already exists
Dim iAchat As Integer = posAchat(unAchat.article.id)
If iAchat <> -1 Then
' we found
Dim achatCourant As Achat = CType(_achats(iAchat), Achat)
achatCourant.qte += unAchat.qte
Else
' we didn't find
_achats.Add(unAchat)
End If
' increment the basket total
_totalPanier += unAchat.totalAchat
End Sub
' remove a purchase
Public Sub enlever(ByVal idAchat As Integer)
' we're looking to buy
Dim iachat As Integer = posAchat(idAchat)
' if found, remove
If iachat <> -1 Then
Dim achatCourant As Achat = CType(_achats(iachat), Achat)
' remove from basket
_achats.RemoveAt(iachat)
' decrement the basket total
_totalPanier -= achatCourant.totalAchat
End If
End Sub
Private Function posAchat(ByVal idArticle As Integer) As Integer
' search for a purchase in the purchase list
' returns its position in the list or -1 if not found
Dim achatCourant As Achat
Dim trouvé As Boolean = False
Dim i As Integer = 0
While Not trouvé AndAlso i < _achats.Count
' regular purchase
achatCourant = CType(_achats(i), Achat)
' comparison with article searched
If achatCourant.article.id = idArticle Then
Return i
End If
'next purchase
i += 1
End While
' not found
Return -1
End Function
' identity function
Public Overrides Function ToString() As String
Return _achats.ToString
End Function
End Class
End Namespace
Comentários:
- A classe [ShoppingCart] possui as seguintes propriedades e métodos:
a lista de compras do cliente - uma lista de objetos do tipo [Purchase] | |
adiciona uma compra à lista de compras | |
remove a compra com idPurchase | |
o valor total das compras no carrinho | |
retorna a string que representa o carrinho de compras |
- O método [posAchat] é um método utilitário que devolve a posição na lista de compras de uma compra identificada pelo número do artigo. A lista de compras é gerida de forma a que um artigo comprado várias vezes ocupe apenas uma posição na lista. Assim, uma compra pode ser identificada pelo número do artigo. O método [posAchat] devolve -1 se a compra procurada não existir.
- O método [add] adiciona uma nova compra à lista de compras. Isto adiciona uma nova entrada à lista de compras se o artigo comprado ainda não existisse na lista, ou aumenta a quantidade comprada se já existisse.
- O método [remove] permite remover uma compra identificada por um número da lista de compras. Se a compra não existir, o método não executa nenhuma ação.
- O valor total da compra [totalPanier] é atualizado à medida que os itens são adicionados e removidos.
1.4.4.4. A classe [PurchaseItems]
A interface [IArticlesDomain] será implementada pela seguinte classe [PurchaseItems]:
Imports istia.st.articles.dao
Namespace istia.st.articles.domain
Public Class AchatsArticles
Implements IArticlesDomain
'private fields
Private _articlesDao As IArticlesDao
Private _erreurs As ArrayList
' manufacturer
Public Sub New(ByVal articlesDao As IArticlesDao)
_articlesDao = articlesDao
End Sub
' error list
Public ReadOnly Property erreurs() As ArrayList Implements IArticlesDomain.erreurs
Get
Return _erreurs
End Get
End Property
' list of items
Public Function getAllArticles() As IList Implements IArticlesDomain.getAllArticles
' list of all items
Try
Return _articlesDao.getAllArticles
Catch ex As Exception
_erreurs = New ArrayList
_erreurs.Add("Erreur d'accès aux données : " + ex.Message)
End Try
End Function
' get an item identified by its number
Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDomain.getArticleById
' a special item
Try
Return _articlesDao.getArticleById(idArticle)
Catch ex As Exception
_erreurs = New ArrayList
_erreurs.Add("Erreur d'accès aux données : " + ex.Message)
End Try
End Function
' buy a basket
Public Sub acheter(ByVal panier As Panier) Implements IArticlesDomain.acheter
' basket purchase - stocks of purchased items must be decremented
_erreurs = New ArrayList
Dim achat As achat
Dim achats As ArrayList = panier.achats
For i As Integer = achats.Count - 1 To 0 Step -1
' decrement stock item i
achat = CType(achats(i), achat)
Try
If _articlesDao.changerStockArticle(achat.article.id, -achat.qte) = 0 Then
' we couldn't do the operation
_erreurs.Add("L'achat " + achat.ToString + " n'a pu se faire - Vérifiez les stocks")
Else
' the transaction has been completed - the purchase is removed from the basket
panier.enlever(achat.article.id)
End If
Catch ex As Exception
_erreurs = New ArrayList
_erreurs.Add("Erreur d'accès aux données : " + ex.Message)
End Try
Next
End Sub
End Class
End Namespace
Comentários:
- Esta classe implementa os quatro métodos da interface [IArticlesDomain]. Possui dois campos privados:
o objeto de acesso aos dados | |
a lista de possíveis erros. Pode ser acedida através da propriedade pública [errors] |
- Para criar uma instância da classe, deve fornecer o objeto que permite o acesso aos dados:
- Os métodos [getAllArticles] e [getArticleById] dependem dos métodos com o mesmo nome na camada [dao]
- O método [buy] valida a compra de um carrinho de compras. Esta validação envolve simplesmente a redução do stock dos artigos comprados. Um artigo só pode ser comprado se houver stock suficiente. Se não for esse o caso, a compra é rejeitada: o artigo permanece no carrinho de compras e é reportado um erro na lista [errors]. Uma compra validada é removida do carrinho e o stock do artigo correspondente é reduzido pela quantidade comprada.
1.4.4.5. Geração do assembly da camada [domain]
O projeto do Visual Studio está configurado para gerar o assembly [webarticles-domain.dll]. Este é gerado na pasta [bin] do projeto:
![]() | ![]() |
1.4.4.6. Testes NUnit para a camada [domain]
A estrutura do projeto de teste do Visual Studio é a seguinte:

Comentários:
- O projeto [tests] é do tipo [biblioteca de classes]
- os testes [NUnit] requerem uma referência ao assembly [nunit.framework.dll]
- A classe de teste [NUnit] recupera uma instância do objeto em teste através do Spring. Portanto,
- na pasta [bin], os ficheiros de classe Spring
- em [Referências], uma referência ao assembly [Spring-Core.dll] da pasta [bin]
- em [bin], um ficheiro de configuração para o Spring
- A classe de teste requer o assembly [webarticles-dao.dll] da camada [dao] e o assembly [webarticles-domain.dll] da camada [domain]. Estes foram colocados na pasta [bin] e as suas referências adicionadas às referências do projeto.
Uma classe de teste NUnit para a camada [domain] pode ter o seguinte aspeto:
Imports NUnit.Framework
Imports istia.st.articles.dao
Imports istia.st.articles.domain
Imports Spring.Objects.Factory.Xml
Imports System.IO
Namespace istia.st.articles.tests
<TestFixture()> _
Public Class NunitTestArticlesDomain
' the test object
Private articlesDomain As IArticlesDomain
Private articlesDao As IArticlesDao
<SetUp()> _
Public Sub init()
' retrieve an instance of the Spring object manufacturer
Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
' request instantiation of the articles dao object
articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
' then the articlesdomain aobject
articlesDomain = CType(factory.GetObject("articlesdomain"), IArticlesDomain)
End Sub
<Test()> _
Public Sub getAllArticles()
' visual check
listArticles()
End Sub
<Test()> _
Public Sub getArticleById()
' article deletion
articlesDao.clearAllArticles()
' check
Dim articles As IList = articlesDomain.getAllArticles
Assert.AreEqual(0, articles.Count)
' 2 items added
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check
articles = articlesDomain.getAllArticles
Assert.AreEqual(2, articles.Count)
' research article 3
Dim unArticle As Article = articlesDomain.getArticleById(3)
' check
Assert.AreEqual(unArticle.nom, "article3")
' research article 4
unArticle = articlesDao.getArticleById(4)
' check
Assert.AreEqual(unArticle.nom, "article4")
End Sub
<Test()> _
Public Sub acheterPanier()
' article deletion
articlesDao.clearAllArticles()
' check
Dim articles As IList = articlesDomain.getAllArticles
Assert.AreEqual(0, articles.Count)
' 2 items added
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check
articles = articlesDomain.getAllArticles
Assert.AreEqual(2, articles.Count)
' create a basket with two purchases
Dim panier As New panier
panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 10))
panier.ajouter(New Achat(New Article(4, "article4", 40, 40, 4), 10))
' checks
Assert.AreEqual(700, panier.totalPanier, 0.000001)
Assert.AreEqual(2, panier.achats.Count)
' shopping cart validation
articlesDomain.acheter(panier)
' checks
Assert.AreEqual(0, articlesDomain.erreurs.Count)
Assert.AreEqual(0, panier.achats.Count)
' research article 3
Dim unArticle As Article = articlesDomain.getArticleById(3)
' check
Assert.AreEqual(unArticle.stockactuel, 20)
' research article 4
unArticle = articlesDao.getArticleById(4)
' check
Assert.AreEqual(unArticle.stockactuel, 30)
' new basket
panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 100))
' shopping cart validation
articlesDomain.acheter(panier)
' checks
Assert.AreEqual(1, articlesDomain.erreurs.Count)
' research article 3
unArticle = articlesDomain.getArticleById(3)
' check
Assert.AreEqual(unArticle.stockactuel, 20)
End Sub
<Test()> _
Public Sub testRetirerAchats()
' delete contents of ARTICLES
articlesDao.clearAllArticles()
' reads the ARTICLES table
Dim articles As IList = articlesDao.getAllArticles()
Assert.AreEqual(0, articles.Count)
' insertion
Dim article3 As New Article(3, "article3", 30, 30, 3)
articlesDao.ajouteArticle(article3)
Dim article4 As New Article(4, "article4", 40, 40, 4)
articlesDao.ajouteArticle(article4)
' reads the ARTICLES table
articles = articlesDomain.getAllArticles()
Assert.AreEqual(2, articles.Count)
' create a basket with two purchases
Dim monPanier As New Panier
monPanier.ajouter(New Achat(article3, 10))
monPanier.ajouter(New Achat(article4, 10))
' checks
Assert.AreEqual(700.0, monPanier.totalPanier, 0.000001)
Assert.AreEqual(2, monPanier.achats.Count)
' add a previously purchased item
monPanier.ajouter(New Achat(article3, 10))
' checks
' the total must be increased to 1000
Assert.AreEqual(1000.0, monPanier.totalPanier, 0.000001)
' always 2 items in the basket
Assert.AreEqual(2, monPanier.achats.Count)
' qty item 3 increased to 20
Dim unAchat As Achat = CType(monPanier.achats(0), Achat)
Assert.AreEqual(20, unAchat.qte)
' article 3 is removed from the basket
monPanier.enlever(3)
' checks
' the total must be increased to 400
Assert.AreEqual(400.0, monPanier.totalPanier, 0.000001)
' 1 item only in basket
Assert.AreEqual(1, monPanier.achats.Count)
' this must be article no. 4
Assert.AreEqual(4, CType(monPanier.achats(0), Achat).article.id)
End Sub
' screen listing
Private Sub listArticles()
Dim articles As IList = articlesDomain.getAllArticles
For i As Integer = 0 To articles.Count - 1
Console.WriteLine(CType(articles(i), Article).ToString)
Next
End Sub
End Class
End Namespace
Comentários:
- Queríamos escrever um programa de teste para a interface [IArticlesDomain] que fosse independente da sua classe de implementação. Por isso, utilizámos o Spring para ocultar o nome da classe de implementação do programa de teste.
- O método de atributo <Setup()> recupera do Spring uma referência aos objetos [articlesdomain] e [articlesdao] a serem testados. Estes estão definidos no seguinte ficheiro [spring-config.xml]:
<?xml version="1.0" encoding="iso-8859-1" ?>
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
<objects>
<object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoArrayList, webarticles-dao" />
<object id="articlesdomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
<constructor-arg index="0">
<ref object="articlesdao" />
</constructor-arg>
</object>
</objects>
Este ficheiro indica:
- (continuação)
- para o singleton [articlesdao], o nome da classe de implementação [istia.st.articles.dao.ArticlesDaoArrayList] e onde encontrá-la [webarticles-dao.dll]. Uma vez que a instanciação não requer quaisquer parâmetros, nenhum é definido aqui.
- para o singleton [articlesdomain], o nome da classe de implementação [istia.st.articles.domain.AchatsArticles] e onde encontrá-la [webarticles-domain.dll]. A classe [AchatsArticles] possui um construtor com um parâmetro: o singleton que gere o acesso à camada [dao]. Aqui, este é definido como o singleton [articlesdao] definido anteriormente.
- A classe de teste obtém uma instância da classe em teste [articlesdomain], bem como uma instância da classe de acesso aos dados [articlesdao]. Este último ponto é controverso. A classe de teste, teoricamente, não deveria precisar de aceder à camada [dao], da qual nem sequer deveria ter conhecimento. Aqui, ignorámos esta «ética», que, se seguida, nos teria obrigado a criar novos métodos na nossa interface [IArticlesDomain].
Para testar a camada [domain], geramos a DLL [tests-webarticles-domain.dll] na pasta [bin] do projeto [tests]:
![]() | ![]() |
Em seguida, utilizando a aplicação [Nunit-Gui], carregamos esta DLL e executamos os testes:

Os leitores que estiverem a visualizar este documento no ecrã verão que todos os testes foram bem-sucedidos. Consideraremos agora a camada [domain] como operacional.
1.4.5. Conclusão
Lembre-se de que queremos construir a seguinte aplicação web de três camadas:
![]() |
O modelo M da nossa aplicação MVC já está escrito e testado. É-nos fornecido em duas DLLs [webarticles-dao.dll, webarticles-domain.dll]. Podemos passar para a última camada, a camada [web], que contém o controlador C e as vistas V. Vamos primeiro considerar um método apresentado no documento [Web Development with ASP.NET 1.1]
- O controlador C é implementado por dois ficheiros [global.asax, main.aspx]
- As vistas V são tratadas por páginas aspx
1.5. A camada [web]
A arquitetura MVC da aplicação web será a seguinte:
![]() |
As classes de negócio [domínio], as classes de acesso aos dados [DAO] e a fonte de dados | |
as páginas ASPX | |
Todas as solicitações de clientes HTTP passam pelos dois controladores a seguir: global.asax: trata de eventos relacionados com o arranque inicial da aplicação main.aspx: processa cada pedido do cliente individualmente |
1.5.1. As vistas
As vistas correspondem às apresentadas no início deste documento:
list.aspx | As visualizações estão localizadas na pasta [views] da aplicação ![]() | |
info.aspx | ||
cart.aspx | ||
empty-cart.aspx | ||
errors.aspx |
1.5.2. Os controladores
Como mencionado, o controlador será composto por dois elementos:
- [global.asax, global.asax.vb]: utilizados principalmente para inicializar a aplicação e configurar o contexto para todos os dados a serem partilhados entre diferentes clientes
- [main.aspx, main.aspx.vb]: o controlador propriamente dito, que lida com os pedidos HTTP dos clientes.
As várias solicitações dos clientes serão enviadas para o controlador [main.aspx] e conterão um parâmetro chamado [action] que especifica a ação solicitada pelo cliente:
request | significado | ação do controlador | respostas possíveis |
o cliente deseja a lista de itens | - solicita a lista de itens da camada profissão | - [LISTA] - [ERROS] | |
O cliente solicita informações sobre um dos itens apresentados na vista [LIST] | - solicita o item à camada de negócios | - [INFO] - [ERROS] | |
O cliente compra um artigo | - solicita o artigo à camada de negócios e adiciona-o ao carrinho do cliente | - [INFO] se houver erro na quantidade - [LIST] se não houver erro | |
o cliente deseja remover um artigo do seu carrinho | - recuperar o carrinho da sessão e modifica-o | - [CARRINHO] - [CARRINHO VAZIO] - [ERROS] | |
O cliente deseja visualizar o seu carrinho | - recupera o carrinho da sessão | - [CARRINHO DE COMPRAS] - [CARRINHO VAZIO] - [ERROS] | |
O cliente terminou as compras e avança para o checkout | - Atualiza a base de dados com os níveis de stock dos artigos comprados - esvazia o carrinho do cliente dos artigos cuja compra foi confirmada | - [LISTA] - [ERROS] |
1.5.3. Configuração da aplicação
O nosso objetivo será configurar a aplicação de forma a torná-la o mais flexível possível no que diz respeito a alterações como:
- alterações aos URLs das várias vistas
- alterações nas classes que implementam as interfaces [IArticlesDao] e [IArticlesDomain]
- alterações no SGBD, na base de dados ou na tabela de artigos
1.5.3.1. Alterações de URL
Os nomes dos URLs das vistas serão colocados no ficheiro de configuração [web.config] da aplicação, juntamente com alguns outros parâmetros:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
..
<appSettings>
<add key="urlMain" value="/webarticles/main.aspx"/>
<add key="urlInfos" value="vues/infos.aspx"/>
<add key="urlErreurs" value="vues/erreurs.aspx"/>
<add key="urlListe" value="vues/liste.aspx"/>
<add key="urlPanier" value="vues/panier.aspx"/>
<add key="urlPanierVide" value="vues/paniervide.aspx"/>
</appSettings>
</configuration>
1.5.3.2. Alterar as classes que implementam as interfaces
De acordo com o princípio das arquiteturas de três camadas, as camadas devem estar isoladas umas das outras. Este isolamento é alcançado da seguinte forma:
- as camadas comunicam entre si através de interfaces, e não de classes concretas
- o código de uma camada nunca instancia a própria classe de outra camada para a utilizar. Simplesmente solicita uma instância da implementação da interface a uma ferramenta externa — neste caso, o Spring — para a camada que pretende utilizar. Para tal, sabemos que não precisa de conhecer o nome da classe de implementação, mas apenas o nome do singleton do Spring para o qual pretende uma referência.
Na nossa aplicação, o Spring será configurado no ficheiro [web.config] da aplicação web da seguinte forma:
<?xml version="1.0" encoding="iso-8859-1" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
<resource uri="config://spring/objects" />
</context>
<objects>
<object id="articlesDao" type="istia.st.articles.dao.ArticlesDaoArrayList, webarticles-dao" />
<object id="articlesDomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
<constructor-arg index="0">
<ref object="articlesDao" />
</constructor-arg>
</object>
</objects>
</spring>
<appSettings>
<add key="urlMain" value="/webarticles/main.aspx"/>
<add key="urlInfos" value="vues/infos.aspx"/>
<add key="urlErreurs" value="vues/erreurs.aspx"/>
<add key="urlListe" value="vues/liste.aspx"/>
<add key="urlPanier" value="vues/panier.aspx"/>
<add key="urlPanierVide" value="vues/paniervide.aspx"/>
</appSettings>
</configuration>
Para aceder à camada [business], uma classe na camada [web] pode solicitar o singleton [articlesDomain]. O Spring irá então instanciar um objeto do tipo [istia.st.articles.domain.AchatsArticles]. Para esta instanciação, é necessário um objeto do tipo [articlesDao], ou seja, um objeto do tipo [istia.st.articles.dao.ArticlesDaoArrayList]. O Spring irá então instanciar esse objeto. No final da operação, a camada [web] que solicitou o singleton [articlesDomain] dispõe de toda a cadeia que a liga à fonte de dados:
![]() |
1.5.3.3. Alterações relacionadas com o SGBD ou a base de dados
Este ponto será ignorado aqui, uma vez que estamos a trabalhar numa aplicação de teste sem um DBMS. Abordaremos a implementação de uma camada [dao] baseada num DBMS numa secção posterior.
1.5.4. A biblioteca de tags <asp:>
Considere a vista [ERRORS], que apresenta uma lista de erros:

A vista [ERRORS] é responsável por exibir uma lista de erros que o controlador [main.aspx] colocou no contexto da solicitação sob o nome [context.Items("errors")]. Existem várias maneiras de escrever uma página deste tipo. Aqui, estamos interessados apenas na parte de exibição de erros.
Lembre-se de que uma página ASPX possui uma camada de apresentação HTML e uma camada de código .NET que prepara os dados que a camada de apresentação deve exibir. Estas duas partes podem estar no mesmo ficheiro [aspx] (solução WebMatrix) ou em dois ficheiros: [aspx] para a apresentação, [aspx.vb] para o código. A última solução é a utilizada pelo Visual Studio. Para complicar as coisas, a camada de apresentação HTML também pode conter código .NET, o que tende a esbater a separação entre as camadas [controlador] e [apresentação] da vista. Esta abordagem é geralmente fortemente desaconselhada. Remover todo o código da secção [apresentação] exigia a criação de bibliotecas de tags. Estas «escondem» o código sob o disfarce de tags semelhantes às tags HTML. Apresentamos duas soluções possíveis para a página [ERRORS].
A nossa primeira solução utiliza código .NET na secção [apresentação] da página. A página ASPX recupera a lista de erros presentes no pedido na sua secção de controlador [errors.aspx.vb]:
Protected erreurs As ArrayList
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
' error recovery
erreurs = CType(context.Items("erreurs"), ArrayList)
End Sub
e, em seguida, exibi-los na secção [presentation, errors.aspx]:
<h2>Les erreurs suivantes se sont produites :</h2>
<ul>
<%
for i as integer=0 to erreurs.count-1
response.write("<li>" & erreurs(i).ToString & "</li>")
next
%>
</ul>
A segunda solução utiliza a tag <asp:repeater> da biblioteca de tags <asp:> do ASP.NET. Se criar uma página ASPX graficamente, esta tag está disponível como um controlo de servidor que pode arrastar para o formulário de design. Se escrever o código ASPX manualmente, pode consultar a biblioteca de tags.
Com a biblioteca de tags <asp:>, o código ASPX para a vista [ERRORS] anterior passa a ser o seguinte:
<asp:Repeater id="rptErreurs" runat="server">
<HeaderTemplate>
<h3>Les erreurs suivantes se sont produites :
</h3>
<ul>
</HeaderTemplate>
<ItemTemplate>
<li>
<%# Container.DataItem %>
</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
O
A tag é utilizada para repetir um modelo HTML nos vários itens de uma fonte de dados. Os seus vários elementos são os seguintes:
o modelo HTML a apresentar antes dos elementos da fonte de dados serem apresentados | |
o modelo HTML a repetir para cada item na fonte de dados. A expressão [<%# Container.DataItem %>] é utilizada para apresentar o valor do item atual na fonte de dados | |
o modelo HTML a apresentar após a exibição dos itens da fonte de dados |
A fonte de dados está associada à tag, normalmente na secção [controller] da página:
Protected WithEvents rptErreurs As System.Web.UI.WebControls.Repeater
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
..
' link errors to rptErreurs
With rptErreurs
.DataSource = context.Items("erreurs")
.DataBind()
End With
End Sub
Esta ligação também pode ser feita durante o design da página se a fonte de dados já for conhecida, como um banco de dados existente.
Nas nossas vistas, utilizaremos outra tag: <asp:datagrid>, que nos permite apresentar uma fonte de dados sob a forma de tabela.
1.5.5. Estrutura da solução do Visual Studio para a aplicação [webarticles]
Uma aplicação web é um puzzle com muitas peças. Atribuir-lhe uma arquitetura MVC geralmente aumenta o número dessas peças. A estrutura da aplicação [webarticles] no [Visual Studio] é a seguinte:
![]() | ![]() | ![]() |
![]() |
Comentários:
- O projeto [web] é do tipo [Biblioteca de Classes] e não do tipo [Aplicação Web ASP.NET], como seria de esperar. O tipo [Aplicação Web ASP.NET] requer a presença do servidor web IIS na máquina de desenvolvimento ou numa máquina remota. O servidor IIS não está incluído por predefinição nas máquinas com Windows XP Home Edition. No entanto, muitos computadores são vendidos com esta versão. Para permitir que os leitores que utilizam o Windows XP implementem a aplicação em estudo, utilizaremos o servidor Web Cassini (ver apêndice), disponível gratuitamente na Microsoft, e substituiremos o projeto [Aplicação Web ASP.NET] por um projeto [Biblioteca de Classes]. Isto acarreta algumas desvantagens, que são explicadas nos apêndices.
- As DLLs utilizadas pela aplicação são as seguintes:
contém as classes da camada de acesso aos dados | |
contém as classes da camada de negócios | |
contém as classes Spring que nos permitem integrar as camadas web, domínio e DAO | |
classes de registo — utilizadas pelo Spring |
Estas DLLs são colocadas na pasta [bin] e adicionadas às referências do projeto.
1.5.6. As vistas ASPX
Conforme recomendado anteriormente, utilizaremos a biblioteca de tags <asp:> nas nossas vistas ASPX.
1.5.6.1. O componente de utilizador [entete.ascx]
Para garantir a consistência entre as diferentes vistas, estas partilharão o mesmo cabeçalho, que exibe o nome da aplicação juntamente com o menu:
![]() | ![]() |
O menu é dinâmico e definido pelo controlador. O controlador inclui um atributo-chave «actions» no pedido enviado para a página ASPX, com um valor associado que consiste numa matriz de elementos Hashtable(). Cada elemento desta matriz é um dicionário destinado a gerar uma opção do menu do cabeçalho. Cada dicionário tem duas chaves:
- href: o URL associado à opção do menu
- link: o texto do menu
Vamos transformar o cabeçalho num controlo de utilizador. Um controlo de utilizador encapsula uma secção de uma página (layout e código associado) num componente que pode depois ser reutilizado noutras páginas. Aqui, queremos reutilizar o componente [entete] noutras vistas da aplicação. O código de apresentação estará em [entete.ascx] e o código de controlo associado em [entete.ascx.vb]. O código de apresentação utilizará um componente <asp:repeater> para apresentar a tabela de opções do menu:
![]() |
N.º | tipo | nome | função |
1 | repetidor | rptMenu fonte de dados: uma matriz de dicionários com duas chaves: href, link | exibir opções do menu |
O código de apresentação da página será o seguinte:
Comentários:
- O componente [repeater] é definido nas linhas 6–14
- Cada elemento na fonte de dados associada ao repetidor é um dicionário com duas chaves: href (linha 9) e link (linha 10)
O código de controlo associado será o seguinte:
Comentários:
- O componente [EnteteWebArticles] possui uma propriedade [actions] pública e de gravação exclusiva - linha 7
- Esta propriedade permite que o componente <asp:repeater> denominado [rptMenu] — linha 10 — seja vinculado à matriz de opções calculada pelo controlador da aplicação — linhas 11–12.
As outras vistas na aplicação utilizarão o cabeçalho definido por [entete.ascx]. A página [erreurs.aspx], por exemplo, incluirá o cabeçalho utilizando o seguinte código:
Comentários:
- A linha 1 especifica que a tag <WA:entete> deve estar associada ao componente definido pelo ficheiro [entete.ascx]. Os atributos [TagPrefix] e [TagName] são opcionais.
- Depois de feito isto, o componente é inserido no código de apresentação da página através da linha 9. Em tempo de execução, esta tag irá incluir o código da página [entete.ascx] na página ASPX que a contém. O código de controlo [erreurs.aspx.vb] irá tratar da inicialização deste componente. Pode fazê-lo da seguinte forma:
Comentários:
- A linha 6 cria um objeto do tipo [EnteteWebArticles], que é o tipo do componente que está a ser criado
- A linha 11 inicializa a propriedade [actions] deste objeto
1.5.6.2. A vista [liste.aspx]
1.5.6.2.1. Introdução
Esta vista apresenta a lista de artigos disponíveis para venda:
![]() | ![]() |
É apresentada na sequência de um pedido para /main?action=list ou /main?action=cartvalidation. Os elementos do pedido do controlador são os seguintes:
Objeto Hashtable() - a matriz de opções do menu | |
ArrayList de objetos do tipo [Item] | |
Objeto String - mensagem a exibir na parte inferior da página |
Cada link [Info] na tabela HTML de artigos tem um URL no formato [?action=info&id=ID], em que ID é o campo id do artigo exibido.
1.5.6.2.2. Componentes da página
![]() |
N.º | tipo | nome | função |
componente de utilizador | cabeçalho | cabeçalho de exibição | |
DataGrid | DataGridArtigos 3 - coluna relacionada: cabeçalho: Nome, campo: nome 4 - coluna relacionada: cabeçalho: Preço, campo: preço 5 - coluna de hipertexto: texto: Informações, campo URL: id, formato de URL: /webarticles/main.aspx?action=info&id={0} | exibir itens à venda | |
rótulo | lblMessage | exibir uma mensagem |
Vamos rever como definir estas propriedades:
- No Visual Studio, selecione o [DataGrid] para aceder à sua folha de propriedades:

- Utilize o link [AutoFormat] acima para gerir o layout da grelha apresentada
- e o link [Property Generator] para gerir o seu conteúdo
1.5.6.2.3. Código de apresentação [liste.aspx]
Comentários:
- A linha 9 define o cabeçalho da página
- As linhas 12–24 definem as propriedades do [DataGrid]
- A linha 26 define o rótulo [lblMessage]
1.5.6.2.4. Código do controlador [liste.aspx.vb]
Comentários:
- Os componentes da página aparecem nas linhas 13–15. Note-se que foi necessário criar um objeto [EnteteWebArticles] utilizando o operador [new], ao passo que isso não foi necessário com os outros componentes. Sem esta criação explícita, ocorreu um erro de tempo de execução indicando que o objeto [entete] não referenciava nada. Este ponto merece uma investigação mais aprofundada. Ainda não foi investigado.
- A tabela de opções do menu de cabeçalho é recuperada do contexto para inicializar o componente [entete] da página — linha 20
- A lista de artigos é obtida do contexto — linha 22
- para inicializar o componente [DataGridArticles] — linhas 24–27
- O componente [lblMessage] é inicializado com uma mensagem colocada no contexto — linha 29
1.5.6.3. A vista [infos.aspx]
1.5.6.3.1. Introdução
Esta vista apresenta informações sobre um item e permite também a sua compra:

É apresentada na sequência de um pedido /main?action=infos&id=ID ou de um pedido /main?action=achat&id=ID quando a quantidade comprada está incorreta. Os parâmetros de pedido do controlador são os seguintes:
Objeto Hashtable() - a matriz de opções do menu | |
objeto do tipo [Artigo] - item a exibir | |
Objeto String - mensagem a exibir em caso de erro com a quantidade | |
Objeto String - valor a exibir no campo de entrada [Qty] |
Os campos [msg] e [qte] são utilizados em caso de erro de introdução de dados relativo à quantidade:

Esta página contém um formulário que é enviado através do botão [Comprar]. O URL de destino para o pedido POST é [?action=purchase&id=ID], em que ID é o ID do artigo adquirido.
1.5.6.3.2. Componentes da página
![]() |
N.º | tipo | nome | função |
componente de utilizador | cabeçalho | cabeçalho de exibição | |
literal | litID | exibir número do item | |
DataGrid | DataGridArticle 3 - coluna relacionada: cabeçalho: Nome, campo: nome 4 - coluna relacionada: cabeçalho: Preço, campo: preço 5 - coluna relacionada: cabeçalho: Stock atual, campo: stockAtual 6 - coluna relacionada: cabeçalho: Stock mínimo, campo: stockMinimum | exibir um item | |
Enviar HTML | Enviar o formulário | ||
Entrada HTML runat=server | txtQty | Introduza a quantidade comprada | |
label | lblMsgQte | qualquer mensagem de erro |
1.5.6.3.3. O código de apresentação [infos.aspx]
Comentários:
- o cabeçalho está incluído na página - linha 9
- O literal [litId] é definido na linha 10
- O DataGrid [DataGridArticles] é definido nas linhas 12–34
- O formulário é definido nas linhas 36–46. É do tipo POST.
- O destino POST é fornecido por uma variável [strAction] - linha 36. Esta variável deve ser definida pelo controlador.
- O campo de entrada para a quantidade comprada é definido na linha 41. É um componente HTML do lado do servidor (runat=server). No lado do código, é acedido através de um objeto.
- A linha 42 define o rótulo [lblMsgQte], que conterá quaisquer mensagens de erro relativas à quantidade introduzida
1.5.6.3.4. O código de controlo [infos.aspx.vb]
Comentários:
- Os componentes da página estão definidos nas linhas 10–14
- A classe define uma propriedade pública [strAction] utilizada para definir o destino POST do formulário - linhas 17-25
- O artigo a ser exibido é recuperado do contexto da aplicação — linha 30
- A matriz de opções do menu de cabeçalho é recuperada do contexto para inicializar o componente [entete] da página — linha 32
- linhas 33–39: o componente [DataGridArticle] é vinculado a uma fonte de dados do tipo [ArrayList] contendo apenas o artigo recuperado na linha 30
- os componentes [lblMsgQte, txtQte] são inicializados com informações retiradas do contexto - linhas 42-45
- a propriedade [straction] também é inicializada com informações retiradas do contexto — linha 47. Esta variável é utilizada para gerar o atributo [action] do formulário HTML presente na página:
1.5.6.4. A vista [panier.aspx]
1.5.6.4.1. Introdução
Esta vista apresenta o conteúdo do carrinho de compras:

É apresentada em resposta a um pedido como /main?action=cart ou /main?action=cancelpurchase&id=ID. Os parâmetros de pedido do controlador são os seguintes:
object Hashtable() - o conjunto de opções do menu | |
objeto do tipo [Cart] - o carrinho a apresentar |
Cada link [Remover] na tabela HTML dos itens do carrinho de compras tem um URL no formato [?action=removeitem&id=ID], em que ID é o campo [id] do item a ser removido do carrinho.
1.5.6.4.2. Componentes da página
![]() |
N.º | tipo | nome | função |
componente de utilizador | cabeçalho | cabeçalho de exibição | |
DataGrid | DataGridCompras 3 - coluna relacionada - cabeçalho: Item, campo: nome 4 - coluna relacionada - cabeçalho: Qtd., campo: qtd. 5 - coluna relacionada - cabeçalho: Preço, campo: preço 6 - coluna relacionada - cabeçalho: Total, campo: total, formatação {0:C} 7 - coluna de hipertexto - Texto: Remover, URL: id, formato da URL: /webarticles/main.aspx?action=retirerachat&id={0} | exibir a lista de itens comprados | |
rótulo | lblTotal | exibir o valor a pagar |
1.5.6.4.3. O código de apresentação [panier.aspx]
Comentários
- A linha 9 inclui o cabeçalho
- As linhas 12–27 definem o componente [DataGridAchats]
- Linha 29: o componente [lblTotal] é definido
1.5.6.4.4. O código de controlo [panier.aspx.vb]
Comentários:
- Os componentes da página são declarados nas linhas 11–13
- A inicialização do componente [header] é idêntica à encontrada nas páginas já estudadas — linha 17
- O carrinho de compras a ser exibido é recuperado da sessão — linha 19
- A exibição deste carrinho de compras utilizando o componente [DataGridAchats] coloca problemas. A dificuldade decorre da inicialização do componente. Vamos rever as suas colunas:
- Coluna [Article] associada ao campo [name] na fonte de dados
- Coluna [Qty] associada ao campo [qty] na fonte de dados
- Coluna [Price] associada ao campo [price] na fonte de dados
- Coluna [Total] associada ao campo [total] na fonte de dados
A fonte de dados que temos é o carrinho de compras e a sua lista de compras. Esta última servirá como fonte de dados para o [DataGrid]. No entanto, os objetos [Purchase] que irão preencher as linhas do [DataGrid] não possuem as propriedades [name, qty, price, total] esperadas pelo [DataGrid]. Por isso, criamos aqui, especificamente para o [DataGrid], uma fonte de dados cujos elementos têm as características esperadas pelo [DataGrid]. Estes elementos serão do tipo [PurchaseLine], uma classe criada para este fim e derivada da classe [Purchase] — linhas 36–66
- Uma vez definida a classe [PurchaseLine], a fonte de dados para [DataGridPurchases] é construída a partir do carrinho de compras encontrado na sessão — linhas 20–30
- O valor total da compra é exibido utilizando a propriedade [totalPanier] da classe [Panier] — linha 32
1.5.6.5. A vista [emptyCart.aspx]
1.5.6.5.1. Introdução
Esta vista apresenta informações que indicam que o carrinho de compras está vazio:

É apresentado após um pedido para /main?action=panier ou /main?action=retirerachat&id=ID. Os parâmetros do pedido do controlador são os seguintes:
Objeto Hashtable() - a matriz de opções do menu |
1.5.6.5.2. Componentes da página
![]() |
N.º | tipo | nome | função |
componente de utilizador | cabeçalho | cabeçalho de exibição |
1.5.6.5.3. O código de apresentação [emptycart.aspx]
Comentários:
- O cabeçalho está incluído na linha 9
1.5.6.5.4. O código de controlo [paniervide.aspx.vb]
Comentários:
- Inicializamos simplesmente o único componente dinâmico da página - linha 10
1.5.6.6. A vista [errors.aspx]
1.5.6.6.1. Introdução
Esta vista é apresentada em caso de erros:

É apresentada após qualquer pedido que resulte num erro, exceto no caso da ação de compra com uma quantidade incorreta, que é tratada pela vista [INFOS]. Os elementos do pedido do controlador são os seguintes:
Objeto Hashtable() - a matriz de opções do menu | |
ArrayList de objetos [String] que representam as mensagens de erro a exibir |
1.5.6.6.2. Componentes da página
![]() |
N.º | tipo | nome | função |
componente de utilizador | cabeçalho | cabeçalho de exibição | |
repetidor | rptErrors | exibir a lista de erros |
1.5.6.6.3. O código de apresentação [errors.aspx]
Comentários:
- O cabeçalho é definido na linha 9
- O componente [rptErrors] é definido nas linhas 13–19. O seu conteúdo provém de uma fonte de dados do tipo [ArrayList] que contém objetos [String].
1.5.6.6.4. O código de controlo [errors.aspx.vb]
Comentários:
- O componente [header] é inicializado como habitualmente, nas linhas 9 e 14
- O componente [rptErrors] é inicializado com a lista de erros [ArrayList] encontrada no contexto — linhas 16–19
1.5.7. Os controladores global.asax e main.aspx
Ainda precisamos de escrever o núcleo da nossa aplicação web: o controlador. A sua função é:
- recuperar o pedido do cliente,
- processar a ação solicitada pelo cliente utilizando as classes de negócio,
- enviar a vista apropriada em resposta.
1.5.7.1. O controlador [global.asax.vb]
Quando a aplicação recebe a sua primeira solicitação, o procedimento [Application_Start] no ficheiro [global.asax.vb] é executado. Isto acontecerá apenas uma vez. O objetivo do procedimento [Application_Start] é inicializar os objetos necessários à aplicação web, que serão partilhados em modo de leitura apenas por todas as threads do cliente. Estes objetos partilhados podem ser colocados em dois locais:
- os campos privados do controlador
- o contexto de execução da aplicação (Application)
O método [Application_Start] no ficheiro [global.asax.vb] irá realizar as seguintes ações:
- verificar o ficheiro [web.config] para obter os parâmetros necessários para que a aplicação funcione corretamente. Estes foram descritos na secção 1.5.3.
- colocará uma lista de quaisquer erros no contexto da aplicação na forma de um objeto [ArrayList errors]. Esta lista estará vazia se não houver erros, mas existirá de qualquer forma.
- Se houver erros, o método [Application_Start] pára aí. Caso contrário, solicita uma referência a um singleton do tipo [IArticlesDomain], que será o objeto de negócio que o controlador utilizará para as suas necessidades. Conforme explicado em 1.5.3.2, o controlador solicitará este singleton à estrutura Spring. Esta operação de instanciação pode resultar em vários erros. Se for esse o caso, estes erros serão novamente armazenados no objeto [errors] do contexto da aplicação.
O controlador [global.asax.vb] possui um procedimento [Session_Start] que é executado sempre que um novo cliente chega. Neste procedimento, iremos criar um carrinho de compras vazio para o cliente. Este carrinho de compras será mantido ao longo de todas as solicitações deste cliente específico. O código poderia ser o seguinte:
Comentários:
- Os parâmetros esperados no [web.config] são definidos numa matriz - linha 18
- Estes são procurados no [web.config]. Se estiverem presentes, são armazenados no contexto da aplicação; caso contrário, é registado um erro na lista de erros [errors] — linhas 21-33
- Se não houver erros, é solicitada ao Spring uma referência ao singleton [articlesDomain], que gere o acesso à camada [domain] da aplicação - linhas 35-47. Quaisquer erros são registados em [errors].
- Os erros são registados no contexto da aplicação - linha 49
- O procedimento é encerrado se houver algum erro - linha 51
- Criamos uma matriz de três dicionários. Cada um tem duas chaves: href e link. Esta matriz representa as três opções de menu possíveis — linhas 52–71
- Esta matriz é armazenada no contexto da aplicação — linha 73
- Para cada novo cliente, o procedimento [Session_Start] é executado. É criado um carrinho de compras vazio na sessão do cliente — linhas 78–81
1.5.7.2. O controlador [main.aspx.vb]
O controlador [main.aspx.vb] lida com todos os pedidos do cliente. Todos estes pedidos seguem o formato [/webarticles/main.aspx?action=XX]. Um pedido é processado da seguinte forma:
- O objeto [errors] no contexto da aplicação é verificado. Se não estiver vazio, isso significa que ocorreram erros durante a inicialização da aplicação e que a aplicação não pode ser executada. Em resposta, a vista [ERRORS] é enviada.
- O parâmetro [action] da solicitação será recuperado e verificado. Se não corresponder a uma ação conhecida, a visualização [ERRORS] é enviada com uma mensagem de erro apropriada.
- Se o parâmetro [action] for válido, a solicitação do cliente é encaminhada para um procedimento específico da ação para processamento:
método | solicitação | processamento | respostas possíveis |
GET /main?action=list | - solicitar a lista de itens da classe de negócios - exibi-la | [LIST] ou [ERRORS] | |
GET /main?action=info&id=ID | - recuperar o item com id=ID da da classe de negócios - exibi-lo | [INFO] ou [ERRORS] | |
POST /main?action=purchase&id=ID - A quantidade comprada está incluída nos parâmetros enviados | - solicitar o item com id=ID da classe de negócios - adicione-o ao carrinho na sessão do cliente | [LIST] ou [INFO] ou [ERRORS] | |
GET /main?action=removePurchase&id=ID | - Remover o item com id=ID da lista de compras na sessão do cliente | [CART] | |
GET /main?action=cart | - Exibir a sessão do cliente | [CART] ou [EMPTY_CART] | |
GET /main?action=cartvalidation | - Reduzir os níveis de stock de todos os artigos na [LIST] ou [ERRORS] | [LIST] ou [ERRORS] |
O modelo do controlador [main.aspx.vb] poderia ter o seguinte aspeto:
Comentários:
- A classe tem dois campos privados que serão partilhados entre os métodos — linhas 15–16:
- articlesDomain: o singleton para aceder à camada [domain]
- options: o array de dicionários que contém as opções do menu
- O procedimento [Page_Load]:
- inicializará os dois campos privados da classe
- recupera o parâmetro [action] da solicitação e executa o método que lida com essa ação.
1.5.7.3. O método [Page_Load]
Este evento é o primeiro a ocorrer na página. O código é o seguinte:
Comentários:
- Sempre que a página é carregada, garantimos que a inicialização da aplicação realizada pelo [global.asax] foi bem-sucedida.
- Para tal, recuperamos a lista de erros colocada pelo [global.asax] a partir do contexto da aplicação — linha 4
- Se esta lista não estiver vazia, exibimos a vista [ERRORS] — linhas 6–10
- Recuperamos o singleton [articlesDomain] colocado por [global.asax] no contexto da aplicação e armazenamo-lo no campo privado [articlesDomain] para que esteja disponível para os vários métodos da classe — linha 12
- Executamos uma operação semelhante com a matriz de opções de menu — linha 14
- recuperamos o parâmetro [action] da solicitação — linha 16
- Executamos o método correspondente à ação solicitada. Uma ação não especificada é tratada como a ação [list] — linhas 16–36
1.5.7.4. Tratamento da ação [list]
Isto envolve a exibição da lista de itens:

O código é o seguinte:
Comentários:
- Quaisquer erros são colocados numa [ArrayList] - linha 4
- A lista de itens é solicitada ao singleton [articlesDomain] - linhas 5-12
- Se houver erros, a vista [ERRORS] é renderizada - linhas 13-19
- caso contrário, a vista [LIST] é devolvida - linhas 20-24
1.5.7.5. Tratamento da ação [info]
O cliente solicitou informações sobre um item específico:

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

O código é o seguinte:
Comentários:
- O item colocado na sessão é recuperado - linha 5
- Se não estiver lá (a sessão pode ter expirado), a vista [LIST] é exibida - linhas 7-10
- A quantidade comprada é recuperada da consulta - linha 12
- A sua validade é verificada - linhas 13-29
- se inválida, dependendo do caso, é apresentada a vista [LIST] - linha 16 ou a vista [INFO] - linhas 24-28
- Se tudo estiver normal, a compra é adicionada ao carrinho - linhas 31-32
- depois a vista [LIST] é enviada - linha 34
1.5.7.7. Processamento da ação [cart]
O cliente efetuou várias compras e pretende visualizar o carrinho de compras:

O código é o seguinte:
Comentários:
- Recuperamos o carrinho da sessão - linha 4. Não verificamos aqui se realmente recuperamos algo. Devemos fazê-lo porque a sessão pode ter expirado.
- Se o carrinho estiver vazio, enviamos a vista [EMPTY CART] — linhas 6-10
- Caso contrário, enviamos a vista [SHOPPINGCART] — linhas 11–14
1.5.7.8. Processamento da ação [removepurchase]
O cliente pretende remover uma compra do seu carrinho:

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

O código é o seguinte:
Comentários:
- Recuperamos o carrinho da sessão - linha 6. Não verificamos aqui se realmente recuperamos algo. Devemos fazer isso porque a sessão pode ter expirado.
- Se a sessão tiver expirado, teremos um ponteiro [nothing] para o carrinho, e o método [buy] — linha 9 — lançará uma exceção, e a vista [ERRORS] será exibida. No entanto, a mensagem de erro não será clara para o utilizador.
- Linhas 8–16: Tentamos validar o carrinho de compras recuperado da sessão. Algumas compras podem falhar se a quantidade solicitada exceder o stock do artigo solicitado. Estes casos são armazenados pelo método [buy] numa lista de erros, que é recuperada na linha 11.
- Se houver erros, a vista [ERRORS] é enviada — linhas 18–23
- caso contrário, a vista [LIST] é enviada — linhas 25–26
1.6. Conclusão
Aqui, desenvolvemos uma aplicação utilizando o padrão MVC. Em abril de 2005, não parecem existir quaisquer frameworks profissionais de desenvolvimento MVC para ASP.NET comparáveis aos disponíveis para Java (Struts, Spring, etc.). Espera-se que o projeto [Spring.net] lance um em breve. Até lá, o método descrito acima fornece uma abordagem viável de desenvolvimento MVC para aplicações de média dimensão.































