Skip to content

1. 第1部分

该文档的 PDF 版本可在此处获取 |HERE|。

文档中的示例已发布 |HERE|。

1.1. Introduction

本文目标

  • 编写一个三层Web应用程序 [interface utilisateur, métier, accès aux données]
  • 使用 Spring 配置应用程序 IOC
  • 通过更改三层中的任意一层实现来编写不同版本。

使用的工具

  • 开发使用 Visual Studio.net —— 详见附录第 3.1 节;
  • Cassini Web 服务器用于运行——参见附录第 3.2 节;
  • Nunit 用于单元测试——详见附录第 3.4 节;
  • Spring:用于Web应用程序各层的集成与配置——详见附录第3.3节;

按初级-中级-高级的难度等级划分,本文档属于 [intermédiaire-avancé] 级别。理解本文需要具备多种先决条件。其中部分内容可通过我撰写的其他文档获取。若涉及此类情况,我会予以注明。当然,这仅为建议,读者亦可使用自己偏好的文档。

本文延续了为 Java 编写的文档 [基于Struts、Spring和Java的三层架构与MVC架构] 的主线。我们将使用 VB.NET 构建一个用 Java 编写的三层 Web 应用程序 MVC。 本文旨在阐明:Java与.NET开发平台之间具有高度相似性,因此在其中一个领域掌握的技能可直接应用于另一个领域。

目前似乎尚无广受认可的 MVC ASP.NET 开发方案。以下方案采用了 [使用 ASP.NET 1.1 进行 Web 开发 (2004)] 文档中介绍的方法。 虽然该方法的优点在于采用了J2EE开发中普遍的概念,但必须将其视为其本来的样子,即c.a.d。它只是众多MVC开发方法中的一种。 一旦某种MVC开发方法在ASP.NET中获得广泛认可,就应采用该方法。目前正在开发的Spring .NET版本,很可能成为首个解决方案。

1.2. webarticles 应用程序

本文介绍了一个简化的电子商务Web应用程序的组成部分。该应用程序将允许Web用户:

  • 浏览来自数据库的商品列表
  • 将部分商品放入电子购物车
  • 确认购物车。此确认操作仅会更新数据库中已购商品的库存。

向用户展示的视图包括:

- “LISTE”视图,展示待售商品列表
- 视图 [INFOS],提供产品的详细信息:
  • 视图 [PANIER] 和 [PANIERVIDE],用于显示客户购物车中的内容
  • 视图 [ERREURS],用于报告应用程序中的任何错误

Image

1.3. 应用程序的总体架构

我们希望构建一个具有以下三层结构的应用程序:

  • 通过使用接口,使这三层相互独立
  • 各层之间的集成由Spring实现
  • 每层均使用独立的命名空间:web(UI层)、domain(业务层)和dao(数据访问层)。

该应用程序将遵循 MVC 架构(模型-视图-控制器)。若参照上述分层图,MVC 架构将按以下方式融入其中:

处理客户端请求的流程如下:

  1. 客户端向控制器发送请求。此处的控制器将是一个 .aspx 页面,它将承担特殊角色。该页面处理所有客户端请求,是应用程序的入口,即 MVC 架构中的 C 层。
  1. 控制器处理该请求。为此,它可能需要业务层的协助,即MVC架构中的M模型。
  2. 控制器从业务层收到响应。客户端的请求已处理完毕。这可能导致多种可能的响应。一个典型的例子是
    • 如果请求无法正确处理,则显示错误页面
    • 否则则返回确认页面
  3. 控制器选择要发送给客户端的响应(即视图)。该响应通常是一个包含动态元素的页面。控制器将这些元素提供给视图。
  4. 视图被发送给客户端。这就是MVC中的“V”。

1.4. 模型

这里我们研究的是 MVC 中的 M。该模型由以下元素组成:

  1. 业务类
  2. 数据访问类
  3. 数据库

1.4.1. 数据库

该数据库仅包含一个名为 ARTICLES 的表。该表是通过以下 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
);
/* 约束 */
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);
/* 主键 */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
唯一标识商品的主键
nom
商品名称
prix
其价格
stockactuel
当前库存
stockminimum
库存低于该数值时需下达补货订单

1.4.2. 模型的命名空间

此处提供的模型 M 包含两个命名空间:

  • istia.st.articles.dao:包含 [dao] 层的数据访问类
  • istia.st.articles.domain:包含 [domain] 层面的业务类

每个命名空间都将生成一个专属的“程序集”文件:

assembly
contenu
rôle
webarticles-dao
- [IArticlesDao]:访问 [dao] 层的接口。
这是 [domain] 层唯一可见的接口。它看不到其他接口。
- [Article]:定义商品的类
- [ArticlesDaoArrayList]:实现
[IArticlesDao] 接口的实现类,其中包含一个 [ArrayList]
数据访问层
- 完全位于
[dao]
Web 应用程序
webarticles-domain
- [IArticlesDomain]:访问 [domain] 层的接口。这是 Web 层唯一可见的接口,它看不到其他接口。
- [AchatsArticles]:一个实现 [IArticlesDomain] 的类
- [Achat]:表示客户购买记录的类
- [Panier]:表示客户所有购买记录的类
代表了
- 完全位于
[domain] 层中
三层Web应用程序

1.4.3. [dao]层

[dao]层包含以下组件:

  • [IArticlesDao]:访问 [dao] 层的接口

  • [Article]:定义文章的类

  • [ArticlesDaoArrayList]:通过类 [ArrayList] 实现接口 [IArticlesDao] 的类

[dao] 层中的 [Visual Studio] 项目结构如下:

Image

备注

  • 项目 [dao] 属于 [bibliothèque de classes] 类型
  • 类已被放置在以文件夹 [istia] 为根的树形结构中。它们均位于命名空间 [istia.st.articles.dao] 中。

1.4.3.1. 类 [Article]

定义文章的类如下:

Imports System

Namespace istia.st.articles.dao

    Public Class Article

         ' 私有字段
        Private _id As Integer
        Private _nom As String
        Private _prix As Double
        Private _stockactuel As Integer
        Private _stockminimum As Integer

         ' 商品ID
        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

         ' 商品名称
        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

         ' 商品价格
        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

         ' 商品当前库存
        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

         ' 商品最低库存
        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

         ' 默认制造商
        Public Sub New()
        End Sub

         ' 带属性的制造商
        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

         ' 商品识别方法
        Public Overrides Function ToString() As String
            Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
        End Function
    End Class
End Namespace

该类提供:

  1. 一个构造函数,用于设置文章的5项信息:[id, nom, prix, stockactuel, stockminimum]
  2. 用于读取和写入这5项信息的公共属性。
  3. 对商品中插入的数据进行验证。若数据有误,将抛出异常。
  4. 一个方法 toString,用于以字符串形式获取条目的值。这在应用程序调试中通常非常有用。

1.4.3.2. [IArticlesDao] 接口

接口 [IArticlesDao] 定义如下:

Imports System
Imports System.Collections

Namespace istia.st.articles.dao

    Public Interface IArticlesDao
         ' 所有商品列表
        Function getAllArticles() As IList
         ' 添加商品
        Function ajouteArticle(ByVal unArticle As Article) As Integer
         ' 删除商品
        Function supprimeArticle(ByVal idArticle As Integer) As Integer
         ' 修改商品
        Function modifieArticle(ByVal unArticle As Article) As Integer
         ' 搜索商品
        Function getArticleById(ByVal idArticle As Integer) As Article
         ' 删除所有商品
        Sub clearAllArticles()
         ' 更改商品库存
        Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
    End Interface
End Namespace

接口中各方法的作用如下:

getAllArticles
返回数据源中的所有文章
clearAllArticles
清空数据源
getArticleById
返回由主键标识的对象 [Article]
ajouteArticle
允许向数据源添加一条记录
modifieArticle
允许修改数据源中的商品
supprimerArticle
用于从数据源中删除商品
changerStockArticle
允许修改数据源中某商品的库存

该接口向客户端程序提供了一系列仅由其签名定义的方法。它不涉及这些方法的实际实现方式。这为应用程序带来了灵活性。客户端程序调用的是接口,而非其具体的实现。

具体实现的选择将通过 Spring 配置文件来完成。 为了说明在测试Web应用程序时,仅需关注数据访问接口而非其实现类,我们将首先通过一个简单的对象[ArrayList]来实现数据源。后续我们将介绍基于SGBD的解决方案。

1.4.3.3. 实现类 [ArticlesDaoArrayList]

实现类 [ArticlesDaoArrayList] 定义如下:

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

         ' 默认制造商
        Public Sub New()
             ' 正在编写几篇文章
            For i As Integer = 1 To nbArticles
                articles.Add(New Article(i, "article" + i.ToString, i * 10, i * 10, i * 10))
            Next
        End Sub

         ' 显示所有文章列表
        Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
             ' 返回文章列表
            SyncLock Me
                Return articles
            End SyncLock
        End Function

         ' 删除所有文章
        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
             ' 清空商品列表
            SyncLock Me
                articles.Clear()
            End SyncLock
        End Sub

         ' 根据键值获取某条记录
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
             ' 在列表中搜索商品
            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

         ' 将商品添加到商品列表
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
             ' 将商品添加到商品列表中
            SyncLock Me
                 ' 验证该商品是否已存在
                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
                 ' 添加该条目
                articles.Add(unArticle)
                 ' 返回结果
                Return 1
            End SyncLock
        End Function

         ' 修改商品
        Public Function modifieArticle(ByVal articleNouveau As Article) As Integer Implements IArticlesDao.modifieArticle
             ' 修改文章
            SyncLock Me
                 ' 检查是否存在
                Dim ipos As Integer = posArticle(articles, articleNouveau.id)
                 ' 若不存在
                If ipos = -1 Then Return 0
                 ' 若存在 - 则修改
                articles(ipos) = articleNouveau
                 ' 返回结果
                Return 1
            End SyncLock
        End Function

         ' 删除通过其键标识的商品
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
             ' 删除商品
            SyncLock Me
                 ' 检查是否存在
                Dim ipos As Integer = posArticle(articles, idArticle)
                 ' 若不存在
                If ipos = -1 Then Return 0
                 ' 若存在 - 则删除
                articles.RemoveAt(ipos)
                 ' 返回结果
                Return 1
            End SyncLock
        End Function

         ' 根据商品键修改商品库存
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
             ' 更改某商品的库存
            SyncLock Me
                 ' 检查是否存在
                Dim ipos As Integer = posArticle(articles, idArticle)
                 ' 若不存在
                If ipos = -1 Then Return 0
                 ' 若存在 - 且条件允许则修改其库存
                Dim unArticle As Article = CType(articles(ipos), Article)
                 ' 仅在库存充足时才修改
                If unArticle.stockactuel + mouvement >= 0 Then
                    unArticle.stockactuel += mouvement
                    Return 1
                Else
                    Return 0
                End If
            End SyncLock
        End Function

         ' 根据其键查找商品
        Private Function posArticle(ByVal listArticles As ArrayList, ByVal idArticle As Integer) As Integer
             ' 返回商品 [idArticle] 在列表中的位置,若未找到则返回 -1
            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
             ' 未找到
            Return -1
        End Function
    End Class

End Namespace

注释

  • 数据源由私有字段 [articles] 模拟,其类型为 [ArrayList]
  • 该类的构造函数默认在数据源中创建 4 条记录。
  • 所有数据访问方法均已进行同步,以避免数据源的并发访问问题。在任何给定时刻,仅有一个线程可访问某个特定方法。
  • 方法 [posArticle] 用于根据编号查询数据源 [ArrayList] 中某条记录的位置 [0..N]。 如果该文章不存在,该方法返回位置 -1。其他方法会反复调用此方法。
  • 方法 [ajouteArticle] 用于在商品列表中添加商品。它返回插入的商品数量:1。如果该商品已存在,则抛出异常。
  • 方法 [modifieArticle] 用于修改现有商品。该方法返回修改的商品数量:若商品存在则返回 1,否则返回 0。
  • 方法 [supprimeArticle] 用于删除现有商品。该方法返回被删除的商品数量:若商品存在则返回 1,否则返回 0。
  • 方法 [getAllArticles] 返回所有文章的列表
  • 方法 [getArticleById] 用于根据编号获取特定商品。若该商品不存在,则返回值 [nothing]。
  • 该代码本身并不复杂。我们留给读者自行阅读并理解。

1.4.3.4. 生成 [dao] 层程序集

Visual Studio 项目已配置为生成 [webarticles-dao.dll] 程序集。该程序集生成在项目的 [bin] 文件夹中:

1.4.3.5. [dao] 层的 NUnit 测试

在 Java 中,类通过 [Junit] 框架进行测试。在 .NET 中,Nunit 框架提供了相同的单元测试功能:

Image

测试 Visual Studio 项目的结构如下:

Image

注释

  • 项目 [tests] 属于 [bibliothèque de classes] 类型
  • 测试 [NUnit] 需要引用程序集 [nunit.framework.dll]
  • 测试类 [NUnit] 通过 Spring 获取待测对象的实例。因此,在
    • 在 [bin] 文件夹中,Spring 的类文件
    • 在 [References] 中,有一个对文件夹 [bin] 中组件 [Spring-Core.dll] 的引用
    • 在 [bin] 中,有一个 Spring 的配置文件
  • 测试类需要 [dao] 层中的 [webarticles-dao.dll] 程序集。该程序集已放置在 [bin] 文件夹中,并将其引用添加到项目引用中。

测试类 [NUnit] 需要访问 [NUnit.Framework] 命名空间中的类。因此,其中包含以下导入语句:

Imports NUnit.Framework

命名空间 [NUnit.Framework] 位于“程序集” [nunit.framework.dll] 中,需将其添加到项目引用中:

如果已安装 [Nunit],则 [nunit.framework.dll] 应出现在建议列表中。只需双击该程序集即可将其添加到项目中:

Image

[dao] 层中的测试类 [NUnit] 可能如下所示:

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
         ' 待测试对象
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
             ' 获取 Spring 对象生成器的实例
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
             ' 请求实例化 articlesdao 对象
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
        End Sub

        <Test()> _
        Public Sub testGetAllArticles()
             ' 目视检查
            listArticles()
        End Sub

        <Test()> _
        Public Sub testClearAllArticles()
             ' 删除所有文章
            articlesDao.clearAllArticles()
             ' 查询所有文章
            Dim articles As IList = articlesDao.getAllArticles
             ' 验证:数量应为 0
            Assert.AreEqual(0, articles.Count)
        End Sub

        <Test()> _
        Public Sub testAjouteArticle()
             ' 删除所有商品
            articlesDao.clearAllArticles()
             ' 验证:商品表应为空
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
             ' 添加两条商品
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
             ' 验证:应有两条商品
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
             ' 目视检查
            listArticles()
        End Sub

        <Test()> _
        Public Sub testSupprimeArticle()
             ' 删除所有商品
            articlesDao.clearAllArticles()
             ' 验证:商品表应为空
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
             ' 添加两件商品
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
             ' 验证:应有 2 条商品
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
             ' 删除第4个商品
            articlesDao.supprimeArticle(4)
             ' 验证:应剩余1个商品
            articles = articlesDao.getAllArticles
            Assert.AreEqual(1, articles.Count)
             ' 目视检查
            listArticles()
        End Sub

        <Test()> _
        Public Sub testModifieArticle()
             ' 删除所有项目
            articlesDao.clearAllArticles()
             ' 核对
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
             ' 添加2个项目
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
             ' 验证
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
             ' 搜索第3项
            Dim unArticle As Article = articlesDao.getArticleById(3)
             ' 验证
            Assert.AreEqual(unArticle.nom, "article3")
             ' 搜索第4项
            unArticle = articlesDao.getArticleById(4)
             ' 验证
            Assert.AreEqual(unArticle.nom, "article4")
             ' 修改第4条
            articlesDao.modifieArticle(New Article(4, "article4", 44, 44, 44))
             ' 核对
            unArticle = articlesDao.getArticleById(4)
            Assert.AreEqual(unArticle.prix, 44, 0.000001)
             ' 目视检查
            listArticles()
        End Sub

        <Test()> _
        Public Sub testGetArticleById()
             ' 删除条目
            articlesDao.clearAllArticles()
             ' 核查
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
             ' 新增2项
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
             ' 核对
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
             ' 第3项搜索
            Dim unArticle As Article = articlesDao.getArticleById(3)
             ' 验证
            Assert.AreEqual(unArticle.nom, "article3")
             ' 第4条搜索
            unArticle = articlesDao.getArticleById(4)
             ' 验证
            Assert.AreEqual(unArticle.nom, "article4")
        End Sub

         ' 屏幕列表
        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()
             ' 删除所有商品
            articlesDao.clearAllArticles()
             ' 商品搜索 1
            Dim article As article = articlesDao.getArticleById(1)
             ' 验证
            Assert.IsNull(article)
             ' 修改不存在的商品
            Dim i As Integer = articlesDao.modifieArticle(New article(1, "1", 1, 1, 1))
             ' 未修改任何行
            Assert.AreEqual(i, 0)
             ' 删除不存在的商品
            i = articlesDao.supprimeArticle(1)
             ' 未删除任何行
            Assert.AreEqual(0, i)
        End Sub

        <Test()> _
        Public Sub testChangerStockArticle()
             ' 删除所有条目
            articlesDao.clearAllArticles()
             ' 添加了一篇文章
            Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
            Assert.AreEqual(nbArticles, 1)
             ' 添加了一篇文章
            nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            Assert.AreEqual(nbArticles, 1)
             ' 创建了 100 个线程
            Dim taches(99) As Thread
            For i As Integer = 0 To taches.Length - 1
                 ' 创建主题 i
                taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
                 ' 设置线程名称
                taches(i).Name = "tache_" & i
                 ' 启动线程 i 的执行
                taches(i).Start()
            Next
             ' 等待所有线程结束
            For i As Integer = 0 To taches.Length - 1
                taches(i).Join()
            Next
             ' 验证 - 商品3的库存必须为1
            Dim unArticle As Article = articlesDao.getArticleById(3)
            Assert.AreEqual(unArticle.nom, "article3")
            Assert.AreEqual(1, unArticle.stockactuel)
             ' 将商品4的库存减1
            Dim erreur As Boolean = False
            Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
             ' 验证:其库存不应发生变化
            Assert.AreEqual(0, nbLignes)
             ' 目视检查
            listArticles()
        End Sub

        Public Sub décrémente()
             ' 启动线程
            System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
             ' 线程减少库存
            articlesDao.changerStockArticle(3, -1)
             ' 线程结束
            System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
        End Sub

    End Class
End Namespace

注释

  • 我们希望编写一个测试接口 [IArticlesDao] 的程序,且该程序应与该接口的实现类无关。因此,我们使用 Spring 来隐藏实现类的名称,使其对测试程序不可见。
  • 属性方法 <Setup()> 从 Spring 中获取待测试对象 [articlesdao] 的引用。该对象在以下文件 [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>

该文件指明了接口 [IArticlesDao] 的实现类 [istia.st.articles.dao.ArticlesDaoArrayList] 的名称,以及其所在位置 [ webarticles-dao.dll]。由于实例化不需要参数,因此此处未定义任何参数。

  • 大多数测试用例都易于理解。建议读者阅读相关注释。
  • 方法 [testChangerStockArticle] 需要稍作说明。它创建了 100 个线程,负责递减指定商品的库存。
        <Test()> _
        Public Sub testChangerStockArticle()
             ' 删除所有商品
            articlesDao.clearAllArticles()
             ' 添加商品
            Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
            Assert.AreEqual(nbArticles, 1)
             ' 添加商品
            nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            Assert.AreEqual(nbArticles, 1)
             ' 创建 100 个线程
            Dim taches(99) As Thread
            For i As Integer = 0 To taches.Length - 1
                 ' 创建主题 i
                taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
                 ' 设置线程名称
                taches(i).Name = "tache_" & i
                 ' 启动线程 i 的执行
                taches(i).Start()
            Next
             ' 等待所有线程结束
            For i As Integer = 0 To taches.Length - 1
                taches(i).Join()
            Next
             ' 验证 - 商品3的库存应为1
            Dim unArticle As Article = articlesDao.getArticleById(3)
            Assert.AreEqual(unArticle.nom, "article3")
            Assert.AreEqual(1, unArticle.stockactuel)
             ' 减少商品4的库存
            Dim erreur As Boolean = False
            Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
             ' 验证:其库存不应发生变化
            Assert.AreEqual(0, nbLignes)
             ' 目视检查
            listArticles()
        End Sub

此处旨在测试对数据源的并发访问。负责更新库存的方法如下:

        Public Sub décrémente()
             ' 启动线程
            System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
             ' 线程减少库存
            articlesDao.changerStockArticle(3, -1)
             ' 线程结束
            System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
        End Sub

它将第3号商品的库存减少1。如果参考方法代码[testChangerStockArticle],可以看到:

  • 第3号商品的库存初始值为101
  • 100 个线程将各自将该库存减少 1
  • 因此,在所有线程执行完毕后,库存应为1

此外,在该方法中,我们还尝试将第4号商品的库存设置为负值。此操作应会失败。

为了测试 [dao] 层,我们在 [tests] 项目的 [bin] 文件夹中生成 DLL 和 [tests-webarticles-dao.dll]:

然后,使用应用程序 [Nunit-Gui],我们加载该 DLL 并执行测试:

Image

在左侧窗口中,可以看到已测试的方法列表。每个方法名称前面的圆点颜色表示该方法的测试结果:成功(绿色)或失败(红色)。 在屏幕上查看本文档的读者将看到所有测试均已通过。接下来,我们将认为我们拥有一个可运行的 [dao] 层。

1.4.4. [domain]层

[domain]层包含以下组件:

  • [IArticlesDomain]:访问 [domain] 层的接口

  • [Achat]:定义采购的类

  • [Panier]:定义购物车的类

  • [AchatsArticles]:[IArticlesDomain] 接口的实现类

[domain] 层的 [Visual Studio] 解决方案结构如下:

Image

备注

  • 项目 [domain] 属于 [bibliothèque de classes] 类型
  • 类已放置在以文件夹 [istia] 为根的树形结构中。它们均位于命名空间 [istia.st.articles.domain] 中。
  • [dao]层中的DLL已放置在新项目的[bin]文件夹中。此外,DLL已作为引用添加到该项目中。

1.4.4.1. 接口 [IArticlesDomain]

接口 [IArticlesDomain] 将 [métier] 层与 [web] 层解耦。 后者通过该接口访问 [métier/domain] 层,而无需关心实际实现该接口的类。该接口定义了以下用于访问业务层的操作:

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

Namespace istia.st.articles.domain
    Public Interface IArticlesDomain
         ' 方法
        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
返回关联数据源的对象列表 [Article]
Function getArticleById(ByVal idArticle As Integer) As Article
返回由 [idArticle] 标识的对象 [Article]
acheter(ByVal panier As Panier)
通过将已购商品的库存减去购买数量来验证客户购物车——如果库存不足,可能会失败
ReadOnly Property erreurs() As ArrayList
返回发生的错误列表——若无错误则为空

1.4.4.2. 类 [Achat]

类 [Achat] 表示客户的购买:

Imports istia.st.articles.dao

Namespace istia.st.articles.domain

    Public Class Achat

         ' 私有字段
        Private _article As article
        Private _qte As Integer

         ' 默认构造函数
        Public Sub New()
        End Sub

         ' 带参数的构造函数
        Public Sub New(ByVal unArticle As article, ByVal qte As Integer)
             ' 通过属性访问
            Me.article = unArticle
            Me.qte = qte
        End Sub

         ' 已购商品
        Public Property article() As article
            Get
                Return _article
            End Get
            Set(ByVal Value As article)
                _article = Value
            End Set
        End Property

         ' 购买数量
        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

         ' 采购总额
        Public ReadOnly Property totalAchat() As Double
            Get
                Return _qte * _article.prix
            End Get
        End Property

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

End Namespace

注释

  • 类 [Achat] 具有以下属性和方法:
Public Property article() As article
已购买的商品
Public Property qte() As Integer
购买数量
Public ReadOnly Property totalAchat() As Double
购买金额
Public Overrides Function ToString() As String
对象标识符
  • 它有一个构造函数,用于初始化定义购买的属性 [article, qte]。

1.4.4.3. 类 [Panier]

类 [Panier] 表示客户的全部购买记录:

Namespace istia.st.articles.domain

Public Class Panier

     ' 私有字段
    Private _achats As New ArrayList
        Private _totalPanier As Double = 0

     ' 默认制造商
    Public Sub New()
    End Sub

     ' 购买清单
    Public ReadOnly Property achats() As ArrayList
        Get
            Return _achats
        End Get
    End Property

     ' 购物总额
    Public ReadOnly Property totalPanier() As Double
        Get
            Return _totalPanier
        End Get
    End Property

     ' 方法
    Public Sub ajouter(ByVal unAchat As Achat)
         ' 检查采购是否已存在
        Dim iAchat As Integer = posAchat(unAchat.article.id)
        If iAchat <> -1 Then
             ' 已找到
                Dim achatCourant As Achat = CType(_achats(iAchat), Achat)
            achatCourant.qte += unAchat.qte
        Else
             ' 未找到
            _achats.Add(unAchat)
        End If
         ' 增加购物车总额
        _totalPanier += unAchat.totalAchat
    End Sub

     ' 移除一件商品
    Public Sub enlever(ByVal idAchat As Integer)
         ' 正在搜索该商品
            Dim iachat As Integer = posAchat(idAchat)
         ' 若已找到,则移除
        If iachat <> -1 Then
                Dim achatCourant As Achat = CType(_achats(iachat), Achat)
             ' 从购物车中移除
            _achats.RemoveAt(iachat)
             ' 减少购物车总金额
            _totalPanier -= achatCourant.totalAchat
        End If
    End Sub

    Private Function posAchat(ByVal idArticle As Integer) As Integer
         ' 在购买列表中查找购买记录
         ' 返回其在列表中的位置,若未找到则返回-1
        Dim achatCourant As Achat
        Dim trouvé As Boolean = False
        Dim i As Integer = 0
            While Not trouvé AndAlso i < _achats.Count
                 ' 当前购买
                achatCourant = CType(_achats(i), Achat)
                 ' 与搜索商品进行比对
                If achatCourant.article.id = idArticle Then
                    Return i
                End If
                 '下一笔采购
                i += 1
            End While
             ' 未找到
            Return -1
    End Function

     ' 身份验证
    Public Overrides Function ToString() As String
        Return _achats.ToString
    End Function
End Class

End Namespace

注释

  • 类 [Panier] 具有以下属性和方法:
ReadOnly Property achats() As ArrayList
客户采购清单 - 类型为 [Achat] 的对象列表
ajouter(ByVal unAchat As Achat)
将一项购买添加到购物清单中
enlever(ByVal idAchat As Integer)
从商品列表中删除该商品 idAchat
ReadOnly Property totalPanier() As Double
购物车总金额
Function ToString() As String
返回购物车的标识符字符串
  • 方法 [posAchat] 是一个实用方法,用于根据所购商品的编号获取该商品在购物清单中的位置。 购物清单的管理机制确保同一商品即使多次购买,在列表中也仅占据一个位置。因此,可以通过所购商品的编号来识别该笔购买。如果未找到所查询的购买记录,方法 [posAchat] 将返回 -1。
  • 方法 [ajouter] 将新的采购记录添加到采购清单中。这意味着:如果所购商品尚未存在于清单中,则向清单添加新条目;如果已存在,则增加其采购数量。
  • 方法 [enlever] 用于从采购清单中移除一个由编号标识的采购条目。如果该采购条目不存在,该方法将不返回任何信息且不执行任何操作。
  • [totalPanier] 方法会随着购物条目的增删,持续更新购物总额。

1.4.4.4. 类 [AchatsArticles]

接口 [IArticlesDomain] 将由以下类 [AchatsArticles] 实现:

Imports istia.st.articles.dao

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

         '私有字段
        Private _articlesDao As IArticlesDao
        Private _erreurs As ArrayList

         ' 构造函数
        Public Sub New(ByVal articlesDao As IArticlesDao)
            _articlesDao = articlesDao
        End Sub

         ' 错误列表
        Public ReadOnly Property erreurs() As ArrayList Implements IArticlesDomain.erreurs
            Get
                Return _erreurs
            End Get
        End Property

         ' 文章列表
        Public Function getAllArticles() As IList Implements IArticlesDomain.getAllArticles
             ' 所有商品列表
            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

         ' 按编号获取某条目
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDomain.getArticleById
             ' 特定商品
            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


         ' 购买购物车
        Public Sub acheter(ByVal panier As Panier) Implements IArticlesDomain.acheter
             ' 购买购物车 - 所购商品的库存需相应减少
            _erreurs = New ArrayList
            Dim achat As achat
            Dim achats As ArrayList = panier.achats
            For i As Integer = achats.Count - 1 To 0 Step -1
                 ' 减少商品 i 的库存
                achat = CType(achats(i), achat)
                Try
                    If _articlesDao.changerStockArticle(achat.article.id, -achat.qte) = 0 Then
                         ' 操作失败
                        _erreurs.Add("L'achat " + achat.ToString + " n'a pu se faire - Vérifiez les stocks")
                    Else
                         ' 操作成功 - 从购物车中移除该商品
                        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

注释

  • 该类实现了接口 [IArticlesDomain] 的四个方法。它有两个私有字段:
_articlesDao As IArticlesDao
数据访问对象
_erreurs As ArrayList
可能出现的错误列表。可通过公共属性 [erreurs] 访问
  • 来构建该类的实例,必须提供用于访问数据的对象:
Sub New(ByVal articlesDao As IArticlesDao)
  • 方法 [getAllArticles] 和 [getArticleById] 基于 [dao] 层中同名的方法
  • 方法 [acheter] 用于验证购物车的购买操作。该验证仅需对已购商品的库存进行减量处理。 只有当商品库存充足时,才能进行购买。若库存不足,购买将被拒绝:商品仍保留在购物车中,并在列表 [erreurs] 中报告错误。已确认的购买将从购物车中移除,且相应商品的库存将减少购买数量。

1.4.4.5. 生成 [domain] 层程序集

Visual Studio 项目已配置为生成 [webarticles-domain.dll] 程序集。该程序集生成在项目的 [bin] 文件夹中:

1.4.4.6. NUnit 层面的测试(基于 [domain] 层)

测试 Visual Studio 项目的结构如下:

Image

备注

  • 项目 [tests] 属于 [bibliothèque de classes] 类型
  • [NUnit] 测试需要引用 [nunit.framework.dll] 程序集
  • 测试类 [NUnit] 通过 Spring 获取待测对象的实例。因此,在
  • 在 [bin] 文件夹中,Spring 的类文件
  • 在 [References] 中,有一个对文件夹 [bin] 内 [Spring-Core.dll] 组件的引用
  • 在 [bin] 中,有一个 Spring 的配置文件
  • 该测试类需要 [dao] 层中的 [webarticles-dao.dll] 程序集以及 [domain] 层中的 [webarticles-domain.dll] 程序集。 这些组件已放置在 [bin] 文件夹中,并将其引用添加到了项目引用中。

来自 [domain] 层的测试类 NUnit 可能如下所示:

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

         ' 待测试对象
        Private articlesDomain As IArticlesDomain
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
             ' 获取 Spring 对象生成器的实例
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
             ' 请求实例化 articles dao 对象
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
             ' 随后实例化商品领域对象
            articlesDomain = CType(factory.GetObject("articlesdomain"), IArticlesDomain)
        End Sub

        <Test()> _
        Public Sub getAllArticles()
             ' 目视检查
            listArticles()
        End Sub

        <Test()> _
        Public Sub getArticleById()
             ' 删除商品
            articlesDao.clearAllArticles()
             ' 验证
            Dim articles As IList = articlesDomain.getAllArticles
            Assert.AreEqual(0, articles.Count)
             ' 添加2个商品
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
             ' 核对
            articles = articlesDomain.getAllArticles
            Assert.AreEqual(2, articles.Count)
             ' 第3条目搜索
            Dim unArticle As Article = articlesDomain.getArticleById(3)
             ' 验证
            Assert.AreEqual(unArticle.nom, "article3")
             ' 搜索第4条
            unArticle = articlesDao.getArticleById(4)
             ' 验证
            Assert.AreEqual(unArticle.nom, "article4")
        End Sub

        <Test()> _
        Public Sub acheterPanier()
             ' 删除条目
            articlesDao.clearAllArticles()
             ' 验证
            Dim articles As IList = articlesDomain.getAllArticles
            Assert.AreEqual(0, articles.Count)
             ' 添加2个条目
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
             ' 验证
            articles = articlesDomain.getAllArticles
            Assert.AreEqual(2, articles.Count)
             ' 创建包含两件商品的购物车
            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))
             ' 验证
            Assert.AreEqual(700, panier.totalPanier, 0.000001)
            Assert.AreEqual(2, panier.achats.Count)
             ' 购物车确认
            articlesDomain.acheter(panier)
             ' 验证
            Assert.AreEqual(0, articlesDomain.erreurs.Count)
            Assert.AreEqual(0, panier.achats.Count)
             ' 商品搜索 3
            Dim unArticle As Article = articlesDomain.getArticleById(3)
             ' 验证
            Assert.AreEqual(unArticle.stockactuel, 20)
             ' 商品搜索 4
            unArticle = articlesDao.getArticleById(4)
             ' 验证
            Assert.AreEqual(unArticle.stockactuel, 30)
             ' 新建购物车
            panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 100))
             ' 确认购物车
            articlesDomain.acheter(panier)
             ' 验证
            Assert.AreEqual(1, articlesDomain.erreurs.Count)
             ' 商品搜索 3
            unArticle = articlesDomain.getArticleById(3)
             ' 验证
            Assert.AreEqual(unArticle.stockactuel, 20)
        End Sub

        <Test()> _
        Public Sub testRetirerAchats()
             ' 删除 ARTICLES 的内容
            articlesDao.clearAllArticles()
             ' 读取表 ARTICLES
            Dim articles As IList = articlesDao.getAllArticles()
            Assert.AreEqual(0, articles.Count)
             ' 插入
            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)
             ' 读取表 ARTICLES
            articles = articlesDomain.getAllArticles()
            Assert.AreEqual(2, articles.Count)
             ' 创建包含两件商品的购物车
            Dim monPanier As New Panier
            monPanier.ajouter(New Achat(article3, 10))
            monPanier.ajouter(New Achat(article4, 10))
             ' 验证
            Assert.AreEqual(700.0, monPanier.totalPanier, 0.000001)
            Assert.AreEqual(2, monPanier.achats.Count)
             ' 添加已购买的商品
            monPanier.ajouter(New Achat(article3, 10))
             ' 验证
             ' 总金额应调整为1000
            Assert.AreEqual(1000.0, monPanier.totalPanier, 0.000001)
             ' 购物车中仍有 2 件商品
            Assert.AreEqual(2, monPanier.achats.Count)
             ' 商品3的数量应调整为20
            Dim unAchat As Achat = CType(monPanier.achats(0), Achat)
            Assert.AreEqual(20, unAchat.qte)
             ' 从购物车中移除商品3
            monPanier.enlever(3)
             ' 验证
             ' 总金额应调整为400
            Assert.AreEqual(400.0, monPanier.totalPanier, 0.000001)
             ' 购物车中仅剩1件商品
            Assert.AreEqual(1, monPanier.achats.Count)
             ' 这应该是第4件商品
            Assert.AreEqual(4, CType(monPanier.achats(0), Achat).article.id)
        End Sub

         ' 屏幕列表
        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

注释

  • 我们希望编写一个用于测试接口 [IArticlesDomain] 的测试程序,且该程序应与该接口的实现类无关。因此,我们使用 Spring 来隐藏实现类的名称,使其对测试程序不可见。
  • 属性方法 <Setup()> 从 Spring 中获取待测试对象 [articlesdomain] 和 [articlesdao] 的引用。这些对象在以下 [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>

该文件显示:

  • (续)
    • 对于单例 [articlesdao],其实现类名为 [istia.st.articles.dao.ArticlesDaoArrayList],且位于 [ webarticles-dao.dll]。由于实例化无需参数,此处未定义任何参数。
    • 对于单例 [articlesdomain],其实现类名为 [istia.st.articles.domain.AchatsArticles],且位于 [ webarticles-domain.dll]。 类 [AchatsArticles] 有一个单参数构造函数:管理对 [dao] 层访问的单例。在此,该单例被定义为之前定义的单例 [articlesdao]。
  • 测试类获取了待测类 [articlesdomain] 的实例,以及数据访问类 [articlesdao] 的实例。最后这一点存在争议。 理论上,测试类本不应访问 [dao] 层,因为它甚至不应该知道该层的存在。在此,我们打破了这一“规范”——若要遵守该规范,我们本需在 [IArticlesDomain] 接口中创建新的方法。

为了测试 [domain] 层,我们在 [tests] 项目的 [bin] 文件夹中生成 DLL 和 [tests-webarticles-domain.dll]:

然后,使用应用程序 [Nunit-Gui],我们加载该 DLL 并执行测试:

Image

在屏幕上查看此文档的读者将看到所有测试均已通过。接下来,我们将认为已有一个可运行的 [domain] 层。

1.4.5. 结论

需要提醒的是,我们计划构建以下三层Web应用程序:

我们应用程序的 M 模型 MVC 现已编写并经过测试。它由两个文件 DLL 和 [webarticles-dao.dll, webarticles-domain.dll] 提供。 我们可以转到最后一层,即包含控制器 C 和视图 V 的 [web] 层。首先,我们将探讨 [Développement WEB avec ASP.NET 1.1 ] 文档中介绍的一种方法

  • 控制器 C 由两个文件 [global.asax, main.aspx] 实现
  • 视图 V 由 aspx 页面实现

1.5. [web]层

该 Web 应用程序的架构 MVC 将如下所示:

M=modèle
业务类 [domain]、数据访问类 [dao] 以及数据源
V=vues
页面 ASPX
C=contrôleur
所有来自客户端的请求 HTTP 均通过以下两个控制器:
global.asax:处理与应用程序初始启动相关的事件
main.aspx:单独处理每个客户端的请求

1.5.1. 视图

这些视图与文档开头介绍的视图一致:

LISTE
liste.aspx
这些视图汇总在应用程序的 [vues] 文件夹中
INFOS
infos.aspx
PANIER
panier.aspx
PANIERVIDE
paniervide.aspx
ERREURS
erreurs.aspx

1.5.2. 控制器

如前所述,控制器将由两个部分组成:

  1. [global.asax,global.asax.vb]:主要用于初始化应用程序,并将所有需在不同客户端之间共享的数据放入应用程序上下文中
  2. [main.aspx, main.aspx.vb]:真正的控制器,负责处理来自客户端的 HTTP 请求。

来自客户端的各种请求将发送至控制器 [main.aspx],并包含一个名为 [action] 的参数,该参数指定了客户端请求的操作:

请求
含义
控制器操作
可能的响应
action=liste
客户想要
商品列表
- 向业务层请求商品列表
业务层
- [LISTE]
- [ERREURS]
action=infos
客户要求
关于视图中显示的
视图中显示的
[LISTE]
- 向业务层请求该商品
- [INFOS]
- [ERREURS]
action=achat
客户购买商品
- 向业务层请求商品
并将商品加入客户购物车
- [INFOS](若数量出错)
- [LISTE] 若无数量错误
action=retirerachat
客户希望删除
商品
- 从会话中获取购物车
并进行修改
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=panier
客户希望查看其
购物车
- 从会话中获取购物车
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=validationpanier
客户已完成购物
并进入支付阶段
- 在数据库中更新
已购买商品的库存
- 将客户购物车中
- 清空客户购物车中
- [LISTE]
- [ERREURS]

1.5.3. 应用程序配置

我们将努力配置应用程序,使其在面对以下变更时尽可能灵活:

  1. 不同视图中 URL 的变更
  2. 实现 [IArticlesDao] 和 [IArticlesDomain] 接口的类发生变更
  3. SGBD、数据库及商品表的变更

1.5.3.1. URL的变更

视图的名称 URL 将与其他一些参数一起放入应用程序配置文件 [web.config] 中:

<?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. 接口实现类的变更

根据三层架构的理念,各层之间应相互隔离。这种隔离通过以下方式实现:

  • 各层之间通过接口而非具体类进行通信
  • 某一层级的代码绝不会直接实例化另一层级的类来使用它。它只需向外部工具(此处为 Spring)请求所需使用层级接口的实现实例。为此,我们知道它无需知道实现类的名称,只需知道其希望获取引用所对应的 Spring 单例的名称即可。

在我们的应用程序中,Spring将在Web应用程序的[web.config]文件中按以下方式进行配置:

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

为了访问 [métier] 层,[web] 层中的某个类可以请求单例 [articlesDomain]。 Spring 随后将实例化一个 [istia.st.articles.domain.AchatsArticles] 类型的对象。为此,它需要一个 [articlesDao] 类型的对象,即一个 [istia.st.articles.dao.ArticlesDaoArrayList] 类型的对象。 Spring 随后将实例化该对象。操作完成后,请求了单例 [articlesDomain] 的 [web] 层便拥有了连接其与数据源的完整链:

1.5.3.3. 与 SGBD 或数据库相关的变更

此处将忽略该点,因为我们当前处于一个不包含 SGBD 的测试应用程序中。我们将在后续阶段讨论基于 SGBD 实现 [dao] 层的方法。

1.5.4. <asp:> 标签库

考虑显示错误列表的 [ERREURS] 视图:

Image

视图 [ERREURS] 负责显示错误列表,该列表由控制器 [main.aspx] 以名称 [context.Items("erreurs")] 放置在请求上下文中。编写此类页面有多种方法。 本文仅关注错误显示部分。

需要提醒的是,ASPX页面包含一个呈现部分(HTML)和一个代码部分(NET),后者负责准备呈现部分需要显示的数据。 这两个部分可以位于同一个文件 [aspx] 中(方案 WebMatrix),也可以分别位于两个文件中:[aspx] 用于呈现,[aspx.vb] 用于代码。后者是 Visual Studio 采用的方案。 更复杂的是,HTML 布局部分本身也可能包含 .NET 代码,这往往会模糊 [contrôleur] 和 [présentation] 视图的分离。 通常强烈不建议采用这种方案。为了从 [présentation] 部分移除所有代码,需要创建标签库。这些标签库通过类似 HTML 标签的外观来“隐藏”代码。 针对 [ERREURS] 页面,我们提供两种可能的解决方案。

我们的第一种方案是在页面的 [présentation] 部分使用 .NET 代码。ASPX 页面在其控制器部分 [erreurs.aspx.vb] 中获取请求中的错误列表:

        Protected erreurs As ArrayList

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
             ' 检索错误
            erreurs = CType(context.Items("erreurs"), ArrayList)
        End Sub

然后将其显示在 [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>

第二种解决方案使用ASP.NET的<asp:>标签库中的<asp:repeater>标签。 如果通过图形化方式构建页面 ASPX,该标签将以服务器组件的形式呈现,可直接拖放到设计表单中。如果手动编写代码 ASPX,则可将其视为标签库。

借助 <asp:> 标签库,前一个视图 [ERREURS] 的代码 ASPX 将变为如下形式:

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

标签

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

用于在数据源的不同元素上重复一个模式 HTML。其各个元素如下:

HeaderTemplate
在显示数据源元素之前需显示的样式 HTML
ItemTemplate
在数据源的每个元素上重复显示的图案 HTML。表达式 [<%# Container.DataItem %>] 用于显示数据源当前元素的值
FooterTemplate
在显示完数据源中的所有元素后,显示模式 HTML

数据源通常在页面的 [contrôleur] 部分与标签相关联:

        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
..
             ' 将错误关联到 rptErreurs
            With rptErreurs
                .DataSource = context.Items("erreurs")
                .DataBind()
            End With
        End Sub

如果数据源已知(例如现有数据库),也可以在页面设计阶段建立这种关联。

在视图中,我们将使用另一个标签:<asp:datagrid>,它允许以表格形式显示数据源。

1.5.5. [webarticles] 应用程序的 Visual Studio 解决方案结构

Web 应用程序就像一个由众多元素组成的拼图。采用 MVC 架构通常会增加这些元素的数量。[webarticles] 应用程序在 [Visual Studio] 下的结构如下:

  

备注

  • 项目 [web] 的类型为 [bibliothèque de classes],而非按逻辑预期应有的 [Application web ASP.NET]。 类型 [Application web ASP.NET] 要求在开发机或远程机器上存在 Web 服务器 IIS。 IIS 服务器在 Windows XP 家庭版系统中默认并不存在。然而,许多 PC 系统正是搭载此版本销售的。 为了让使用 Windows XP 的读者能够实现本教程中的应用程序,我们将使用微软免费提供的 Cassini Web 服务器(参见附录),并将项目 [Application web ASP.NET] 替换为项目 [bibliothèque de classes]。 这会带来一些不便,具体说明详见附录。
  • 该应用程序使用的DLL文件如下:
webarticles-dao.dll
包含数据访问层的类
webarticles-domain.dll
包含业务层的类
Spring.Core.dll
包含 Spring 类,用于集成 Web、Domain 和 DAO 层
log4net.dll
日志类——由Spring使用

这些 DLL 文件被放置在 [bin] 文件夹中,并添加到项目引用中。

1.5.6. 视图 ASPX

如前所述,我们将在 ASPX 视图中使用 <asp:> 标签库。

1.5.6.1. 用户组件 [entete.ascx]

为了使不同视图保持一致性,它们将共享同一个页眉,该页眉显示应用程序名称及菜单:

该菜单是动态的,由控制器确定。控制器会在发送给页面 ASPX 的请求中,添加一个名为“actions”的键属性,其关联值为一个 Hashtable() 类型的元素数组。该数组中的每个元素都是一个字典,用于生成页眉菜单中的一个选项。 每个字典包含两个键:

  • href:与菜单选项关联的URL
  • lien:菜单的文本

我们将页眉转换为用户控件。用户控件将页面的一部分(包括布局和相关代码)封装成一个组件,以便在其他页面中重复使用。在此,我们希望在应用程序的其他视图中复用 [entete] 组件。 布局代码将位于 [entete.ascx] 中,相关控件代码则位于 [entete.ascx.vb] 中。布局代码将使用 <asp:repeater> 组件来显示菜单选项列表:

type
nom
rôle
1
中继器
rptMenu
数据源:一个包含两个键的字典数组
包含两个键:href、链接
显示菜单选项

页面呈现代码如下:

<%@ 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("链接") %>
                            </a>
                        </td>
                    </ItemTemplate>
                </asp:Repeater>
            </tr>
        </table>
        <hr>

注释

  • 组件 [repeater] 在第 6-14 行中定义
  • 与重复器关联的数据源中的每个元素都是一个包含两个键的字典:href(第9行)和lien(第10行)

相关的控制代码如下:

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())
                ' 将操作表与其组件关联
                With rptMenu
                    .DataSource = Value
                    .DataBind()
                End With
            End Set
        End Property
    End Class
End Namespace

注释

  • 类型为 [EnteteWebArticles] 的组件具有一个只读的公共属性 [actions] - 第 7 行
  • 该属性用于将名为 [rptMenu] 的 <asp:repeater> 组件(第 10 行)与应用程序控制器计算出的选项数组(第 11-12 行)相关联。

应用程序的其他视图将使用由 [entete.ascx] 定义的页眉。例如,页面 [erreurs.aspx] 将通过以下代码包含该页眉:

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

注释

  • 第 1 行声明 <WA:entete> 标签应与由文件 [entete.ascx] 定义的组件相关联。属性 [TagPrefix] 和 [TagName] 可自由设置。
  • 完成上述设置后,第9行将该组件插入到页面的呈现代码中。在运行时,该标签将使包含它的页面ASPX的代码中,包含页面[entete.ascx]的代码。 控制代码 [erreurs.aspx.vb] 将负责初始化该组件。其实现方式如下:
    Public Class ErreursWebarticles
        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
...
             ' 将菜单选项与 rptmenu 关联
            entete.actions = CType(context.Items("options"), Hashtable())
...
        End Sub
    End Class

注释

  • 第 6 行创建了一个类型为 [EnteteWebArticles] 的对象,该类型即为所创建组件的类型
  • 第 11 行初始化了该对象的 [actions] 属性

1.5.6.2. 视图 [liste.aspx]

1.5.6.2.1. Introduction

该视图显示了可供销售的商品列表:

该视图在执行 /main?action=liste/main?action=validationpanier 请求后显示。控制器请求的参数如下:

actions
Hashtable() 对象 - 菜单选项数组
listarticles
ArrayList 类型为 [Article] 的对象
message
String 对象 - 页面底部显示的消息

文章表 HTML 中的每个链接 [Infos] 都有一个 URL,其形式为 [?action=infos&id=ID],其中 ID 是所显示文章的 id 字段。

1.5.6.2.2. 页面组件
type
nom
rôle
1
用户组件
页眉
显示页眉
2
DataGrid
DataGridArticles
3 - 关联列:表头:姓名,字段:姓名
4 - 关联列:标题:价格,字段:价格
5 - 超链接列:文本:信息,URL 字段:id,
格式 URL:/webarticles/main.aspx?action=infos&id={0}
显示在售商品
6
标签
lblMessage
显示消息

回顾一下如何设置这些属性:

  • 在 Visual Studio 中,选择 [DataGrid] 以访问其属性表:

Image

  • 使用上方的链接 [Mise en forme automatique] 来管理显示表格的样式
  • 并使用链接 [Générateur de propriétés] 来管理其内容
1.5.6.2.3. 展示代码 [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>

注释

  • 第 9 行定义了页面的页眉
  • 第 12-24 行定义了 [DataGrid] 的特征
  • 第26行定义了标签[lblMessage]
1.5.6.2.4. 控制器代码 [liste.aspx.vb]
Imports System
Imports System.Collections
Imports System.Data
Imports istia.st.articles.dao

Namespace istia.st.articles.web

     ' 管理商品列表显示页面
    Public Class ListeWebarticles
        Inherits System.Web.UI.Page

         ' 页面组件
        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
             ' 根据上下文信息准备视图 [liste]
             ' 将菜单选项与 rptmenu 关联
            entete.actions = CType(context.Items("options"), Hashtable())
             ' 从数据表中获取文章
            Dim articles As ArrayList = CType(context.Items("articles"), ArrayList)
             ' 将其与页面上的组件 [DataGrid] 关联
            With DataGridArticles
                .DataSource = articles
                .DataBind()
            End With
             ' 显示消息
            lblMessage.Text = context.Items("message").ToString
        End Sub
    End Class
End Namespace

注释

  • 页面组件出现在第13-15行。需要注意的是,我们需要创建一个带有操作符[new]的对象[EnteteWebArticles],而对于其他组件则无需如此。 若未显式创建该对象,将出现运行错误,提示对象 [entete] 未引用任何内容。这一问题值得深入探究,但目前尚未进行深入分析。
  • 页眉菜单的选项数组是从上下文中获取的,用于初始化页面中的组件 [entete] —— 第 20 行
  • 从上下文中获取商品列表 - 第 22 行
  • 用于初始化组件 [DataGridArticles] - 第 24-27 行
  • 组件 [lblMessage] 通过上下文中的消息进行初始化 - 第 29 行

1.5.6.3. 视图 [infos.aspx]

1.5.6.3.1. Introduction

该视图显示商品信息,并支持购买操作:

Image

当购买数量有误时,该视图会在执行 /main?action=infos&id=ID/main?action=achat&id=ID 请求后显示。控制器请求的参数如下:

actions
Hashtable() 对象 - 菜单选项数组
article
类型为 [Article] 的对象 - 要显示的商品
msg
String 对象 - 数量出错时显示的提示信息
qte
字符串对象 - 在输入字段中显示的值[Qte]

当数量输入出错时,将使用字段 [msg] 和 [qte]:

Image

该页面包含一个表单,该表单通过按钮 [Acheter] 提交。 POST 的目标 URL 是 [?action=achat&id=ID],其中 ID 是所购商品的 ID。

1.5.6.3.2. 页面组件
type
nom
rôle
1
用户组件
页眉
显示页眉
2
字面量
litID
显示商品编号
3 à 6
DataGrid
DataGridArticle
3 - 关联列:表头:姓名,字段:姓名
4 - 关联列:标题:价格,字段:价格
5 - 关联列:标题:当前库存,字段:stockActuel
6 - 关联列:标题:最低库存,字段:stockMinimum
显示商品
7
HTML 提交
 
提交表单
8
HTML 输入
runat=server
txtQte
输入购买数量
9
标签
lblMsgQte
可能出现的错误信息
1.5.6.3.3. 展示代码 [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>

备注

  • 页眉已包含在页面中 - 第 9 行
  • 字符串 [litId] 定义在第 10 行
  • DataGrid 和 [DataGridArticles] 在第 12-34 行定义
  • 表单在第36-46行定义。其类型为POST。
  • POST 的目标由变量 [strAction] 提供——第 36 行。该变量需由控制器定义。
  • 购买数量的输入字段定义在第41行。这是一个服务器端组件(runat=server),类型为HTML。在代码端,可通过一个对象访问该字段。
  • 第 42 行定义了标签 [lblMsgQte],该标签将用于显示有关输入数量的错误信息(如有)
1.5.6.3.4. 验证代码 [infos.aspx.vb]
Imports istia.st.articles.dao
Imports System
Imports System.Collections

Namespace istia.st.articles.web

     ' 管理商品信息页面
    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

         ' 表单将提交至 URL
        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

         ' 显示页面
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
             ' 获取请求信息
            Dim unArticle As Article = CType(Session.Item("article"), Article)
             ' 将菜单选项与 rptmenu 关联
            entete.actions = CType(context.Items("options"), Hashtable())
             ' 将文章关联到 [DataGrid]
            Dim articles As New ArrayList
            articles.Add(unArticle)
            With DataGridArticle
                .DataSource = articles
                .DataBind()
            End With
             ' 标签 ID
            litId.Text = unArticle.id.ToString
             ' 错误消息
            lblMsgQte.Text = context.Items("msg").ToString
             ' 上一个数量
            txtQte.Value = context.Items("qte").ToString
             ' 操作
            strAction = "?action=achat&id=" + unArticle.id.ToString
        End Sub
End Namespace

注释

  • 页面组件在第10-14行中定义
  • 该类定义了一个公共属性 [strAction],用于指定表单中 POST 的目标——第 17-25 行
  • 要显示的文章是从应用程序上下文中获取的——第30行
  • 页眉菜单的选项数组从上下文中获取,用于初始化页面的 [entete] 组件——第 32 行
  • 第 33-39 行,[DataGridArticle] 组件与类型为 [ArrayList] 的数据源相关联,该数据源仅包含第 30 行获取的商品
  • [lblMsgQte, txtQte] 组件使用上下文中的信息进行初始化 - 第 42-45 行
  • 属性 [straction] 同样使用上下文中的信息进行初始化——第 47 行。该变量用于生成页面中表单 HTML 的属性 [action]:
        <form method="post" action="<%=strAction%>">
....
        </form>

1.5.6.4. 视图 [panier.aspx]

1.5.6.4.1. Introduction

此视图显示购物车内容:

Image

该页面是在执行 /main?action=panier/main?action=retirerachat&id=ID 请求后显示的。控制器请求的参数如下:

actions
Hashtable() 对象——菜单选项数组
panier
类型为 [Panier] 的对象——待显示的购物车

购物车购买列表 HTML 中的每个链接 [Retirer] 都包含一个 URL,其形式为 [?action=retirerachat&id=ID],其中 ID 是 [id]字段,该字段对应要从购物车中移除的商品。

1.5.6.4.2. 页面组件
type
nom
rôle
1
用户组件
页眉
显示页眉
2
DataGrid
DataGridAchats
3- 关联列 - 表头:商品,字段:名称
4 - 关联列 - 表头:数量,字段:数量
5 - 关联列 - 列头:价格,字段:价格
6 - 关联列 - 标题:总计,字段:总计,格式 {0:C}
7 - 超链接列 - 文本:退货,URL:id,URL格式:/webarticles/main.aspx?action=retirerachat&id={0}
显示已购商品列表
8
标签
lblTotal
显示应付金额
1.5.6.4.3. 展示代码 [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>

备注

  • 第 9 行包含标题
  • 第 12-27 行,定义了组件 [DataGridAchats]
  • 第 29 行,定义了组件 [lblTotal]
1.5.6.4.4. 控制代码 [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
     ' 管理购物车显示页面
    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
             ' 将菜单选项与rptmenu关联
            entete.actions = CType(context.Items("options"), Hashtable())
             ' 获取购物车
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
             ' 将购买项转移到购买行数组中
            Dim achats(unPanier.achats.Count - 1) As LigneAchat
             ' 更改 ArrayList 中的元素类型
            For i As Integer = 0 To achats.Length - 1
                achats(i) = New LigneAchat(CType(unPanier.achats(i), Achat))
            Next
             ' 将数据与页面组件 [DataGrid] 关联
            With DataGridAchats
                .DataSource = achats
                .DataBind()
            End With
             ' 显示应付总额
            lblTotal.Text = unPanier.totalPanier.ToString
        End Sub

         ' 基于采购对象生成的采购行
        Private Class LigneAchat
            Inherits Achat

             ' 生成器接收一个采购
            Public Sub New(ByVal unAchat As Achat)
                Me.article = unAchat.article
                Me.qte = unAchat.qte
            End Sub

             ' id:返回所购商品的ID
            Public ReadOnly Property id() As Integer
                Get
                    Return article.id
                End Get
            End Property

             ' 名称:所购商品的名称
            Public ReadOnly Property nom() As String
                Get
                    Return article.nom
                End Get
            End Property

             ' 所购商品的价格
            Public ReadOnly Property prix() As Double
                Get
                    Return article.prix
                End Get
            End Property

        End Class
    End Class
End Namespace

注释

  • 页面组件在第 11-13 行中声明
  • [entete]组件的初始化与之前分析过的页面中的初始化方式相同——第17行
  • 待显示的购物车数据从会话中获取——第19行
  • 使用组件 [DataGridAchats] 显示该购物车时遇到问题。困难源于该组件的初始化。回顾该组件的列:
    • 列 [Article] 关联数据源中的字段 [nom]
    • 列 [Qté] 关联数据源中的字段 [qte]
    • 列 [Prix] 关联到数据源中的字段 [prix]
    • 列 [Total] 关联于数据源中的字段 [total]

我们拥有的数据源是购物车及其购物清单。后者将成为 [DataGrid] 的数据源。 只有那些将填充 [DataGrid] 行数据的 [Achat] 对象,不具备 [DataGrid] 所期望的 [nom, qte, prix, total] 属性。 因此,我们在此专门为 [DataGrid] 创建了一个数据源,其元素具有 [DataGrid] 所期望的特征。 这些元素将属于 [LigneAchat] 类型,这是一个为此专门创建的类,并继承自 [Achat] 类(第 36-66 行)

  • 定义类 [LigneAchat] 后,[DataGridAchats] 的数据源基于会话中找到的购物车构建 - 第 20-30 行
  • 通过类 [Panier] 的属性 [totalPanier] 显示购物金额 - 第 32 行

1.5.6.5. 视图 [paniervide.aspx]

1.5.6.5.1. Introduction

该视图显示了购物车为空的信息:

Image

该视图在执行 /main?action=panier/main?action=retirerachat&id=ID 请求后显示。控制器请求的参数如下:

actions
Hashtable() 对象 - 菜单选项数组
1.5.6.5.2. 页面组件
type
nom
rôle
1
用户组件
页眉
显示页眉
1.5.6.5.3. 呈现代码 [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>

注释

  • 页眉包含在第 9 行
1.5.6.5.4. 控制代码 [paniervide.aspx.vb]
Namespace istia.st.articles.web
     ' 管理空购物车显示页面
    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
             ' 根据上下文信息准备视图 [paniervide]
             ' 将菜单选项与 rptmenu 关联
            entete.actions = CType(context.Items("options"), Hashtable())
        End Sub
    End Class
End Namespace

注释

  • 仅初始化页面中唯一的动态组件 - 第10行

1.5.6.6. 视图 [erreurs.aspx]

1.5.6.6.1. Introduction

出现错误时将显示此视图:

Image

该视图在任何导致错误的请求后显示,但数量错误的购买操作除外,该情况由视图 [INFOS] 处理。控制器请求的元素如下:

actions
Hashtable() 对象 - 菜单选项数组
erreurs
ArrayList 对象,其中 [String] 表示要显示的错误消息
1.5.6.6.2. 页面组件
type
nom
rôle
1
用户组件
页眉
显示页眉
2
重复器
rptErreurs
显示错误列表
1.5.6.6.3. 呈现代码 [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>

注释

  • 标题在第 9 行定义
  • 组件 [rptErreurs] 定义在第 13-19 行。其内容来自数据源,该数据源的类型为 [ArrayList],包含 [String] 对象。
1.5.6.6.4. 控制代码 [erreurs.aspx.vb]
Namespace istia.st.articles.web

     ' 管理错误页面
    Public Class ErreursWebarticles
        Inherits System.Web.UI.Page

         ' 页面组件
        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
             ' 根据上下文信息准备视图 [erreurs]
             ' 将菜单选项与 rptmenu 关联
            entete.actions = CType(context.Items("options"), Hashtable())
             ' 将错误关联到 rptErreurs
            With rptErreurs
                .DataSource = context.Items("erreurs")
                .DataBind()
            End With
        End Sub
    End Class

End Namespace

注释

  • [entete]组件按惯例初始化,见第9行和第14行
  • 组件 [rptErreurs] 使用在上下文中找到的 [ArrayList] 类型的错误列表进行初始化——第 16-19 行

1.5.7. 控制器 global.asax、main.aspx

接下来需要编写我们 Web 应用程序的核心——控制器。其作用在于:

  • 获取客户端的请求,
  • 借助业务类处理客户端请求的操作,
  • 返回相应的视图。

1.5.7.1. 控制器 [global.asax.vb]

当应用程序接收到首个请求时,将执行文件 [global.asax.vb] 中的过程 [Application_Start]。这仅会发生一次。 [Application_Start] 过程旨在初始化 Web 应用程序所需的对象,这些对象将作为只读资源由所有客户端线程共享。这些共享对象可放置在两个位置:

  • 控制器中的私有字段
  • 应用程序的运行上下文(Application)

应用程序 [global.asax.vb] 中的方法 [Application_Start] 将执行以下操作:

  • 检查文件 [web.config] 中是否包含应用程序正常运行所需的参数。这些参数已在第 1.5.3 节中描述。
  • 将可能出现的错误列表以 [ArrayList erreurs] 对象的形式放入应用程序上下文中。若无错误,该列表将为空,但仍会存在。
  • 若发生错误,[Application_Start]方法即在此终止。否则,它将请求一个类型为[IArticlesDomain]的单例引用,该对象将成为控制器用于处理业务需求的业务对象。 如 1.5.3.2 节所述,控制器将向 Spring 框架请求该单例。此实例化操作可能引发各种错误。若发生错误,这些错误将再次被记录在应用程序上下文中的 [erreurs] 对象中。

控制器 [global.asax.vb] 包含一个名为 [Session_Start] 的过程,该过程会在每次有新客户访问时执行。在此过程中,将为该客户创建一个空购物车。该购物车将在该特定客户的后续请求中持续保留。相关代码可能如下所示:

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

         ' 初始化应用程序
        Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)

             ' 本地数据
            Dim parameters() As String = {"urlMain", "urlErreurs", "urlInfos", "urlListe", "urlPanier", "urlPanierVide"}
            Dim erreurs As New ArrayList

             ' 获取应用程序的初始化参数
            Dim param As String
            For i As Integer = 0 To parameters.Length - 1
                 ' 从配置文件中读取
                param = ConfigurationSettings.AppSettings(parameters(i))
                If param Is Nothing Then
                     ' 记录错误
                    erreurs.Add("Paramètre [" + parameters(i) + "] absent dans le fichier [web.config]")
                Else
                     ' 将参数存储在应用程序中
                    Application.Item(parameters(i)) = param
                End If
            Next
             ' 有错误吗?
            If erreurs.Count = 0 Then
                 ' 创建访问业务层的对象 IArticlesDomain
                Dim contexte As IApplicationContext = CType(ConfigurationSettings.GetConfig("spring/context"), IApplicationContext)
                Dim articlesDomain As IArticlesDomain
                Try
                    articlesDomain = CType(contexte.GetObject("articlesDomain"), IArticlesDomain)
                     ' 将对象存储在应用程序中
                    Application.Item("articlesDomain") = articlesDomain
                Catch ex As Exception
                     ' 将错误保存
                    erreurs.Add("Erreur lors de la construction de l'objet d'accès à la couche métier [" + ex.ToString + "]")
                End Try
            End If
             ' 将错误信息写入应用程序
            Application.Item("erreurs") = erreurs
             ' 如果出现错误,则结束
            If erreurs.Count <> 0 Then Return
             ' 构建菜单选项数组
            Dim options As New Hashtable
             ' 从控制器中获取 URL
            Dim urlMain As String = CType(Application.Item("urlMain"), String)
            Dim uneOption As Hashtable
             ' 商品列表
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=liste")
            uneOption.Add("lien", "Liste des articles")
            options.Add("liste", uneOption)
             ' 购物车
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=panier")
            uneOption.Add("lien", "Voir le panier")
            options.Add("panier", uneOption)
             ' 确认购物车
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=validationpanier")
            uneOption.Add("lien", "Valider le panier")
            options.Add("validationpanier", uneOption)
             ' 将菜单选项添加到应用程序中
            Application.Item("options") = options
            Return
        End Sub

         ' 初始化会话
        Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
             ' 为客户创建购物车
            Session.Item("panier") = New Panier
        End Sub
    End Class
End Namespace

注释

  • [web.config] 中所需的参数在数组中定义——第 18 行
  • 在 [web.config] 中检索这些参数。若存在,则将其存储在应用程序上下文中;否则,会在 [erreurs] 的错误列表中记录一条错误(第 21-33 行)
  • 若无错误,则向 Spring 请求单例 [articlesDomain] 的引用,该单例负责管理对应用程序 [domain] 层的访问 - 第 35-47 行。 可能出现的错误将记录在 [erreurs] 中。
  • 错误记录在应用程序上下文中——第49行
  • 若发生错误则退出该过程——第 51 行
  • 创建一个包含三个字典的数组。每个字典都有两个键:href 和 lien。该数组表示三种可能的菜单选项——第 52-71 行
  • 该数组存储在应用程序上下文中 - 第 73 行
  • 每次有新客户时,都会执行 [Session_Start] 过程。在此过程中,会在客户的会话中创建一个空购物车 - 第 78-81 行

1.5.7.2. 控制器 [main.aspx.vb]

控制器 [main.aspx.vb] 会处理所有来自客户的请求。实际上,这些请求都采用 [/webarticles/main.aspx?action=XX] 的格式。请求将按以下方式处理:

  • 将验证应用程序上下文中的 [erreurs] 对象。若该对象不为空,则表示应用程序初始化时发生错误,导致无法运行。此时将返回视图 [ERREURS] 作为响应。
  • 将获取并验证请求中的参数 [action]。如果该参数不对应已知的操作,则发送视图 [ERREURS] 并附上相应的错误信息。
  • 如果参数 [action] 有效,则将客户端的请求传递给该操作的特定处理程序进行处理:
方法
请求
处理
可能的响应
doListe
GET /main?action=liste
- 查询
业务类
- 显示
[LISTE] 或 [ERREURS]
doInfos
GET /main?action=infos&id=ID
- 向
业务类
- 显示
[INFOS] 或 [ERREURS]
doAchat
POST /main?action=achat&id=ID
- 购买数量属于提交的参数之一
- 向
业务类中
- 将其加入购物车,并
客户会话中
[LISTE] 或 [INFOS] 或 [ERREURS]
doRetirerAchat
GET /main?action=退货&id=ID
- 从
购物车中的
客户会话
[PANIER]
doPanier
GET /main?action=panier
- 显示
客户会话
[PANIER] 或 [PANIERVIDE]
doValidationPanier
GET /main?action=validationpanier
- 在数据库中减少
库存
库存
客户会话
[LISTE] 或 [ERREURS]

控制器 [main.aspx.vb] 的骨架代码可能如下:

Imports System.Collections
Imports System
Imports System.Data

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

Namespace istia.st.articles.web

     ' Web 应用程序控制器类
    Public Class MainWebArticles
        Inherits System.Web.UI.Page

         ' 私有字段
        Private articlesDomain As IArticlesDomain
        Private options As Hashtable

         ' 加载页面
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
....
        End Sub

         ' 操作处理方法

         ' 文章列表
        Public Sub doListe()
...
        End Sub

         ' 商品详情
        Public Sub doInfos()
...
        End Sub

         ' 购买商品
        Public Sub doAchat()
...
        End Sub

         ' 取消购买
        Public Sub doRetirerAchat()
...
        End Sub

         ' 查看购物车
        Public Sub doPanier()
...
        End Sub

         ' 结账
        Public Sub doValidationPanier()
...
        End Sub

    End Class

End Namespace

注释

  • 该类有两个私有字段,将在方法之间共享——第 15-16 行:
    • articlesDomain:访问 [domain] 层的单例
    • options:菜单选项字典数组
  • 过程 [Page_Load]:
    • 将初始化该类的两个私有字段
    • 从请求中获取参数 [action],并执行该操作的处理方法。

1.5.7.3. 方法 [Page_Load]

这是页面上发生的第一个事件。代码如下:

         ' 加载页面
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
             ' 检查应用程序是否已正确启动
            Dim erreurs As ArrayList = CType(Application.Item("erreurs"), ArrayList)
             ' 如有错误,则发送视图 [erreurs]
            If erreurs.Count <> 0 Then
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {}
                Server.Transfer(CType(Application("urlErreurs"), String))
            End If
             ' 获取业务类访问对象
            articlesDomain = CType(Application.Item("articlesDomain"), IArticlesDomain)
             ' 以及菜单选项
            options = CType(Application.Item("options"), Hashtable)
             ' 获取待执行的操作
            Dim action As String = Request.QueryString("action")
            If action Is Nothing Then
                action = "liste"
            End If
             ' 执行该操作
            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

注释

  • 每次加载页面时,需确保由 [global.asax] 执行的应用程序初始化已顺利完成。
  • 为此,从应用程序上下文中获取由 [global.asax] 放置的错误列表(第 4 行)
  • 如果该列表不为空,则显示视图 [ERREURS] - 第 6-10 行
  • 从应用程序上下文中获取由 [global.asax] 放置的单例 [articlesDomain],并将其存储在私有字段 [articlesDomain] 中,以便该类中的各种方法都能使用它 - 第 12 行
  • 对菜单选项数组进行类似操作 - 第 14 行
  • 从请求中获取参数 [action] —— 第 16 行
  • 执行与请求操作对应的方法。未预设的操作被视为操作 [liste] - 第 16-36 行

1.5.7.4. 处理操作 [liste]

此操作用于显示物料清单:

Image

代码如下:

         ' 商品列表
        Public Sub doListe()
             ' 错误处理
            Dim erreurs As New ArrayList
             ' 请求商品列表
            Dim articles As IList
            Try
                articles = articlesDomain.getAllArticles
            Catch ex As Exception
                 ' 记录错误
                erreurs.Add(ex.ToString)
            End Try
             ' 有错误吗?
            If erreurs.Count <> 0 Then
                 ' 显示视图 [erreurs]
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
            End If
             ' 显示视图 [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

备注

  • 将可能出现的错误记录在 [ArrayList] 中 - 第 4 行
  • 向单例 [articlesDomain] 请求文章列表 - 第 5-12 行
  • 若出现错误,则发送视图 [ERREURS] - 第 13-19 行
  • 否则发送视图 [LISTE] - 第 20-24 行

1.5.7.5. 处理操作 [infos]

客户查询了某件商品的信息:

Image

代码如下:

         ' 商品信息
        Public Sub doInfos()
             ' 错误处理
            Dim erreurs As New ArrayList
             ' 获取所请求文章的ID
            Dim strId As String = Request.QueryString("id")
             ' 有结果吗?
            If strId Is Nothing Then
                 ' 异常情况 - 返回错误页面
                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
             ' 是否为整数?
            Dim id As Integer
            Try
                id = Integer.Parse(strId)
            Catch ex As Exception
                 ' 异常 - 发送错误页面
                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
             ' 请求键 id 的条目
            Dim unArticle As Article
            Try
                unArticle = articlesDomain.getArticleById(id)
            Catch ex As Exception
                 ' 数据访问问题
                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
             ' 是否已获取到商品?
            If unArticle Is Nothing Then
                 ' 该条目不存在
                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
             ' 已获取该条目 - 将其放入当前会话
            Session.Item("article") = unArticle
             ' 正在准备显示
            context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
            context.Items("msg") = ""
            context.Items("qte") = ""
            Server.Transfer(CType(Application.Item("urlInfos"), String))
        End Sub

备注

  • 将可能出现的错误放入 [ArrayList] - 第 4 行
  • 从请求中获取所请求文章的标识符 - 第6行
  • 验证该标识符。它必须存在且必须为整数。若不符合条件,则发送视图 [ERREURS] 并附带相应的错误消息 - 第 7-27 行
  • 验证ID后,向单例[articlesDomain]查询该商品。若发生异常,则返回视图[ERREURS] - 第29-39行
  • 如果未找到该商品,则发送视图 [ERREURS] - 第 41-48 行
  • 如果找到了该条目,它将被放入用户的会话中,然后显示在视图 [INFOS] 的第 50-56 行

1.5.7.6. 处理操作 [achat]

客户购买了显示在视图 [INFOS] 中的商品。

Image

代码如下:

         ' 购买商品
        Public Sub doAchat()
             ' 购买商品
             ' 从当前会话中检索商品
            Dim unArticle As Article = CType(Session.Item("article"), Article)
             ' 是否有商品?
            If unArticle Is Nothing Then
                 ' 不正常 - 显示商品列表
                doListe()
            End If
             ' 获取已发布的数量
            Dim strQte As String = Request.Form("txtQte")
             ' 有结果吗?
            If strQte Is Nothing Then
                 ' 不正常——正在发送商品列表
                doListe()
            End If
             ' 是否为整数?
            Dim qte As Integer
            Try
                qte = Integer.Parse(strQte)
                If (qte <= 0) Then Throw New Exception
            Catch ex As Exception
                 ' 异常 - 发送包含错误信息的详情页
                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
             ' 一切正常 - 将商品加入客户购物车
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            unPanier.ajouter(New Achat(unArticle, qte))
             ' 显示商品列表
            doListe()
        End Sub

备注

  • 从会话中检索商品 - 第 5 行
  • 如果该文章不存在(会话可能已过期),则显示视图 [LISTE] - 第 7-10 行
  • 从查询中获取购买数量 - 第12行
  • 验证其有效性 - 第13-29行
  • 若无效,根据具体情况,发送视图 [LISTE] - 第 16 行 或视图 [INFOS] - 第 24-28 行
  • 若一切正常,将商品添加至购物车 - 第31-32行
  • 随后发送视图 [LISTE] - 第 34 行

1.5.7.7. 处理操作 [panier]

客户进行了多项购买,并要求查看购物车:

Image

代码如下:

         ' 查看购物车
        Public Sub doPanier()
             '从会话中获取购物车
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
             ' 购物车为空?
            If unPanier.achats.Count = 0 Then
                 ' 显示空购物车
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlPanierVide"), String))
            End If
             ' 显示非空购物车
            context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable), CType(options("validationpanier"), Hashtable)}
            Server.Transfer(CType(Application.Item("urlPanier"), String))
        End Sub

注释

  • 在第4行从会话中获取购物车。此处未验证是否成功获取数据。应进行此验证,因为会话可能已过期。
  • 如果购物车为空,则发送视图 [PANIERVIDE] - 第 6-10 行
  • 否则,发送视图 [PANIER] - 第 11-14 行

1.5.7.8. 处理操作 [retirerachat]

客户希望从购物车中移除一件商品:

Image

代码如下:

         ' 删除一件商品
        Public Sub doRetirerAchat()
             '从会话中获取购物车
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
             ' 移除商品
            Try
                 ' 获取已移除商品的ID
                Dim idArticle As Integer = Integer.Parse(Request.QueryString("id"))
                 ' 将其从购物车中移除
                unPanier.enlever(idArticle)
            Catch ex As Exception
                 ' 显示商品列表
                doListe()
            End Try
             ' 显示购物车
            doPanier()
        End Sub

注释

  • 在第4行从会话中获取购物车。此处未验证是否成功获取数据。应进行此验证,因为会话可能已过期。
  • 从请求中获取待移除商品的ID [id] - 第8行。
  • 将对应的购买项从购物车中移除——第10行
  • 此处未验证已购商品ID的有效性。若ID类型无效,将触发异常并在第11-13行进行处理。若ID有效但不存在,则第10行的[panier.enlever]方法不执行任何操作。
  • 显示新的购物车 - 第16行

1.5.7.9. 处理操作 [validerpanier]

客户希望确认购物车:

Image

代码如下:

         ' 购物车结算
        Public Sub doValidationPanier()
             ' 初始状态,无错误
            Dim erreurs As ArrayList
             '从会话中获取购物车
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
             ' 尝试提交购物车
            Try
                articlesDomain.acheter(unPanier)
                 ' 记录可能的购买错误
                erreurs = articlesDomain.erreurs
            Catch ex As Exception
                 ' 记录错误
                erreurs = New ArrayList
                erreurs.Add(String.Format("Erreur lors de la validation du panier [{0}]", ex.Message))
            End Try
             ' 若存在错误,则跳转至错误页面
            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
             ' 一切正常 - 显示商品列表并显示成功提示
            context.Items("message") = "Votre panier a été validé"
            doListe()
        End Sub

注释

  • 在第6行从会话中获取购物车。此处未验证是否成功获取数据。应进行此验证,因为会话可能已过期。
  • 若会话已过期,将获得指向购物车的指针 [nothing],此时第 9 行中的方法 [acheter] 将抛出异常,并返回视图 [ERREURS]。 但该错误信息对用户而言可能不够直观。
  • 第 8-16 行,尝试验证从会话中获取的购物车。如果所请求的数量超过了所选商品的库存,某些购买可能无法通过验证。这些情况由方法 [acheter] 记录在错误列表中,该列表在第 11 行被获取。
  • 若存在错误,则发送视图 [ERREURS] —— 第 18-23 行
  • 否则将发送视图 [LISTE] — 第 25-26 行

1.6. Conclusion

我们在此开发了一个基于 MVC 模型的应用程序。目前(2005年4月)似乎尚不存在像 Java 领域(Struts、Spring、 ……)。[Spring.net]项目预计将很快推出此类框架。在此之前,前述方法可为中等规模的应用程序提供可行的MVC开发方案。