1. Parte 1
El PDF del documento está disponible |AQUÍ|.
Los ejemplos del documento están disponibles |AQUÍ|.
1.1. Introduction
Objetivos del artículo:
- escribir una aplicación web de tres capas [interface utilisateur, métier, accès aux données]
- configurar la aplicación con Spring IOC
- escribir diferentes versiones cambiando la implementación de una u otra de las tres capas.
Herramientas utilizadas:
- Visual Studio.net para el desarrollo —véase el apartado 3.1 del anexo—;
- Servidor web Cassini para la ejecución: véase el apartado 3.2 del anexo;
- Nunit para las pruebas unitarias – véase el apartado 3.4 del anexo;
- Spring para la integración y la configuración de las capas de la aplicación web —véase el apartado 3.3 del anexo—;
En una escala principiante-intermedio-avanzado, este documento se sitúa en la parte [intermédiaire-avancé]. Su comprensión requiere varios requisitos previos. Algunos de ellos pueden adquirirse en documentos que he escrito. En ese caso, los cito. Es evidente que se trata solo de una sugerencia y que el lector puede utilizar sus documentos favoritos.
- lenguaje VB.net: [Introducción a VB.NET a través de ejemplos (2004)];
- programación web en VB.net: [Desarrollo web con ASP.NET 1.1 (2004)];
- uso del aspecto IoC de Spring: [Spring IoC para .NET (2005)];
- documentación Spring.net: [Spring.NET | Homepage ]
Este documento sigue la línea argumental de un documento escrito para Java [Architectures à 3 couches et architectures MVC avec Struts, Spring et Java ]. Construimos en VB.NET la aplicación web MVC de tres capas escrita en Java. La idea que queremos transmitir aquí es que las plataformas de desarrollo Java y .NET son lo suficientemente similares entre sí como para que los conocimientos adquiridos en uno de estos dos ámbitos puedan reutilizarse en el otro.
No parece existir una solución de desarrollo MVC ASP.NET ampliamente reconocida. La solución que sigue retoma el método introducido en el documento [Développement WEB avec ASP.NET 1.1]. Si bien esta tiene el mérito de utilizar conceptos extendidos en el desarrollo J2EE, no debe tomarse más que por lo que es, c.a.d. un método entre otros de desarrollo MVC. En cuanto un método de desarrollo MVC en ASP.NET sea ampliamente aceptado, habrá que adoptar este último. La versión .NET de Spring, actualmente en desarrollo, bien podría ser una primera solución.
1.2. La aplicación webarticles
Presentamos aquí los elementos de una aplicación web simplificada de comercio electrónico. Esta permitirá a los clientes de la web:
- consultar una lista de artículos procedentes de una base de datos
- añadir algunos de ellos a una cesta electrónica
- validar dicho carrito. Esta validación tendrá como único efecto actualizar, en la base de datos, las existencias de los artículos comprados.
Las diferentes vistas que se mostrarán al usuario serán las siguientes:
- la vista «LISTE», que presenta una lista de los artículos en venta ![]() | - la vista [INFOS], que ofrece información adicional sobre un producto: ![]() |
- las vistas [PANIER] y [PANIERVIDE], que muestran el contenido de la cesta del cliente
![]() | ![]() |
- la vista [ERREURS], que señala cualquier error de la aplicación

1.3. Arquitectura general de la aplicación
Queremos crear una aplicación con la siguiente estructura de tres capas:
![]() |
- Las tres capas son independientes gracias al uso de interfaces
- La integración de las diferentes capas se lleva a cabo mediante Spring
- cada capa tiene espacios de nombres separados: web (capa UI), domain (capa de negocio) y dao (capa de acceso a datos).
La aplicación seguirá una arquitectura MVC (Modelo - Vista - Controlador). Si retomamos el esquema de capas anterior, la arquitectura MVC se integra en él de la siguiente manera:
![]() |
El procesamiento de una solicitud de un cliente se lleva a cabo siguiendo los siguientes pasos:
- el cliente realiza una solicitud al controlador. Este controlador será, en este caso, una página .aspx a la que se le asignará una función específica. Esta página recibe todas las solicitudes de los clientes. Es la puerta de entrada de la aplicación. Es la C de MVC.
- El controlador procesa esta solicitud. Para ello, puede necesitar la ayuda de la capa de negocio, lo que se denomina el modelo M en la estructura MVC.
- El controlador recibe una respuesta de la capa de negocio. La solicitud del cliente ha sido procesada. Esto puede dar lugar a varias respuestas posibles. Un ejemplo clásico es
- una página de errores si la solicitud no se ha podido procesar correctamente
- una página de confirmación en caso contrario
- el controlador elige la respuesta (= vista) que se enviará al cliente. Esta suele ser una página que contiene elementos dinámicos. El controlador proporciona estos elementos a la vista.
- La vista se envía al cliente. Es la V de MVC.
1.4. El modelo
Aquí estudiamos la M de MVC. El modelo se compone aquí de los siguientes elementos:
- las clases de negocio
- las clases de acceso a datos
- la base de datos
1.4.1. La base de datos
La base de datos solo contiene una tabla llamada ARTICLES. Esta se ha generado con los siguientes 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
);
/* restricciones */
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);
/* clave primaria */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
clave primaria que identifica un artículo de forma única | |
nombre del artículo | |
su precio | |
su stock actual | |
el stock por debajo del cual se debe realizar un pedido de reposición |
1.4.2. Los espacios de nombres del modelo
El modelo M se proporciona aquí en forma de dos espacios de nombres:
- istia.st.articles.dao: contiene las clases de acceso a los datos de la capa [dao]
- istia.st.articles.domain: contiene las clases de negocio de la capa [domain]
Cada uno de estos espacios de nombres se generará dentro de un archivo «assembly» propio:
contenu | rôle | |
- [IArticlesDao]: la interfaz de acceso a la capa [dao]. Es la única interfaz que ve la capa [domain]. No ve ninguna otra. - [Article]: clase que define un artículo - [ArticlesDaoArrayList]: clase de implementación de la interfaz [IArticlesDao] con una clase [ArrayList] | capa de acceso a datos - se encuentra íntegramente en la capa [dao] de la arquitectura de tres capas de la aplicación web | |
- [IArticlesDomain]: la interfaz de acceso a la capa [domain]. Es la única interfaz que ve la capa web. No ve ninguna otra. - [AchatsArticles]: una clase que implementa [IArticlesDomain] - [Achat]: clase que representa la compra de un cliente - [Panier]: clase que representa el conjunto de compras de un cliente | representa el modelo de compras en la web - se encuentra íntegramente en la capa [domain] de la arquitectura Estructura de tres capas de la aplicación web |
1.4.3. La capa [dao]
La capa [dao] contiene los siguientes elementos:
-
[IArticlesDao]: la interfaz de acceso a la capa [dao]
-
[Article]: clase que define un artículo
-
[ArticlesDaoArrayList]: clase de implementación de la interfaz [IArticlesDao] con una clase [ArrayList]
La estructura del proyecto [Visual Studio] de la capa [dao] es la siguiente:

Comentarios:
- el proyecto [dao] es de tipo [bibliothèque de classes]
- Las clases se han colocado en una estructura de árbol con raíz en la carpeta [istia]. Todas ellas se encuentran en el espacio de nombres [istia.st.articles.dao].
1.4.3.1. La clase [Article]
La clase que define un artículo es la siguiente:
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 de artículo
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
' nombre del artículo
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
' precio del artículo
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 actual del artículo
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 del artículo
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 defecto
Public Sub New()
End Sub
' fabricante con propiedades
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 identificación del artículo
Public Overrides Function ToString() As String
Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
End Function
End Class
End Namespace
Esta clase ofrece:
- un constructor que permite establecer los 5 datos de un artículo: [id, nom, prix, stockactuel, stockminimum]
- propiedades públicas que permiten leer y escribir los 5 datos.
- una verificación de los datos introducidos en el artículo. En caso de datos erróneos, se lanza una excepción.
- un método toString que permite obtener el valor de un artículo en forma de cadena de caracteres. Esto suele ser útil para la depuración de una aplicación.
1.4.3.2. La interfaz [IArticlesDao]
La interfaz [IArticlesDao] se define de la siguiente manera:
Imports System
Imports System.Collections
Namespace istia.st.articles.dao
Public Interface IArticlesDao
' lista de todos los artículos
Function getAllArticles() As IList
' añade un artículo
Function ajouteArticle(ByVal unArticle As Article) As Integer
' elimina un artículo
Function supprimeArticle(ByVal idArticle As Integer) As Integer
' modifica un artículo
Function modifieArticle(ByVal unArticle As Article) As Integer
' busca un artículo
Function getArticleById(ByVal idArticle As Integer) As Article
' elimina todos los artículos
Sub clearAllArticles()
' cambia el stock de un artículo
Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
End Interface
End Namespace
La función de los distintos métodos de la interfaz es la siguiente:
devuelve todos los artículos de la fuente de datos | |
vacía la fuente de datos | |
devuelve el objeto [Article] identificado por su clave primaria | |
permite añadir un artículo a la fuente de datos | |
permite modificar un artículo de la fuente de datos | |
permite eliminar un artículo de la fuente de datos | |
permite modificar el stock de un artículo de la fuente de datos |
La interfaz pone a disposición de los programas cliente una serie de métodos definidos únicamente por sus firmas. No se ocupa de la forma en que estos métodos se implementarán realmente. Esto aporta flexibilidad a la aplicación. El programa cliente realiza sus llamadas a una interfaz y no a una implementación concreta de la misma.
![]() |
La elección de una implementación concreta se realizará mediante un archivo de configuración de Spring. Para demostrar que, a la hora de probar la aplicación web, lo único que importa es la interfaz de acceso a los datos y no su clase de implementación, vamos a implementar primero la fuente de datos mediante un simple objeto [ArrayList]. Más adelante, presentaremos una solución basada en SGBD.
1.4.3.3. La clase de implementación [ArticlesDaoArrayList]
La clase de implementación [ArticlesDaoArrayList] se define de la siguiente manera:
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 defecto
Public Sub New()
' se están elaborando algunos artículos
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 los artículos
Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
' se devuelve la lista de artículos
SyncLock Me
Return articles
End SyncLock
End Function
' eliminación de todos los artículos
Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
' se vacía la lista de artículos
SyncLock Me
articles.Clear()
End SyncLock
End Sub
' obtener un artículo identificado por su clave
Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
' se busca el artículo en la 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
' añadir un artículo a la lista de artículos
Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
' se añade el artículo a la lista de artículos
SyncLock Me
' se comprueba que no exista ya
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
' se añade el artículo
articles.Add(unArticle)
' se devuelve el resultado
Return 1
End SyncLock
End Function
' modificar un artículo
Public Function modifieArticle(ByVal articleNouveau As Article) As Integer Implements IArticlesDao.modifieArticle
' se modifica un artículo
SyncLock Me
' se comprueba que existe
Dim ipos As Integer = posArticle(articles, articleNouveau.id)
' en caso de que no exista
If ipos = -1 Then Return 0
' existe: lo modificamos
articles(ipos) = articleNouveau
' se devuelve el resultado
Return 1
End SyncLock
End Function
' eliminar un artículo identificado por su clave
Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
' eliminación de un artículo
SyncLock Me
' se comprueba si existe
Dim ipos As Integer = posArticle(articles, idArticle)
' en caso de que no exista
If ipos = -1 Then Return 0
' existe: se elimina
articles.RemoveAt(ipos)
' se devuelve el resultado
Return 1
End SyncLock
End Function
' modificar el stock de un artículo identificado por su clave
Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
' modificar el stock de un artículo
SyncLock Me
' se comprueba si existe
Dim ipos As Integer = posArticle(articles, idArticle)
' en caso de que no exista
If ipos = -1 Then Return 0
' existe: se modifica su stock si es posible
Dim unArticle As Article = CType(articles(ipos), Article)
' solo se modifica el stock si es suficiente
If unArticle.stockactuel + mouvement >= 0 Then
unArticle.stockactuel += mouvement
Return 1
Else
Return 0
End If
End SyncLock
End Function
' buscar un artículo identificado por su clave
Private Function posArticle(ByVal listArticles As ArrayList, ByVal idArticle As Integer) As Integer
' devuelve la posición del artículo [idArticle] en la lista o -1 si no se encuentra
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
' no encontrado
Return -1
End Function
End Class
End Namespace
Comentarios:
- la fuente de datos se simula mediante el campo privado [articles] de tipo [ArrayList]
- El constructor de la clase crea por defecto 4 entradas en la fuente de datos.
- Todos los métodos de acceso a los datos se han sincronizado para evitar problemas de acceso concurrente a la fuente de datos. En un momento dado, solo un hilo tiene acceso a un método determinado.
- El método [posArticle] permite conocer la posición [0..N] en la fuente [ArrayList] de un artículo identificado por su n.º. Si el artículo no existe, el método devuelve la posición -1. Este método es utilizado repetidamente por los demás métodos.
- El método [ajouteArticle] permite añadir un artículo a la lista de artículos. Devuelve el número de artículos insertados: 1. Si el artículo ya existía, se lanza una excepción.
- El método [modifieArticle] permite modificar un artículo existente. Devuelve el número de artículos modificados: 1 si el artículo existía, 0 en caso contrario.
- El método [supprimeArticle] permite eliminar un artículo existente. Devuelve el número de artículos eliminados: 1 si el artículo existía, 0 en caso contrario.
- El método [getAllArticles] proporciona la lista de todos los artículos
- El método [getArticleById] permite obtener un artículo identificado por su número. Se obtiene el valor [nothing] si el artículo no existe.
- El código no presenta ninguna dificultad real. Dejamos que el lector lo revise y lo comprenda.
1.4.3.4. Generación del ensamblado de la capa [dao]
El proyecto de Visual Studio está configurado para generar el ensamblado [webarticles-dao.dll]. Este se genera en la carpeta [bin] del proyecto:
![]() | ![]() |
1.4.3.5. Pruebas Nunit de la capa [dao]
En Java, las clases se prueban con el marco [Junit]. En .NET, el marco Nunit ofrece las mismas posibilidades de pruebas unitarias:

La estructura del proyecto de pruebas de Visual Studio es la siguiente:

Comentarios:
- el proyecto [tests] es de tipo [bibliothèque de classes]
- las pruebas [NUnit] requieren una referencia al ensamblado [nunit.framework.dll]
- La clase de prueba [NUnit] recupera una instancia del objeto a probar a través de Spring. Por lo tanto, se encuentra
- en la carpeta [bin], los archivos de clases de Spring
- en [References], una referencia al ensamblado [Spring-Core.dll] de la carpeta [bin]
- en [bin], un archivo de configuración para Spring
- la clase de prueba necesita el ensamblado [webarticles-dao.dll] de la capa [dao]. Este se ha colocado en la carpeta [bin] y su referencia se ha añadido a las referencias del proyecto.
Una clase de prueba [NUnit] requiere acceso a las clases del espacio de nombres [NUnit.Framework]. Por lo tanto, contiene la siguiente instrucción de importación:
El espacio de nombres [NUnit.Framework] se encuentra en el «ensamblado» [nunit.framework.dll], que hay que añadir a las referencias del proyecto:
![]() | ![]() |
El ensamblado [nunit.framework.dll] debe aparecer en la lista propuesta si se ha realizado la instalación de [Nunit]. Basta con hacer doble clic en el ensamblado para añadirlo al proyecto:

La clase de prueba [NUnit] de la capa [dao] podría ser la siguiente:
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
' el objeto a probar
Private articlesDao As IArticlesDao
<SetUp()> _
Public Sub init()
' se recupera una instancia del generador de objetos Spring
Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
' se solicita la instanciación del objeto articlesdao
articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
End Sub
<Test()> _
Public Sub testGetAllArticles()
' verificación visual
listArticles()
End Sub
<Test()> _
Public Sub testClearAllArticles()
' se eliminan todos los artículos
articlesDao.clearAllArticles()
' se solicitan todos los artículos
Dim articles As IList = articlesDao.getAllArticles
' verificación: debe haber 0
Assert.AreEqual(0, articles.Count)
End Sub
<Test()> _
Public Sub testAjouteArticle()
' eliminación de todos los artículos
articlesDao.clearAllArticles()
' verificación: la tabla de artículos debe estar vacía
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' se añaden dos artículos
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' verificación: debe haber dos artículos
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' verificación visual
listArticles()
End Sub
<Test()> _
Public Sub testSupprimeArticle()
' eliminación de todos los artículos
articlesDao.clearAllArticles()
' verificación: la tabla de artículos debe estar vacía
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' se añaden dos artículos
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' verificación: debe haber 2 artículos
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' se elimina el artículo 4
articlesDao.supprimeArticle(4)
' verificación: debe quedar 1 artículo
articles = articlesDao.getAllArticles
Assert.AreEqual(1, articles.Count)
' verificación visual
listArticles()
End Sub
<Test()> _
Public Sub testModifieArticle()
' eliminación de todos los artículos
articlesDao.clearAllArticles()
' verificación
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' adición de 2 artículos
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' verificación
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' búsqueda del artículo 3
Dim unArticle As Article = articlesDao.getArticleById(3)
' verificación
Assert.AreEqual(unArticle.nom, "article3")
' búsqueda del artículo 4
unArticle = articlesDao.getArticleById(4)
' verificación
Assert.AreEqual(unArticle.nom, "article4")
' modificación del artículo 4
articlesDao.modifieArticle(New Article(4, "article4", 44, 44, 44))
' verificación
unArticle = articlesDao.getArticleById(4)
Assert.AreEqual(unArticle.prix, 44, 0.000001)
' verificación visual
listArticles()
End Sub
<Test()> _
Public Sub testGetArticleById()
' eliminación de artículos
articlesDao.clearAllArticles()
' verificación
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' adición de 2 artículos
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' verificación
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' búsqueda del artículo 3
Dim unArticle As Article = articlesDao.getArticleById(3)
' verificación
Assert.AreEqual(unArticle.nom, "article3")
' búsqueda del artículo 4
unArticle = articlesDao.getArticleById(4)
' verificación
Assert.AreEqual(unArticle.nom, "article4")
End Sub
' listado en pantalla
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()
' eliminación de todos los artículos
articlesDao.clearAllArticles()
' búsqueda de artículo 1
Dim article As article = articlesDao.getArticleById(1)
' verificación
Assert.IsNull(article)
' modificación de un artículo inexistente
Dim i As Integer = articlesDao.modifieArticle(New article(1, "1", 1, 1, 1))
' no se ha modificado ninguna línea
Assert.AreEqual(i, 0)
' eliminación de un artículo inexistente
i = articlesDao.supprimeArticle(1)
' no se ha eliminado ninguna línea
Assert.AreEqual(0, i)
End Sub
<Test()> _
Public Sub testChangerStockArticle()
' eliminación de todos los artículos
articlesDao.clearAllArticles()
' adición de un artículo
Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
Assert.AreEqual(nbArticles, 1)
' se ha añadido un artículo
nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
Assert.AreEqual(nbArticles, 1)
' creación de 100 hilos
Dim taches(99) As Thread
For i As Integer = 0 To taches.Length - 1
' se crea el hilo i
taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
' se establece el nombre del hilo
taches(i).Name = "tache_" & i
' se inicia la ejecución del hilo i
taches(i).Start()
Next
' se espera a que finalicen todos los subprocesos
For i As Integer = 0 To taches.Length - 1
taches(i).Join()
Next
' comprobaciones: el artículo 3 debe tener un stock de 1
Dim unArticle As Article = articlesDao.getArticleById(3)
Assert.AreEqual(unArticle.nom, "article3")
Assert.AreEqual(1, unArticle.stockactuel)
' se reduce el stock del artículo 4
Dim erreur As Boolean = False
Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
' verificación: su stock no debe haber cambiado
Assert.AreEqual(0, nbLignes)
' verificación visual
listArticles()
End Sub
Public Sub décrémente()
' hilo iniciado
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
' el hilo reduce el stock
articlesDao.changerStockArticle(3, -1)
' hilo finalizado
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
End Sub
End Class
End Namespace
Comentarios:
- se ha querido escribir un programa de prueba de la interfaz [IArticlesDao] que fuera independiente de la clase de implementación de la misma. Por ello, se ha utilizado Spring para ocultar al programa de prueba el nombre de la clase de implementación.
- El método de atributo <Setup()> obtiene de Spring una referencia al objeto [articlesdao] que se va a probar. Este se define en el siguiente archivo [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 archivo indica el nombre de la clase de implementación [istia.st.articles.dao.ArticlesDaoArrayList] de la interfaz [IArticlesDao] y dónde encontrarla [ webarticles-dao.dll]. Dado que la instanciación no requiere parámetros, aquí no se define ninguno.
- La mayoría de las pruebas son fáciles de entender. Se recomienda al lector que lea los comentarios.
- El método [testChangerStockArticle] requiere algunas explicaciones. Crea 100 subprocesos encargados de reducir el stock de un artículo determinado.
<Test()> _
Public Sub testChangerStockArticle()
' eliminación de todos los artículos
articlesDao.clearAllArticles()
' Añadido de un artículo
Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
Assert.AreEqual(nbArticles, 1)
' Añadido de un artículo
nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
Assert.AreEqual(nbArticles, 1)
' creación de 100 subprocesos
Dim taches(99) As Thread
For i As Integer = 0 To taches.Length - 1
' se crea el hilo i
taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
' se establece el nombre del hilo
taches(i).Name = "tache_" & i
' se inicia la ejecución del hilo i
taches(i).Start()
Next
' se espera a que finalicen todos los subprocesos
For i As Integer = 0 To taches.Length - 1
taches(i).Join()
Next
' comprobaciones: el artículo 3 debe tener un stock de 1
Dim unArticle As Article = articlesDao.getArticleById(3)
Assert.AreEqual(unArticle.nom, "article3")
Assert.AreEqual(1, unArticle.stockactuel)
' se reduce el stock del artículo 4
Dim erreur As Boolean = False
Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
' verificación: su stock no debe haber cambiado
Assert.AreEqual(0, nbLignes)
' verificación visual
listArticles()
End Sub
El objetivo aquí es probar los accesos concurrentes a la fuente de datos. El método encargado de actualizar el stock es el siguiente:
Public Sub décrémente()
' hilo iniciado
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
' el subproceso reduce el stock
articlesDao.changerStockArticle(3, -1)
' hilo finalizado
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
End Sub
Disminuye en una unidad el stock del artículo n.º 3. Si nos fijamos en el código del método [testChangerStockArticle], vemos que:
- el stock del artículo n.º 3 se inicializa en 101
- los 100 subprocesos reducirán este stock en una unidad cada uno
- por lo tanto, al final de la ejecución de todos los subprocesos, el stock debería ser 1
Por otra parte, siempre en este método, se intenta que el stock del artículo n.º 4 pase a un valor negativo. Debe fallar.
Para probar la capa [dao], generamos DLL [tests-webarticles-dao.dll] en la carpeta [bin] del proyecto [tests]:
![]() | ![]() |
A continuación, con la aplicación [Nunit-Gui], cargamos este DLL y ejecutamos las pruebas:

En la ventana de la izquierda, vemos la lista de métodos probados. El color del punto que precede al nombre de cada método indica si el método ha tenido éxito (verde) o ha fallado (rojo). El lector que visualice este documento en pantalla podrá ver que todas las pruebas se han superado. A partir de ahora, consideraremos que tenemos una capa [dao] operativa.
1.4.4. La capa [domain]
La capa [domain] contiene los siguientes elementos:
-
[IArticlesDomain]: la interfaz de acceso a la capa [domain]
-
[Achat]: clase que define una compra
-
[Panier]: clase que define una cesta de la compra
-
[AchatsArticles]: clase de implementación de la interfaz [IArticlesDomain]
La estructura de la solución [Visual Studio] de la capa [domain] es la siguiente:

Comentarios:
- el proyecto [domain] es de tipo [bibliothèque de classes]
- las clases se han colocado en una estructura de árbol con raíz en la carpeta [istia]. Todas ellas se encuentran en el espacio de nombres [istia.st.articles.domain].
- La clase DLL de la capa [dao] se ha colocado en la carpeta [bin] del nuevo proyecto. Además, esta clase DLL se ha añadido como referencia al proyecto.
1.4.4.1. La interfaz [IArticlesDomain]
La interfaz [IArticlesDomain] desacopla la capa [métier] de la capa [web]. Esta última accede a la capa [métier/domain] a través de esta interfaz sin preocuparse por la clase que realmente la implementa. La interfaz define las siguientes acciones para el acceso a la capa de negocio:
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
devuelve la lista de objetos [Article] de la fuente de datos asociada | |
devuelve el objeto [Article] identificado por [idArticle] | |
valida la cesta del cliente restando de las existencias de los artículos comprados la cantidad adquirida; puede fallar si las existencias son insuficientes | |
devuelve la lista de errores que se han producido; vacía si no hay errores |
1.4.4.2. La clase [Achat]
La clase [Achat] representa una compra del cliente:
Imports istia.st.articles.dao
Namespace istia.st.articles.domain
Public Class Achat
' campos privados
Private _article As article
Private _qte As Integer
' constructor por defecto
Public Sub New()
End Sub
' constructor con parámetros
Public Sub New(ByVal unArticle As article, ByVal qte As Integer)
' se pasa por las propiedades
Me.article = unArticle
Me.qte = qte
End Sub
' artículo comprado
Public Property article() As article
Get
Return _article
End Get
Set(ByVal Value As article)
_article = Value
End Set
End Property
' cantidad 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 de la compra
Public ReadOnly Property totalAchat() As Double
Get
Return _qte * _article.prix
End Get
End Property
' identidad
Public Overrides Function ToString() As String
Return "[" + _article.ToString + "," + _qte.ToString + "]"
End Function
End Class
End Namespace
Comentarios:
- La clase [Achat] tiene las siguientes propiedades y métodos:
El artículo comprado | |
la cantidad comprada | |
el importe de la compra | |
cadena de identidad del objeto |
- tiene un constructor que permite inicializar las propiedades [article, qte] que definen una compra.
1.4.4.3. La clase [Panier]
La clase [Panier] representa el conjunto de compras del cliente:
Namespace istia.st.articles.domain
Public Class Panier
' campos privados
Private _achats As New ArrayList
Private _totalPanier As Double = 0
' fabricante por defecto
Public Sub New()
End Sub
' lista de compras
Public ReadOnly Property achats() As ArrayList
Get
Return _achats
End Get
End Property
' total de compras
Public ReadOnly Property totalPanier() As Double
Get
Return _totalPanier
End Get
End Property
' métodos
Public Sub ajouter(ByVal unAchat As Achat)
' se comprueba si la compra ya existe
Dim iAchat As Integer = posAchat(unAchat.article.id)
If iAchat <> -1 Then
' se ha encontrado
Dim achatCourant As Achat = CType(_achats(iAchat), Achat)
achatCourant.qte += unAchat.qte
Else
' no se ha encontrado
_achats.Add(unAchat)
End If
' se incrementa el total de la cesta
_totalPanier += unAchat.totalAchat
End Sub
' eliminar una compra
Public Sub enlever(ByVal idAchat As Integer)
' se busca la compra
Dim iachat As Integer = posAchat(idAchat)
' si se ha encontrado, se elimina
If iachat <> -1 Then
Dim achatCourant As Achat = CType(_achats(iachat), Achat)
' se elimina de la cesta
_achats.RemoveAt(iachat)
' se reduce el total de la cesta
_totalPanier -= achatCourant.totalAchat
End If
End Sub
Private Function posAchat(ByVal idArticle As Integer) As Integer
' busca una compra en la lista de compras
' devuelve su posición en la lista o -1 si no se encuentra
Dim achatCourant As Achat
Dim trouvé As Boolean = False
Dim i As Integer = 0
While Not trouvé AndAlso i < _achats.Count
' compra actual
achatCourant = CType(_achats(i), Achat)
' comparación con el artículo buscado
If achatCourant.article.id = idArticle Then
Return i
End If
'compra siguiente
i += 1
End While
' no encontrado
Return -1
End Function
' función de identidad
Public Overrides Function ToString() As String
Return _achats.ToString
End Function
End Class
End Namespace
Comentarios:
- La clase [Panier] tiene las siguientes propiedades y métodos:
Lista de compras del cliente: lista de objetos de tipo [Achat] | |
añade una compra a la lista de compras | |
elimina la compra del artículo idAchat | |
el importe total de las compras de la cesta | |
devuelve la cadena de identidad de la cesta |
- El método [posAchat] es un método de utilidad que permite obtener la posición en la lista de compras de una compra identificada por el número del artículo comprado. La lista de compras se gestiona de tal manera que un artículo comprado varias veces solo ocupa una posición en la lista. De este modo, una compra puede identificarse mediante el número del artículo comprado. El método [posAchat] devuelve -1 si la compra buscada no existe.
- El método [ajouter] añade una nueva compra a la lista de compras. Esto equivale a añadir una nueva entrada a la lista de compras si el artículo comprado no existía ya en la lista, o a incrementar la cantidad comprada si ya existía.
- El método [enlever] permite eliminar una compra identificada por un número de la lista de compras. Si la compra no existe, el método no muestra ningún mensaje y no realiza ninguna acción.
- El importe de las compras [totalPanier] se mantiene a medida que se añaden y eliminan compras.
1.4.4.4. La clase [AchatsArticles]
La interfaz [IArticlesDomain] se implementará mediante la siguiente clase [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
' constructor
Public Sub New(ByVal articlesDao As IArticlesDao)
_articlesDao = articlesDao
End Sub
' lista de errores
Public ReadOnly Property erreurs() As ArrayList Implements IArticlesDomain.erreurs
Get
Return _erreurs
End Get
End Property
' lista de artículos
Public Function getAllArticles() As IList Implements IArticlesDomain.getAllArticles
' lista de todos los artículos
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
' obtener un artículo identificado por su n.º
Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDomain.getArticleById
' un artículo concreto
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 una cesta
Public Sub acheter(ByVal panier As Panier) Implements IArticlesDomain.acheter
' compra de una cesta: se deben reducir las existencias de los artículos comprados
_erreurs = New ArrayList
Dim achat As achat
Dim achats As ArrayList = panier.achats
For i As Integer = achats.Count - 1 To 0 Step -1
' reducir el stock del artículo i
achat = CType(achats(i), achat)
Try
If _articlesDao.changerStockArticle(achat.article.id, -achat.qte) = 0 Then
' no se ha podido realizar la operación
_erreurs.Add("L'achat " + achat.ToString + " n'a pu se faire - Vérifiez les stocks")
Else
' la operación se ha realizado: se elimina la compra de la cesta
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
Comentarios:
- esta clase implementa los cuatro métodos de la interfaz [IArticlesDomain]. Tiene dos campos privados:
el objeto de acceso a los datos | |
la lista de posibles errores. Se puede acceder a ella a través de la propiedad pública [erreurs] |
- para crear una instancia de la clase, hay que proporcionar el objeto que permite el acceso a los datos:
- los métodos [getAllArticles] y [getArticleById] se basan en los métodos del mismo nombre de la capa [dao]
- El método [acheter] valida la compra de una cesta. Esta validación consiste simplemente en reducir las existencias de los artículos comprados. La compra de un artículo solo es posible si el stock lo permite. Si no es así, la compra se rechaza: el artículo permanece en la cesta y se señala un error en la lista [erreurs]. Una compra validada se retira de la cesta y el stock del artículo correspondiente se reduce en la cantidad comprada.
1.4.4.5. Generación del ensamblado de la capa [domain]
El proyecto de Visual Studio está configurado para generar el ensamblado [webarticles-domain.dll]. Este se genera en la carpeta [bin] del proyecto:
![]() | ![]() |
1.4.4.6. Pruebas NUnit de la capa [domain]
La estructura del proyecto de prueba de Visual Studio es la siguiente:

Comentarios:
- el proyecto [tests] es de tipo [bibliothèque de classes]
- las pruebas [NUnit] requieren una referencia al ensamblado [nunit.framework.dll]
- la clase de prueba [NUnit] recupera una instancia del objeto a probar a través de Spring. Por lo tanto, se encuentra
- en la carpeta [bin], los archivos de clases de Spring
- en [References], una referencia al ensamblado [Spring-Core.dll] de la carpeta [bin]
- en [bin], un archivo de configuración para Spring
- la clase de prueba necesita el ensamblado [webarticles-dao.dll] de la capa [dao] y el ensamblado [webarticles-domain.dll] de la capa [domain]. Estos se han colocado en la carpeta [bin] y sus referencias se han añadido a las referencias del proyecto.
Una clase de prueba NUnit de la capa [domain] podría ser la siguiente:
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
' el objeto a probar
Private articlesDomain As IArticlesDomain
Private articlesDao As IArticlesDao
<SetUp()> _
Public Sub init()
' se recupera una instancia del generador de objetos Spring
Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
' se solicita la instanciación del objeto articles dao
articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
' y, a continuación, la del objeto «articlesdomain»
articlesDomain = CType(factory.GetObject("articlesdomain"), IArticlesDomain)
End Sub
<Test()> _
Public Sub getAllArticles()
' verificación visual
listArticles()
End Sub
<Test()> _
Public Sub getArticleById()
' eliminación de los artículos
articlesDao.clearAllArticles()
' verificación
Dim articles As IList = articlesDomain.getAllArticles
Assert.AreEqual(0, articles.Count)
' adición de 2 artículos
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' verificación
articles = articlesDomain.getAllArticles
Assert.AreEqual(2, articles.Count)
' búsqueda del artículo 3
Dim unArticle As Article = articlesDomain.getArticleById(3)
' verificación
Assert.AreEqual(unArticle.nom, "article3")
' búsqueda del artículo 4
unArticle = articlesDao.getArticleById(4)
' verificación
Assert.AreEqual(unArticle.nom, "article4")
End Sub
<Test()> _
Public Sub acheterPanier()
' eliminación de artículos
articlesDao.clearAllArticles()
' verificación
Dim articles As IList = articlesDomain.getAllArticles
Assert.AreEqual(0, articles.Count)
' adición de 2 artículos
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' verificación
articles = articlesDomain.getAllArticles
Assert.AreEqual(2, articles.Count)
' creación de una cesta con dos 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))
' verificaciones
Assert.AreEqual(700, panier.totalPanier, 0.000001)
Assert.AreEqual(2, panier.achats.Count)
' Validación de la cesta
articlesDomain.acheter(panier)
' verificaciones
Assert.AreEqual(0, articlesDomain.erreurs.Count)
Assert.AreEqual(0, panier.achats.Count)
' búsqueda de artículo 3
Dim unArticle As Article = articlesDomain.getArticleById(3)
' verificación
Assert.AreEqual(unArticle.stockactuel, 20)
' búsqueda de artículo 4
unArticle = articlesDao.getArticleById(4)
' verificación
Assert.AreEqual(unArticle.stockactuel, 30)
' nueva cesta
panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 100))
' confirmación de la cesta
articlesDomain.acheter(panier)
' verificaciones
Assert.AreEqual(1, articlesDomain.erreurs.Count)
' búsqueda de artículo 3
unArticle = articlesDomain.getArticleById(3)
' verificación
Assert.AreEqual(unArticle.stockactuel, 20)
End Sub
<Test()> _
Public Sub testRetirerAchats()
' eliminación del contenido de ARTICLES
articlesDao.clearAllArticles()
' lee la tabla ARTICLES
Dim articles As IList = articlesDao.getAllArticles()
Assert.AreEqual(0, articles.Count)
' inserción
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)
' lee la tabla ARTICLES
articles = articlesDomain.getAllArticles()
Assert.AreEqual(2, articles.Count)
' creación de una cesta con dos compras
Dim monPanier As New Panier
monPanier.ajouter(New Achat(article3, 10))
monPanier.ajouter(New Achat(article4, 10))
' comprobaciones
Assert.AreEqual(700.0, monPanier.totalPanier, 0.000001)
Assert.AreEqual(2, monPanier.achats.Count)
' añadir un artículo ya comprado
monPanier.ajouter(New Achat(article3, 10))
' comprobaciones
' el total debe pasar a 1000
Assert.AreEqual(1000.0, monPanier.totalPanier, 0.000001)
' sigue habiendo 2 artículos en la cesta
Assert.AreEqual(2, monPanier.achats.Count)
' la cantidad del artículo 3 debe pasar a 20
Dim unAchat As Achat = CType(monPanier.achats(0), Achat)
Assert.AreEqual(20, unAchat.qte)
' se retira el artículo 3 de la cesta
monPanier.enlever(3)
' comprobaciones
' el total debe haber pasado a 400
Assert.AreEqual(400.0, monPanier.totalPanier, 0.000001)
' solo 1 artículo en la cesta
Assert.AreEqual(1, monPanier.achats.Count)
' debe ser el artículo n.º 4
Assert.AreEqual(4, CType(monPanier.achats(0), Achat).article.id)
End Sub
' lista en pantalla
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
Comentarios:
- Queríamos escribir un programa de prueba para la interfaz [IArticlesDomain] que fuera independiente de su clase de implementación. Por ello, hemos utilizado Spring para ocultar al programa de prueba el nombre de la clase de implementación.
- El método de atributo <Setup()> obtiene de Spring una referencia a los objetos [articlesdomain] y [articlesdao] que se van a probar. Estos se definen en el siguiente archivo [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 archivo indica:
- (continuación)
- para el singleton [articlesdao], el nombre de la clase de implementación [istia.st.articles.dao.ArticlesDaoArrayList] y dónde encontrarla [ webarticles-dao.dll]. Dado que la instanciación no requiere parámetros, aquí no se define ninguno.
- para el singleton [articlesdomain], el nombre de la clase de implementación [istia.st.articles.domain.AchatsArticles] y dónde encontrarla [ webarticles-domain.dll]. La clase [AchatsArticles] tiene un constructor con un parámetro: el singleton que gestiona el acceso a la capa [dao]. Aquí, este se define como el singleton [articlesdao] definido anteriormente.
- La clase de prueba obtiene una instancia de la clase que se va a probar, [articlesdomain], así como una instancia de la clase de acceso a datos [articlesdao]. Este último punto es controvertido. En teoría, la clase de prueba no debería necesitar acceder a la capa [dao], que ni siquiera se supone que conozca. Aquí hemos hecho caso omiso de esta «ética» que, de respetarse, nos habría obligado a crear nuevos métodos en nuestra interfaz [IArticlesDomain].
Para probar la capa [domain], generamos DLL y [tests-webarticles-domain.dll] en la carpeta [bin] del proyecto [tests]:
![]() | ![]() |
A continuación, con la aplicación [Nunit-Gui], cargamos este DLL y ejecutamos las pruebas:

El lector que visualice este documento en pantalla podrá ver que todas las pruebas se han superado. A partir de ahora, daremos por hecho que contamos con una capa [domain] operativa.
1.4.5. Conclusión
Recordemos que queremos construir la siguiente aplicación web de tres capas:
![]() |
El modelo M de nuestra aplicación MVC ya está escrito y probado. Se nos proporciona en dos DLL [webarticles-dao.dll, webarticles-domain.dll]. Podemos pasar a la última capa, la capa [web], que contiene el controlador C y las vistas V. Consideraremos en primer lugar un método presentado en el documento [Développement WEB avec ASP.NET 1.1 ]
- el controlador C está implementado en dos archivos [global.asax, main.aspx]
- las vistas V están implementadas mediante páginas aspx
1.5. La capa [web]
La arquitectura MVC de la aplicación web será la siguiente:
![]() |
las clases de negocio [domain], las clases de acceso a datos [dao] y la fuente de datos | |
las páginas ASPX | |
todas las solicitudes de los clientes HTTP pasan por los dos controladores siguientes: global.asax: gestiona los eventos relacionados con el inicio inicial de la aplicación main.aspx: procesa individualmente la solicitud de cada cliente |
1.5.1. Las vistas
Las vistas son las mismas que se presentaron al principio del documento:
liste.aspx | Las vistas se encuentran en la carpeta [vues] de la aplicación ![]() | |
infos.aspx | ||
panier.aspx | ||
paniervide.aspx | ||
erreurs.aspx |
1.5.2. Los controladores
Como se ha indicado, el controlador estará formado por dos elementos:
- [global.asax,global.asax.vb]: utilizado principalmente para inicializar la aplicación e incorporar a su contexto todos los datos que deben compartirse entre los distintos clientes
- [main.aspx, main.aspx.vb]: el controlador propiamente dicho, el que procesa las solicitudes HTTP de los clientes.
Las diferentes solicitudes de los clientes se dirigirán al controlador [main.aspx] y contendrán un parámetro denominado [action] que especifica la acción solicitada por el cliente:
solicitud | significado | acción del controlador | respuestas posibles |
el cliente quiere la lista de artículos | - solicita la lista de artículos a la capa de negocio | - [LISTE] - [ERREURS] | |
el cliente solicita información sobre uno de los artículos mostrados en la vista [LISTE] | - solicita el artículo a la capa de negocio | - [INFOS] - [ERREURS] | |
el cliente compra un artículo | - solicita el artículo a la capa de negocio y lo añade a la cesta del cliente | - [INFOS] si hay un error de cantidad - [LISTE] si no hay error | |
el cliente quiere eliminar una compra de su cesta | - recupera la cesta de la sesión y la modifica | - [PANIER] - [PANIERVIDE] - [ERREURS] | |
el cliente quiere ver su cesta | - recupera la cesta de la compra de la sesión | - [PANIER] - [PANIERVIDE] - [ERREURS] | |
el cliente ha finalizado sus compras y pasa a la fase de pago | - actualiza en la base de datos las existencias de los artículos comprados - vacía la cesta del cliente de los artículos cuya compra ha sido validada | - [LISTE] - [ERREURS] |
1.5.3. Configuración de la aplicación
Intentaremos configurar la aplicación de manera que sea lo más flexible posible ante cambios tales como:
- el cambio de las clases URL de las diferentes vistas
- el cambio de las clases que implementan las interfaces [IArticlesDao] y [IArticlesDomain]
- el cambio de SGBD, de la base de datos, de la tabla de artículos
1.5.3.1. Los cambios en URL
Los nombres de las vistas URL se colocarán en el archivo de configuración de la aplicación [web.config] junto con algunos otros 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. El cambio de las clases de implementación de las interfaces
En el espíritu de las arquitecturas de tres capas, las capas deben ser independientes unas de otras. Esta independencia se consigue de la siguiente manera:
- las capas se comunican entre sí a través de interfaces y no de clases concretas
- el código de una capa nunca instancia por sí mismo la clase de otra capa para utilizarla. Simplemente solicita a una herramienta externa, en este caso Spring, una instancia de implementación de la interfaz de la capa que desea utilizar. Para ello, sabemos que no necesita conocer el nombre de la clase de implementación, sino solo el nombre del singleton de Spring del que desea una referencia.
En nuestra aplicación, Spring se configurará en el archivo [web.config] de la aplicación web de la siguiente manera:
<?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 acceder a la capa [métier], una clase de la capa [web] podrá solicitar el singleton [articlesDomain]. Spring instanciará entonces un objeto de tipo [istia.st.articles.domain.AchatsArticles]. Para esta instanciación, necesita un objeto de tipo [articlesDao], es decir, un objeto de tipo [istia.st.articles.dao.ArticlesDaoArrayList]. Spring instanciará entonces dicho objeto. Al final de la operación, la capa [web] que solicitó el singleton [articlesDomain] dispone de toda la cadena que la conecta con la fuente de datos:
![]() |
1.5.3.3. Los cambios relacionados con SGBD o con la base de datos
Este punto se omitirá aquí, ya que nos encontramos en una aplicación de prueba sin SGBD. En una segunda fase abordaremos la implementación de una capa [dao] basada en un SGBD.
1.5.4. La biblioteca de etiquetas <asp:>
Consideremos la vista [ERREURS] que muestra una lista de errores:

La vista [ERREURS] se encarga de mostrar una lista de errores que el controlador [main.aspx] ha colocado en el contexto de la solicitud con el nombre [context.Items("erreurs")]. Hay varias formas de escribir una página de este tipo. Aquí solo nos interesa la parte de visualización de los errores.
Recordemos que una página ASPX tiene una parte de presentación HTML y una parte de código .NET que prepara los datos que la parte de presentación debe mostrar. Estas dos partes pueden estar en un mismo archivo [aspx] (solución WebMatrix) o en dos archivos: [aspx] para la presentación, [aspx.vb] para el código. Esta última solución es la de Visual Studio. Para complicar las cosas, la parte de presentación HTML también puede contener código .NET, lo que tiende a difuminar la separación entre [contrôleur] y [présentation] a simple vista. Por lo general, se desaconseja encarecidamente esta solución. Para eliminar todo el código de la parte [présentation] fue necesario crear bibliotecas de etiquetas. Estas «ocultan» el código bajo la apariencia de etiquetas análogas a las etiquetas HTML. Presentamos dos posibles soluciones para la página [ERREURS].
Nuestra primera solución utiliza código .NET en la parte [présentation] de la página. La página ASPX recupera la lista de errores presente en la solicitud en su parte de controlador [erreurs.aspx.vb]:
Protected erreurs As ArrayList
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
' se recuperan los errores
erreurs = CType(context.Items("erreurs"), ArrayList)
End Sub
y luego lo muestra en la sección [présentation, erreurs.aspx]:
<h2>Les erreurs suivantes se sont produites :</h2>
<ul>
<%
for i as integer=0 to erreurs.count-1
response.write("<li>" & erreurs(i).ToString & "</li>")
next
%>
</ul>
La segunda solución utiliza la etiqueta <asp:repeater> de la biblioteca de etiquetas <asp:> de ASP.NET. Si se crea una página ASPX gráficamente, esta etiqueta está disponible en forma de componente de servidor que se coloca en el formulario de diseño. Si se escribe el código ASPX a mano, se puede hablar de biblioteca de etiquetas.
Con la biblioteca de etiquetas <asp:>, el código ASPX de la vista [ERREURS] anterior queda así:
<asp:Repeater id="rptErreurs" runat="server">
<HeaderTemplate>
<h3>Les erreurs suivantes se sont produites :
</h3>
<ul>
</HeaderTemplate>
<ItemTemplate>
<li>
<%# Container.DataItem %>
</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
La etiqueta
sirve para repetir un patrón HTML en los distintos elementos de una fuente de datos. Sus distintos elementos son los siguientes:
el patrón HTML que se mostrará antes de que se muestren los elementos de la fuente de datos | |
el motivo HTML que se repetirá para cada uno de los elementos de la fuente de datos. La expresión [<%# Container.DataItem %>] sirve para mostrar el valor del elemento actual de la fuente de datos | |
el patrón HTML que se mostrará una vez que se hayan mostrado los elementos de la fuente de datos |
La fuente de datos se vincula a la etiqueta, normalmente en la parte [contrôleur] de la 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
..
' se vinculan los errores a rptErreurs
With rptErreurs
.DataSource = context.Items("erreurs")
.DataBind()
End With
End Sub
Esta vinculación también se puede realizar en el momento del diseño de la página si ya se conoce la fuente de datos, por ejemplo, una base de datos existente.
En nuestras vistas utilizaremos otra etiqueta: <asp:datagrid>, que permite mostrar una fuente de datos en forma de tabla.
1.5.5. Estructura de la solución de Visual Studio de la aplicación [webarticles]
Una aplicación web es un rompecabezas con numerosos elementos. Dotarla de una arquitectura MVC suele aumentar el número de estos. La estructura de la aplicación [webarticles] bajo [Visual Studio] es la siguiente:
![]() | ![]() | ![]() |
![]() |
Comentarios:
- el proyecto [web] es de tipo [bibliothèque de classes] y no de tipo [Application web ASP.NET], como cabría esperar lógicamente. El tipo [Application web ASP.NET] requiere la presencia del servidor web IIS en la máquina de desarrollo o en una máquina remota. El servidor IIS no existe de serie en los equipos Windows XP Familial. Sin embargo, muchos PC se venden con esta versión. Para que los lectores que dispongan de Windows XP puedan implementar la aplicación estudiada, utilizaremos el servidor web Cassini (véase el anexo), disponible gratuitamente en Microsoft, y sustituiremos el proyecto [Application web ASP.NET] por un proyecto [bibliothèque de classes]. Esto conlleva algunos inconvenientes que se explican en los anexos.
- Los DLL utilizados por la aplicación son los siguientes:
reúne las clases de la capa de acceso a datos | |
agrupa las clases de la capa de negocio | |
contiene las clases Spring que nos permiten integrar las capas web, domain y dao | |
clases de registros: utilizadas por Spring |
Estos DLL se colocan en la carpeta [bin] y se añaden a las referencias del proyecto.
1.5.6. Las vistas ASPX
Tal y como se ha recomendado anteriormente, utilizaremos la biblioteca de etiquetas <asp:> en nuestras vistas ASPX.
1.5.6.1. El componente de usuario [entete.ascx]
Para dar cierta homogeneidad a las diferentes vistas, estas compartirán un mismo encabezado, el que muestra el nombre de la aplicación con el menú:
![]() | ![]() |
El menú es dinámico y lo define el controlador. Este incluye en la solicitud enviada a la página ASPX un atributo de clave «actions» cuyo valor asociado es una matriz de elementos de tipo Hashtable(). Cada elemento de esta matriz es un diccionario destinado a generar una opción del menú del encabezado. Cada diccionario tiene dos claves:
- href: el URL asociado a la opción del menú
- link: el texto del menú
Convertiremos el encabezado en un control de usuario. Un control de usuario encapsula una parte de la página (presentación y código asociado) en un componente reutilizable posteriormente en otras páginas. En este caso, queremos reutilizar el componente [entete] en las demás vistas de la aplicación. El código de presentación estará en [entete.ascx] y el código de control asociado en [entete.ascx.vb]. El código de presentación utilizará un componente <asp:repeater> para mostrar la tabla de opciones del menú:
![]() |
n° | type | nom | rôle |
1 | repetidor | rptMenu fuente de datos: una tabla de diccionarios con dos claves: href, enlace | mostrar las opciones del menú |
El código de presentación de la página será el siguiente:
Comentarios:
- el componente [repeater] se define en las líneas 6-14
- cada elemento de la fuente de datos asociada al repetidor es un diccionario con dos claves: href (línea 9) y enlace (línea 10)
El código de control asociado será el siguiente:
Comentarios:
- el componente de tipo [EnteteWebArticles] tiene una propiedad pública [actions] de solo escritura - línea 7
- esta propiedad permite asociar al componente <asp:repeater> denominado [rptMenu] (línea 10) la tabla de opciones calculada por el controlador de la aplicación (líneas 11-12).
Las demás vistas de la aplicación utilizarán el encabezado definido por [entete.ascx]. La página [erreurs.aspx], por ejemplo, incluirá el encabezado mediante el siguiente código:
Comentarios:
- La línea 1 declara que la etiqueta <WA:entete> deberá asociarse al componente definido por el archivo [entete.ascx]. Los atributos [TagPrefix] y [TagName] son libres.
- Una vez hecho esto, la inserción del componente en el código de presentación de la página se realiza con la línea 9. Al ejecutarse, esta etiqueta tendrá como efecto incluir en el código de la página ASPX, que la contiene, el de la página [entete.ascx]. El código de control [erreurs.aspx.vb] se encargará de inicializar este componente. Puede hacerlo de la siguiente manera:
Comentarios:
- la línea 6 crea un objeto de tipo [EnteteWebArticles], que es el tipo del componente creado
- la línea 11 inicializa la propiedad [actions] de este objeto
1.5.6.2. La vista [liste.aspx]
1.5.6.2.1. Introduction
Esta vista muestra la lista de artículos disponibles para la venta:
![]() | ![]() |
Se muestra tras una solicitud /main?action=liste o /main?action=validationpanier. Los elementos de la solicitud del controlador son los siguientes:
objeto Hashtable() - la tabla de opciones del menú | |
ArrayList de objetos de tipo [Article] | |
objeto String: mensaje que se mostrará al pie de página |
Cada enlace [Infos] de la tabla HTML de artículos tiene un URL de la forma [?action=infos&id=ID], donde ID es el campo id delartículo mostrado.
1.5.6.2.2. Componentes de la página
![]() |
n° | type | nom | rôle |
componente de usuario | encabezado | mostrar el encabezado | |
DataGrid | DataGridArticles 3 - columna relacionada: encabezado: Nombre, campo: nombre 4 - columna relacionada: encabezado: Precio, campo: precio 5 - columna de hipervínculo: texto: Información, campo URL: id, formato URL: /webarticles/main.aspx?action=infos&id={0} | mostrar los artículos en venta | |
etiqueta | lblMessage | Mostrar un mensaje |
Recordemos cómo se definen estas propiedades:
- en Visual Studio, seleccionamos [DataGrid] para acceder a su hoja de propiedades:

- utilizamos el enlace [Mise en forme automatique] anterior para gestionar el formato de la tabla mostrada
- y el enlace [Générateur de propriétés] para gestionar su contenido
1.5.6.2.3. Código de presentación [liste.aspx]
Comentarios:
- la línea 9 define el encabezado de la página
- las líneas 12-24 definen las características de [DataGrid]
- la línea 26 define la etiqueta [lblMessage]
1.5.6.2.4. Código del controlador [liste.aspx.vb]
Comentarios:
- los componentes de la página aparecen en las líneas 13-15. Cabe señalar que fue necesario crear un objeto [EnteteWebArticles] con un operador [new], mientras que esto no fue necesario con los demás componentes. Sin esta creación explícita, se producía un error de ejecución que indicaba que el objeto [entete] no hacía referencia a nada. Este punto merecería ser analizado en profundidad. No se ha hecho.
- La tabla de opciones del menú de la cabecera se toma del contexto para inicializar el componente [entete] de la página - línea 20
- la lista de artículos se toma del contexto - línea 22
- para inicializar el componente [DataGridArticles] —líneas 24-27
- El componente [lblMessage] se inicializa con un mensaje incluido en el contexto - línea 29
1.5.6.3. La vista [infos.aspx]
1.5.6.3.1. Introduction
Esta vista muestra información sobre un artículo y también permite su compra:

Se muestra tras una solicitud /main?action=infos&id=ID o una solicitud /main?action=achat&id=ID cuando la cantidad comprada es errónea. Los elementos de la solicitud del controlador son los siguientes:
objeto Hashtable() - la tabla de opciones del menú | |
objeto de tipo [Article]: artículo que se va a mostrar | |
objeto String - mensaje que se mostrará en caso de error en la cantidad | |
objeto String - valor a mostrar en el campo de entrada [Qte] |
Los campos [msg] y [qte] se utilizan en caso de error de introducción de datos en la cantidad:

Esta página contiene un formulario que se envía mediante el botón [Acheter]. El destino de URL de POST es [?action=achat&id=ID], donde ID es el ID del artículo comprado.
1.5.6.3.2. Los componentes de la página
![]() |
n° | type | nom | rôle |
componente de usuario | encabezado | mostrar el encabezado | |
literal | litID | Mostrar el n.º de artículo | |
DataGrid | DataGridArticle 3 - columna relacionada: encabezado: Nombre, campo: nombre 4 - columna relacionada: encabezado: Precio, campo: precio 5 - columna relacionada: encabezado: Stock actual, campo: stockActuel 6 - columna relacionada: encabezado: Stock mínimo, campo: stockMinimum | Mostrar un artículo | |
HTML Enviar | Enviar el formulario | ||
HTML Entrada runat=servidor | txtQte | introducir la cantidad comprada | |
etiqueta | lblMsgQte | posible mensaje de error |
1.5.6.3.3. El código de presentación [infos.aspx]
Comentarios:
- el encabezado está incluido en la página - línea 9
- El literal [litId] se define en la línea 10
- DataGrid y [DataGridArticles] se definen en las líneas 12-34
- el formulario se define en las líneas 36-46. Es de tipo POST.
- El destino de POST viene dado por una variable [strAction] - línea 36. Esta variable deberá ser definida por el controlador.
- El campo de introducción de la cantidad comprada se define en la línea 41. Se trata de un componente HTML de servidor (runat=server). En cuanto al código, se accede a él a través de un objeto.
- La línea 42 define la etiqueta [lblMsgQte], que contendrá un posible mensaje de error sobre la cantidad introducida
1.5.6.3.4. El código de control [infos.aspx.vb]
Comentarios:
- los componentes de la página se definen en las líneas 10-14
- La clase define una propiedad pública [strAction] que sirve para definir el destino del POST del formulario (líneas 17-25)
- el artículo que se va a mostrar se recupera del contexto de la aplicación (línea 30)
- la tabla de opciones del menú de la cabecera se toma del contexto para inicializar el componente [entete] de la página (línea 32)
- líneas 33-39, el componente [DataGridArticle] se vincula a una fuente de datos de tipo [ArrayList] que solo contiene el artículo recuperado en la línea 30
- los componentes [lblMsgQte, txtQte] se inicializan con información tomada del contexto - líneas 42-45
- la propiedad [straction] también se inicializa con información tomada del contexto (línea 47). Esta variable sirve para generar el atributo [action] del formulario HTML presente en la página:
1.5.6.4. La vista [panier.aspx]
1.5.6.4.1. Introduction
Esta vista muestra el contenido de la cesta:

Se muestra tras una solicitud /main?action=panier o /main?action=retirerachat&id=ID. Los elementos de la solicitud del controlador son los siguientes:
objeto Hashtable() - la tabla de opciones del menú | |
objeto de tipo [Panier]: la cesta que se va a mostrar |
Cada enlace [Retirer] de la tabla HTML de las compras de la cesta tiene un URL de la forma [?action=retirerachat&id=ID], donde ID es el campo [id] del artículo que se quiere eliminar de la cesta.
1.5.6.4.2. Los componentes de la página
![]() |
n° | type | nom | rôle |
componente de usuario | encabezado | mostrar el encabezado | |
DataGrid | DataGridAchats 3- columna relacionada - encabezado: Artículo, campo: nombre 4 - columna relacionada - encabezado: Cantidad, campo: cant 5 - columna relacionada - encabezado: Precio, campo: precio 6 - columna relacionada - encabezado: Total, campo: total, formato {0:C} 7 - columna de hipervínculo - Texto: Retirar, URL: id, formato URL: /webarticles/main.aspx?action=retirerachat&id={0} | mostrar la lista de artículos comprados | |
etiqueta | lblTotal | Mostrar el importe a pagar |
1.5.6.4.3. El código de presentación [panier.aspx]
Comentarios
- la línea 9 incluye el encabezado
- líneas 12-27, se define el componente [DataGridAchats]
- En la línea 29, se define el componente [lblTotal]
1.5.6.4.4. El código de control [panier.aspx.vb]
Comentarios:
- los componentes de la página se declaran en las líneas 11-13
- La inicialización del componente [entete] es idéntica a la que se encuentra en las páginas ya estudiadas - línea 17
- la cesta que se va a mostrar se recupera de la sesión (línea 19)
- la visualización de esta cesta mediante el componente [DataGridAchats] plantea problemas. La dificultad radica en la inicialización del componente. Recordemos sus columnas:
- columna [Article] asociada al campo [nom] de la fuente de datos
- columna [Qté] asociada al campo [qte] de la fuente de datos
- columna [Prix] asociada al campo [prix] de la fuente de datos
- columna [Total] asociada al campo [total] de la fuente de datos
La fuente de datos de la que disponemos es la cesta y su lista de la compra. Esta última será la fuente de datos de [DataGrid]. Solo los objetos [Achat] que alimentarán las líneas del [DataGrid] no tienen las propiedades [nom, qte, prix, total] esperadas por el [DataGrid]. Por lo tanto, se crea aquí, especialmente para el [DataGrid], una fuente de datos cuyos elementos tienen las características esperadas por el [DataGrid]. Estos elementos serán de tipo [LigneAchat], una clase creada para la ocasión y derivada de la clase [Achat] (líneas 36-66)
- Una vez definida la clase [LigneAchat), la fuente de datos de [DataGridAchats] se construye a partir de la cesta encontrada en la sesión - líneas 20-30
- el importe de las compras se muestra gracias a la propiedad [totalPanier] de la clase [Panier] - línea 32
1.5.6.5. La vista [paniervide.aspx]
1.5.6.5.1. Introduction
Esta vista muestra la información que indica que la cesta está vacía:

Se muestra tras una solicitud /main?action=panier o /main?action=retirerachat&id=ID. Los elementos de la solicitud del controlador son los siguientes:
objeto Hashtable() - la tabla de opciones del menú |
1.5.6.5.2. Los componentes de la página
![]() |
n° | type | nom | rôle |
componente de usuario | encabezado | Mostrar el encabezado |
1.5.6.5.3. El código de presentación [paniervide.aspx]
Comentarios:
- el encabezado se incluye en la línea 9
1.5.6.5.4. El código de control [paniervide.aspx.vb]
Comentarios:
- Nos limitamos a inicializar el único componente dinámico de la página - línea 10
1.5.6.6. La vista [erreurs.aspx]
1.5.6.6.1. Introduction
Esta vista se muestra en caso de errores:

Se muestra tras cualquier solicitud que genere un error, excepto en el caso de la acción de compra con una cantidad errónea, que se gestiona mediante la vista [INFOS]. Los elementos de la solicitud del controlador son los siguientes:
objeto Hashtable() - la tabla de opciones del menú | |
ArrayList de objetos [String] que representan los mensajes de error que se deben mostrar |
1.5.6.6.2. Los componentes de la página
![]() |
n° | type | nom | rôle |
componente de usuario | encabezado | mostrar el encabezado | |
repetidor | rptErreurs | Mostrar la lista de errores |
1.5.6.6.3. El código de presentación [erreurs.aspx]
Comentarios:
- el encabezado se define en la línea 9
- El componente [rptErreurs] se define en las líneas 13-19. Su contenido proviene de una fuente de datos que será de tipo [ArrayList] de objetos [String].
1.5.6.6.4. El código de control [erreurs.aspx.vb]
Comentarios:
- el componente [entete] se inicializa como de costumbre, líneas 9 y 14
- el componente [rptErreurs] se inicializa con la lista de errores de tipo [ArrayList] encontrada en el contexto - líneas 16-19
1.5.7. Los controladores global.asax, main.aspx
Queda por escribir el núcleo de nuestra aplicación web: el controlador. Su función consiste en:
- recoger la solicitud del cliente,
- procesar la acción solicitada por este utilizando las clases de negocio,
- enviar como respuesta la vista adecuada.
1.5.7.1. El controlador [global.asax.vb]
Cuando la aplicación recibe su primera solicitud, se ejecuta el procedimiento [Application_Start] del archivo [global.asax.vb]. Será la única vez. El procedimiento [Application_Start] tiene como objetivo inicializar los objetos necesarios para la aplicación web y que serán compartidos en modo de solo lectura por todos los subprocesos de los clientes. Estos objetos compartidos pueden colocarse en dos lugares:
- los campos privados del controlador
- el contexto de ejecución de la aplicación (Application)
El método [Application_Start] de la aplicación [global.asax.vb] realizará las siguientes acciones:
- comprobará la presencia, en el archivo [web.config], de los parámetros necesarios para el correcto funcionamiento de la aplicación. Estos se han descrito en el apartado 1.5.3.
- introducirá en el contexto de la aplicación la lista de posibles errores en forma de un objeto [ArrayList erreurs]. Esta lista estará vacía si no hay errores, pero existirá de todos modos.
- Si se han producido errores, el método [Application_Start] se detiene ahí. De lo contrario, solicita la referencia de un singleton de tipo [IArticlesDomain], que será el objeto de negocio que el controlador utilizará para sus necesidades. Como se ha explicado en 1.5.3.2, el controlador solicitará este singleton al framework Spring. Esta operación de instanciación puede dar lugar a diferentes errores. Si es así, estos se almacenarán, una vez más, en el objeto [erreurs] del contexto de la aplicación.
El controlador [global.asax.vb] dispone de un procedimiento [Session_Start] que se ejecuta cada vez que llega un nuevo cliente. En este procedimiento, se creará una cesta vacía para el cliente. Esta cesta se mantendrá a lo largo de las solicitudes de este cliente en particular. El código podría ser el siguiente:
Comentarios:
- los parámetros esperados en [web.config] se definen en una tabla (línea 18)
- se buscan en [web.config]. Si están presentes, se almacenan en el contexto de la aplicación; de lo contrario, se registra un error en la lista de errores [erreurs] (líneas 21-33)
- si no hay errores, se solicita a Spring una referencia del singleton [articlesDomain] que gestiona el acceso a la capa [domain] de la aplicación - líneas 35-47. Los posibles errores se registran en [erreurs].
- Los errores se registran en el contexto de la aplicación —línea 49
- Se sale del procedimiento si se han producido errores (línea 51).
- Se crea una matriz de tres diccionarios. Cada uno de ellos tiene dos claves: href y enlace. Esta matriz representa las tres opciones de menú posibles - líneas 52-71
- Esta matriz se almacena en el contexto de la aplicación - línea 73
- Cada vez que hay un nuevo cliente, se ejecuta el procedimiento [Session_Start]. En él se crea una cesta vacía en la sesión del cliente - líneas 78-81
1.5.7.2. El controlador [main.aspx.vb]
El controlador [main.aspx.vb] procesa todas las solicitudes de los clientes. De hecho, todas ellas tienen el formato [/webarticles/main.aspx?action=XX]. Una solicitud se procesará de la siguiente manera:
- se comprobará el objeto [erreurs] del contexto de la aplicación. Si no está vacío, significa que se han producido errores durante la inicialización de la aplicación y que esta no puede funcionar. En ese caso, se enviará, como respuesta, la vista [ERREURS].
- Se recuperará y verificará el parámetro [action] de la solicitud. Si no corresponde a una acción conocida, se envía la vista [ERREURS] con un mensaje de error adecuado.
- Si el parámetro [action] es válido, la solicitud del cliente se pasa a un procedimiento específico de la acción para su procesamiento:
método | solicitud | procesamiento | respuestas posibles |
GET /main?action=liste | - solicitar la lista de artículos a la clase de negocio - mostrarlo | [LISTE] o [ERREURS] | |
GET /main?action=infos&id=ID | - solicitar el artículo con id=ID a la clase de negocio - mostrarlo | [INFOS] o [ERREURS] | |
POST /main?action=compra&id=ID - la cantidad comprada forma parte de los parámetros enviados | - solicitar el artículo con id=ID a la clase de negocio - incluirlo en la cesta en la sesión del cliente | [LISTE] o [INFOS] o [ERREURS] | |
GET /main?action=retirerachat&id=ID | - retirar el artículo con id=ID de la lista de compras de la cesta de la sesión del cliente | [PANIER] | |
GET /main?action=carrito | - Mostrar la cesta de la sesión del cliente | [PANIER] o [PANIERVIDE] | |
GET /main?action=validacióncarrito | - Reducir en la base de datos las existencias de todos los artículos presentes en la cesta de sesión del cliente | [LISTE] o [ERREURS] |
El esqueleto del controlador [main.aspx.vb] podría ser el siguiente:
Comentarios:
- la clase tiene dos campos privados que se compartirán entre los métodos (líneas 15-16):
- articlesDomain: el singleton de acceso a la capa [domain]
- opciones: la matriz de diccionarios de opciones de menú
- el procedimiento [Page_Load]:
- inicializará los dos campos privados de la clase
- recuperará el parámetro [action] de la consulta y ejecutará el método de procesamiento de esta acción.
1.5.7.3. El método [Page_Load]
Este evento es el primero en producirse en la página. El código es el siguiente:
Comentarios:
- cada vez que se carga la página, se comprueba que la inicialización de la aplicación realizada por [global.asax] se haya llevado a cabo correctamente.
- Para ello, se recupera en el contexto de la aplicación la lista de errores colocada allí por [global.asax] - línea 4
- Si esta lista no está vacía, se muestra la vista [ERREURS] (líneas 6-10)
- se recupera el singleton [articlesDomain] colocado por [global.asax] en el contexto de la aplicación y se almacena en el campo privado [articlesDomain] para que esté disponible para los distintos métodos de la clase - línea 12
- Se realiza una operación similar con la matriz de opciones del menú - línea 14
- se recupera el parámetro [action] de la solicitud - línea 16
- ejecutamos el método correspondiente a la acción solicitada. Una acción no prevista se considera la acción [liste] - líneas 16-36
1.5.7.4. Procesamiento de la acción [liste]
Se trata de mostrar la lista de artículos:

El código es el siguiente:
Comentarios:
- los posibles errores se colocan en un [ArrayList] - línea 4
- se solicita la lista de artículos al singleton [articlesDomain] - líneas 5-12
- si se han producido errores, se envía la vista [ERREURS] - líneas 13-19
- si no, se envía la vista [LISTE] - líneas 20-24
1.5.7.5. Procesamiento de la acción [infos]
El cliente ha solicitado información sobre un artículo concreto:

El código es el siguiente:
Comentarios:
- los posibles errores se colocan en un [ArrayList] - línea 4
- el identificador del artículo solicitado se recupera en la consulta - línea 6
- se comprueba este identificador. Debe estar presente y debe ser un número entero. Si no es así, se envía la vista [ERREURS] con el mensaje de error correspondiente - líneas 7-27
- una vez verificado el identificador, se solicita el artículo al singleton [articlesDomain]. Si se produce una excepción, se envía la vista [ERREURS] - líneas 29-39
- si no se ha encontrado el artículo, se envía la vista [ERREURS] - líneas 41-48
- Si se ha encontrado el artículo, se coloca en la sesión del usuario y se muestra en la vista [INFOS], líneas 50-56
1.5.7.6. Procesamiento de la acción [achat]
El cliente ha comprado el artículo mostrado en la vista [INFOS].

El código es el siguiente:
Comentarios:
- se recupera el artículo añadido a la sesión - línea 5
- si no está ahí (la sesión podría haber caducado), se muestra la vista [LISTE] - líneas 7-10
- se recupera la cantidad comprada en la consulta - línea 12
- se comprueba su validez - líneas 13-29
- en caso de no ser válida, según el caso, se envía la vista [LISTE] - línea 16 o la vista [INFOS] - líneas 24-28
- si todo es correcto, la compra se guarda en la cesta - líneas 31-32
- A continuación, se envía la vista [LISTE] - línea 34
1.5.7.7. Procesamiento de la acción [panier]
El cliente ha realizado varias compras y solicita ver la cesta:

El código es el siguiente:
Comentarios:
- se recupera la cesta en la sesión (línea 4). Aquí no se comprueba si se ha recuperado algo. Habría que hacerlo, ya que la sesión podría haber caducado.
- Si la cesta está vacía, se envía la vista [PANIERVIDE] (líneas 6-10)
- ; de lo contrario, se envía la vista [PANIER] (líneas 11-14)
1.5.7.8. Procesamiento de la acción [retirerachat]
El cliente desea eliminar un artículo de su cesta:

El código es el siguiente:
Comentarios:
- se recupera la cesta en la sesión (línea 4). Aquí no se comprueba si se ha recuperado algo. Habría que hacerlo, ya que la sesión podría haber caducado.
- Se recupera en la consulta el identificador [id] del artículo que se va a eliminar - línea 8.
- La compra correspondiente se elimina de la cesta (línea 10).
- Aquí no se ha comprobado la validez del identificador del artículo comprado. Si este tiene un tipo no válido, se producirá una excepción que se tratará en las líneas 11-13. Si es válido pero no existe, el método [panier.enlever] (línea 10) no hace nada.
- Se muestra la nueva cesta - línea 16
1.5.7.9. Procesamiento de la acción [validerpanier]
El cliente desea validar su cesta:

El código es el siguiente:
Comentarios:
- se recupera la cesta en la sesión (línea 6). Aquí no se comprueba si se ha recuperado algo. Habría que hacerlo, ya que la sesión podría haber caducado.
- En caso de que la sesión haya caducado, tendremos un puntero [nothing] para la cesta y el método [acheter] —línea 9— lanzará una excepción y se enviará la vista [ERREURS]. Sin embargo, el mensaje de error no será muy claro para el usuario.
- En las líneas 8-16, se intenta validar la cesta recuperada de la sesión. Algunas compras pueden no validarse si la cantidad solicitada excede el stock del artículo solicitado. Estos casos se almacenan mediante el método [acheter] en una lista de errores que se recupera en la línea 11.
- Si hay errores, se envía la vista [ERREURS] (líneas 18-23)
- ; de lo contrario, se envía la vista [LISTE] (líneas 25-26)
1.6. Conclusion
Aquí hemos desarrollado una aplicación según un modelo MVC. No parece existir (abril de 2005) ningún «framework» profesional de desarrollo MVC en ASP.NET como los que existen en Java (Struts, Spring, ...). El proyecto [Spring.net] debería ofrecer uno en breve. A la espera de ello, el método anterior permite un desarrollo MVC viable para aplicaciones de tamaño medio.































