Skip to content

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.

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

Image

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:

  1. 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.
  2. 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.
  3. 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
  4. 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.
  5. 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:

  1. las clases de negocio
  2. las clases de acceso a datos
  3. 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);
id
clave primaria que identifica un artículo de forma única
nom
nombre del artículo
prix
su precio
stockactuel
su stock actual
stockminimum
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:

assembly
contenu
rôle
webarticles-dao
- [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
webarticles-domain
- [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:

Image

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:

  1. un constructor que permite establecer los 5 datos de un artículo: [id, nom, prix, stockactuel, stockminimum]
  2. propiedades públicas que permiten leer y escribir los 5 datos.
  3. una verificación de los datos introducidos en el artículo. En caso de datos erróneos, se lanza una excepción.
  4. 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:

getAllArticles
devuelve todos los artículos de la fuente de datos
clearAllArticles
vacía la fuente de datos
getArticleById
devuelve el objeto [Article] identificado por su clave primaria
ajouteArticle
permite añadir un artículo a la fuente de datos
modifieArticle
permite modificar un artículo de la fuente de datos
supprimerArticle
permite eliminar un artículo de la fuente de datos
changerStockArticle
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:

Image

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

Image

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:

Imports NUnit.Framework

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:

Image

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:

Image

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:

Image

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
Function getAllArticles() As IList
devuelve la lista de objetos [Article] de la fuente de datos asociada
Function getArticleById(ByVal idArticle As Integer) As Article
devuelve el objeto [Article] identificado por [idArticle]
acheter(ByVal panier As Panier)
valida la cesta del cliente restando de las existencias de los artículos comprados la cantidad adquirida; puede fallar si las existencias son insuficientes
ReadOnly Property erreurs() As ArrayList
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:
Public Property article() As article
El artículo comprado
Public Property qte() As Integer
la cantidad comprada
Public ReadOnly Property totalAchat() As Double
el importe de la compra
Public Overrides Function ToString() As String
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:
ReadOnly Property achats() As ArrayList
Lista de compras del cliente: lista de objetos de tipo [Achat]
ajouter(ByVal unAchat As Achat)
añade una compra a la lista de compras
enlever(ByVal idAchat As Integer)
elimina la compra del artículo idAchat
ReadOnly Property totalPanier() As Double
el importe total de las compras de la cesta
Function ToString() As String
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:
_articlesDao As IArticlesDao
el objeto de acceso a los datos
_erreurs As ArrayList
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:
Sub New(ByVal articlesDao As IArticlesDao)
  • 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:

Image

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:

Image

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:

M=modèle
las clases de negocio [domain], las clases de acceso a datos [dao] y la fuente de datos
V=vues
las páginas ASPX
C=contrôleur
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
liste.aspx
Las vistas se encuentran en la carpeta [vues] de la aplicación
INFOS
infos.aspx
PANIER
panier.aspx
PANIERVIDE
paniervide.aspx
ERREURS
erreurs.aspx

1.5.2. Los controladores

Como se ha indicado, el controlador estará formado por dos elementos:

  1. [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
  2. [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
action=liste
el cliente quiere la lista de
artículos
- solicita la lista de artículos a la capa
de negocio
- [LISTE]
- [ERREURS]
action=infos
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]
action=achat
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
action=retirerachat
el cliente quiere eliminar una
compra de su cesta
- recupera la cesta de la sesión
y la modifica
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=panier
el cliente quiere ver su
cesta
- recupera la cesta de la compra de la sesión
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=validationpanier
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:

  1. el cambio de las clases URL de las diferentes vistas
  2. el cambio de las clases que implementan las interfaces [IArticlesDao] y [IArticlesDomain]
  3. 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:

Image

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

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

sirve para repetir un patrón HTML en los distintos elementos de una fuente de datos. Sus distintos elementos son los siguientes:

HeaderTemplate
el patrón HTML que se mostrará antes de que se muestren los elementos de la fuente de datos
ItemTemplate
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
FooterTemplate
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:
webarticles-dao.dll
reúne las clases de la capa de acceso a datos
webarticles-domain.dll
agrupa las clases de la capa de negocio
Spring.Core.dll
contiene las clases Spring que nos permiten integrar las capas web, domain y dao
log4net.dll
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ú:

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:

<%@ Control codebehind="entete.ascx.vb" Language="vb" autoeventwireup="false" inherits="istia.st.articles.web.EnteteWebArticles" %>
        <table>
            <tr>
                <td>
                    <h2>Magasin virtuel</h2></td>
                <asp:Repeater id="rptMenu" runat="server">
                    <ItemTemplate>
                        <td>
                            |<a href='<%# Container.DataItem("href") %>'>
                                <%# Container.DataItem("enlace") %>
                            </a>
                        </td>
                    </ItemTemplate>
                </asp:Repeater>
            </tr>
        </table>
        <hr>

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:

Namespace istia.st.articles.web
    Public Class EnteteWebArticles
        Inherits System.Web.UI.UserControl

        Protected WithEvents rptMenu As System.Web.UI.WebControls.Repeater

        Public WriteOnly Property actions() As Hashtable()
            Set(ByVal Value As Hashtable())
                ' se asocia la tabla de acciones a su componente
                With rptMenu
                    .DataSource = Value
                    .DataBind()
                End With
            End Set
        End Property
    End Class
End Namespace

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:

<%@ Register TagPrefix="WA" TagName="entete" Src="entete.ascx" %>
<%@ Page inherits="istia.st.articles.web.ErreursWebarticles" autoeventwireup="false" Language="vb" %>
<HTML>
    <HEAD>
        <TITLE>webarticles</TITLE>
        <META http-equiv="Content-Type" content="text/html; charset=windows-1252">
    </HEAD>
    <body>
        <WA:entete id="entete" runat="server"></WA:entete>
        <h3>Les erreurs suivantes se sont produites :
        </h3>    

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:
    Public Class ErreursWebarticles
        Inherits System.Web.UI.Page

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

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
             ' se vinculan las opciones del menú a rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
...
        End Sub
    End Class

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:

actions
objeto Hashtable() - la tabla de opciones del menú
listarticles
ArrayList de objetos de tipo [Article]
message
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
type
nom
rôle
1
componente de usuario
encabezado
mostrar el encabezado
2
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
6
etiqueta
lblMessage
Mostrar un mensaje

Recordemos cómo se definen estas propiedades:

  • en Visual Studio, seleccionamos [DataGrid] para acceder a su hoja de propiedades:

Image

  • 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]
<%@ Page codebehind="liste.aspx.vb" inherits="istia.st.articles.web.ListeWebarticles" autoeventwireup="false" Language="vb" %>
<%@ Register TagPrefix="WA" TagName="entete" Src="entete.ascx"%>
<HTML>
    <HEAD>
        <TITLE>webarticles</TITLE>
        <META http-equiv="Content-Type" content="text/html; charset=windows-1252">
    </HEAD>
    <body>
        <WA:entete id="entete" runat="server"></WA:entete>
        <h2>Liste des articles</h2>
        <P>
            <asp:DataGrid id="DataGridArticles" runat="server" ForeColor="Black" BackColor="LightGoldenrodYellow"
                BorderColor="Tan" CellPadding="2" BorderWidth="1px" GridLines="None" AutoGenerateColumns="False">
                <SelectedItemStyle ForeColor="GhostWhite" BackColor="DarkSlateBlue"></SelectedItemStyle>
                <AlternatingItemStyle BackColor="PaleGoldenrod"></AlternatingItemStyle>
                <HeaderStyle Font-Bold="True" BackColor="Tan"></HeaderStyle>
                <FooterStyle BackColor="Tan"></FooterStyle>
                <Columns>
                    <asp:BoundColumn DataField="nom" HeaderText="Nom"></asp:BoundColumn>
                    <asp:BoundColumn DataField="prix" HeaderText="Prix" DataFormatString="{0:C}"></asp:BoundColumn>
                    <asp:HyperLinkColumn Text="Infos" DataNavigateUrlField="id" DataNavigateUrlFormatString="/webarticles/main.aspx?action=infos&amp;id={0}"></asp:HyperLinkColumn>
                </Columns>
                <PagerStyle HorizontalAlign="Center" ForeColor="DarkSlateBlue" BackColor="PaleGoldenrod"></PagerStyle>
            </asp:DataGrid></P>
        <P>
            <asp:Label id="lblMessage" runat="server" BackColor="#FFC080"></asp:Label></P>
    </body>
</HTML>

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]
Imports System
Imports System.Collections
Imports System.Data
Imports istia.st.articles.dao

Namespace istia.st.articles.web

     ' gestiona la página de visualización de la lista de artículos
    Public Class ListeWebarticles
        Inherits System.Web.UI.Page

         ' componentes de la página
        Protected WithEvents lblMessage As System.Web.UI.WebControls.Label
        Protected WithEvents DataGridArticles As System.Web.UI.WebControls.DataGrid
        Protected WithEvents entete As New EnteteWebArticles

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
             ' se prepara la vista [liste] a partir de la información del contexto
             ' se vinculan las opciones del menú a rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
             ' se recuperan los artículos en una tabla de datos
            Dim articles As ArrayList = CType(context.Items("articles"), ArrayList)
             ' se vinculan al componente [DataGrid] de la página
            With DataGridArticles
                .DataSource = articles
                .DataBind()
            End With
             ' se muestra el mensaje
            lblMessage.Text = context.Items("message").ToString
        End Sub
    End Class
End Namespace

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:

Image

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:

actions
objeto Hashtable() - la tabla de opciones del menú
article
objeto de tipo [Article]: artículo que se va a mostrar
msg
objeto String - mensaje que se mostrará en caso de error en la cantidad
qte
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:

Image

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
type
nom
rôle
1
componente de usuario
encabezado
mostrar el encabezado
2
literal
litID
Mostrar el n.º de artículo
3 à 6
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
7
HTML Enviar
 
Enviar el formulario
8
HTML Entrada
runat=servidor
txtQte
introducir la cantidad comprada
9
etiqueta
lblMsgQte
posible mensaje de error
1.5.6.3.3. El código de presentación [infos.aspx]
<%@ Register TagPrefix="WA" TagName="entete" Src="entete.ascx" %>
<%@ Page codebehind="infos.aspx.vb" inherits="istia.st.articles.web.InfosWebarticles" autoeventwireup="false" Language="vb" %>
<HTML>
    <HEAD>
        <TITLE>webarticles</TITLE>
        <META http-equiv="Content-Type" content="text/html; charset=windows-1252">
    </HEAD>
    <body>
        <WA:entete id="entete" runat="server"></WA:entete>
        <h2>Article d'id [<asp:Literal id="litId" runat="server"></asp:Literal>]</h2>
        <P>
            <asp:DataGrid id="DataGridArticle" runat="server" BackColor="White" BorderColor="#E7E7FF" CellPadding="3"
                BorderWidth="1px" BorderStyle="None" GridLines="Horizontal" AutoGenerateColumns="False">
                <SelectedItemStyle Font-Bold="True" ForeColor="#F7F7F7" BackColor="#738A9C"></SelectedItemStyle>
                <AlternatingItemStyle BackColor="#F7F7F7"></AlternatingItemStyle>
                <ItemStyle HorizontalAlign="Center" ForeColor="#4A3C8C" BackColor="#E7E7FF"></ItemStyle>
                <HeaderStyle Font-Bold="True" HorizontalAlign="Center" ForeColor="#F7F7F7" BackColor="#4A3C8C"></HeaderStyle>
                <FooterStyle ForeColor="#4A3C8C" BackColor="#B5C7DE"></FooterStyle>
                <Columns>
                    <asp:BoundColumn DataField="nom" HeaderText="Nom">
                        <HeaderStyle HorizontalAlign="Center"></HeaderStyle>
                    </asp:BoundColumn>
                    <asp:BoundColumn DataField="prix" HeaderText="Prix" DataFormatString="{0:C}">
                        <HeaderStyle HorizontalAlign="Center"></HeaderStyle>
                    </asp:BoundColumn>
                    <asp:BoundColumn DataField="stockactuel" HeaderText="Stock actuel">
                        <HeaderStyle HorizontalAlign="Center"></HeaderStyle>
                    </asp:BoundColumn>
                    <asp:BoundColumn DataField="stockminimum" HeaderText="Stock minimum">
                        <HeaderStyle HorizontalAlign="Center"></HeaderStyle>
                    </asp:BoundColumn>
                </Columns>
                <PagerStyle HorizontalAlign="Right" ForeColor="#4A3C8C" BackColor="#E7E7FF" Mode="NumericPages"></PagerStyle>
            </asp:DataGrid></P>
        <HR width="100%" SIZE="1">
        <form method="post" action="<%=strAction%>">
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qté</td>
                    <td><INPUT type="text" maxLength="3" size="3" id="txtQte" runat="server"></td>
                    <td><asp:Label id="lblMsgQte" runat="server" />
                    </td>
                </tr>
            </table>
        </form>
    </body>
</HTML>

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]
Imports istia.st.articles.dao
Imports System
Imports System.Collections

Namespace istia.st.articles.web

     ' gestiona la página de información de un artículo
    Public Class InfosWebarticles
        Inherits System.Web.UI.Page
        Protected WithEvents lblMsgQte As System.Web.UI.WebControls.Label
        Protected WithEvents litId As System.Web.UI.WebControls.Literal
        Protected WithEvents txtQte As System.Web.UI.HtmlControls.HtmlInputText
        Protected WithEvents DataGridArticle As System.Web.UI.WebControls.DataGrid
        Protected WithEvents entete As New EnteteWebArticles

         ' el URL donde se enviará el formulario
        Private _strAction As String
        Public Property strAction() As String
            Get
                Return _strAction
            End Get
            Set(ByVal Value As String)
                _strAction = Value
            End Set
        End Property

         ' visualización de la página
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
             ' se recuperan los datos de la solicitud
            Dim unArticle As Article = CType(Session.Item("article"), Article)
             ' se vinculan las opciones del menú a rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
             ' se vincula el artículo a [DataGrid]
            Dim articles As New ArrayList
            articles.Add(unArticle)
            With DataGridArticle
                .DataSource = articles
                .DataBind()
            End With
             ' la etiqueta id
            litId.Text = unArticle.id.ToString
             ' el mensaje de error
            lblMsgQte.Text = context.Items("msg").ToString
             ' la cantidad anterior
            txtQte.Value = context.Items("qte").ToString
             ' la acción
            strAction = "?action=achat&id=" + unArticle.id.ToString
        End Sub
End Namespace

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:
        <form method="post" action="<%=strAction%>">
....
        </form>

1.5.6.4. La vista [panier.aspx]

1.5.6.4.1. Introduction

Esta vista muestra el contenido de la cesta:

Image

Se muestra tras una solicitud /main?action=panier o /main?action=retirerachat&id=ID. Los elementos de la solicitud del controlador son los siguientes:

actions
objeto Hashtable() - la tabla de opciones del menú
panier
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
type
nom
rôle
1
componente de usuario
encabezado
mostrar el encabezado
2
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
8
etiqueta
lblTotal
Mostrar el importe a pagar
1.5.6.4.3. El código de presentación [panier.aspx]
<%@ Page codebehind="panier.aspx.vb" inherits="istia.st.articles.web.PanierWebarticles" autoeventwireup="false" Language="vb" %>
<%@ Register TagPrefix="WA" TagName="entete" Src="entete.ascx" %>
<HTML>
    <HEAD>
        <TITLE>webarticles</TITLE>
        <META http-equiv="Content-Type" content="text/html; charset=windows-1252">
    </HEAD>
    <body>
        <WA:entete id="entete" runat="server"></WA:entete>
        <h2>Contenu de votre panier</h2>
        <P>
            <asp:DataGrid id="DataGridAchats" runat="server" BorderWidth="1px" GridLines="Vertical" CellPadding="4"
                BackColor="White" BorderStyle="None" BorderColor="#DEDFDE" ForeColor="Black" AutoGenerateColumns="False">
                <SelectedItemStyle Font-Bold="True" ForeColor="White" BackColor="#CE5D5A"></SelectedItemStyle>
                <AlternatingItemStyle BackColor="White"></AlternatingItemStyle>
                <ItemStyle BackColor="#F7F7DE"></ItemStyle>
                <HeaderStyle Font-Bold="True" ForeColor="White" BackColor="#6B696B"></HeaderStyle>
                <FooterStyle BackColor="#CCCC99"></FooterStyle>
                <Columns>
                    <asp:BoundColumn DataField="nom" HeaderText="Article"></asp:BoundColumn>
                    <asp:BoundColumn DataField="qte" HeaderText="Qt&#233;"></asp:BoundColumn>
                    <asp:BoundColumn DataField="prix" HeaderText="Prix"></asp:BoundColumn>
                    <asp:BoundColumn DataField="totalAchat" HeaderText="Total" DataFormatString="{0:C}"></asp:BoundColumn>
                    <asp:HyperLinkColumn Text="Retirer" DataNavigateUrlField="id" DataNavigateUrlFormatString="/webarticles/main.aspx?action=retirerachat&amp;id={0}"></asp:HyperLinkColumn>
                </Columns>
                <PagerStyle HorizontalAlign="Right" ForeColor="Black" BackColor="#F7F7DE" Mode="NumericPages"></PagerStyle>
            </asp:DataGrid></P>
        <P>Total de la commande :
            <asp:Label id="lblTotal" runat="server"></asp:Label>&nbsp;euros</P>
    </body>
</HTML>

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]
Imports System
Imports System.Collections
Imports System.Data
Imports istia.st.articles.dao
Imports istia.st.articles.domain

Namespace istia.st.articles.web
     ' gestiona la página de visualización de la cesta
    Public Class PanierWebarticles
        Inherits System.Web.UI.Page
        Protected WithEvents DataGridAchats As System.Web.UI.WebControls.DataGrid
        Protected WithEvents lblTotal As System.Web.UI.WebControls.Label
        Protected WithEvents entete As New EnteteWebArticles

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
             ' vincula las opciones del menú a rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
             ' se recupera la cesta
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
             ' se transfieren las compras a una tabla de líneas de compra
            Dim achats(unPanier.achats.Count - 1) As LigneAchat
             ' se cambia el tipo de los elementos de ArrayList
            For i As Integer = 0 To achats.Length - 1
                achats(i) = New LigneAchat(CType(unPanier.achats(i), Achat))
            Next
             ' se vinculan los datos a los componentes [DataGrid] de la página
            With DataGridAchats
                .DataSource = achats
                .DataBind()
            End With
             ' se muestra el total a pagar
            lblTotal.Text = unPanier.totalPanier.ToString
        End Sub

         ' línea de compra creada a partir de un objeto de compra
        Private Class LigneAchat
            Inherits Achat

             ' el constructor recibe una compra
            Public Sub New(ByVal unAchat As Achat)
                Me.article = unAchat.article
                Me.qte = unAchat.qte
            End Sub

             ' id: devuelve el id del artículo comprado
            Public ReadOnly Property id() As Integer
                Get
                    Return article.id
                End Get
            End Property

             ' nombre: nombre del artículo comprado
            Public ReadOnly Property nom() As String
                Get
                    Return article.nom
                End Get
            End Property

             ' precio del artículo comprado
            Public ReadOnly Property prix() As Double
                Get
                    Return article.prix
                End Get
            End Property

        End Class
    End Class
End Namespace

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:

Image

Se muestra tras una solicitud /main?action=panier o /main?action=retirerachat&id=ID. Los elementos de la solicitud del controlador son los siguientes:

actions
objeto Hashtable() - la tabla de opciones del menú
1.5.6.5.2. Los componentes de la página
type
nom
rôle
1
componente de usuario
encabezado
Mostrar el encabezado
1.5.6.5.3. El código de presentación [paniervide.aspx]
<%@ Register TagPrefix="WA" TagName="entete" Src="entete.ascx"%>
<%@ Page codebehind="paniervide.aspx.vb" inherits="istia.st.articles.web.PaniervideWebarticles" autoeventwireup="false" Language="vb" %>
<HTML>
    <HEAD>
        <TITLE>webarticles</TITLE>
        <META http-equiv="Content-Type" content="text/html; charset=windows-1252">
    </HEAD>
    <body>
        <WA:entete id="entete" runat="server"></WA:entete>
        <h2>Contenu de votre panier</h2>
        <P>Votre panier est vide</P>
    </body>
</HTML>

Comentarios:

  • el encabezado se incluye en la línea 9
1.5.6.5.4. El código de control [paniervide.aspx.vb]
Namespace istia.st.articles.web
     ' gestiona la página de visualización de una cesta vacía
    Public Class PaniervideWebarticles
        Inherits System.Web.UI.Page
        Protected WithEvents entete As New EnteteWebArticles

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
             ' prepara la vista [paniervide] a partir de la información del contexto
             ' se vinculan las opciones del menú a rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
        End Sub
    End Class
End Namespace

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:

Image

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:

actions
objeto Hashtable() - la tabla de opciones del menú
erreurs
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
type
nom
rôle
1
componente de usuario
encabezado
mostrar el encabezado
2
repetidor
rptErreurs
Mostrar la lista de errores
1.5.6.6.3. El código de presentación [erreurs.aspx]
<%@ Register TagPrefix="WA" TagName="entete" Src="entete.ascx" %>
<%@ Page codebehind="erreurs.aspx.vb" inherits="istia.st.articles.web.ErreursWebarticles" autoeventwireup="false" Language="vb" %>
<HTML>
    <HEAD>
        <TITLE>webarticles</TITLE>
        <META http-equiv="Content-Type" content="text/html; charset=windows-1252">
    </HEAD>
    <body>
        <WA:entete id="entete" runat="server"></WA:entete>
        <h3>Les erreurs suivantes se sont produites :
        </h3>
        <ul>
            <asp:Repeater id="rptErreurs" runat="server">
                <ItemTemplate>
                    <li>
                        <%# Container.DataItem %>
                    </li>
                </ItemTemplate>
            </asp:Repeater></ul>
    </body>
</HTML>

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]
Namespace istia.st.articles.web

     ' gestiona la página de errores
    Public Class ErreursWebarticles
        Inherits System.Web.UI.Page

         ' componentes de la página
        Protected WithEvents rptErreurs As System.Web.UI.WebControls.Repeater
        Protected WithEvents entete As New EnteteWebArticles

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
             ' se prepara la vista [erreurs] a partir de la información del contexto
             ' se vinculan las opciones del menú a rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
             ' se vinculan los errores a rptErreurs
            With rptErreurs
                .DataSource = context.Items("erreurs")
                .DataBind()
            End With
        End Sub
    End Class

End Namespace

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:

Imports System
Imports System.Web
Imports System.Web.SessionState
Imports System.Configuration
Imports istia.st.articles.domain
Imports System.Collections
Imports Spring.Context

Namespace istia.st.articles.web

    Public Class GlobalWebArticles
        Inherits System.Web.HttpApplication

         ' inicialización de la aplicación
        Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)

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

             ' se recuperan los parámetros de inicialización de la aplicación
            Dim param As String
            For i As Integer = 0 To parameters.Length - 1
                 ' lectura en el archivo de configuración
                param = ConfigurationSettings.AppSettings(parameters(i))
                If param Is Nothing Then
                     ' se registra el error
                    erreurs.Add("Paramètre [" + parameters(i) + "] absent dans le fichier [web.config]")
                Else
                     ' se almacena el parámetro en la aplicación
                    Application.Item(parameters(i)) = param
                End If
            Next
             ' ¿Hay errores?
            If erreurs.Count = 0 Then
                 ' se crea un objeto IArticlesDomain de acceso a la capa de negocio
                Dim contexte As IApplicationContext = CType(ConfigurationSettings.GetConfig("spring/context"), IApplicationContext)
                Dim articlesDomain As IArticlesDomain
                Try
                    articlesDomain = CType(contexte.GetObject("articlesDomain"), IArticlesDomain)
                     ' se guarda el objeto en la aplicación
                    Application.Item("articlesDomain") = articlesDomain
                Catch ex As Exception
                     ' se almacena el error
                    erreurs.Add("Erreur lors de la construction de l'objet d'accès à la couche métier [" + ex.ToString + "]")
                End Try
            End If
             ' los errores se colocan en la aplicación
            Application.Item("erreurs") = erreurs
             ' se ha terminado si ha habido errores
            If erreurs.Count <> 0 Then Return
             ' se crea una tabla de opciones de menú
            Dim options As New Hashtable
             ' se recupera el URL del controlador
            Dim urlMain As String = CType(Application.Item("urlMain"), String)
            Dim uneOption As Hashtable
             ' lista de artículos
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=liste")
            uneOption.Add("lien", "Liste des articles")
            options.Add("liste", uneOption)
             ' cesta
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=panier")
            uneOption.Add("lien", "Voir le panier")
            options.Add("panier", uneOption)
             ' validación de la cesta
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=validationpanier")
            uneOption.Add("lien", "Valider le panier")
            options.Add("validationpanier", uneOption)
             ' se añaden las opciones del menú a la aplicación
            Application.Item("options") = options
            Return
        End Sub

         ' iniciar sesión
        Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
             ' creamos una cesta para el cliente
            Session.Item("panier") = New Panier
        End Sub
    End Class
End Namespace

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
doListe
GET /main?action=liste
- solicitar la lista de artículos
a la clase de negocio
- mostrarlo
[LISTE] o [ERREURS]
doInfos
GET /main?action=infos&id=ID
- solicitar el artículo con id=ID a
la clase de negocio
- mostrarlo
[INFOS] o [ERREURS]
doAchat
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]
doRetirerAchat
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]
doPanier
GET /main?action=carrito
- Mostrar la cesta de la
sesión del cliente
[PANIER] o [PANIERVIDE]
doValidationPanier
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:

Imports System.Collections
Imports System
Imports System.Data

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

Namespace istia.st.articles.web

     ' clase controladora de la aplicación web
    Public Class MainWebArticles
        Inherits System.Web.UI.Page

         ' campos privados
        Private articlesDomain As IArticlesDomain
        Private options As Hashtable

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

         ' métodos de procesamiento de acciones

         ' lista de artículos
        Public Sub doListe()
...
        End Sub

         ' información sobre un artículo
        Public Sub doInfos()
...
        End Sub

         ' compra de un artículo
        Public Sub doAchat()
...
        End Sub

         ' eliminación de una compra
        Public Sub doRetirerAchat()
...
        End Sub

         ' visualización de la cesta
        Public Sub doPanier()
...
        End Sub

         ' compra de la cesta
        Public Sub doValidationPanier()
...
        End Sub

    End Class

End Namespace

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:

         ' carga de la página
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
             ' se comprueba si la aplicación se ha iniciado correctamente
            Dim erreurs As ArrayList = CType(Application.Item("erreurs"), ArrayList)
             ' si hay errores, se envía la vista [erreurs]
            If erreurs.Count <> 0 Then
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {}
                Server.Transfer(CType(Application("urlErreurs"), String))
            End If
             ' se recupera el objeto de acceso a la clase de negocio
            articlesDomain = CType(Application.Item("articlesDomain"), IArticlesDomain)
             ' así como las opciones del menú
            options = CType(Application.Item("options"), Hashtable)
             ' se recupera la acción a realizar
            Dim action As String = Request.QueryString("action")
            If action Is Nothing Then
                action = "liste"
            End If
             ' se ejecuta la acción
            Select Case action
                Case "liste"
                    doListe()
                Case "infos"
                    doInfos()
                Case "achat"
                    doAchat()
                Case "panier"
                    doPanier()
                Case "retirerachat"
                    doRetirerAchat()
                Case "validationpanier"
                    doValidationPanier()
                Case Else
                    doListe()
            End Select
        End Sub

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:

Image

El código es el siguiente:

         ' lista de artículos
        Public Sub doListe()
             ' gestión de errores
            Dim erreurs As New ArrayList
             ' se solicita la lista de artículos
            Dim articles As IList
            Try
                articles = articlesDomain.getAllArticles
            Catch ex As Exception
                 ' se registra el error
                erreurs.Add(ex.ToString)
            End Try
             ' ¿Hay errores?
            If erreurs.Count <> 0 Then
                 ' visualización de la vista [erreurs]
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
            End If
             ' visualización de la vista [liste]
            context.Items("articles") = articles
            context.Items("options") = New Hashtable() {CType(options("panier"), Hashtable)}
            If context.Items("message") Is Nothing Then context.Items("message") = ""
            Server.Transfer(CType(Application.Item("urlListe"), String))
        End Sub

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:

Image

El código es el siguiente:

         ' información sobre un artículo
        Public Sub doInfos()
             ' gestión de errores
            Dim erreurs As New ArrayList
             ' se recupera el ID del artículo solicitado
            Dim strId As String = Request.QueryString("id")
             ' ¿Hay algo?
            If strId Is Nothing Then
                 ' no es normal: se envía la página de errores
                erreurs.Add("action incorrecte (action=infos, id=rien)")
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
                Exit Sub
            End If
             ' ¿Tenemos un entero?
            Dim id As Integer
            Try
                id = Integer.Parse(strId)
            Catch ex As Exception
                 ' no es normal: se envía la página de errores
                erreurs.Add("action incorrecte (action=infos, id[" + strId + "] invalide)")
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
                Exit Sub
            End Try
             ' se solicita el artículo de clave id
            Dim unArticle As Article
            Try
                unArticle = articlesDomain.getArticleById(id)
            Catch ex As Exception
                 ' problema de acceso a los datos
                erreurs.Add("Problème d'accès aux données (" + ex.ToString + ")")
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
                Exit Sub
            End Try
             ' ¿Se ha recuperado un artículo?
            If unArticle Is Nothing Then
                 ' el artículo no existe
                erreurs.Add("L'article d'id=" + id.ToString + " n'existe pas")
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
                Exit Sub
            End If
             ' se tiene el artículo; se incluye en la sesión actual
            Session.Item("article") = unArticle
             ' se prepara su visualización
            context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
            context.Items("msg") = ""
            context.Items("qte") = ""
            Server.Transfer(CType(Application.Item("urlInfos"), String))
        End Sub

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

Image

El código es el siguiente:

         ' compra de un artículo
        Public Sub doAchat()
             ' compra de un artículo
             ' se recupera el artículo que estaba en la sesión
            Dim unArticle As Article = CType(Session.Item("article"), Article)
             ' ¿Hay algo?
            If unArticle Is Nothing Then
                 ' no es normal: se muestra la lista de artículos
                doListe()
            End If
             ' se recupera la cantidad enviada
            Dim strQte As String = Request.Form("txtQte")
             ' ¿Hay algo?
            If strQte Is Nothing Then
                 ' no es normal: se envía la lista de artículos
                doListe()
            End If
             ' ¿Tenemos un entero?
            Dim qte As Integer
            Try
                qte = Integer.Parse(strQte)
                If (qte <= 0) Then Throw New Exception
            Catch ex As Exception
                 ' no es normal - se envía la página de información con el mensaje de error
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                context.Items("msg") = "Quantité incorrecte"
                context.Items("qte") = strQte
                Server.Transfer(CType(Application.Item("urlInfos"), String))
            End Try
             ' todo va bien - se añade la compra al carrito del cliente
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            unPanier.ajouter(New Achat(unArticle, qte))
             ' se muestra la lista de artículos
            doListe()
        End Sub

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:

Image

El código es el siguiente:

         ' visualización de la cesta
        Public Sub doPanier()
             'se recupera la cesta de la sesión
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
             ' ¿Cesta vacía?
            If unPanier.achats.Count = 0 Then
                 ' visualización de la cesta vacía
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlPanierVide"), String))
            End If
             ' Visualización de la cesta no vacía
            context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable), CType(options("validationpanier"), Hashtable)}
            Server.Transfer(CType(Application.Item("urlPanier"), String))
        End Sub

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:

Image

El código es el siguiente:

         ' eliminación de una compra
        Public Sub doRetirerAchat()
             'se recupera la cesta de la compra de la sesión
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
             ' se elimina la compra
            Try
                 ' se recupera el ID del artículo eliminado
                Dim idArticle As Integer = Integer.Parse(Request.QueryString("id"))
                 ' se elimina de la cesta
                unPanier.enlever(idArticle)
            Catch ex As Exception
                 ' se muestra la lista de artículos
                doListe()
            End Try
             ' se muestra la cesta
            doPanier()
        End Sub

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:

Image

El código es el siguiente:

         ' compra de la cesta
        Public Sub doValidationPanier()
             ' al inicio, sin errores
            Dim erreurs As ArrayList
             'se recupera la cesta en la sesión
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
             ' se intenta validar la cesta
            Try
                articlesDomain.acheter(unPanier)
                 ' se anotan los posibles errores de compra
                erreurs = articlesDomain.erreurs
            Catch ex As Exception
                 ' se anota el error
                erreurs = New ArrayList
                erreurs.Add(String.Format("Erreur lors de la validation du panier [{0}]", ex.Message))
            End Try
             ' si hay errores, se muestra la página de errores
            If erreurs.Count <> 0 Then
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable), CType(options("panier"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
                Exit Sub
            End If
             ' todo va bien: se muestra la lista de artículos con un mensaje de éxito
            context.Items("message") = "Votre panier a été validé"
            doListe()
        End Sub

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.