Skip to content

1. 第 1 部分

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

文档中的示例可在此处查看 |HERE|。

1.1. 引言

本文的目标

  • 编写一个三层Web应用程序 [用户界面、业务逻辑、数据访问]
  • 使用 Spring IOC 配置该应用程序
  • 通过修改这三个层中一个或多个层的实现,编写不同版本的应用程序。

使用的工具

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

按初级-中级-高级的难度等级划分,本文档属于[中级-高级]类别。理解本文需要具备多种先决条件。其中部分内容可在笔者撰写的其他文档中找到。遇到此类情况,我会进行引用。不言而喻,这仅为建议,读者可自由选用自己偏好的资源。

本文档的结构与针对 Java 编写的文档 [基于 Struts、Spring 和 Java 的三层架构与 MVC 架构] 相同。我们将使用 VB.NET 构建一个用 Java 编写的的三层 MVC Web 应用程序。我们在此想强调的是,Java 和 .NET 开发平台具有高度的相似性,因此在其中一个领域掌握的技能可以应用到另一个领域。

目前似乎尚无广受认可的 ASP.NET MVC 开发方案。 下文介绍的解决方案采用了文档[Web Development with ASP.NET 1.1]中提出的方法。虽然该方法具有运用J2EE开发中常见概念的优势,但仍应将其视为众多MVC开发方法中的一种。一旦ASP.NET中出现被广泛接受的MVC开发方法,就应予以采用。目前正在开发的.NET版Spring很可能成为首个解决方案。

1.2. webarticles 应用程序

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

  • 查看数据库中的商品列表
  • 将部分商品加入在线购物车
  • 确认购物车。此确认操作将仅更新数据库中已购商品的库存记录。

向用户展示的不同视图如下:

- “列表”视图,用于显示待售商品列表
- [INFO] 视图,提供有关产品的更多信息:
  • [购物车] 和 [清空购物车] 视图,用于显示客户购物车中的商品
  • [ERRORS] 视图,用于报告任何应用程序错误

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
);
/* constraints */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);
/* primary key */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
用于唯一标识商品的主键
名称
项目名称
价格
其价格
当前库存
当前库存
最低库存
库存水平低于该值时必须下达补货订单

1.4.2. 该模型的命名空间

此处以两个命名空间的形式提供了模型 M:

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

每个命名空间都将生成在各自的“assembly”文件中:

assembly
内容
role
webarticles-dao
- [IArticlesDao]:用于访问 [dao] 层的接口。
这是 [domain] 层可见的唯一接口。它看不到其他接口。
- [Article]:定义文章的类
- [ArticlesDaoArrayList]:
[IArticlesDao] 接口的实现类,使用 [ArrayList]
数据访问层
- 完全位于
Web 应用程序的三层架构中的
Web 应用程序
webarticles-domain
- [IArticlesDomain]:用于访问 [domain] 层的接口。这是 Web 层唯一可见的接口,它看不到其他接口。
- [AchatsArticles]:一个实现 [IArticlesDomain] 的类
- [Purchase]:表示客户购买行为的类
- [Cart]:表示客户总购买量的类
代表 Web 购买的模型
Web - 完全位于
[domain] 层
Web 应用程序的三层架构

1.4.3. [DAO]层

[DAO] 层包含以下元素:

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

  • [Article]:定义文章的类

  • [ArticlesDaoArrayList]:使用 [ArrayList] 类实现 [IArticlesDao] 接口的类

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

Image

注释

  • [dao] 项目属于 [类库] 类型
  • 这些类已按树形结构放置在以 [istia] 文件夹为根目录的目录中。它们均位于 [istia.st.articles.dao] 命名空间下。

1.4.3.1. [Article] 类

定义文章的类如下:

Imports System

Namespace istia.st.articles.dao

    Public Class Article

        ' private fields
        Private _id As Integer
        Private _nom As String
        Private _prix As Double
        Private _stockactuel As Integer
        Private _stockminimum As Integer

        ' id article
        Public Property id() As Integer
            Get
                Return _id
            End Get
            Set(ByVal Value As Integer)
                If Value <= 0 Then
                    Throw New Exception("Le champ id [" + Value.ToString + "] est invalide")
                End If
                Me._id = Value
            End Set
        End Property

        ' item name
        Public Property nom() As String
            Get
                Return _nom
            End Get
            Set(ByVal Value As String)
                If Value Is Nothing OrElse Value.Trim.Equals("") Then
                    Throw New Exception("Le champ nom [" + Value + "] est invalide")
                End If
                Me._nom = Value
            End Set
        End Property

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

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

        ' minimum stock item
        Public Property stockminimum() As Integer
            Get
                Return _stockminimum
            End Get
            Set(ByVal Value As Integer)
                If Value < 0 Then
                    Throw New Exception("Le champ stockMinimum [" + Value.ToString + "] est invalide")
                End If
                Me._stockminimum = Value
            End Set
        End Property

        ' default builder
        Public Sub New()
        End Sub

        ' builder with properties
        Public Sub New(ByVal id As Integer, ByVal nom As String, ByVal prix As Double, ByVal stockactuel As Integer, ByVal stockminimum As Integer)
            Me.id = id
            Me.nom = nom
            Me.prix = prix
            Me.stockactuel = stockactuel
            Me.stockminimum = stockminimum
        End Sub

        ' article identification method
        Public Overrides Function ToString() As String
            Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
        End Function
    End Class
End Namespace

该类提供:

  1. 一个用于设置项目 5 项信息的构造函数:[id, name, price, currentStock, minimumStock]
  2. 用于读写这5项信息的公共属性。
  3. 对商品输入数据的验证。若数据无效,将抛出异常。
  4. 一个 toString 方法,用于将项的值转换为字符串。这在调试应用程序时通常非常有用。

1.4.3.2. [IArticlesDao] 接口

[IArticlesDao] 接口定义如下:

Imports System
Imports System.Collections

Namespace istia.st.articles.dao

    Public Interface IArticlesDao
        ' list of all items
        Function getAllArticles() As IList
        ' add an article
        Function ajouteArticle(ByVal unArticle As Article) As Integer
        ' deletes an article
        Function supprimeArticle(ByVal idArticle As Integer) As Integer
        ' modify an article
        Function modifieArticle(ByVal unArticle As Article) As Integer
        ' search for an article
        Function getArticleById(ByVal idArticle As Integer) As Article
        ' deletes all articles
        Sub clearAllArticles()
        ' changes the stock of an item
        Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
    End Interface
End Namespace

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

getAllArticles
返回数据源中的所有项目
clearAllArticles
清空数据源
getArticleById
返回由主键标识的 [Article] 对象
addArticle
允许您向数据源添加一篇文章
modifyArticle
允许您修改数据源中的文章
deleteItem
允许您从数据源中删除一项
updateItemStock
允许您修改数据源中某项的库存

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

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

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

        ' default builder
        Public Sub New()
            ' we build a few items
            For i As Integer = 1 To nbArticles
                articles.Add(New Article(i, "article" + i.ToString, i * 10, i * 10, i * 10))
            Next
        End Sub

        ' list of all items
        Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
            ' returns the list of items
            SyncLock Me
                Return articles
            End SyncLock
        End Function

        ' delete all items
        Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
            ' empty the list of items
            SyncLock Me
                articles.Clear()
            End SyncLock
        End Sub

        ' obtain an item identified by its key
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
            ' search for the item in the list
            SyncLock Me
                Dim ipos As Integer = posArticle(articles, idArticle)
                If ipos <> -1 Then
                    Return CType(articles(ipos), Article)
                Else
                    Return Nothing
                End If
            End SyncLock
        End Function

        ' add an item to the list of items
        Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
            ' add the item to the list of items
            SyncLock Me
                ' we check that it doesn't already exist
                Dim ipos As Integer = posArticle(articles, unArticle.id)
                If ipos <> -1 Then
                    Throw New Exception("L'article d'id [" + unArticle.id.ToString + "] existe déjà")
                End If
                ' we add the article
                articles.Add(unArticle)
                ' we return the result
                Return 1
            End SyncLock
        End Function

        ' modify an article
        Public Function modifieArticle(ByVal articleNouveau As Article) As Integer Implements IArticlesDao.modifieArticle
            ' modify an article
            SyncLock Me
                ' we check that
                Dim ipos As Integer = posArticle(articles, articleNouveau.id)
                ' if it doesn't exist
                If ipos = -1 Then Return 0
                ' it exists - we modify it
                articles(ipos) = articleNouveau
                ' we return the result
                Return 1
            End SyncLock
        End Function

        ' delete an item identified by its key
        Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
            ' article deletion
            SyncLock Me
                ' we check that
                Dim ipos As Integer = posArticle(articles, idArticle)
                ' if it doesn't exist
                If ipos = -1 Then Return 0
                ' it exists - we remove it
                articles.RemoveAt(ipos)
                ' we return the result
                Return 1
            End SyncLock
        End Function

        ' change the stock of an item identified by its key
        Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
            ' change the stock of an item
            SyncLock Me
                ' we check that
                Dim ipos As Integer = posArticle(articles, idArticle)
                ' if it doesn't exist
                If ipos = -1 Then Return 0
                ' it exists - you modify your stock if you can
                Dim unArticle As Article = CType(articles(ipos), Article)
                ' only change stock if it is sufficient
                If unArticle.stockactuel + mouvement >= 0 Then
                    unArticle.stockactuel += mouvement
                    Return 1
                Else
                    Return 0
                End If
            End SyncLock
        End Function

        ' search for an item identified by its key
        Private Function posArticle(ByVal listArticles As ArrayList, ByVal idArticle As Integer) As Integer
            ' returns the position of item [idArticle] in the list or -1 if not found
            Dim unArticle As Article
            For i As Integer = 0 To listArticles.Count - 1
                unArticle = CType(listArticles(i), Article)
                If unArticle.id = idArticle Then
                    Return i
                End If
            Next
            ' not found
            Return -1
        End Function
    End Class

End Namespace

评论

  • 数据源由类型为 [ArrayList] 的私有字段 [articles] 模拟
  • 该类的构造函数默认会在数据源中创建 4 个项目。
  • 所有数据访问方法均已进行同步,以防止数据源的并发访问问题。在任何给定时刻,仅有一个线程可以访问某个特定方法。
  • [posArticle] 方法返回由编号标识的文章在 [ArrayList] 数据源中的位置 [0..N]。如果文章不存在,该方法返回位置 -1。其他方法会反复调用此方法。
  • [addArticle] 方法将一篇文章添加到文章列表中。它返回插入的文章数量:1。如果该文章已存在,则抛出异常。
  • [modifyItem] 方法允许修改现有项目。它返回被修改的项目数:若该项目存在则返回 1,否则返回 0。
  • [deleteArticle] 方法用于删除现有文章。它返回被删除的文章数量:若文章存在则返回 1,否则返回 0。
  • [getAllArticles] 方法返回所有文章的列表
  • [getArticleById] 方法根据 ID 检索一篇文章。如果文章不存在,则返回值 [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] 项目是一个 [类库]
  • [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
        ' the test object
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
            ' retrieve an instance of the Spring object manufacturer
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
            ' request instantiation of articlesdao object
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
        End Sub

        <Test()> _
        Public Sub testGetAllArticles()
            ' visual check
            listArticles()
        End Sub

        <Test()> _
        Public Sub testClearAllArticles()
            ' delete all articles
            articlesDao.clearAllArticles()
            ' all articles are requested
            Dim articles As IList = articlesDao.getAllArticles
            ' verification: there must be 0
            Assert.AreEqual(0, articles.Count)
        End Sub

        <Test()> _
        Public Sub testAjouteArticle()
            ' delete all items
            articlesDao.clearAllArticles()
            ' check: the item table must be empty
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' we add two items
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' check: there must be two items
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' visual check
            listArticles()
        End Sub

        <Test()> _
        Public Sub testSupprimeArticle()
            ' delete all items
            articlesDao.clearAllArticles()
            ' check: the item table must be empty
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' we add two items
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' check: there must be 2 items
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' we delete article 4
            articlesDao.supprimeArticle(4)
            ' check: there must be 1 item left
            articles = articlesDao.getAllArticles
            Assert.AreEqual(1, articles.Count)
            ' visual check
            listArticles()
        End Sub

        <Test()> _
        Public Sub testModifieArticle()
            ' delete all items
            articlesDao.clearAllArticles()
            ' check
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' 2 items added
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' check
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' article 3 search
            Dim unArticle As Article = articlesDao.getArticleById(3)
            ' check
            Assert.AreEqual(unArticle.nom, "article3")
            ' research article 4
            unArticle = articlesDao.getArticleById(4)
            ' check
            Assert.AreEqual(unArticle.nom, "article4")
            ' modification article 4
            articlesDao.modifieArticle(New Article(4, "article4", 44, 44, 44))
            ' check
            unArticle = articlesDao.getArticleById(4)
            Assert.AreEqual(unArticle.prix, 44, 0.000001)
            ' visual check
            listArticles()
        End Sub

        <Test()> _
        Public Sub testGetArticleById()
            ' article deletion
            articlesDao.clearAllArticles()
            ' check
            Dim articles As IList = articlesDao.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' 2 items added
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' check
            articles = articlesDao.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' research article 3
            Dim unArticle As Article = articlesDao.getArticleById(3)
            ' check
            Assert.AreEqual(unArticle.nom, "article3")
            ' research article 4
            unArticle = articlesDao.getArticleById(4)
            ' check
            Assert.AreEqual(unArticle.nom, "article4")
        End Sub

        ' screen listing
        Private Sub listArticles()
            Dim articles As IList = articlesDao.getAllArticles
            For i As Integer = 0 To articles.Count - 1
                Console.WriteLine(CType(articles(i), Article).ToString)
            Next
        End Sub

        <Test()> _
        Public Sub testArticleAbsent()
            ' delete all items
            articlesDao.clearAllArticles()
            ' research article 1
            Dim article As article = articlesDao.getArticleById(1)
            ' check
            Assert.IsNull(article)
            ' modification of a non-existent item
            Dim i As Integer = articlesDao.modifieArticle(New article(1, "1", 1, 1, 1))
            ' had to modify no line
            Assert.AreEqual(i, 0)
            ' deletion of non-existent item
            i = articlesDao.supprimeArticle(1)
            ' had to delete no line
            Assert.AreEqual(0, i)
        End Sub

        <Test()> _
        Public Sub testChangerStockArticle()
            ' delete all items
            articlesDao.clearAllArticles()
            ' add an item
            Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
            Assert.AreEqual(nbArticles, 1)
            ' add an item
            nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            Assert.AreEqual(nbArticles, 1)
            ' creation of 100 threads
            Dim taches(99) As Thread
            For i As Integer = 0 To taches.Length - 1
                ' create thread i
                taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
                ' set the thread name
                taches(i).Name = "tache_" & i
                ' start execution of thread i
                taches(i).Start()
            Next
            ' wait for all threads to finish
            For i As Integer = 0 To taches.Length - 1
                taches(i).Join()
            Next
            ' checks - item 3 must have a stock of 1
            Dim unArticle As Article = articlesDao.getArticleById(3)
            Assert.AreEqual(unArticle.nom, "article3")
            Assert.AreEqual(1, unArticle.stockactuel)
            ' item 4 stock is decremented
            Dim erreur As Boolean = False
            Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
            ' check: its stock must not have changed
            Assert.AreEqual(0, nbLignes)
            ' visual check
            listArticles()
        End Sub

        Public Sub décrémente()
            ' thread launched
            System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
            ' thread decrements stock
            articlesDao.changerStockArticle(3, -1)
            ' thread terminated
            System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
        End Sub

    End Class
End Namespace

注释

  • 我们希望编写一个针对 [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()
            ' delete all items
            articlesDao.clearAllArticles()
            ' add an item
            Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
            Assert.AreEqual(nbArticles, 1)
            ' add an item
            nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            Assert.AreEqual(nbArticles, 1)
            ' creation of 100 threads
            Dim taches(99) As Thread
            For i As Integer = 0 To taches.Length - 1
                ' create thread i
                taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
                ' set the thread name
                taches(i).Name = "tache_" & i
                ' start execution of thread i
                taches(i).Start()
            Next
            ' wait for all threads to finish
            For i As Integer = 0 To taches.Length - 1
                taches(i).Join()
            Next
            ' checks - item 3 must have a stock of 1
            Dim unArticle As Article = articlesDao.getArticleById(3)
            Assert.AreEqual(unArticle.nom, "article3")
            Assert.AreEqual(1, unArticle.stockactuel)
            ' item 4 stock is decremented
            Dim erreur As Boolean = False
            Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
            ' check: its stock must not have changed
            Assert.AreEqual(0, nbLignes)
            ' visual check
            listArticles()
        End Sub

这是为了测试对数据源的并发访问。负责更新库存的方法如下:

        Public Sub décrémente()
            ' thread launched
            System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
            ' thread decrements stock
            articlesDao.changerStockArticle(3, -1)
            ' thread terminated
            System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
        End Sub

它将第 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] 层的接口

  • [Purchase]:定义购买行为的类

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

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

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

Image

注释

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

1.4.4.1. [IArticlesDomain] 接口

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

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

Namespace istia.st.articles.domain
    Public Interface IArticlesDomain
        ' methods
        Sub acheter(ByVal panier As Panier)
        Function getAllArticles() As IList
        Function getArticleById(ByVal idArticle As Integer) As Article
        ReadOnly Property erreurs() As ArrayList
    End Interface
End Namespace
Function getAllArticles() As IList
返回来自关联数据源的 [Article] 对象列表
Function getArticleById(ByVal idArticle As Integer) As Article
返回由 [idArticle] 标识的 [Article] 对象
buy(ByVal cart As Cart)
处理客户的购物车,将已购商品的库存减去购买数量——若库存不足,操作可能失败
只读属性 errors() As ArrayList
返回发生的错误列表——若无错误则为空

1.4.4.2. [Purchase] 类

[Purchase] 类表示客户的购买行为:

Imports istia.st.articles.dao

Namespace istia.st.articles.domain

    Public Class Achat

        ' private fields
        Private _article As article
        Private _qte As Integer

        ' default builder
        Public Sub New()
        End Sub

        ' builder with parameters
        Public Sub New(ByVal unArticle As article, ByVal qte As Integer)
            ' we go through the properties
            Me.article = unArticle
            Me.qte = qte
        End Sub

        ' item purchased
        Public Property article() As article
            Get
                Return _article
            End Get
            Set(ByVal Value As article)
                _article = Value
            End Set
        End Property

        ' qty purchased
        Public Property qte() As Integer
            Get
                Return _qte
            End Get
            Set(ByVal Value As Integer)
                If Value < 0 Then
                    Throw New Exception("Quantité [" + Value.ToString + "] invalide")
                End If
                _qte = Value
            End Set
        End Property

        ' total purchase
        Public ReadOnly Property totalAchat() As Double
            Get
                Return _qte * _article.prix
            End Get
        End Property

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

End Namespace

注释

  • [Purchase] 类具有以下属性和方法:
Public Property item() As item
已购买的商品
Public Property qty() As Integer
购买数量
Public ReadOnly Property purchaseTotal() As Double
购买金额
Public Override Function ToString() As String
对象的字符串表示形式
  • 它有一个构造函数,用于初始化定义购买行为的 [item, qty] 属性。

1.4.4.3. [Cart] 类

[Cart] 类表示客户的总购买量:

Namespace istia.st.articles.domain

Public Class Panier

    ' private fields
    Private _achats As New ArrayList
        Private _totalPanier As Double = 0

    ' default builder
    Public Sub New()
    End Sub

    ' list of purchases
    Public ReadOnly Property achats() As ArrayList
        Get
            Return _achats
        End Get
    End Property

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

    ' methods
    Public Sub ajouter(ByVal unAchat As Achat)
        ' find out if the purchase already exists
        Dim iAchat As Integer = posAchat(unAchat.article.id)
        If iAchat <> -1 Then
            ' we found
                Dim achatCourant As Achat = CType(_achats(iAchat), Achat)
            achatCourant.qte += unAchat.qte
        Else
            ' we didn't find
            _achats.Add(unAchat)
        End If
        ' increment the basket total
        _totalPanier += unAchat.totalAchat
    End Sub

    ' remove a purchase
    Public Sub enlever(ByVal idAchat As Integer)
        ' we're looking to buy
            Dim iachat As Integer = posAchat(idAchat)
        ' if found, remove
        If iachat <> -1 Then
                Dim achatCourant As Achat = CType(_achats(iachat), Achat)
            ' remove from basket
            _achats.RemoveAt(iachat)
            ' decrement the basket total
            _totalPanier -= achatCourant.totalAchat
        End If
    End Sub

    Private Function posAchat(ByVal idArticle As Integer) As Integer
        ' search for a purchase in the purchase list
        ' returns its position in the list or -1 if not found
        Dim achatCourant As Achat
        Dim trouvé As Boolean = False
        Dim i As Integer = 0
            While Not trouvé AndAlso i < _achats.Count
                ' regular purchase
                achatCourant = CType(_achats(i), Achat)
                ' comparison with article searched
                If achatCourant.article.id = idArticle Then
                    Return i
                End If
                'next purchase
                i += 1
            End While
            ' not found
            Return -1
    End Function

    ' identity function
    Public Overrides Function ToString() As String
        Return _achats.ToString
    End Function
End Class

End Namespace

注释

  • [ShoppingCart] 类具有以下属性和方法:
只读属性 purchases() 类型为 ArrayList
客户的购物清单——由 [Purchase] 类型对象组成的列表
add(ByVal aPurchase As Purchase)
将一项购买添加到购买列表中
remove(ByVal purchaseId As Integer)
移除 idPurchase 对应的购买记录
只读属性 totalBasket() As Double
购物车中所有商品的总金额
Function ToString() As String
返回表示购物车的字符串
  • [posAchat] 方法是一个实用方法,用于返回由商品编号标识的购买项在购买列表中的位置。购物清单的管理机制确保同一商品即使被多次购买,在列表中也仅占用一个位置。因此,可以通过商品编号来识别购买项。如果要查找的购买项不存在,[posAchat] 方法将返回 -1。
  • [add] 方法将新的购买记录添加到购买列表中。如果所购商品尚未存在于列表中,则向购买列表添加新条目;如果已存在,则增加购买数量。
  • [remove] 方法允许您从购买列表中移除一个由编号标识的购买记录。如果该购买记录不存在,则该方法不执行任何操作。
  • 随着商品的添加和移除,购物车总金额 [totalPanier] 会相应更新。

1.4.4.4. [PurchaseItems] 类

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

Imports istia.st.articles.dao

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

        'private fields
        Private _articlesDao As IArticlesDao
        Private _erreurs As ArrayList

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

        ' error list
        Public ReadOnly Property erreurs() As ArrayList Implements IArticlesDomain.erreurs
            Get
                Return _erreurs
            End Get
        End Property

        ' list of items
        Public Function getAllArticles() As IList Implements IArticlesDomain.getAllArticles
            ' list of all items
            Try
                Return _articlesDao.getAllArticles
            Catch ex As Exception
                _erreurs = New ArrayList
                _erreurs.Add("Erreur d'accès aux données : " + ex.Message)
            End Try
        End Function

        ' get an item identified by its number
        Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDomain.getArticleById
            ' a special item
            Try
                Return _articlesDao.getArticleById(idArticle)
            Catch ex As Exception
                _erreurs = New ArrayList
                _erreurs.Add("Erreur d'accès aux données : " + ex.Message)
            End Try
        End Function


        ' buy a basket
        Public Sub acheter(ByVal panier As Panier) Implements IArticlesDomain.acheter
            ' basket purchase - stocks of purchased items must be decremented
            _erreurs = New ArrayList
            Dim achat As achat
            Dim achats As ArrayList = panier.achats
            For i As Integer = achats.Count - 1 To 0 Step -1
                ' decrement stock item i
                achat = CType(achats(i), achat)
                Try
                    If _articlesDao.changerStockArticle(achat.article.id, -achat.qte) = 0 Then
                        ' we couldn't do the operation
                        _erreurs.Add("L'achat " + achat.ToString + " n'a pu se faire - Vérifiez les stocks")
                    Else
                        ' the transaction has been completed - the purchase is removed from the basket
                        panier.enlever(achat.article.id)
                    End If
                Catch ex As Exception
                    _erreurs = New ArrayList
                    _erreurs.Add("Erreur d'accès aux données : " + ex.Message)
                End Try
            Next
        End Sub
    End Class

End Namespace

注释

  • 该类实现了 [IArticlesDomain] 接口的四个方法。它有两个私有字段:
_articlesDao As IArticlesDao
数据访问对象
_errors As ArrayList
可能出现的错误列表。可通过公共属性 [errors] 访问
  • 要创建该类的实例,必须提供允许访问数据的对象:
Sub New(ByVal articlesDao As IArticlesDao)
  • [getAllArticles] 和 [getArticleById] 方法依赖于 [dao] 层中同名的方法
  • [buy] 方法用于验证购物车的购买操作。该验证仅涉及将所购商品的库存量减去购买数量。只有当库存充足时,才能购买该商品。若库存不足,购买请求将被拒绝:商品仍保留在购物车中,并在 [errors] 列表中报告错误。验证通过的购买商品将从购物车中移除,且相应商品的库存量将减少购买数量。

1.4.4.5. 生成 [domain] 层程序集

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

1.4.4.6. 针对 [domain] 层的 NUnit 测试

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

Image

注释

  • [tests] 项目属于 [类库] 类型
  • [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

        ' the test object
        Private articlesDomain As IArticlesDomain
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
            ' retrieve an instance of the Spring object manufacturer
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
            ' request instantiation of the articles dao object
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
            ' then the articlesdomain aobject
            articlesDomain = CType(factory.GetObject("articlesdomain"), IArticlesDomain)
        End Sub

        <Test()> _
        Public Sub getAllArticles()
            ' visual check
            listArticles()
        End Sub

        <Test()> _
        Public Sub getArticleById()
            ' article deletion
            articlesDao.clearAllArticles()
            ' check
            Dim articles As IList = articlesDomain.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' 2 items added
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' check
            articles = articlesDomain.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' research article 3
            Dim unArticle As Article = articlesDomain.getArticleById(3)
            ' check
            Assert.AreEqual(unArticle.nom, "article3")
            ' research article 4
            unArticle = articlesDao.getArticleById(4)
            ' check
            Assert.AreEqual(unArticle.nom, "article4")
        End Sub

        <Test()> _
        Public Sub acheterPanier()
            ' article deletion
            articlesDao.clearAllArticles()
            ' check
            Dim articles As IList = articlesDomain.getAllArticles
            Assert.AreEqual(0, articles.Count)
            ' 2 items added
            articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
            articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
            ' check
            articles = articlesDomain.getAllArticles
            Assert.AreEqual(2, articles.Count)
            ' create a basket with two purchases
            Dim panier As New panier
            panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 10))
            panier.ajouter(New Achat(New Article(4, "article4", 40, 40, 4), 10))
            ' checks
            Assert.AreEqual(700, panier.totalPanier, 0.000001)
            Assert.AreEqual(2, panier.achats.Count)
            ' shopping cart validation
            articlesDomain.acheter(panier)
            ' checks
            Assert.AreEqual(0, articlesDomain.erreurs.Count)
            Assert.AreEqual(0, panier.achats.Count)
            ' research article 3
            Dim unArticle As Article = articlesDomain.getArticleById(3)
            ' check
            Assert.AreEqual(unArticle.stockactuel, 20)
            ' research article 4
            unArticle = articlesDao.getArticleById(4)
            ' check
            Assert.AreEqual(unArticle.stockactuel, 30)
            ' new basket
            panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 100))
            ' shopping cart validation
            articlesDomain.acheter(panier)
            ' checks
            Assert.AreEqual(1, articlesDomain.erreurs.Count)
            ' research article 3
            unArticle = articlesDomain.getArticleById(3)
            ' check
            Assert.AreEqual(unArticle.stockactuel, 20)
        End Sub

        <Test()> _
        Public Sub testRetirerAchats()
            ' delete contents of ARTICLES
            articlesDao.clearAllArticles()
            ' reads the ARTICLES table
            Dim articles As IList = articlesDao.getAllArticles()
            Assert.AreEqual(0, articles.Count)
            ' insertion
            Dim article3 As New Article(3, "article3", 30, 30, 3)
            articlesDao.ajouteArticle(article3)
            Dim article4 As New Article(4, "article4", 40, 40, 4)
            articlesDao.ajouteArticle(article4)
            ' reads the ARTICLES table
            articles = articlesDomain.getAllArticles()
            Assert.AreEqual(2, articles.Count)
            ' create a basket with two purchases
            Dim monPanier As New Panier
            monPanier.ajouter(New Achat(article3, 10))
            monPanier.ajouter(New Achat(article4, 10))
            ' checks
            Assert.AreEqual(700.0, monPanier.totalPanier, 0.000001)
            Assert.AreEqual(2, monPanier.achats.Count)
            ' add a previously purchased item
            monPanier.ajouter(New Achat(article3, 10))
            ' checks
            ' the total must be increased to 1000
            Assert.AreEqual(1000.0, monPanier.totalPanier, 0.000001)
            ' always 2 items in the basket
            Assert.AreEqual(2, monPanier.achats.Count)
            ' qty item 3 increased to 20
            Dim unAchat As Achat = CType(monPanier.achats(0), Achat)
            Assert.AreEqual(20, unAchat.qte)
            ' article 3 is removed from the basket
            monPanier.enlever(3)
            ' checks
            ' the total must be increased to 400
            Assert.AreEqual(400.0, monPanier.totalPanier, 0.000001)
            ' 1 item only in basket
            Assert.AreEqual(1, monPanier.achats.Count)
            ' this must be article no. 4
            Assert.AreEqual(4, CType(monPanier.achats(0), Achat).article.id)
        End Sub

        ' screen listing
        Private Sub listArticles()
            Dim articles As IList = articlesDomain.getAllArticles
            For i As Integer = 0 To articles.Count - 1
                Console.WriteLine(CType(articles(i), Article).ToString)
            Next
        End Sub

    End Class


End Namespace

注释

  • 我们希望编写一个针对 [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应用程序:

我们 MVC 应用程序的 M 层(模型层)现已编写并经过测试。它以两个 DLL 文件的形式提供给我们 [webarticles-dao.dll, webarticles-domain.dll]。我们可以继续进行到最后一层,即 [Web] 层,该层包含 C 控制器和 V 视图。我们将首先考虑文档 [Web Development with ASP.NET 1.1] 中介绍的一种方法

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

1.5. [web]层

Web 应用程序的 MVC 架构如下:

M = 模型
业务类 [领域]、数据访问类 [DAO] 以及数据源
V = 视图
ASPX 页面
C = 控制器
所有 HTTP 客户端请求都会经过以下两个控制器:
global.asax:处理与应用程序初始启动相关的事件
main.aspx:单独处理每个客户端的请求

1.5.1. 视图

视图对应本文开头所列的视图:

列表
list.aspx
视图位于应用程序的 [views] 文件夹中
信息
info.aspx
购物车
cart.aspx
清空购物车
empty-cart.aspx
错误
errors.aspx

1.5.2. 控制器

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

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

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

request
含义
控制器操作
可能的响应
操作=列表
客户端希望获取
- 向图层请求项目列表
职业
- [LIST]
- [ERRORS]
action=info
客户端请求
关于视图中显示的
视图中显示的
[LIST]
- 从业务层请求该项目
- [INFO]
- [ERRORS]
操作=购买
客户购买商品
- 向业务层请求该商品
并将该商品加入客户的购物车
- [INFO] 若数量有误
- [LIST] 若无错误
action=移除购买
客户希望从购物车中移除
商品
- 从会话中获取购物车
并对其进行修改
- [购物车]
- [清空购物车]
- [错误]
action=cart
客户希望查看他们的
购物车
- 从会话中获取购物车
- [购物车]
- [购物车为空]
- [错误]
action=validate-cart
顾客已完成购物
并进入结账流程
- 根据已购商品的库存数量
- 将已确认购买的商品
已确认购买的商品
- [列表]
- [错误]

1.5.3. 应用程序配置

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

  1. 各种视图的 URL 变更
  2. 实现 [IArticlesDao] 和 [IArticlesDomain] 接口的类发生变更
  3. 数据库管理系统(DBMS)、数据库或文章表的变更

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>

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

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

由于我们正在处理一个不包含 DBMS 的测试应用程序,因此此处将忽略这一部分。我们将在后续章节中探讨基于 DBMS 的 [dao] 层的实现。

1.5.4. <asp:> 标签库

请看 [ERRORS] 视图,它用于显示错误列表:

Image

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

请记住,一个 ASPX 页面包含一个 HTML 呈现层和一个 .NET 代码层,后者负责准备呈现层必须显示的数据。这两个部分可以位于同一个 [aspx] 文件中(WebMatrix 解决方案),也可以分别位于两个文件中:[aspx] 用于呈现,[aspx.vb] 用于代码。后者是 Visual Studio 采用的方案。 更复杂的是,HTML 呈现层也可能包含 .NET 代码,这往往会模糊视图中 [控制器] 层与 [呈现] 层之间的界限。通常强烈不建议采用这种做法。要从 [呈现] 部分移除所有代码,需要创建标签库。这些标签库通过类似 HTML 标签的形式“隐藏”了代码。我们为 [ERRORS] 页面提供两种可能的解决方案。

我们的第一个解决方案是在页面的 [presentation] 部分使用 .NET 代码。该 ASPX 页面在其控制器部分 [errors.aspx.vb] 中检索请求中存在的错误列表:

        Protected erreurs As ArrayList

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

然后在 [presentation, errors.aspx] 部分中显示它们:

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

第二个解决方案使用了来自 ASP.NET <asp:> 标签库的 <asp:repeater> 标签。如果您通过图形化方式构建 ASPX 页面,该标签作为服务器控件可供使用,您可以将其拖放到设计表单上。如果您手动编写 ASPX 代码,则可以引用该标签库。

借助 <asp:> 标签库,前文 [ERRORS] 视图的 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 模板

数据源通常绑定在页面的 [controller] 部分中的 标签上:

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

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
..
            ' link errors to rptErreurs
            With rptErreurs
                .DataSource = context.Items("erreurs")
                .DataBind()
            End With
        End Sub

如果数据源已知(例如现有数据库),则此绑定也可在页面设计阶段完成。

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

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

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

  

评论

  • 该 [web] 项目属于 [类库] 类型,而非人们通常预期的 [ASP.NET Web 应用程序] 类型。[ASP.NET Web 应用程序] 类型要求开发机或远程机器上安装 IIS Web 服务器。 Windows XP 家庭版计算机默认未预装 IIS 服务器。然而,许多个人电脑都预装了该版本。为了让使用 Windows XP 的读者能够实现本章所研究的应用程序,我们将使用微软免费提供的 Cassini Web 服务器(参见附录),并将 [ASP.NET Web 应用程序] 项目替换为 [类库] 项目。这会带来一些缺点,具体说明见附录。
  • 该应用程序使用的 DLL 如下:
webarticles-dao.dll
包含数据访问层的类
webarticles-domain.dll
包含业务层的类
Spring.Core.dll
包含 Spring 类,用于集成 Web、领域和 DAO 层
log4net.dll
日志类——由 Spring 使用

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

1.5.6. ASPX 视图

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

1.5.6.1. 用户组件 [entete.ascx]

为确保不同视图之间的一致性,它们将共享相同的页眉,该页眉会显示应用程序名称以及菜单:

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

  • href:与菜单选项关联的 URL
  • 链接:菜单文字

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

否。
类型
名称
角色
1
重复器
rptMenu
数据源:一个字典数组
包含两个键:href、link
显示菜单选项

页面呈现代码如下:

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

评论

  • [repeater] 组件在第 6–14 行中定义
  • 与该重复器关联的数据源中的每个元素都是一个字典,包含两个键:href(第 9 行)和 link(第 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())
                ' associate the action table with its component
                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 行将该组件插入到页面的呈现代码中。在运行时,该标签会将 [entete.ascx] 页面中的代码包含到包含它的 ASPX 页面中。控件代码 [erreurs.aspx.vb] 将负责初始化此组件。具体操作如下:
    Public Class ErreursWebarticles
        Inherits System.Web.UI.Page

        ' page components
...
        Protected WithEvents entete As New EnteteWebArticles

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
            ' link menu options to 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. 简介

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

该视图在收到对 /main?action=list/main?action=cartvalidation 的请求后显示。控制器请求的参数如下:

操作
Hashtable() 对象 - 菜单选项数组
listarticles
[Item] 类型对象的 ArrayList
message
字符串对象 - 页面底部显示的消息

文章 HTML 表格中的每个 [Info] 链接都包含一个 URL,格式为 [?action=info&id=ID],其中 ID 是所显示文章的 id 字段。

1.5.6.2.2. 页面组件
类型
名称
角色
1
用户组件
标题
显示标题
2
数据网格
DataGridArticles
3 - 关联列:标题:名称,字段:name
4 - 关联列:标题:价格,字段:price
5 - 超文本列:文本:Info,URL 字段:id,
URL 格式:/webarticles/main.aspx?action=info&id={0}
显示待售商品
6
标签
lblMessage
显示一条消息

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

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

Image

  • 使用上方的 [AutoFormat] 链接来管理显示网格的布局
  • 并使用 [属性生成器] 链接来管理其内容
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

    ' manages the item list display page
    Public Class ListeWebarticles
        Inherits System.Web.UI.Page

        ' page components
        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
            ' prepare the [list] view using context information
            ' link menu options to rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
            ' retrieve items from a datatable
            Dim articles As ArrayList = CType(context.Items("articles"), ArrayList)
            ' we link them to the [DataGrid] component of the page
            With DataGridArticles
                .DataSource = articles
                .DataBind()
            End With
            ' the message
            lblMessage.Text = context.Items("message").ToString
        End Sub
    End Class
End Namespace

注释

  • 页面组件出现在第 13–15 行。请注意,我们需要使用 [new] 运算符创建 [EnteteWebArticles] 对象,而其他组件则无需如此。如果不进行这种显式创建,我们会遇到运行时错误,提示 [entete] 对象未引用任何内容。这一点值得进一步调查。目前尚未进行调查。
  • 第 20 行从上下文中获取了页眉菜单选项表,用于初始化页面的 [entete] 组件
  • 文章列表从上下文中获取——第 22 行
  • 以初始化 [DataGridArticles] 组件——第 24–27 行
  • [lblMessage] 组件通过上下文中放置的消息进行初始化——第 29 行

1.5.6.3. [infos.aspx]视图

1.5.6.3.1. 简介

此视图显示有关某项商品的信息,并允许购买该商品:

Image

当发送 /main?action=infos&id=ID 请求,或发送 /main?action=achat&id=ID 请求且购买数量有误时,将显示此视图。控制器请求参数如下:

操作
Hashtable() 对象 - 菜单选项数组
item
类型为 [Article] 的对象 - 要显示的项目
msg
字符串对象 - 数量出现错误时显示的消息
qty
字符串对象 - 在 [数量] 输入字段中显示的值

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

Image

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

1.5.6.3.2. 页面组件
类型
名称
角色
1
用户组件
标题
显示标题
2
字面量
litID
显示项目编号
3 至 6
DataGrid
DataGridArticle
3 - 关联列:标题:名称,字段:name
4 - 关联列:标题:价格,字段:price
5 - 关联列:标题:当前库存,字段:currentStock
6 - 关联列:标题:最低库存,字段:stockMinimum
显示一项
7
HTML 提交
 
提交表单
8
HTML 输入框
runat=server
txtQty
请输入购买数量
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 行定义。它是一个服务器端 HTML 组件(runat=server)。在代码端,通过一个对象访问它。
  • 第 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

    ' manages an article information page
    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

        ' the URL where the form will be posted
        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

        ' page display
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
            ' retrieve query info
            Dim unArticle As Article = CType(Session.Item("article"), Article)
            ' link menu options to rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
            ' we link the article to [DataGrid]
            Dim articles As New ArrayList
            articles.Add(unArticle)
            With DataGridArticle
                .DataSource = articles
                .DataBind()
            End With
            ' the id label
            litId.Text = unArticle.id.ToString
            ' the error message
            lblMsgQte.Text = context.Items("msg").ToString
            ' the previous qty
            txtQte.Value = context.Items("qte").ToString
            ' the URL action
            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. 简介

此视图显示购物车中的内容:

Image

该视图会在响应诸如 /main?action=cart/main?action=cancelpurchase&id=ID 之类的请求时显示。控制器的请求参数如下:

操作
object Hashtable() - 菜单选项数组
cart
类型为 [Cart] 的对象 - 要显示的购物车

购物车商品HTML表格中的每个 [Remove] 链接都包含一个格式为 [?action=removeitem&id=ID] 的URL,其中ID是待从购物车中移除的商品的 [id] 字段。

1.5.6.4.2. 页面组件
编号
类型
名称
角色
1
用户组件
标题
显示标题
2
DataGrid
DataGridPurchases
3 - 关联列 - 标题:项目,字段:名称
4 - 关联列 - 标题:数量,字段:qty
5 - 关联列 - 标题:价格,字段:price
6 - 关联列 - 标题:总计,字段:total,格式 {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
    ' manages the basket display page
    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
            ' link menu options to rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
            ' we pick up the basket
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            ' transfer purchases to a table of purchase lines
            Dim achats(unPanier.achats.Count - 1) As LigneAchat
            ' change the type of ArrayList elements
            For i As Integer = 0 To achats.Length - 1
                achats(i) = New LigneAchat(CType(unPanier.achats(i), Achat))
            Next
            ' link the data to the [DataGrid] components of the page
            With DataGridAchats
                .DataSource = achats
                .DataBind()
            End With
            ' total payable is displayed
            lblTotal.Text = unPanier.totalPanier.ToString
        End Sub

        ' purchase line built from a Purchase object
        Private Class LigneAchat
            Inherits Achat

            ' builder receives a purchase
            Public Sub New(ByVal unAchat As Achat)
                Me.article = unAchat.article
                Me.qte = unAchat.qte
            End Sub

            ' id: returns the id of the item purchased
            Public ReadOnly Property id() As Integer
                Get
                    Return article.id
                End Get
            End Property

            ' name: name of item purchased
            Public ReadOnly Property nom() As String
                Get
                    Return article.nom
                End Get
            End Property

            ' price of item purchased
            Public ReadOnly Property prix() As Double
                Get
                    Return article.prix
                End Get
            End Property

        End Class
    End Class
End Namespace

注释

  • 页面组件在第11至13行进行声明
  • [header] 组件的初始化方式与之前学习的页面完全一致——第 17 行
  • 待显示的购物车是从会话中获取的——第 19 行
  • 使用 [DataGridAchats] 组件显示此购物车会遇到问题。困难源于该组件的初始化。让我们回顾一下其列:
    • [Article] 列与数据源中的 [name] 字段相关联
    • [Qty] 列与数据源中的 [qty] 字段相关联
    • [Price] 列与数据源中的 [price] 字段相关联
    • [Total] 列与数据源中的 [total] 字段相关联

我们拥有的数据源是购物车及其购物清单。后者将作为 [DataGrid] 的数据源。然而,用于填充 [DataGrid] 行内容的 [Purchase] 对象并不具备 [DataGrid] 所期望的 [name, qty, price, total] 属性。 因此,我们在此专门为 [DataGrid] 创建一个数据源,其元素具备 [DataGrid] 所期望的特征。这些元素将采用 [PurchaseLine] 类型,这是一个为此目的创建的类,并继承自 [Purchase] 类——第 36–66 行

  • 定义 [PurchaseLine] 类后,[DataGridPurchases] 的数据源将基于会话中的购物车构建——第 20–30 行
  • 使用 [Panier] 类的 [totalPanier] 属性显示总购买金额——第 32 行

1.5.6.5. [emptyCart.aspx] 视图

1.5.6.5.1. 简介

此视图显示购物车为空的相关信息:

Image

在向 /main?action=panier/main?action=retirerachat&id=ID 发送请求后,该页面将被显示。控制器请求参数如下:

操作
Hashtable() 对象 - 菜单选项数组
1.5.6.5.2. 页面组件
编号
类型
名称
角色
1
用户组件
标题
显示标题
1.5.6.5.3. 呈现代码 [emptycart.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
    ' manages the empty basket display page
    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
            ' prepare the [emptybasket] view using context information
            ' link menu options to rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
        End Sub
    End Class
End Namespace

注释

  • 我们只需初始化页面中唯一的动态组件——第 10 行

1.5.6.6. [errors.aspx] 视图

1.5.6.6.1. 简介

当发生错误时,将显示此视图:

Image

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

操作
Hashtable() 对象 - 菜单选项数组
错误
表示要显示的错误消息的 [String] 对象的 ArrayList
1.5.6.6.2. 页面组件
编号
类型
名称
角色
1
用户组件
标题
显示标题
2
重复器
rptErrors
显示错误列表
1.5.6.6.3. 呈现代码 [errors.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 行定义
  • [rptErrors] 组件在第 13–19 行定义。其内容来自一个类型为 [ArrayList] 的数据源,该数据源包含 [String] 对象。
1.5.6.6.4. 控件代码 [errors.aspx.vb]
Namespace istia.st.articles.web

    ' manages the error page
    Public Class ErreursWebarticles
        Inherits System.Web.UI.Page

        ' page components
        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
            ' prepare the [errors] view using context information
            ' link menu options to rptmenu
            entete.actions = CType(context.Items("options"), Hashtable())
            ' link errors to rptErreurs
            With rptErreurs
                .DataSource = context.Items("erreurs")
                .DataBind()
            End With
        End Sub
    End Class

End Namespace

注释

  • [header] 组件按常规方式初始化,第 9 行和第 14 行
  • [rptErrors] 组件使用上下文中找到的 [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 errors] 对象的形式,将应用程序上下文中的所有错误列表放入其中。如果没有错误,该列表将为空,但该列表仍会存在。
  • 若存在错误,[Application_Start] 方法将在此处终止。 否则,它将请求一个类型为 [IArticlesDomain] 的单例引用,该单例将成为控制器用于满足其需求的业务对象。如 1.5.3.2 节所述,控制器将从 Spring 框架中请求此单例。此实例化操作可能会导致各种错误。如果发生错误,这些错误将再次存储在应用程序上下文的 [errors] 对象中。

[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

        ' init application
        Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)

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

            ' retrieve application initialization parameters
            Dim param As String
            For i As Integer = 0 To parameters.Length - 1
                ' read from conf file
                param = ConfigurationSettings.AppSettings(parameters(i))
                If param Is Nothing Then
                    ' we note the error
                    erreurs.Add("Paramètre [" + parameters(i) + "] absent dans le fichier [web.config]")
                Else
                    ' the parameter is stored in the application
                    Application.Item(parameters(i)) = param
                End If
            Next
            ' mistakes?
            If erreurs.Count = 0 Then
                ' create a IArticlesDomain business layer access object
                Dim contexte As IApplicationContext = CType(ConfigurationSettings.GetConfig("spring/context"), IApplicationContext)
                Dim articlesDomain As IArticlesDomain
                Try
                    articlesDomain = CType(contexte.GetObject("articlesDomain"), IArticlesDomain)
                    ' the object is stored in the application
                    Application.Item("articlesDomain") = articlesDomain
                Catch ex As Exception
                    ' we memorize the error
                    erreurs.Add("Erreur lors de la construction de l'objet d'accès à la couche métier [" + ex.ToString + "]")
                End Try
            End If
            ' errors are placed in the application
            Application.Item("erreurs") = erreurs
            ' it's over if there were mistakes
            If erreurs.Count <> 0 Then Return
            ' build an array of menu options
            Dim options As New Hashtable
            ' retrieve the URL from the controller
            Dim urlMain As String = CType(Application.Item("urlMain"), String)
            Dim uneOption As Hashtable
            ' list of items
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=liste")
            uneOption.Add("lien", "Liste des articles")
            options.Add("liste", uneOption)
            ' basket
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=panier")
            uneOption.Add("lien", "Voir le panier")
            options.Add("panier", uneOption)
            ' shopping cart validation
            uneOption = New Hashtable
            uneOption.Add("href", urlMain + "?action=validationpanier")
            uneOption.Add("lien", "Valider le panier")
            options.Add("validationpanier", uneOption)
            ' set menu options in the application
            Application.Item("options") = options
            Return
        End Sub

        ' init session
        Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
            ' create a basket for the customer
            Session.Item("panier") = New Panier
        End Sub
    End Class
End Namespace

注释

  • [web.config] 中预期的参数在第 18 行定义为一个数组
  • 系统会在 [web.config] 中搜索这些参数。若存在,则将其存储在应用上下文中;否则,会在 [errors] 错误列表中记录错误 - 第 21-33 行
  • 若无错误,则向 Spring 请求 [articlesDomain] 单例的引用,该单例负责管理对应用程序 [domain] 层的访问 - 第 35-47 行。任何错误都会记录在 [errors] 中。
  • 错误将记录在应用程序上下文中 - 第 49 行
  • 若发生任何错误,则退出该过程 - 第 51 行
  • 我们创建一个包含三个字典的数组。每个字典有两个键:href 和 link。该数组代表三个可能的菜单选项——第 52–71 行
  • 该数组存储在应用程序上下文中——第 73 行
  • 对于每位新客户,都会执行 [Session_Start] 过程。在客户的会话中创建一个空购物车——第 78–81 行

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

[main.aspx.vb] 控制器处理所有客户端请求。这些请求均遵循 [/webarticles/main.aspx?action=XX] 的格式。请求的处理流程如下:

  • 检查应用程序上下文中的 [errors] 对象。如果该对象不为空,则表示应用程序初始化期间发生错误,应用程序无法运行。此时将返回 [ERRORS] 视图。
  • 将检索并检查请求的 [action] 参数。如果它不对应于已知的操作,则发送 [ERRORS] 视图并附上相应的错误消息。
  • 如果 [action] 参数有效,则将客户端的请求传递给该操作对应的特定处理程序进行处理:
方法
请求
处理
可能的响应
doList
GET /main?action=list
- 请求项目列表
从业务类
- 显示
[LIST] 或 [ERRORS]
doInfo
GET /main?action=info&id=ID
- 从
业务类中
- 显示该项
[INFO] 或 [ERRORS]
doPurchase
POST /main?action=purchase&id=ID
- 购买数量包含在提交的参数中
- 从
业务类中
- 将其添加到
客户会话中
[LIST] 或 [INFO] 或 [ERRORS]
doRetirePurchase
GET /main?action=removePurchase&id=ID
- 从
购物清单中
[CART]
[CART]
doCart
GET /main?action=cart
- 显示
客户端会话
[CART] 或 [EMPTY_CART]
doCartValidation
GET /main?action=cartvalidation
- 减少
所有商品的库存
在客户
[LIST] 或 [ERRORS]
[LIST] 或 [ERRORS]

控制器模板 [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 application controller class
    Public Class MainWebArticles
        Inherits System.Web.UI.Page

        ' private fields
        Private articlesDomain As IArticlesDomain
        Private options As Hashtable

        ' page loading
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
....
        End Sub

        ' stock processing methods

        ' list of items
        Public Sub doListe()
...
        End Sub

        ' article info
        Public Sub doInfos()
...
        End Sub

        ' purchase an item
        Public Sub doAchat()
...
        End Sub

        ' delete a purchase
        Public Sub doRetirerAchat()
...
        End Sub

        ' view basket
        Public Sub doPanier()
...
        End Sub

        ' bUY BASKET
        Public Sub doValidationPanier()
...
        End Sub

    End Class

End Namespace

注释

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

1.5.7.3. [Page_Load] 方法

此事件是页面上最早触发的事件。代码如下:

        ' page loading
        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
            ' check that the application has started correctly
            Dim erreurs As ArrayList = CType(Application.Item("erreurs"), ArrayList)
            ' if there are errors, we send the view [errors]
            If erreurs.Count <> 0 Then
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {}
                Server.Transfer(CType(Application("urlErreurs"), String))
            End If
            ' retrieve the business class access object
            articlesDomain = CType(Application.Item("articlesDomain"), IArticlesDomain)
            ' and menu options
            options = CType(Application.Item("options"), Hashtable)
            ' retrieve the action to be performed
            Dim action As String = Request.QueryString("action")
            If action Is Nothing Then
                action = "liste"
            End If
            ' execute the action
            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 行
  • 如果该列表不为空,则显示 [ERRORS] 视图——第 6–10 行
  • 我们从应用程序上下文中检索由 [global.asax] 放置的 [articlesDomain] 单例,并将其存储在私有字段 [articlesDomain] 中,以便该类中的各种方法可以使用它——第 12 行
  • 我们对菜单选项数组执行类似的操作——第 14 行
  • 从请求中获取 [action] 参数——第 16 行
  • 我们执行与请求操作对应的方法。未指定的操作将被视为 [list] 操作——第 16–36 行

1.5.7.4. 处理 [list] 操作

这涉及显示项目列表:

Image

代码如下:

        ' list of items
        Public Sub doListe()
            ' error management
            Dim erreurs As New ArrayList
            ' the list of items is requested
            Dim articles As IList
            Try
                articles = articlesDomain.getAllArticles
            Catch ex As Exception
                ' we note the error
                erreurs.Add(ex.ToString)
            End Try
            ' mistakes?
            If erreurs.Count <> 0 Then
                ' display view [errors]
                context.Items("erreurs") = erreurs
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlErreurs"), String))
            End If
            ' display view [list]
            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 行
  • 如果存在错误,则渲染 [ERRORS] 视图 - 第 13-19 行
  • 否则,返回 [LIST] 视图 - 第 20-24 行

1.5.7.5. 处理 [info] 操作

客户端请求了关于某个特定项的信息:

Image

代码如下:

        ' article info
        Public Sub doInfos()
            ' error management
            Dim erreurs As New ArrayList
            ' retrieve the id of the requested item
            Dim strId As String = Request.QueryString("id")
            ' do we have anything?
            If strId Is Nothing Then
                ' not normal - sends error page
                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
            ' do we have an integer?
            Dim id As Integer
            Try
                id = Integer.Parse(strId)
            Catch ex As Exception
                ' not normal - sends error page
                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
            ' the key item id is requested
            Dim unArticle As Article
            Try
                unArticle = articlesDomain.getArticleById(id)
            Catch ex As Exception
                ' data access issues
                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
            ' has an item been recovered?
            If unArticle Is Nothing Then
                ' article does not exist
                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
            ' we have the article - we put it in the current session
            Session.Item("article") = unArticle
            ' prepare your display
            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 行
  • 从请求中获取所请求项的ID - 第6行
  • 对该ID进行验证。它必须存在且必须是整数。若不满足条件,则显示[ERRORS]视图并附上相应的错误信息 - 第7-27行
  • ID验证通过后,从[articlesDomain]单例中请求该项目。若发生异常,则发送[ERRORS]视图 - 第29-39行
  • 若未找到该项目,则发送 [ERRORS] 视图——第 41–48 行
  • 若找到该商品,则将其放入用户的会话中,然后在 [INFO] 视图中显示——第 50-56 行

1.5.7.6. 处理 [purchase] 操作

客户已购买 [INFO] 视图中显示的商品。

Image

代码如下:

        ' purchase an item
        Public Sub doAchat()
            ' purchase an item
            ' we retrieve the article that was in the session
            Dim unArticle As Article = CType(Session.Item("article"), Article)
            ' do we have anything?
            If unArticle Is Nothing Then
                ' not normal - item list is displayed
                doListe()
            End If
            ' we retrieve the posted qty
            Dim strQte As String = Request.Form("txtQte")
            ' do we have anything?
            If strQte Is Nothing Then
                ' not normal - we send the list of items
                doListe()
            End If
            ' do we have an integer?
            Dim qte As Integer
            Try
                qte = Integer.Parse(strQte)
                If (qte <= 0) Then Throw New Exception
            Catch ex As Exception
                ' not normal - send info page with error message
                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
            ' all's well - we put the purchase in the customer's shopping cart
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            unPanier.ajouter(New Achat(unArticle, qte))
            ' displays the list of items
            doListe()
        End Sub

注释

  • 检索会话中存储的项目 - 第 5 行
  • 如果未找到(会话可能已过期),则显示 [LIST] 视图 - 第 7-10 行
  • 从查询中检索购买数量 - 第 12 行
  • 检查其有效性 - 第13-29行
  • 若无效,根据具体情况,显示 [LIST] 视图 - 第 16 行 或 [INFO] 视图 - 第 24-28 行
  • 如果一切正常,将商品添加到购物车 - 第 31-32 行
  • 随后发送 [LIST] 视图 - 第 34 行

1.5.7.7. 处理 [cart] 操作

客户已购买多件商品,并希望查看购物车:

Image

代码如下:

        ' view basket
        Public Sub doPanier()
            'the basket is retrieved from the session
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            ' empty basket?
            If unPanier.achats.Count = 0 Then
                ' empty basket display
                context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable)}
                Server.Transfer(CType(Application.Item("urlPanierVide"), String))
            End If
            ' display basket not empty
            context.Items("options") = New Hashtable() {CType(options("liste"), Hashtable), CType(options("validationpanier"), Hashtable)}
            Server.Transfer(CType(Application.Item("urlPanier"), String))
        End Sub

注释

  • 我们从会话中检索购物车——第 4 行。此处未检查是否实际检索到数据。我们应该进行此检查,因为会话可能已过期。
  • 如果购物车为空,则发送 [EMPTY CART] 视图——第 6-10 行
  • 否则,我们发送 [SHOPPINGCART] 视图——第 11–14 行

1.5.7.8. 处理 [removepurchase] 操作

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

Image

代码如下:

        ' delete a purchase
        Public Sub doRetirerAchat()
            'the basket is retrieved from the session
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            ' we remove the purchase
            Try
                ' retrieve the id of the removed item
                Dim idArticle As Integer = Integer.Parse(Request.QueryString("id"))
                ' take it out of the basket
                unPanier.enlever(idArticle)
            Catch ex As Exception
                ' displays the list of items
                doListe()
            End Try
            ' the basket is displayed
            doPanier()
        End Sub

注释

  • 从会话中检索购物车 - 第 4 行。此处未检查是否实际检索到数据。应进行此检查,因为会话可能已过期。
  • 从查询中获取待删除商品的 ID [id] —— 第 8 行。
  • 从购物车中移除对应的商品 - 第 10 行
  • 此处未对已购商品 ID 的有效性进行验证。若其类型无效,将引发异常并在第 11–13 行进行处理。若 ID 有效但不存在,则第 10 行的 [cart.remove] 方法将不执行任何操作。
  • 显示新的购物车 - 第 16 行

1.5.7.9. 处理 [validateCart] 操作

客户希望确认购物车:

Image

代码如下:

        ' bUY BASKET
        Public Sub doValidationPanier()
            ' at the start, no mistakes
            Dim erreurs As ArrayList
            'the basket is retrieved from the session
            Dim unPanier As Panier = CType(Session.Item("panier"), Panier)
            ' we try to validate the basket
            Try
                articlesDomain.acheter(unPanier)
                ' note any purchasing errors
                erreurs = articlesDomain.erreurs
            Catch ex As Exception
                ' we note the error
                erreurs = New ArrayList
                erreurs.Add(String.Format("Erreur lors de la validation du panier [{0}]", ex.Message))
            End Try
            ' if errors then error page
            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
            ' all is well - the list of items is displayed with a success message
            context.Items("message") = "Votre panier a été validé"
            doListe()
        End Sub

注释

  • 我们从会话中检索购物车——第 6 行。此处未检查是否实际检索到内容。我们应该进行此检查,因为会话可能已过期。
  • 如果会话已过期,购物车的指针将为 [nothing],第 9 行的 [buy] 方法将抛出异常,并显示 [ERRORS] 视图。然而,错误信息对用户来说可能不够清晰。
  • 第 8–16 行:我们尝试验证从会话中检索到的购物车。如果请求的数量超过所请求商品的库存,某些购买可能会失败。这些情况由 [buy] 方法存储在错误列表中,该列表在第 11 行被检索。
  • 如果存在错误,则发送 [ERRORS] 视图——第 18–23 行
  • 否则,发送 [LIST] 视图——第 25–26 行

1.6. 结论

在此,我们开发了一个采用 MVC 模式的应用程序。截至 2005 年 4 月,似乎尚无任何与 Java 平台(如 Struts、Spring 等)相媲美的专业 ASP.NET MVC 开发框架。[Spring.net] 项目预计将很快发布一个。在此之前,上述方法为中等规模的应用程序提供了一种可行的 MVC 开发方案。