Skip to content

1. Частина 1

PDF цього документа доступний за посиланням |TYT|.

Приклади з документа доступні за посиланням |TYT|.

1.1. Introduction

Цілі статті:

  • написати трирівневий веб-додаток [interface utilisateur, métier, accès aux données]
  • налаштувати додаток за допомогою Spring IOC
  • написати різні версії, змінюючи реалізацію того чи іншого з трьох рівнів.

Використані інструменти:

  • Visual Studio.net для розробки — див. додаток, розділ 3.1;
  • Веб-сервер Cassini для виконання — див. додаток, розділ 3.2;
  • Nunit для модульних тестів — див. додаток, пункт 3.4;
  • Spring для інтеграції та конфігурації рівнів веб-додатку — див. додаток, розділ 3.3;

За шкалою «початковий — середній — просунутий» цей документ належить до рівня [intermédiaire-avancé]. Для його розуміння необхідні певні попередні знання. Деякі з них можна отримати з документів, які я написав. У цьому випадку я наводжу посилання на них. Зрозуміло, що це лише рекомендація, і читач може використовувати свої улюблені документи.

Цей документ повторює основну лінію документа, написаного для Java [Трирівневі архітектури та архітектури MVC зі Struts, Spring та Java (2005)]. Ми створюємо в VB.NET трирівневий веб-додаток MVC, написаний на Java. Ідея, яку ми хочемо донести, полягає в тому, що платформи розробки Java та .NET є достатньо близькими одна до одної, щоб навички, набуті в одній із цих двох сфер, можна було застосувати в іншій.

Схоже, що широко визнаного рішення для розробки на MVC та ASP.NET не існує. Наведене нижче рішення базується на методі, описаному в документі [Веб-розробка з використанням ASP.NET 1.1 (2004)]. Хоча цей метод має ту перевагу, що використовує концепції, поширені в розробці J2EE, його слід сприймати лише як те, чим він є, c.a.d — один із багатьох методів розробки MVC. Як тільки якийсь метод розробки MVC у ASP.NET отримає широке визнання, слід буде перейти на нього. Версія Spring .NET, яка зараз перебуває на стадії розробки, цілком може стати першим таким рішенням.

1.2. Веб-додаток «articles»

Тут ми представляємо елементи спрощеного веб-додатку для електронної комерції. Він дозволить веб-користувачам:

  • переглядати перелік товарів із бази даних
  • додавати деякі з них до електронного кошика
  • підтвердити його. Єдиним наслідком такого підтвердження буде оновлення в базі даних запасів придбаних товарів.

Користувачеві будуть доступні такі екрани:

- екран «LISTE», на якому відображається перелік товарів, що продаються
- екран [INFOS], що надає додаткову інформацію про товар:
  • види [PANIER] та [PANIERVIDE], що відображають вміст кошика клієнта
  • представлення [ERREURS], яке повідомляє про будь-які помилки в роботі додатка

Image

1.3. Загальна архітектура додатка

Ми хочемо створити додаток із такою трирівневою структурою:

  • три рівні є незалежними завдяки використанню інтерфейсів
  • інтеграція різних рівнів здійснюється за допомогою Spring
  • кожен рівень має окремий простір імен: web (рівень UI), domain (бізнес-рівень) та dao (рівень доступу до даних).

Додаток відповідатиме архітектурі MVC (Модель — Вигляд — Контролер). Якщо повернутися до наведеної вище схеми з шарами, архітектура MVC вписується в неї наступним чином:

Обробка запиту клієнта відбувається за такими етапами:

  1. клієнт надсилає запит до контролера. Цим контролером тут буде сторінка .aspx, якій відведено особливу роль. Вона обробляє всі запити клієнтів. Це вхідні ворота додатка. Це «C» у MVC.
  1. контролер обробляє цей запит. Для цього йому може знадобитися допомога бізнес-шару, який у структурі MVC називається моделлю M.
  2. Контролер отримує відповідь від бізнес-шару. Запит клієнта було оброблено. Це може призвести до кількох можливих відповідей. Класичним прикладом є
    • сторінка з повідомленням про помилку, якщо запит не вдалося обробити належним чином
    • сторінка підтвердження в іншому випадку
  3. контролер обирає відповідь (= представлення), яку потрібно надіслати клієнту. Найчастіше це сторінка, що містить динамічні елементи. Контролер передає їх до представлення.
  4. Вигляд надсилається клієнту. Це «V» у MVC.

1.4. Модель

Тут ми розглядаємо «М» з MVC. Модель складається з таких елементів:

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

assembly
contenu
rôle
webarticles-dao
- [IArticlesDao]: інтерфейс доступу до шару [dao].
Це єдиний інтерфейс, який бачить рівень [domain]. Інших він не бачить.
- [Article]: клас, що визначає товар
- [ArticlesDaoArrayList]: клас реалізації
інтерфейсу [IArticlesDao] за допомогою класу [ArrayList]
шар доступу до даних
- повністю знаходиться в шарі
[dao] трирівневої архітектури
веб-додатку
webarticles-domain
- [IArticlesDomain]: інтерфейс доступу до шару [domain]. Це єдиний інтерфейс, який бачить веб-шар. Інших він не бачить.
- [AchatsArticles]: клас, що реалізує [IArticlesDomain]
- [Achat]: клас, що представляє покупку клієнта
- [Panier]: клас, що представляє сукупність покупок клієнта
представляє модель покупок у
Інтернеті — повністю розміщений у
шарі [domain] архітектури
3-рівнева архітектура веб-додатку

1.4.3. Рівень [dao]

Рівень [dao] містить такі елементи:

  • [IArticlesDao]: інтерфейс доступу до шару [dao]

  • [Article]: клас, що визначає статтю

  • [ArticlesDaoArrayList]: клас реалізації інтерфейсу [IArticlesDao] за допомогою класу [ArrayList]

Структура проєкту [Visual Studio] на рівні [dao] є такою:

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

         ' ідентифікатор товару
        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. Щоб продемонструвати, що для тестування веб-додатку має значення лише інтерфейс доступу до даних, а не його клас реалізації, спочатку ми реалізуємо джерело даних за допомогою простого об’єкта [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] дозволяє визначити позицію [0..N] у джерелі [ArrayList] для позиції, ідентифікованої за її номером. Якщо стаття не існує, метод повертає позицію -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. Тести NUnit для шару [dao]

У Java класи тестуються за допомогою фреймворку [Junit]. У .NET фреймворк Nunit надає ті самі можливості для модульного тестування:

Image

Структура тестового проєкту Visual Studio така:

Image

Коментарі:

  • проект [tests] належить до типу [bibliothèque de classes]
  • тести [NUnit] потребують посилання на збірку [nunit.framework.dll]
  • клас тесту [NUnit] отримує екземпляр об’єкта, що тестується, через Spring. Тому
    • у папці [bin] знаходяться файли класів Spring
    • у [References] — посилання на збірку [Spring-Core.dll] із папки [bin]
    • у [bin] — файл конфігурації для Spring
  • тестовий клас потребує збірки [webarticles-dao.dll] з рівня [dao]. Ця збірка була розміщена в папці [bin], а посилання на неї додано до посилань проекту.

Тестовий клас [NUnit] потребує доступу до класів простору імен [NUnit.Framework]. Тому там міститься така інструкція імпорту:

Imports NUnit.Framework

Простір імен [NUnit.Framework] знаходиться в «асемблі» [nunit.framework.dll], яку потрібно додати до посилань проекту:

Асемблі [nunit.framework.dll] має бути у запропонованому списку, якщо було виконано встановлення [Nunit]. Достатньо двічі клацнути на асемблі, щоб додати його до проекту:

Image

Тестовий клас [NUnit] з шару [dao] може мати такий вигляд:

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

Цей файл вказує назву класу реалізації [istia.st.articles.dao.ArticlesDaoArrayList] інтерфейсу [IArticlesDao] та місце його розташування [ 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 на одиницю. Якщо звернутися до коду методу [testChangerStockArticle], то можна побачити, що:

  • запас товару № 3 ініціалізується до 101
  • 100 потоків зменшать цей запас на одиницю кожен
  • отже, після завершення виконання всіх потоків запас повинен становити 1

Крім того, у цьому ж методі робиться спроба встановити запас товару № 4 на від’ємне значення. Ця спроба має завершитися невдачею.

Щоб протестувати шар [dao], ми генеруємо DLL [tests-webarticles-dao.dll] у папці [bin] проекту [tests]:

Потім за допомогою програми [Nunit-Gui] ми завантажуємо цей файл DLL і виконуємо тести:

Image

У лівому вікні відображається список протестованих методів. Колір крапки перед назвою кожного методу вказує на успішне виконання (зелений) або невдале виконання (червоний) методу. Читач, який переглядає цей документ на екрані, побачить, що всі тести пройшли успішно. Далі ми вважатимемо, що маємо робочий шар [dao].

1.4.4. Шар [domain]

Рівень [domain] містить такі елементи:

  • [IArticlesDomain]: інтерфейс доступу до шару [domain]

  • [Achat]: клас, що визначає покупку

  • [Panier]: клас, що визначає кошик покупок

  • [AchatsArticles]: клас реалізації інтерфейсу [IArticlesDomain]

Структура рішення [Visual Studio] на рівні [domain] є такою:

Image

Коментарі:

  • проект [domain] має тип [bibliothèque de classes]
  • класи розміщено в деревоподібній структурі, кореневою папкою якої є папка [istia]. Усі вони знаходяться в просторі імен [istia.st.articles.domain].
  • DLL з шару [dao] було розміщено в папці [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
повертає об’єкт [Article], ідентифікований за [idArticle]
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] — посилання на збірку [Spring-Core.dll] з папки [bin]
  • у [bin] — файл конфігурації для Spring
  • тестовий клас потребує збірки [webarticles-dao.dll] з рівня [dao] та збірки [webarticles-domain.dll] з рівня [domain]. Вони були розміщені в папці [bin], а їхні посилання додано до списку посилань проекту.

Тестовий клас NUnit з шару [domain] може мати такий вигляд:

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))
             ' запитуємо інстанцію об’єкта DAO «articles»
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
             ' а потім — об’єкта `articlesdomain`
            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], ми генеруємо DLL та [tests-webarticles-domain.dll] у папці [bin] проекту [tests]:

Потім за допомогою програми [Nunit-Gui] ми завантажуємо цей файл DLL і виконуємо тести:

Image

Користувач, який переглядає цей документ на екрані, побачить, що всі тести пройшли успішно. Отже, вважатимемо, що ми маємо робочий шар [domain].

1.4.5. Висновок

Нагадаємо, що ми хочемо побудувати таку трирівневу веб-програму:

Модель M нашого додатка MVC вже написана та протестована. Вона надається нам у двох файлах: DLL та [webarticles-dao.dll, webarticles-domain.dll]. Ми можемо перейти до останнього рівня — рівня [web], який містить контролер C та представлення V. Спочатку розглянемо метод, описаний у документі [Développement WEB avec ASP.NET 1.1 ]

  • контролер C реалізовано у двох файлах [global.asax, main.aspx]
  • представлення V реалізовано за допомогою сторінок aspx

1.5. Рівень [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.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 створить екземпляр такого об’єкта. Наприкінці операції рівень [web], який запитував синглтон [articlesDomain], матиме повний ланцюжок, що пов’язує його з джерелом даних:

1.5.3.3. Зміни, пов’язані з SGBD або базою даних

Цей пункт тут не розглядатиметься, оскільки ми працюємо в тестовому додатку без SGBD. Пізніше ми розглянемо реалізацію шару [dao], що базується на SGBD.

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

Наше перше рішення використовує код .NET у частині [présentation] сторінки. Сторінка 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:repeater> із бібліотеки тегів <asp:> ASP.NET. Якщо створювати сторінку ASPX графічно, цей тег доступний у вигляді серверного компонента, який розміщується на формі проектування. Якщо створювати код ASPX вручну, можна говорити про бібліотеку тегів.

Завдяки бібліотеці тегів <asp:> код ASPX з попереднього виду [ERREURS] набуває такого вигляду:

        <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. Структура рішення Visual Studio для додатка [webarticles]

Веб-додаток — це пазл із безліччю елементів. Впровадження архітектури MVC, як правило, збільшує їхню кількість. Структура додатка [webarticles] у рамках [Visual Studio] є такою:

  

Коментарі:

  • проект [web] належить до типу [bibliothèque de classes], а не до типу [Application web ASP.NET], як можна було б логічно очікувати. Тип [Application web ASP.NET] вимагає наявності веб-сервера IIS на комп’ютері розробника або на віддаленому комп’ютері. Сервер IIS за замовчуванням не встановлений на комп’ютерах із Windows XP Familial. При цьому багато комп’ютерів із PC продаються саме з цією версією. Щоб читачі, які мають Windows XP, могли реалізувати розглянутий додаток, ми будемо використовувати веб-сервер Cassini (див. додаток), який безкоштовно надається компанією Microsoft, і замінимо проект [Application web ASP.NET] на проект [bibliothèque de classes]. Це спричиняє деякі незручності, які пояснено в додатках.
  • Файли DLL, що використовуються додатком, є такими:
webarticles-dao.dll
об’єднує класи рівня доступу до даних
webarticles-domain.dll
об’єднує класи бізнес-шару
Spring.Core.dll
містить класи Spring, які дозволяють нам інтегрувати веб-шар, доменний шар та шар DAO
log4net.dll
класи журналів — використовуються Spring

Ці DLL розміщуються в папці [bin] і додаються до посилань проекту.

1.5.6. Види ASPX

Як було рекомендовано раніше, у наших поданнях ASPX ми використовуватимемо бібліотеку тегів <asp:>.

1.5.6.1. Користувацький компонент [entete.ascx]

Щоб забезпечити певну уніфікованість різних подань, вони матимуть спільний заголовок, який відображає назву додатка разом із меню:

Меню є динамічним і визначається контролером. Контролер додає до запиту, що надсилається на сторінку ASPX, атрибут-ключ «actions», значенням якого є масив елементів типу Hashtable(). Кожен елемент цього масиву є словником, призначеним для формування опції меню заголовка. Кожен словник має два ключі:

  • href: посилання URL, пов’язане з пунктом меню
  • link: текст меню

Ми перетворимо заголовок на елемент управління. Елемент управління інкапсулює фрагмент сторінки (оформлення та відповідний код) у компонент, який потім можна повторно використовувати на інших сторінках. У цьому випадку ми хочемо повторно використовувати компонент [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 та посилання — рядок 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
  • це властивість дозволяє пов'язати з компонентом <asp:repeater> під назвою [rptMenu] — рядок 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 — повідомлення, яке відображається внизу сторінки

Кожне посилання [Infos] з масиву HTML статей має 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. Слід зауважити, що нам довелося створити об’єкт [EnteteWebArticles] з оператором [new], хоча для інших компонентів це не було потрібно. Без цього явного створення виникала помилка виконання, яка вказувала, що об’єкт [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
об'єкт String — значення, яке потрібно відобразити у полі введення [Qte]

Поля [msg] та [qte] використовуються у разі помилки введення кількості:

Image

Ця сторінка містить форму, яка відправляється за допомогою кнопки [Acheter]. Цільовим полем URL для поля POST є [?action=achat&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
label
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

     ' обробляє сторінку з інформацією про статтю
    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
             ' ідентифікатор мітки
            litId.Text = unArticle.id.ToString
             ' повідомлення про помилку
            lblMsgQte.Text = context.Items("msg").ToString
             ' попередня кількість
            txtQte.Value = context.Items("qte").ToString
             ' дія URL
            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. Ця змінна використовується для генерації атрибута [action] форми HTML, що міститься на сторінці:
        <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] — кошик, який потрібно відобразити

Кожне посилання [Retirer] у масиві HTML покупок у кошику має 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: повертає ідентифікатор придбаного товару
            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]. Лише об’єкти [Achat], які заповнюватимуть рядки [DataGrid], не мають властивостей [nom, qte, prix, total], яких очікує [DataGrid]. Тому тут, спеціально для [DataGrid], створюється джерело даних, елементи якого мають характеристики, очікувані об’єктом [DataGrid]. Ці елементи будуть типу [LigneAchat] — класу, створеного спеціально для цієї мети та похідного від класу [Achat] (рядки 36–66)

  • після визначення класу [LigneAchat) джерело даних [DataGridAchats] формується на основі кошика, знайденого в сесії — рядки 20–30
  • сума покупок відображається завдяки властивості [totalPanier] класу [Panier] — рядок 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

Залишається написати серцевину нашого веб-додатку — контролер. Його роль полягає в тому, щоб:

  • отримати запит від клієнта,
  • обробити запитувану клієнтом дію за допомогою бізнес-класів,
  • відправити у відповідь відповідний вигляд.

1.5.7.1. Контролер [global.asax.vb]

Коли додаток отримує свій перший запит, виконується процедура [Application_Start] із файлу [global.asax.vb]. Це відбудеться лише один раз. Процедура [Application_Start] призначена для ініціалізації об’єктів, необхідних для веб-додатка, які будуть спільними для всіх клієнтських потоків у режимі «тільки для читання». Ці спільні об’єкти можуть розміщуватися у двох місцях:

  • у приватних полях контролера
  • контекст виконання додатка (Application)

Метод [Application_Start] додатка [global.asax.vb] виконає такі дії:

  • перевірить наявність у файлі [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 та посилання. Цей масив представляє три можливі варіанти меню — рядки 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
- запросити статтю з id=ID у
бізнес-класу
- відобразити його
[INFOS] або [ERREURS]
doAchat
POST /main?action=achat&id=ID
- кількість, що була придбана, входить до параметрів, що передаються
- запитати товар з id=ID у
бізнес-класу
- додати його до кошика в
сесії клієнта
[LISTE] або [INFOS] або [ERREURS]
doRetirerAchat
GET /main?action=retirerachat&id=ID
- видалити товар з 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

     ' клас контролера веб-додатку
    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
  • отримуємо синглтон [articlesDomain], розміщений [global.asax] у контексті додатка, і зберігаємо його у приватному полі [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
             ' отримання ідентифікатора запитуваного товару
            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
  • після перевірки ідентифікатора товар запитується у синглтоні [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
                 ' отримання ідентифікатора видаленого товару
                Dim idArticle As Integer = Integer.Parse(Request.QueryString("id"))
                 ' вилучення товару з кошика
                unPanier.enlever(idArticle)
            Catch ex As Exception
                 ' відображається список товарів
                doListe()
            End Try
             ' відображається кошик
            doPanier()
        End Sub

Коментарі:

  • кошик отримуємо з сесії — рядок 4. Тут не перевіряємо, чи дійсно щось отримано. Це слід зробити, оскільки сесія могла закінчитися.
  • У запиті отримуємо ідентифікатор [id] товару, який потрібно видалити — рядок 8.
  • Відповідний товар видаляється з кошика — рядок 10
  • Тут не перевіряється дійсність ідентифікатора придбаного товару. Якщо він має недійсний тип, виникне виняток, який буде оброблено в рядках 11–13. Якщо він дійсний, але не існує, метод [panier.enlever] (рядок 10) нічого не робить.
  • виводиться новий кошик — рядок 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] на кошик, і метод [acheter] (рядок 9) викличе виняток, після чого буде відправлено представлення [ERREURS]. Проте повідомлення про помилку буде недостатньо зрозумілим для користувача.
  • У рядках 8–16 відбувається перевірка кошика, отриманого з сесії. Деякі покупки можуть бути не підтверджені, якщо запитувана кількість перевищує запас необхідного товару. Такі випадки фіксуються методом [acheter] у списку помилок, який отримується в рядку 11.
  • якщо є помилки, надсилається представлення [ERREURS] — рядки 18–23
  • інакше надсилається подання [LISTE] — рядки 25–26

1.6. Conclusion

Тут ми розробили додаток за шаблоном MVC. Наразі (квітень 2005 року) не існує професійних «фреймворків» для розробки MVC на ASP.NET, подібних до тих, що існують для Java (Struts, Spring, ...). Проєкт [Spring.net] повинен незабаром запропонувати такий фреймворк. Доки це не відбудеться, описаний вище метод дозволяє здійснювати ефективну розробку MVC для додатків середнього розміру.